Skip to content

Commit 45865e1

Browse files
authored
feat(rule): add "disabled" and "allows" for each dict item (#15)
* feat(rule): add "disabled" and "allows" for each dict item * chore(rule): add JSdoc * docs(rule): ルールの説明文を修正 * fix typo
1 parent 52fac9b commit 45865e1

File tree

8 files changed

+366
-58
lines changed

8 files changed

+366
-58
lines changed

README.md

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,53 @@
66

77
## 表現の一覧
88

9-
- "すること\[助詞]()可能"は冗長な表現です。"すること\[助詞]()可能"を省き簡潔な表現にすると文章が明瞭になります。
10-
- 参考: <http://qiita.com/takahi-i/items/a93dc2ff42af6b93f6e0>
11-
- "すること\[助詞]できる"は冗長な表現です。"すること\[助詞]"を省き簡潔な表現にすると文章が明瞭になります。
12-
- 参考: <http://qiita.com/takahi-i/items/a93dc2ff42af6b93f6e0>
13-
- "であると言えます"は冗長な表現です。"である" または "と言えます"を省き簡潔な表現にすると文章が明瞭になります。
14-
- 参考: <http://www.sekaihaasobiba.com/entry/2014/10/24/204024>
15-
- "であると考えている"は冗長な表現です。"である" または "と考えている"を省き簡潔な表現にすると文章が明瞭になります。
16-
- 参考: <http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html>
17-
- "を行う"は冗長な表現です。"する"など簡潔な表現にすると文章が明瞭になります。
18-
- 参考: <http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html>
19-
- "を実行"は冗長な表現です。"する"など簡潔な表現にすると文章が明瞭になります。
20-
- 参考: <http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html>
9+
### 【dict1】
10+
11+
"すること\[助詞]()可能"は冗長な表現です。"すること\[助詞]()可能"を省き簡潔な表現にすると文章が明瞭になります。
12+
13+
- 参考: <http://qiita.com/takahi-i/items/a93dc2ff42af6b93f6e0>
14+
15+
### 【dict2】
16+
17+
"すること\[助詞]できる"は冗長な表現です。"すること\[助詞]"を省き簡潔な表現にすると文章が明瞭になります。
18+
19+
- 参考: <http://qiita.com/takahi-i/items/a93dc2ff42af6b93f6e0>
20+
21+
### 【dict3】
22+
23+
"であると言えます"は冗長な表現です。"である" または "と言えます"を省き簡潔な表現にすると文章が明瞭になります。
24+
25+
- 参考: <http://www.sekaihaasobiba.com/entry/2014/10/24/204024>
26+
27+
### 【dict4】
28+
29+
"であると考えている"は冗長な表現です。"である" または "と考えている"を省き簡潔な表現にすると文章が明瞭になります。
30+
31+
- 参考: <http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html>
32+
33+
### 【dict5】
34+
35+
"\[サ変名詞]を行う"は冗長な表現です。"\[サ変名詞]する"など簡潔な表現にすると文章が明瞭になります。
36+
37+
[サ変名詞]とは「[名詞]する」というように「する」が後ろについた場合に、動詞の働きをする名詞です。
38+
39+
例)「行動(する)」、「プログラム(する)」
40+
41+
誤検知を防ぐためにデフォルトでは、「[カタナカ]を行う」と「[アルファベット]を行う」は"allows"で無視するように定義されています。
42+
43+
- 参考: <http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html>
44+
45+
### 【dict6】
46+
47+
"\[サ変名詞]を実行"は冗長な表現です。"\[サ変名詞]する"など簡潔な表現にすると文章が明瞭になります。
48+
49+
[サ変名詞]とは「[名詞]する」というように「する」が後ろについた場合に、動詞の働きをする名詞です。
50+
51+
例)「行動(する)」、「プログラム(する)」
52+
53+
誤検知を防ぐためにデフォルトでは、「[カタナカ]を実行」と「[アルファベット]を実行」は"allows"で無視するように定義されています。
54+
55+
- 参考: <http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html>
2156

2257
## Install
2358

@@ -44,9 +79,42 @@ Via CLI
4479
## Options
4580

4681
- `allowNodeTypes`: `string[]`
47-
- 無視したいNode typeを配列で指定
48-
- Node typeは <https://textlint.github.io/docs/txtnode.html#type> を参照
49-
- デフォルトでは、`["BlockQuote", "Link", "ReferenceDef"]`を指定し、引用やリンクのテキストは無視する
82+
- 無視したいNode typeを配列で指定
83+
- Node typeは <https://textlint.github.io/docs/txtnode.html#type> を参照
84+
- デフォルトでは、`["BlockQuote", "Link", "ReferenceDef"]`を指定し、引用やリンクのテキストは無視する
85+
- `dictOptions`: `object`
86+
- それぞれの`dict`に対するオプションを指定する
87+
- プロパティに`dict`の【dict[id]】を書き、値には次の辞書オプションを指定する
88+
- 辞書オプション: `object`
89+
- `disbled`: `boolean`
90+
- `true`を指定するdictを無効化
91+
- `allows`: `string[]`
92+
- エラーを無視したいパターンを[正規表現ライクな文字列](https://github.com/textlint/regexp-string-matcher)で指定
93+
94+
例) [dict1](#dict1)は無効化、[dict5](#dict5)で"処理を行う"をエラーにしない。
95+
96+
```json5
97+
{
98+
"rules": {
99+
"ja-no-redundant-expression": {
100+
"dictOptions": {
101+
"dict1": {
102+
"disabled": true
103+
},
104+
"dict5": {
105+
// "処理を行う" を許可する
106+
allows: [
107+
"/^処理を行う/",
108+
// デフォルトの許可リストは上書きされるので、維持したい場合は追加する
109+
"/^[ァ-ヶ]+を.?行う/",
110+
"/^[a-zA-Z]+を.?行う/"
111+
]
112+
}
113+
}
114+
}
115+
}
116+
}
117+
```
50118

51119
## Changelog
52120

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"textlint-scripts": "^2.1.0"
3838
},
3939
"dependencies": {
40+
"@textlint/regexp-string-matcher": "^1.0.2",
4041
"kuromojin": "^1.3.2",
4142
"morpheme-match": "^1.2.1",
4243
"morpheme-match-all": "^1.2.0",

src/dictionary.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ const punctuations = ["、", "、", ",", ","];
55
module.exports = [
66
{
77
// https://azu.github.io/morpheme-match/?text=省略(することが可能)。
8+
id: "dict1",
9+
disabled: false,
10+
allows: [],
811
message: `"する$2$3$4$5$1"は冗長な表現です。"する$2$3$4$5"を省き簡潔な表現にすると文章が明瞭になります。`,
912
url: "http://qiita.com/takahi-i/items/a93dc2ff42af6b93f6e0",
1013
tokens: [
@@ -29,7 +32,7 @@ module.exports = [
2932
{
3033
pos: "助詞",
3134
_capture: "$3",
32-
_readme: "\\[助詞]"
35+
_readme: "[助詞]"
3336
},
3437
{
3538
surface_form: punctuations,
@@ -49,6 +52,9 @@ module.exports = [
4952
},
5053
{
5154
// https://azu.github.io/morpheme-match/?text=解析(することができます)。
55+
id: "dict2",
56+
disabled: false,
57+
allows: [],
5258
message: `"する$4$3$5$1$2"は冗長な表現です。"する$4$3$5"を省き簡潔な表現にすると文章が明瞭になります。`,
5359
url: "http://qiita.com/takahi-i/items/a93dc2ff42af6b93f6e0",
5460
expected: "$3$1$2",
@@ -82,7 +88,7 @@ module.exports = [
8288
}
8389
return "";
8490
},
85-
_readme: "\\[助詞]"
91+
_readme: "[助詞]"
8692
},
8793
{
8894
surface_form: punctuations,
@@ -104,6 +110,9 @@ module.exports = [
104110
},
105111
{
106112
// https://azu.github.io/morpheme-match/?text=必要(であると言えます)
113+
id: "dict3",
114+
disabled: false,
115+
allows: [],
107116
message: `"で$1$6と$5$2ます"は冗長な表現です。"である$6" または "と$5言えます"を省き簡潔な表現にすると文章が明瞭になります。`,
108117
url: "http://www.sekaihaasobiba.com/entry/2014/10/24/204024",
109118
tokens: [
@@ -166,6 +175,9 @@ module.exports = [
166175
},
167176
{
168177
// https://azu.github.io/morpheme-match/?text=必要(であると考えている)
178+
id: "dict4",
179+
disabled: false,
180+
allows: [],
169181
message: `"である$7と$5考えて$6いる"は冗長な表現です。"である$7" または "と$5考えて$6いる"を省き簡潔な表現にすると文章が明瞭になります。`,
170182
url: "http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html",
171183
expected: "である",
@@ -251,13 +263,23 @@ module.exports = [
251263
},
252264
{
253265
// https://azu.github.io/morpheme-match/?text=動作の(確認を行わなければ)ならない
266+
id: "dict5",
267+
disabled: false,
268+
allows: ["/^[ァ-ヶ]+を.?行う/", "/^[a-zA-Z]+を.?行う/"],
254269
message: `"$1を$5行う"は冗長な表現です。"$1する"など簡潔な表現にすると文章が明瞭になります。`,
270+
description: `[サ変名詞]とは「[名詞]する」というように「する」が後ろについた場合に、動詞の働きをする名詞です。
271+
272+
例)「行動(する)」、「プログラム(する)」
273+
274+
誤検知を防ぐためにデフォルトでは、「[カタカナ]を行う」と「[アルファベット]を行う」は"allows"で無視するように定義されています。
275+
`,
255276
url: "http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html",
256277
tokens: [
257278
{
258279
pos: "名詞",
259280
pos_detail_1: "サ変接続",
260-
_capture: "$1"
281+
_capture: "$1",
282+
_readme: "[サ変名詞]"
261283
},
262284
{
263285
surface_form: "を",
@@ -287,13 +309,23 @@ module.exports = [
287309
]
288310
},
289311
{
312+
id: "dict6",
313+
disabled: false,
314+
allows: ["/^[ァ-ヶ]+を.?実行/", "/^[a-zA-Z]+を.?実行/"],
290315
message: `"$1を$5実行"は冗長な表現です。"$1する"など簡潔な表現にすると文章が明瞭になります。`,
316+
description: `[サ変名詞]とは「[名詞]する」というように「する」が後ろについた場合に、動詞の働きをする名詞です。
317+
318+
例)「行動(する)」、「プログラム(する)」
319+
320+
誤検知を防ぐためにデフォルトでは、「[カタカナ]を実行」と「[アルファベット]を実行」は"allows"で無視するように定義されています。
321+
`,
291322
url: "http://www.atmarkit.co.jp/ait/articles/1001/19/news106_2.html",
292323
tokens: [
293324
{
294325
pos: "名詞",
295326
pos_detail_1: "サ変接続",
296-
_capture: "$1"
327+
_capture: "$1",
328+
_readme: "[サ変名詞]"
297329
},
298330
{
299331
surface_form: "を",

src/index.js

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@
22
"use strict";
33
import { wrapReportHandler } from "textlint-rule-helper";
44
import StringSource from "textlint-util-to-string";
5+
import { matchPatterns } from "@textlint/regexp-string-matcher";
56

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

11+
/**
12+
* textの中身をすべて置換する
13+
* @param {string} text
14+
* @param {string|undefined} from
15+
* @param {string} to
16+
* @returns {string}
17+
*/
1018
const replaceAll = (text, from, to) => {
1119
return text.split(from).join(to);
1220
};
@@ -17,6 +25,30 @@ const replaceTokenWith = (matcherToken, actualToken, specialTo) => {
1725
}
1826
return actualToken.surface_form;
1927
};
28+
29+
/**
30+
* tokensのsurface_formをつなげた文字列を返す
31+
* @param tokens
32+
* @returns {string}
33+
*/
34+
const tokensToString = tokens => {
35+
return tokens.map(token => token.surface_form).join("");
36+
};
37+
38+
/**
39+
* "allows" オプションで許可されているかどうか
40+
* @param {*[]} tokens
41+
* @param {string[]} allows
42+
*/
43+
const isTokensAllowed = (tokens, allows) => {
44+
if (allows.length === 0) {
45+
return false;
46+
}
47+
const matchedText = tokensToString(tokens);
48+
const allowsMatchResults = matchPatterns(matchedText, allows);
49+
return allowsMatchResults.length > 0;
50+
};
51+
2052
const createExpected = ({ text, matcherTokens, skipped, actualTokens }) => {
2153
let resultText = text;
2254
let actualTokenIndex = 0;
@@ -33,7 +65,7 @@ const createExpected = ({ text, matcherTokens, skipped, actualTokens }) => {
3365
});
3466
return resultText;
3567
};
36-
const createMessage = ({ text, matcherTokens, skipped, actualTokens }) => {
68+
const createMessage = ({ id, text, matcherTokens, skipped, actualTokens }) => {
3769
let resultText = text;
3870
let actualTokenIndex = 0;
3971
matcherTokens.forEach((token, index) => {
@@ -48,16 +80,25 @@ const createMessage = ({ text, matcherTokens, skipped, actualTokens }) => {
4880
}
4981
++actualTokenIndex;
5082
});
51-
return resultText;
83+
return `【${id}${resultText}
84+
解説: https://github.com/textlint-ja/textlint-rule-ja-no-redundant-expression#${id}`;
5285
};
5386

5487
const reporter = (context, options = {}) => {
5588
const { Syntax, RuleError, fixer } = context;
5689
const DefaultOptions = {
5790
// https://textlint.github.io/docs/txtnode.html#type
58-
allowNodeTypes: [Syntax.BlockQuote, Syntax.Link, Syntax.ReferenceDef]
91+
allowNodeTypes: [Syntax.BlockQuote, Syntax.Link, Syntax.ReferenceDef],
92+
dictOptions: {}
5993
};
60-
const matchAll = createMatchAll(dictionaryList);
94+
const dictOptions = options.dictOptions || DefaultOptions.dictOptions;
95+
// "disabled": trueな辞書は取り除く
96+
const enabledDictionaryList = dictionaryList.filter(dict => {
97+
const dictOption = dictOptions[dict.id] || {};
98+
const disabled = typeof dictOption.disabled === "boolean" ? dictOption.disabled : dict.disabled;
99+
return !disabled;
100+
});
101+
const matchAll = createMatchAll(enabledDictionaryList);
61102
const skipNodeTypes = options.allowNodeTypes || DefaultOptions.allowNodeTypes;
62103
return wrapReportHandler(
63104
context,
@@ -75,6 +116,14 @@ const reporter = (context, options = {}) => {
75116
*/
76117
const matchResults = matchAll(currentTokens);
77118
matchResults.forEach(matchResult => {
119+
const dictOption = dictOptions[matchResult.dict.id] || {};
120+
// "allows" オプションにマッチした場合はエラーを報告しない
121+
const allows = dictOption.allows || matchResult.dict.allows;
122+
const isAllowed = isTokensAllowed(matchResult.tokens, allows);
123+
if (isAllowed) {
124+
return;
125+
}
126+
// エラー報告
78127
const firstToken = matchResult.tokens[0];
79128
const lastToken = matchResult.tokens[matchResult.tokens.length - 1];
80129
const firstWordIndex = source.originalIndexFromIndex(
@@ -86,11 +135,12 @@ const reporter = (context, options = {}) => {
86135
// replace $1
87136
const message =
88137
createMessage({
138+
id: matchResult.dict.id,
89139
text: matchResult.dict.message,
90140
matcherTokens: matchResult.dict.tokens,
91141
skipped: matchResult.skipped,
92142
actualTokens: matchResult.tokens
93-
}) + (matchResult.dict.url ? `参考: ${matchResult.dict.url}` : "");
143+
});
94144
const expected = matchResult.dict.expected
95145
? createExpected({
96146
text: matchResult.dict.expected,

test/dictionary-test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// MIT © 2019 azu
2+
"use strict";
3+
import assert from "assert";
4+
5+
describe('dictionary', function() {
6+
it("should not have duplicated id", () => {
7+
const dictionary = require("../src/dictionary.js");
8+
dictionary.forEach(item => {
9+
assert.ok(typeof item.id === "string", "should have id property");
10+
const sameIdItems = dictionary.filter(target => target.id === item.id);
11+
assert.ok(sameIdItems.length === 1, "should not have duplicated id item");
12+
});
13+
});
14+
it("should have disabled default value", () => {
15+
const dictionary = require("../src/dictionary.js");
16+
dictionary.forEach(item => {
17+
assert.ok(typeof item.disabled === "boolean", `${item} should have disabled property`);
18+
});
19+
});
20+
it("should have allows default value", () => {
21+
const dictionary = require("../src/dictionary.js");
22+
dictionary.forEach(item => {
23+
assert.ok(Array.isArray(item.allows), `${item}: should have disabled property`);
24+
});
25+
});
26+
});

0 commit comments

Comments
 (0)