Skip to content

Commit

Permalink
feat(rule): add "disabled" and "allows" for each dict item (#15)
Browse files Browse the repository at this point in the history
* feat(rule): add "disabled" and "allows" for each dict item

* chore(rule): add JSdoc

* docs(rule): ルールの説明文を修正

* fix typo
  • Loading branch information
azu authored Jan 4, 2019
1 parent 52fac9b commit 45865e1
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 58 deletions.
98 changes: 83 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,53 @@

## 表現の一覧

- "すること\[助詞]()可能"は冗長な表現です。"すること\[助詞]()可能"を省き簡潔な表現にすると文章が明瞭になります。
- 参考: <http://qiita.com/takahi-i/items/a93dc2ff42af6b93f6e0>
- "すること\[助詞]できる"は冗長な表現です。"すること\[助詞]"を省き簡潔な表現にすると文章が明瞭になります。
- 参考: <http://qiita.com/takahi-i/items/a93dc2ff42af6b93f6e0>
- "であると言えます"は冗長な表現です。"である" または "と言えます"を省き簡潔な表現にすると文章が明瞭になります。
- 参考: <http://www.sekaihaasobiba.com/entry/2014/10/24/204024>
- "であると考えている"は冗長な表現です。"である" または "と考えている"を省き簡潔な表現にすると文章が明瞭になります。
- 参考: <http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html>
- "を行う"は冗長な表現です。"する"など簡潔な表現にすると文章が明瞭になります。
- 参考: <http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html>
- "を実行"は冗長な表現です。"する"など簡潔な表現にすると文章が明瞭になります。
- 参考: <http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html>
### 【dict1】

"すること\[助詞]()可能"は冗長な表現です。"すること\[助詞]()可能"を省き簡潔な表現にすると文章が明瞭になります。

- 参考: <http://qiita.com/takahi-i/items/a93dc2ff42af6b93f6e0>

### 【dict2】

"すること\[助詞]できる"は冗長な表現です。"すること\[助詞]"を省き簡潔な表現にすると文章が明瞭になります。

- 参考: <http://qiita.com/takahi-i/items/a93dc2ff42af6b93f6e0>

### 【dict3】

"であると言えます"は冗長な表現です。"である" または "と言えます"を省き簡潔な表現にすると文章が明瞭になります。

- 参考: <http://www.sekaihaasobiba.com/entry/2014/10/24/204024>

### 【dict4】

"であると考えている"は冗長な表現です。"である" または "と考えている"を省き簡潔な表現にすると文章が明瞭になります。

- 参考: <http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html>

### 【dict5】

"\[サ変名詞]を行う"は冗長な表現です。"\[サ変名詞]する"など簡潔な表現にすると文章が明瞭になります。

[サ変名詞]とは「[名詞]する」というように「する」が後ろについた場合に、動詞の働きをする名詞です。

例)「行動(する)」、「プログラム(する)」

誤検知を防ぐためにデフォルトでは、「[カタナカ]を行う」と「[アルファベット]を行う」は"allows"で無視するように定義されています。

- 参考: <http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html>

### 【dict6】

"\[サ変名詞]を実行"は冗長な表現です。"\[サ変名詞]する"など簡潔な表現にすると文章が明瞭になります。

[サ変名詞]とは「[名詞]する」というように「する」が後ろについた場合に、動詞の働きをする名詞です。

例)「行動(する)」、「プログラム(する)」

誤検知を防ぐためにデフォルトでは、「[カタナカ]を実行」と「[アルファベット]を実行」は"allows"で無視するように定義されています。

- 参考: <http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html>

## Install

Expand All @@ -44,9 +79,42 @@ Via CLI
## Options

- `allowNodeTypes`: `string[]`
- 無視したいNode typeを配列で指定
- Node typeは <https://textlint.github.io/docs/txtnode.html#type> を参照
- デフォルトでは、`["BlockQuote", "Link", "ReferenceDef"]`を指定し、引用やリンクのテキストは無視する
- 無視したいNode typeを配列で指定
- Node typeは <https://textlint.github.io/docs/txtnode.html#type> を参照
- デフォルトでは、`["BlockQuote", "Link", "ReferenceDef"]`を指定し、引用やリンクのテキストは無視する
- `dictOptions`: `object`
- それぞれの`dict`に対するオプションを指定する
- プロパティに`dict`の【dict[id]】を書き、値には次の辞書オプションを指定する
- 辞書オプション: `object`
- `disbled`: `boolean`
- `true`を指定するdictを無効化
- `allows`: `string[]`
- エラーを無視したいパターンを[正規表現ライクな文字列](https://github.com/textlint/regexp-string-matcher)で指定

例) [dict1](#dict1)は無効化、[dict5](#dict5)で"処理を行う"をエラーにしない。

```json5
{
"rules": {
"ja-no-redundant-expression": {
"dictOptions": {
"dict1": {
"disabled": true
},
"dict5": {
// "処理を行う" を許可する
allows: [
"/^処理を行う/",
// デフォルトの許可リストは上書きされるので、維持したい場合は追加する
"/^[ァ-ヶ]+を.?行う/",
"/^[a-zA-Z]+を.?行う/"
]
}
}
}
}
}
```

## Changelog

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"textlint-scripts": "^2.1.0"
},
"dependencies": {
"@textlint/regexp-string-matcher": "^1.0.2",
"kuromojin": "^1.3.2",
"morpheme-match": "^1.2.1",
"morpheme-match-all": "^1.2.0",
Expand Down
40 changes: 36 additions & 4 deletions src/dictionary.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ const punctuations = ["、", "、", ",", ","];
module.exports = [
{
// https://azu.github.io/morpheme-match/?text=省略(することが可能)。
id: "dict1",
disabled: false,
allows: [],
message: `"する$2$3$4$5$1"は冗長な表現です。"する$2$3$4$5"を省き簡潔な表現にすると文章が明瞭になります。`,
url: "http://qiita.com/takahi-i/items/a93dc2ff42af6b93f6e0",
tokens: [
Expand All @@ -29,7 +32,7 @@ module.exports = [
{
pos: "助詞",
_capture: "$3",
_readme: "\\[助詞]"
_readme: "[助詞]"
},
{
surface_form: punctuations,
Expand All @@ -49,6 +52,9 @@ module.exports = [
},
{
// https://azu.github.io/morpheme-match/?text=解析(することができます)。
id: "dict2",
disabled: false,
allows: [],
message: `"する$4$3$5$1$2"は冗長な表現です。"する$4$3$5"を省き簡潔な表現にすると文章が明瞭になります。`,
url: "http://qiita.com/takahi-i/items/a93dc2ff42af6b93f6e0",
expected: "$3$1$2",
Expand Down Expand Up @@ -82,7 +88,7 @@ module.exports = [
}
return "";
},
_readme: "\\[助詞]"
_readme: "[助詞]"
},
{
surface_form: punctuations,
Expand All @@ -104,6 +110,9 @@ module.exports = [
},
{
// https://azu.github.io/morpheme-match/?text=必要(であると言えます)
id: "dict3",
disabled: false,
allows: [],
message: `"で$1$6と$5$2ます"は冗長な表現です。"である$6" または "と$5言えます"を省き簡潔な表現にすると文章が明瞭になります。`,
url: "http://www.sekaihaasobiba.com/entry/2014/10/24/204024",
tokens: [
Expand Down Expand Up @@ -166,6 +175,9 @@ module.exports = [
},
{
// https://azu.github.io/morpheme-match/?text=必要(であると考えている)
id: "dict4",
disabled: false,
allows: [],
message: `"である$7と$5考えて$6いる"は冗長な表現です。"である$7" または "と$5考えて$6いる"を省き簡潔な表現にすると文章が明瞭になります。`,
url: "http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html",
expected: "である",
Expand Down Expand Up @@ -251,13 +263,23 @@ module.exports = [
},
{
// https://azu.github.io/morpheme-match/?text=動作の(確認を行わなければ)ならない
id: "dict5",
disabled: false,
allows: ["/^[ァ-ヶ]+を.?行う/", "/^[a-zA-Z]+を.?行う/"],
message: `"$1を$5行う"は冗長な表現です。"$1する"など簡潔な表現にすると文章が明瞭になります。`,
description: `[サ変名詞]とは「[名詞]する」というように「する」が後ろについた場合に、動詞の働きをする名詞です。
例)「行動(する)」、「プログラム(する)」
誤検知を防ぐためにデフォルトでは、「[カタカナ]を行う」と「[アルファベット]を行う」は"allows"で無視するように定義されています。
`,
url: "http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html",
tokens: [
{
pos: "名詞",
pos_detail_1: "サ変接続",
_capture: "$1"
_capture: "$1",
_readme: "[サ変名詞]"
},
{
surface_form: "を",
Expand Down Expand Up @@ -287,13 +309,23 @@ module.exports = [
]
},
{
id: "dict6",
disabled: false,
allows: ["/^[ァ-ヶ]+を.?実行/", "/^[a-zA-Z]+を.?実行/"],
message: `"$1を$5実行"は冗長な表現です。"$1する"など簡潔な表現にすると文章が明瞭になります。`,
description: `[サ変名詞]とは「[名詞]する」というように「する」が後ろについた場合に、動詞の働きをする名詞です。
例)「行動(する)」、「プログラム(する)」
誤検知を防ぐためにデフォルトでは、「[カタカナ]を実行」と「[アルファベット]を実行」は"allows"で無視するように定義されています。
`,
url: "http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html",
tokens: [
{
pos: "名詞",
pos_detail_1: "サ変接続",
_capture: "$1"
_capture: "$1",
_readme: "[サ変名詞]"
},
{
surface_form: "を",
Expand Down
60 changes: 55 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@
"use strict";
import { wrapReportHandler } from "textlint-rule-helper";
import StringSource from "textlint-util-to-string";
import { matchPatterns } from "@textlint/regexp-string-matcher";

const tokenize = require("kuromojin").tokenize;
const dictionaryList = require("./dictionary");
const createMatchAll = require("morpheme-match-all");

/**
* textの中身をすべて置換する
* @param {string} text
* @param {string|undefined} from
* @param {string} to
* @returns {string}
*/
const replaceAll = (text, from, to) => {
return text.split(from).join(to);
};
Expand All @@ -17,6 +25,30 @@ const replaceTokenWith = (matcherToken, actualToken, specialTo) => {
}
return actualToken.surface_form;
};

/**
* tokensのsurface_formをつなげた文字列を返す
* @param tokens
* @returns {string}
*/
const tokensToString = tokens => {
return tokens.map(token => token.surface_form).join("");
};

/**
* "allows" オプションで許可されているかどうか
* @param {*[]} tokens
* @param {string[]} allows
*/
const isTokensAllowed = (tokens, allows) => {
if (allows.length === 0) {
return false;
}
const matchedText = tokensToString(tokens);
const allowsMatchResults = matchPatterns(matchedText, allows);
return allowsMatchResults.length > 0;
};

const createExpected = ({ text, matcherTokens, skipped, actualTokens }) => {
let resultText = text;
let actualTokenIndex = 0;
Expand All @@ -33,7 +65,7 @@ const createExpected = ({ text, matcherTokens, skipped, actualTokens }) => {
});
return resultText;
};
const createMessage = ({ text, matcherTokens, skipped, actualTokens }) => {
const createMessage = ({ id, text, matcherTokens, skipped, actualTokens }) => {
let resultText = text;
let actualTokenIndex = 0;
matcherTokens.forEach((token, index) => {
Expand All @@ -48,16 +80,25 @@ const createMessage = ({ text, matcherTokens, skipped, actualTokens }) => {
}
++actualTokenIndex;
});
return resultText;
return `【${id}${resultText}
解説: https://github.com/textlint-ja/textlint-rule-ja-no-redundant-expression#${id}`;
};

const reporter = (context, options = {}) => {
const { Syntax, RuleError, fixer } = context;
const DefaultOptions = {
// https://textlint.github.io/docs/txtnode.html#type
allowNodeTypes: [Syntax.BlockQuote, Syntax.Link, Syntax.ReferenceDef]
allowNodeTypes: [Syntax.BlockQuote, Syntax.Link, Syntax.ReferenceDef],
dictOptions: {}
};
const matchAll = createMatchAll(dictionaryList);
const dictOptions = options.dictOptions || DefaultOptions.dictOptions;
// "disabled": trueな辞書は取り除く
const enabledDictionaryList = dictionaryList.filter(dict => {
const dictOption = dictOptions[dict.id] || {};
const disabled = typeof dictOption.disabled === "boolean" ? dictOption.disabled : dict.disabled;
return !disabled;
});
const matchAll = createMatchAll(enabledDictionaryList);
const skipNodeTypes = options.allowNodeTypes || DefaultOptions.allowNodeTypes;
return wrapReportHandler(
context,
Expand All @@ -75,6 +116,14 @@ const reporter = (context, options = {}) => {
*/
const matchResults = matchAll(currentTokens);
matchResults.forEach(matchResult => {
const dictOption = dictOptions[matchResult.dict.id] || {};
// "allows" オプションにマッチした場合はエラーを報告しない
const allows = dictOption.allows || matchResult.dict.allows;
const isAllowed = isTokensAllowed(matchResult.tokens, allows);
if (isAllowed) {
return;
}
// エラー報告
const firstToken = matchResult.tokens[0];
const lastToken = matchResult.tokens[matchResult.tokens.length - 1];
const firstWordIndex = source.originalIndexFromIndex(
Expand All @@ -86,11 +135,12 @@ const reporter = (context, options = {}) => {
// replace $1
const message =
createMessage({
id: matchResult.dict.id,
text: matchResult.dict.message,
matcherTokens: matchResult.dict.tokens,
skipped: matchResult.skipped,
actualTokens: matchResult.tokens
}) + (matchResult.dict.url ? `参考: ${matchResult.dict.url}` : "");
});
const expected = matchResult.dict.expected
? createExpected({
text: matchResult.dict.expected,
Expand Down
26 changes: 26 additions & 0 deletions test/dictionary-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// MIT © 2019 azu
"use strict";
import assert from "assert";

describe('dictionary', function() {
it("should not have duplicated id", () => {
const dictionary = require("../src/dictionary.js");
dictionary.forEach(item => {
assert.ok(typeof item.id === "string", "should have id property");
const sameIdItems = dictionary.filter(target => target.id === item.id);
assert.ok(sameIdItems.length === 1, "should not have duplicated id item");
});
});
it("should have disabled default value", () => {
const dictionary = require("../src/dictionary.js");
dictionary.forEach(item => {
assert.ok(typeof item.disabled === "boolean", `${item} should have disabled property`);
});
});
it("should have allows default value", () => {
const dictionary = require("../src/dictionary.js");
dictionary.forEach(item => {
assert.ok(Array.isArray(item.allows), `${item}: should have disabled property`);
});
});
});
Loading

0 comments on commit 45865e1

Please sign in to comment.