Skip to content

Commit 6d48bb8

Browse files
authored
Breaking Change: forceAppendPeriodオプションの追加 (#4)
- `forceAppendPeriod`: trueの場合は自動的に--fixで句点を追加する - デフォルトはOFFであるためBREAKING CHANGEとなる。 - 句点判定の処理をライブラリ化
1 parent 229d645 commit 6d48bb8

File tree

4 files changed

+136
-53
lines changed

4 files changed

+136
-53
lines changed

README.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,55 @@ Via CLI
3434
textlint --rule ja-no-mixed-period README.md
3535
```
3636

37+
## Examples
38+
39+
**OK**:
40+
41+
```
42+
これは問題ないです。
43+
末尾に感嘆符はある!
44+
「これはセリフ」
45+
english only
46+
- 箇条書きは無視される
47+
```
48+
49+
**NG**:
50+
51+
```
52+
これは句点がありません
53+
末尾にスペースがある。
54+
絵文字が末尾にある。😆
55+
```
56+
3757
## Options
3858

39-
- `periodMark`(string):
59+
- `periodMark`: `string`:
4060
- 文末に使用する句点文字
4161
- デフォルト: "。"
42-
62+
- `allowPeriodMarks`: `string[]`
63+
- 句点文字として許可する文字列の配列
64+
- 例外として許可したい文字列を設定する
65+
- `periodMark`に指定したものは自動的に許可リストに加わる
66+
- デフォルトは空 `[]`
4367
- `allowEmojiAtEnd`(bool):
4468
- 絵文字を末尾に置くことを許可するかどうか
4569
- デフォルト: false
70+
- `forceAppendPeriod`: `boolean`
71+
- 句点で終わって無い場合に`periodMark`を--fix時に追加するかどうか
72+
- デフォルト: false
4673

4774
```json
4875
{
4976
"rules": {
5077
"ja-no-mixed-period": {
51-
"periodMark": ""
78+
// 優先する句点文字
79+
"periodMark": "",
80+
// 句点文字として許可する文字列の配列
81+
"allowPeriodMarks": [],
82+
// 末尾に絵文字を置くことを許可するか
83+
"allowEmojiAtEnd": false,
84+
// 句点で終わって無い場合に`periodMark`を--fix時に追加するかどうか
85+
"forceAppendPeriod": false
5286
}
5387
}
5488
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"textlint-tester": "^2.0.0"
4343
},
4444
"dependencies": {
45-
"emoji-regex": "^6.1.0",
45+
"check-ends-with-period": "^1.0.1",
4646
"textlint-rule-helper": "^2.0.0"
4747
}
4848
}

src/textlint-rule-ja-no-mixed-period.js

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,44 @@
11
// LICENSE : MIT
22
"use strict";
33
const RuleHelper = require("textlint-rule-helper").RuleHelper;
4-
const emojiRegExp = require("emoji-regex")();
54
const japaneseRegExp = /(?:[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]|[\uD840-\uD87F][\uDC00-\uDFFF]|[--])/;
6-
const exceptionMarkRegExp = /[!?\)]/;
7-
const defaultPeriodMark = /[\.]/;
5+
/***
6+
* 典型的な句点のパターン
7+
* これは`periodMark`と交換しても違和感がないものを登録
8+
* @type {RegExp}
9+
*/
10+
const classicPeriodMarkPattern = /[\.]/;
11+
const checkEndsWithPeriod = require("check-ends-with-period");
812
const defaultOptions = {
913
// 優先する句点文字
1014
periodMark: "。",
15+
// 句点文字として許可する文字列の配列
16+
// 例外として許可したい文字列を設定する
17+
// `periodMark`に指定したものは自動的に許可リストに加わる
18+
allowPeriodMarks: [],
1119
// 末尾に絵文字を置くことを許可するか
12-
allowEmojiAtEnd: false
20+
allowEmojiAtEnd: false,
21+
// 句点で終わって無い場合に`periodMark`を--fix時に追加するかどうか
22+
// デフォルトでは自動的に追加しない
23+
forceAppendPeriod: false
1324
};
1425
const reporter = (context, options = {}) => {
15-
const {Syntax, RuleError, report, fixer, getSource} = context;
26+
const { Syntax, RuleError, report, fixer, getSource } = context;
1627
const helper = new RuleHelper(context);
17-
const periodMark = options.periodMark || defaultOptions.periodMark;
18-
const allowEmojiAtEnd = options.allowEmojiAtEnd !== undefined ? options.allowEmojiAtEnd : defaultOptions.allowEmojiAtEnd;
19-
const ignoredNodeTypes = [Syntax.ListItem, Syntax.Link, Syntax.Code, Syntax.Image, Syntax.BlockQuote, Syntax.Emphasis];
28+
// 優先する句点記号
29+
const preferPeriodMark = options.periodMark || defaultOptions.periodMark;
30+
// 優先する句点記号は常に句点として許可される
31+
const allowPeriodMarks = (options.allowPeriodMarks || defaultOptions.allowPeriodMarks).concat(preferPeriodMark);
32+
const allowEmojiAtEnd = options.allowEmojiAtEnd !== undefined
33+
? options.allowEmojiAtEnd
34+
: defaultOptions.allowEmojiAtEnd;
35+
const forceAppendPeriod = options.forceAppendPeriod !== undefined
36+
? options.forceAppendPeriod
37+
: defaultOptions.forceAppendPeriod;
38+
39+
const ignoredNodeTypes = [
40+
Syntax.ListItem, Syntax.Link, Syntax.Code, Syntax.Image, Syntax.BlockQuote, Syntax.Emphasis
41+
];
2042
return {
2143
[Syntax.Paragraph](node){
2244
if (helper.isChildNode(node, ignoredNodeTypes)) {
@@ -27,52 +49,50 @@ const reporter = (context, options = {}) => {
2749
return;
2850
}
2951
const lastStrText = getSource(lastNode);
52+
if (lastStrText.length === 0) {
53+
return;
54+
}
3055
// 日本語が含まれていない文章は無視する
3156
if (!japaneseRegExp.test(lastStrText)) {
3257
return;
3358
}
34-
// サロゲートペアを考慮した文字列長・文字アクセス
35-
const characters = [...lastStrText];
36-
const lastIndex = characters.length - 1;
37-
const lastChar = characters[lastIndex];
38-
if (lastChar === undefined) {
59+
const { valid, periodMark, index } = checkEndsWithPeriod(lastStrText, {
60+
periodMarks: allowPeriodMarks,
61+
allowEmoji: allowEmojiAtEnd
62+
});
63+
// 問題が無い場合は何もしない
64+
if (valid) {
3965
return;
4066
}
41-
// 文末がスペースである場合
42-
// TODO: fixに対応したい
43-
if (/\s/.test(lastChar)) {
44-
report(lastNode, new RuleError(`文末が"${periodMark}"で終わっていません。末尾に不要なスペースがあります。`, {
45-
index: lastIndex
67+
// 文末がスペースである場合はスペースを削除する
68+
if (/\s/.test(periodMark)) {
69+
report(lastNode, new RuleError(`文末が"${preferPeriodMark}"で終わっていません。末尾に不要なスペースがあります。`, {
70+
index,
71+
fix: fixer.replaceTextRange([index, index + periodMark.length], "")
4672
}));
4773
return
4874
}
49-
// 末尾の"文字"が句点以外で末尾に使われる文字であるときは無視する
50-
// 例外: 感嘆符
51-
// 例外: 「」 () ()『』
52-
// http://ncode.syosetu.com/n8977bb/12/
53-
// https://ja.wikipedia.org/wiki/%E7%B5%82%E6%AD%A2%E7%AC%A6
54-
if (exceptionMarkRegExp.test(lastChar)) {
55-
return;
56-
}
57-
if (allowEmojiAtEnd && emojiRegExp.test(lastChar)) {
58-
return;
59-
}
60-
if (lastChar === periodMark) {
61-
return;
62-
}
63-
// "." であるなら "。"に変換
64-
// そうでない場合は末尾に"。"を追加する
65-
let fix = null;
66-
if (defaultPeriodMark.test(lastChar)) {
67-
fix = fixer.replaceTextRange([lastIndex, lastIndex + 1], periodMark);
75+
// 典型的なパターンは自動的に`preferPeriodMark`に置き換える
76+
// 例) "." であるなら "。"に変換
77+
if (classicPeriodMarkPattern.test(periodMark)) {
78+
report(lastNode, new RuleError(`文末が"${preferPeriodMark}"で終わっていません。`, {
79+
index: index,
80+
fix: fixer.replaceTextRange([index, index + preferPeriodMark.length], preferPeriodMark)
81+
}));
6882
} else {
69-
fix = fixer.replaceTextRange([lastIndex + 1, lastIndex + 1], periodMark);
83+
// 句点を忘れているパターン
84+
if (forceAppendPeriod) {
85+
// `forceAppendPeriod`のオプションがtrueならば、自動で句点を追加する。
86+
report(lastNode, new RuleError(`文末が"${preferPeriodMark}"で終わっていません。`, {
87+
index: index,
88+
fix: fixer.replaceTextRange([index + 1, index + 1], preferPeriodMark)
89+
}));
90+
} else {
91+
report(lastNode, new RuleError(`文末が"${preferPeriodMark}"で終わっていません。`, {
92+
index: index
93+
}));
94+
}
7095
}
71-
report(lastNode, new RuleError(`文末が"${periodMark}"で終わっていません。`, {
72-
index: lastIndex,
73-
fix
74-
}));
75-
7696
}
7797
}
7898
};

test/textlint-rule-ja-no-mixed-period-test.js

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ tester.run("textlint-rule-ja-no-mixed-period", rule, {
88
"1行目。 \nHard Breakを入れるパターン。",
99
"1行目 空白はあるけど末尾に句点はある。",
1010
// 例外: 感嘆符などが末尾にある場合は問題なし
11+
"「これはセリフ」",
1112
"末尾に句点はある!",
1213
"english only",
1314
// 例外のNode type
@@ -18,17 +19,26 @@ tester.run("textlint-rule-ja-no-mixed-period", rule, {
1819
`__強調表示も同じく__`,
1920
`> 引用も無視される`,
2021
{
21-
text: "絵文字が末尾にある。😆",
22-
options: {
23-
allowEmojiAtEnd: true
24-
},
22+
text: "絵文字が末尾にある。😆",
23+
options: {
24+
allowEmojiAtEnd: true
25+
},
2526
},
27+
{
28+
text: "これはOK",
29+
options: {
30+
allowPeriodMarks: ["OK"]
31+
},
32+
}
2633
],
2734
invalid: [
2835
// single match
2936
{
3037
text: "これは句点がありません",
3138
output: "これは句点がありません。",
39+
options: {
40+
forceAppendPeriod: true
41+
},
3242
errors: [
3343
{
3444
message: `文末が"。"で終わっていません。`,
@@ -41,6 +51,9 @@ tester.run("textlint-rule-ja-no-mixed-period", rule, {
4151
{
4252
text: "これは句点がありません\n\nこれは句点がありません",
4353
output: "これは句点がありません。\n\nこれは句点がありません。",
54+
options: {
55+
forceAppendPeriod: true
56+
},
4457
errors: [
4558
{
4659
message: `文末が"。"で終わっていません。`,
@@ -56,18 +69,33 @@ tester.run("textlint-rule-ja-no-mixed-period", rule, {
5669
},
5770
{
5871
text: "末尾にスペースがある。 ",
72+
output: "末尾にスペースがある。",
5973
errors: [
6074
{
6175
message: `文末が"。"で終わっていません。末尾に不要なスペースがあります。`,
6276
line: 1,
63-
column: 12
77+
column: 12 // space
78+
}
79+
]
80+
},
81+
{
82+
text: "末尾にスペースがある。 ",
83+
output: "末尾にスペースがある。",
84+
errors: [
85+
{
86+
message: `文末が"。"で終わっていません。末尾に不要なスペースがあります。`,
87+
line: 1,
88+
column: 12 // space の開始位置
6489
}
6590
]
6691
},
6792
// multiple hit items in a line
6893
{
6994
text: "これは句点がありません、これは句点がありません",
7095
output: "これは句点がありません、これは句点がありません。",
96+
options: {
97+
forceAppendPeriod: true
98+
},
7199
errors: [
72100
{
73101
message: `文末が"。"で終わっていません。`,
@@ -81,7 +109,8 @@ tester.run("textlint-rule-ja-no-mixed-period", rule, {
81109
text: "これはダメ",
82110
output: "これはダメ.",
83111
options: {
84-
periodMark: "."
112+
periodMark: ".",
113+
forceAppendPeriod: true
85114
},
86115
errors: [
87116
{

0 commit comments

Comments
 (0)