Skip to content

Commit 76da132

Browse files
authored
feat(ja-space-between-half-and-full-width): add allows option (#64)
* feat(ja-space-between-half-and-full-width): add `allows` option * test: add test case
1 parent ef7480d commit 76da132

File tree

5 files changed

+150
-66
lines changed

5 files changed

+150
-66
lines changed

packages/textlint-rule-ja-space-between-half-and-full-width/README.md

+15-4
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,17 @@ textlint --rule ja-space-between-half-and-full-width README.md
4949
- 対象としたい物のみ指定する
5050
- 例えば、数値と句読点(、。)を例外として扱いたい場合は以下
5151
- `["alphabets"]`
52-
- (非推奨)`exceptPunctuation`: `boolean`
53-
- デフォルト: `true`
54-
- 句読点(、。)を例外として扱うかどうか
55-
- 代わりに `space` オプションを用いて `["alphabets", "numbers"]` と指定する
5652
- `lintStyledNode`: `boolean`
5753
- デフォルト: `false`
5854
- プレーンテキスト以外(リンクや画像のキャプションなど)を lint の対象とするかどうか (プレーンテキストの判断基準は [textlint/textlint-rule-helper: This is helper library for creating textlint rule](https://github.com/textlint/textlint-rule-helper#rulehelperisplainstrnodenode-boolean) を参照してください)
55+
- `allows: string[]`
56+
- デフォルト: `[]`
57+
- 例外として扱う文字列の配列
58+
- [RegExp-like String](https://github.com/textlint/regexp-string-matcher?tab=readme-ov-file#regexp-like-string)も指定可能
59+
- (非推奨)`exceptPunctuation`: `boolean`
60+
- デフォルト: `true`
61+
- 句読点(、。)を例外として扱うかどうか
62+
- 代わりに `space` オプションを用いて `["alphabets", "numbers"]` と指定する
5963

6064
```json
6165
{
@@ -83,6 +87,13 @@ textlint --rule ja-space-between-half-and-full-width README.md
8387
space: []
8488
}
8589

90+
スペースは必須だが、`Eコーマス`だけはスペースなしを許可する。
91+
92+
text: "例外的にEコーマスはスペースなしでも通す",
93+
options: {
94+
space: "always",
95+
allows: ["Eコーマス"]
96+
}
8697

8798
## Changelog
8899

packages/textlint-rule-ja-space-between-half-and-full-width/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"textlintrule"
3030
],
3131
"devDependencies": {
32+
"@textlint/regexp-string-matcher": "^2.0.2",
3233
"textlint-scripts": "^13.3.3"
3334
},
3435
"dependencies": {

packages/textlint-rule-ja-space-between-half-and-full-width/src/index.js

+80-61
Original file line numberDiff line numberDiff line change
@@ -4,68 +4,79 @@ const assert = require("assert");
44
/*
55
全角文字と半角文字の間にスペースを入れるかどうか
66
*/
7-
import {RuleHelper} from "textlint-rule-helper";
8-
import {matchCaptureGroupAll} from "match-index";
7+
import { RuleHelper } from "textlint-rule-helper";
8+
import { matchCaptureGroupAll } from "match-index";
9+
import { matchPatterns } from "@textlint/regexp-string-matcher";
10+
911
const PunctuationRegExp = /[]/;
1012
const ZenRegExpStr = '[、。]|[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]|[\uD840-\uD87F][\uDC00-\uDFFF]|[ぁ-んァ-ヶ]';
1113
const defaultSpaceOptions = {
1214
alphabets: false,
1315
numbers: false,
14-
punctuation: false
16+
punctuation: false,
1517
};
1618
const defaultOptions = {
1719
// プレーンテキスト以外を対象とするか See https://github.com/textlint/textlint-rule-helper#rulehelperisplainstrnodenode-boolean
1820
lintStyledNode: false,
21+
/**
22+
* 例外として無視する文字列
23+
* RegExp-like Stringの配列を指定
24+
* https://github.com/textlint/regexp-string-matcher?tab=readme-ov-file#regexp-like-string
25+
*/
26+
allows: []
1927
};
28+
2029
function reporter(context, options = {}) {
21-
/**
22-
* 入力された `space` オプションを内部処理用に成形する
23-
* @param {string|Array|undefined} opt `space` オプションのインプット
24-
* @param {boolean|undefined} exceptPunctuation `exceptPunctuation` オプションのインプット
25-
* @returns {Object}
26-
*/
30+
/**
31+
* 入力された `space` オプションを内部処理用に成形する
32+
* @param {string|Array|undefined} opt `space` オプションのインプット
33+
* @param {boolean|undefined} exceptPunctuation `exceptPunctuation` オプションのインプット
34+
* @returns {Object}
35+
*/
2736
const parseSpaceOption = (opt, exceptPunctuation) => {
28-
if (typeof opt === 'string') {
29-
assert(opt === "always" || opt === "never", `"space" options should be "always", "never" or an array.`);
30-
31-
if (opt === "always") {
32-
if (exceptPunctuation === false) {
33-
return {...defaultSpaceOptions, alphabets: true, numbers: true, punctuation: true};
34-
} else {
35-
return {...defaultSpaceOptions, alphabets: true, numbers: true};
36-
}
37-
} else if (opt === "never") {
38-
if (exceptPunctuation === false) {
39-
return {...defaultSpaceOptions, punctuation: true};
40-
} else {
41-
return defaultSpaceOptions;
42-
}
37+
if (typeof opt === 'string') {
38+
assert(opt === "always" || opt === "never", `"space" options should be "always", "never" or an array.`);
39+
40+
if (opt === "always") {
41+
if (exceptPunctuation === false) {
42+
return { ...defaultSpaceOptions, alphabets: true, numbers: true, punctuation: true };
43+
} else {
44+
return { ...defaultSpaceOptions, alphabets: true, numbers: true };
45+
}
46+
} else if (opt === "never") {
47+
if (exceptPunctuation === false) {
48+
return { ...defaultSpaceOptions, punctuation: true };
49+
} else {
50+
return defaultSpaceOptions;
51+
}
52+
}
53+
} else if (Array.isArray(opt)) {
54+
assert(
55+
opt.every((v) => Object.keys(defaultSpaceOptions).includes(v)),
56+
`Only "alphabets", "numbers", "punctuation" can be included in the array.`
57+
);
58+
const userOptions = Object.fromEntries(opt.map(key => [key, true]));
59+
return { ...defaultSpaceOptions, ...userOptions };
4360
}
44-
} else if (Array.isArray(opt)) {
45-
assert(
46-
opt.every((v) => Object.keys(defaultSpaceOptions).includes(v)),
47-
`Only "alphabets", "numbers", "punctuation" can be included in the array.`
48-
);
49-
const userOptions = Object.fromEntries(opt.map(key => [key, true]));
50-
return {...defaultSpaceOptions, ...userOptions};
51-
}
52-
53-
return defaultSpaceOptions;
61+
62+
return defaultSpaceOptions;
5463
}
55-
56-
const {Syntax, RuleError, report, fixer, getSource} = context;
64+
65+
const { Syntax, RuleError, report, fixer, getSource } = context;
5766
const helper = new RuleHelper();
5867
const spaceOption = parseSpaceOption(options.space, options.exceptPunctuation);
5968
const lintStyledNode = options.lintStyledNode !== undefined
6069
? options.lintStyledNode
6170
: defaultOptions.lintStyledNode;
71+
const allows = options.allows !== undefined ? options.allows : defaultOptions.allows;
6272
/**
6373
* `text`を対象に例外オプションを取り除くfilter関数を返す
6474
* @param {string} text テスト対象のテキスト全体
6575
* @param {number} padding +1 or -1
6676
* @returns {function(*, *)}
6777
*/
6878
const createFilter = (text, padding) => {
79+
const allowedPatterns = allows.length > 0 ? matchPatterns(text, allows) : [];
6980
/**
7081
* `PunctuationRegExp`で指定された例外を取り除く
7182
* @param {Object} match
@@ -79,15 +90,22 @@ function reporter(context, options = {}) {
7990
if (!spaceOption.punctuation && PunctuationRegExp.test(targetChar)) {
8091
return false;
8192
}
82-
return true;
93+
const isAllowed = allowedPatterns.some((allow) => {
94+
// start ... end
95+
if (allow.startIndex <= match.index && match.index <= allow.endIndex) {
96+
return true;
97+
}
98+
return false
99+
})
100+
return !isAllowed;
83101
}
84102
};
85103
// Never: アルファベットと全角の間はスペースを入れない
86104
const noSpaceBetween = (node, text) => {
87105
const betweenHanAndZen = matchCaptureGroupAll(text, new RegExp(`[A-Za-z0-9]([  ])(?:${ZenRegExpStr})`));
88106
const betweenZenAndHan = matchCaptureGroupAll(text, new RegExp(`(?:${ZenRegExpStr})([  ])[A-Za-z0-9]`));
89107
const reportMatch = (match) => {
90-
const {index} = match;
108+
const { index } = match;
91109
report(node, new RuleError("原則として、全角文字と半角文字の間にスペースを入れません。", {
92110
index: match.index,
93111
fix: fixer.replaceTextRange([index, index + 1], "")
@@ -96,37 +114,37 @@ function reporter(context, options = {}) {
96114
betweenHanAndZen.filter(createFilter(text, 1)).forEach(reportMatch);
97115
betweenZenAndHan.filter(createFilter(text, -1)).forEach(reportMatch);
98116
};
99-
117+
100118
// Always: アルファベットと全角の間はスペースを入れる
101119
const needSpaceBetween = (node, text, options) => {
102-
/**
103-
* オプションを元に正規表現オプジェクトを生成する
104-
* @param {Array} opt `space` オプション
105-
* @param {boolean} btwHanAndZen=true 半角全角の間か全角半角の間か
106-
* @returns {Object}
107-
*/
120+
/**
121+
* オプションを元に正規表現オプジェクトを生成する
122+
* @param {Array} opt `space` オプション
123+
* @param {boolean} btwHanAndZen=true 半角全角の間か全角半角の間か
124+
* @returns {Object}
125+
*/
108126
const generateRegExp = (opt, btwHanAndZen = true) => {
109-
const alphabets = opt.alphabets ? 'A-Za-z' : '';
110-
const numbers = opt.numbers ? '0-9' : '';
111-
112-
let expStr;
113-
if (btwHanAndZen) {
114-
expStr = `([${alphabets}${numbers}])(?:${ZenRegExpStr})`;
115-
} else {
116-
expStr = `(${ZenRegExpStr})[${alphabets}${numbers}]`;
117-
}
118-
119-
return new RegExp(expStr);
127+
const alphabets = opt.alphabets ? 'A-Za-z' : '';
128+
const numbers = opt.numbers ? '0-9' : '';
129+
130+
let expStr;
131+
if (btwHanAndZen) {
132+
expStr = `([${alphabets}${numbers}])(?:${ZenRegExpStr})`;
133+
} else {
134+
expStr = `(${ZenRegExpStr})[${alphabets}${numbers}]`;
135+
}
136+
137+
return new RegExp(expStr);
120138
};
121-
139+
122140
const betweenHanAndZenRegExp = generateRegExp(options);
123141
const betweenZenAndHanRegExp = generateRegExp(options, false);
124142
const errorMsg = '原則として、全角文字と半角文字の間にスペースを入れます。';
125-
143+
126144
const betweenHanAndZen = matchCaptureGroupAll(text, betweenHanAndZenRegExp);
127145
const betweenZenAndHan = matchCaptureGroupAll(text, betweenZenAndHanRegExp);
128146
const reportMatch = (match) => {
129-
const {index} = match;
147+
const { index } = match;
130148
report(node, new RuleError(errorMsg, {
131149
index: match.index,
132150
fix: fixer.replaceTextRange([index + 1, index + 1], " ")
@@ -136,12 +154,12 @@ function reporter(context, options = {}) {
136154
betweenZenAndHan.filter(createFilter(text, 0)).forEach(reportMatch);
137155
};
138156
return {
139-
[Syntax.Str](node){
157+
[Syntax.Str](node) {
140158
if (!lintStyledNode && !helper.isPlainStrNode(node)) {
141159
return;
142160
}
143161
const text = getSource(node);
144-
162+
145163
const noSpace = (key) => key === 'punctuation' ? true : !spaceOption[key];
146164
if (Object.keys(spaceOption).every(noSpace)) {
147165
noSpaceBetween(node, text);
@@ -151,6 +169,7 @@ function reporter(context, options = {}) {
151169
}
152170
}
153171
}
172+
154173
module.exports = {
155174
linter: reporter,
156175
fixer: reporter

packages/textlint-rule-ja-space-between-half-and-full-width/test/index-test.js

+29-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,35 @@ Pull Request、コミットのやりかたなどが書かれています。`,
125125
options: {
126126
space: ["alphabets", "punctuation"]
127127
}
128-
}
128+
},
129+
// allows,
130+
{
131+
text: "Eコーマス",
132+
options: {
133+
space: "always",
134+
allows: [
135+
"Eコーマス"
136+
]
137+
}
138+
},
139+
{
140+
text: "これは A言語、B言語、C言語です。",
141+
options: {
142+
space: "always",
143+
allows: [
144+
"/(\\w)言語/"
145+
]
146+
}
147+
},
148+
{
149+
text: "E コーマス",
150+
options: {
151+
space: "never",
152+
allows: [
153+
"E コーマス"
154+
]
155+
}
156+
},
129157
],
130158
invalid: [
131159
{

yarn.lock

+25
Original file line numberDiff line numberDiff line change
@@ -1659,6 +1659,16 @@
16591659
resolved "https://registry.yarnpkg.com/@textlint/module-interop/-/module-interop-13.3.3.tgz#645b47b9e951030b2d656e2c9266b5587de2a17b"
16601660
integrity sha512-CwfVpRGAxbkhGY9vLLU06Q/dy/RMNnyzbmt6IS2WIyxqxvGaF7QZtFYpKEEm63aemVyUvzQ7WM3yVOoUg6P92w==
16611661

1662+
"@textlint/regexp-string-matcher@^2.0.2":
1663+
version "2.0.2"
1664+
resolved "https://registry.yarnpkg.com/@textlint/regexp-string-matcher/-/regexp-string-matcher-2.0.2.tgz#cef4d8353dac624086069e290d9631ca285df34d"
1665+
integrity sha512-OXLD9XRxMhd3S0LWuPHpiARQOI7z9tCOs0FsynccW2lmyZzHHFJ9/eR6kuK9xF459Qf+740qI5h+/0cx+NljzA==
1666+
dependencies:
1667+
escape-string-regexp "^4.0.0"
1668+
lodash.sortby "^4.7.0"
1669+
lodash.uniq "^4.5.0"
1670+
lodash.uniqwith "^4.5.0"
1671+
16621672
"@textlint/runtime-helper@^0.16.0":
16631673
version "0.16.0"
16641674
resolved "https://registry.yarnpkg.com/@textlint/runtime-helper/-/runtime-helper-0.16.0.tgz#b59967ac861cc873bf3e9cd69739f9bf098a2534"
@@ -4656,11 +4666,26 @@ lodash.ismatch@^4.4.0:
46564666
resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37"
46574667
integrity sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==
46584668

4669+
lodash.sortby@^4.7.0:
4670+
version "4.7.0"
4671+
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
4672+
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
4673+
46594674
lodash.truncate@^4.4.2:
46604675
version "4.4.2"
46614676
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
46624677
integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==
46634678

4679+
lodash.uniq@^4.5.0:
4680+
version "4.5.0"
4681+
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
4682+
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
4683+
4684+
lodash.uniqwith@^4.5.0:
4685+
version "4.5.0"
4686+
resolved "https://registry.yarnpkg.com/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz#7a0cbf65f43b5928625a9d4d0dc54b18cadc7ef3"
4687+
integrity sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q==
4688+
46644689
lodash@^4.17.21:
46654690
version "4.17.21"
46664691
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"

0 commit comments

Comments
 (0)