Skip to content

Commit

Permalink
feat(ja-space-between-half-and-full-width): add allows option (#64)
Browse files Browse the repository at this point in the history
* feat(ja-space-between-half-and-full-width): add `allows` option

* test: add test case
  • Loading branch information
azu authored Apr 20, 2024
1 parent ef7480d commit 76da132
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,17 @@ textlint --rule ja-space-between-half-and-full-width README.md
- 対象としたい物のみ指定する
- 例えば、数値と句読点(、。)を例外として扱いたい場合は以下
- `["alphabets"]`
- (非推奨)`exceptPunctuation`: `boolean`
- デフォルト: `true`
- 句読点(、。)を例外として扱うかどうか
- 代わりに `space` オプションを用いて `["alphabets", "numbers"]` と指定する
- `lintStyledNode`: `boolean`
- デフォルト: `false`
- プレーンテキスト以外(リンクや画像のキャプションなど)を lint の対象とするかどうか (プレーンテキストの判断基準は [textlint/textlint-rule-helper: This is helper library for creating textlint rule](https://github.com/textlint/textlint-rule-helper#rulehelperisplainstrnodenode-boolean) を参照してください)
- `allows: string[]`
- デフォルト: `[]`
- 例外として扱う文字列の配列
- [RegExp-like String](https://github.com/textlint/regexp-string-matcher?tab=readme-ov-file#regexp-like-string)も指定可能
- (非推奨)`exceptPunctuation`: `boolean`
- デフォルト: `true`
- 句読点(、。)を例外として扱うかどうか
- 代わりに `space` オプションを用いて `["alphabets", "numbers"]` と指定する

```json
{
Expand Down Expand Up @@ -83,6 +87,13 @@ textlint --rule ja-space-between-half-and-full-width README.md
space: []
}

スペースは必須だが、`Eコーマス`だけはスペースなしを許可する。

text: "例外的にEコーマスはスペースなしでも通す",
options: {
space: "always",
allows: ["Eコーマス"]
}

## Changelog

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"textlintrule"
],
"devDependencies": {
"@textlint/regexp-string-matcher": "^2.0.2",
"textlint-scripts": "^13.3.3"
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,68 +4,79 @@ const assert = require("assert");
/*
全角文字と半角文字の間にスペースを入れるかどうか
*/
import {RuleHelper} from "textlint-rule-helper";
import {matchCaptureGroupAll} from "match-index";
import { RuleHelper } from "textlint-rule-helper";
import { matchCaptureGroupAll } from "match-index";
import { matchPatterns } from "@textlint/regexp-string-matcher";

const PunctuationRegExp = /[。、]/;
const ZenRegExpStr = '[、。]|[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]|[\uD840-\uD87F][\uDC00-\uDFFF]|[ぁ-んァ-ヶ]';
const defaultSpaceOptions = {
alphabets: false,
numbers: false,
punctuation: false
punctuation: false,
};
const defaultOptions = {
// プレーンテキスト以外を対象とするか See https://github.com/textlint/textlint-rule-helper#rulehelperisplainstrnodenode-boolean
lintStyledNode: false,
/**
* 例外として無視する文字列
* RegExp-like Stringの配列を指定
* https://github.com/textlint/regexp-string-matcher?tab=readme-ov-file#regexp-like-string
*/
allows: []
};

function reporter(context, options = {}) {
/**
* 入力された `space` オプションを内部処理用に成形する
* @param {string|Array|undefined} opt `space` オプションのインプット
* @param {boolean|undefined} exceptPunctuation `exceptPunctuation` オプションのインプット
* @returns {Object}
*/
/**
* 入力された `space` オプションを内部処理用に成形する
* @param {string|Array|undefined} opt `space` オプションのインプット
* @param {boolean|undefined} exceptPunctuation `exceptPunctuation` オプションのインプット
* @returns {Object}
*/
const parseSpaceOption = (opt, exceptPunctuation) => {
if (typeof opt === 'string') {
assert(opt === "always" || opt === "never", `"space" options should be "always", "never" or an array.`);

if (opt === "always") {
if (exceptPunctuation === false) {
return {...defaultSpaceOptions, alphabets: true, numbers: true, punctuation: true};
} else {
return {...defaultSpaceOptions, alphabets: true, numbers: true};
}
} else if (opt === "never") {
if (exceptPunctuation === false) {
return {...defaultSpaceOptions, punctuation: true};
} else {
return defaultSpaceOptions;
}
if (typeof opt === 'string') {
assert(opt === "always" || opt === "never", `"space" options should be "always", "never" or an array.`);

if (opt === "always") {
if (exceptPunctuation === false) {
return { ...defaultSpaceOptions, alphabets: true, numbers: true, punctuation: true };
} else {
return { ...defaultSpaceOptions, alphabets: true, numbers: true };
}
} else if (opt === "never") {
if (exceptPunctuation === false) {
return { ...defaultSpaceOptions, punctuation: true };
} else {
return defaultSpaceOptions;
}
}
} else if (Array.isArray(opt)) {
assert(
opt.every((v) => Object.keys(defaultSpaceOptions).includes(v)),
`Only "alphabets", "numbers", "punctuation" can be included in the array.`
);
const userOptions = Object.fromEntries(opt.map(key => [key, true]));
return { ...defaultSpaceOptions, ...userOptions };
}
} else if (Array.isArray(opt)) {
assert(
opt.every((v) => Object.keys(defaultSpaceOptions).includes(v)),
`Only "alphabets", "numbers", "punctuation" can be included in the array.`
);
const userOptions = Object.fromEntries(opt.map(key => [key, true]));
return {...defaultSpaceOptions, ...userOptions};
}

return defaultSpaceOptions;

return defaultSpaceOptions;
}

const {Syntax, RuleError, report, fixer, getSource} = context;
const { Syntax, RuleError, report, fixer, getSource } = context;
const helper = new RuleHelper();
const spaceOption = parseSpaceOption(options.space, options.exceptPunctuation);
const lintStyledNode = options.lintStyledNode !== undefined
? options.lintStyledNode
: defaultOptions.lintStyledNode;
const allows = options.allows !== undefined ? options.allows : defaultOptions.allows;
/**
* `text`を対象に例外オプションを取り除くfilter関数を返す
* @param {string} text テスト対象のテキスト全体
* @param {number} padding +1 or -1
* @returns {function(*, *)}
*/
const createFilter = (text, padding) => {
const allowedPatterns = allows.length > 0 ? matchPatterns(text, allows) : [];
/**
* `PunctuationRegExp`で指定された例外を取り除く
* @param {Object} match
Expand All @@ -79,15 +90,22 @@ function reporter(context, options = {}) {
if (!spaceOption.punctuation && PunctuationRegExp.test(targetChar)) {
return false;
}
return true;
const isAllowed = allowedPatterns.some((allow) => {
// start ... end
if (allow.startIndex <= match.index && match.index <= allow.endIndex) {
return true;
}
return false
})
return !isAllowed;
}
};
// Never: アルファベットと全角の間はスペースを入れない
const noSpaceBetween = (node, text) => {
const betweenHanAndZen = matchCaptureGroupAll(text, new RegExp(`[A-Za-z0-9]([  ])(?:${ZenRegExpStr})`));
const betweenZenAndHan = matchCaptureGroupAll(text, new RegExp(`(?:${ZenRegExpStr})([  ])[A-Za-z0-9]`));
const reportMatch = (match) => {
const {index} = match;
const { index } = match;
report(node, new RuleError("原則として、全角文字と半角文字の間にスペースを入れません。", {
index: match.index,
fix: fixer.replaceTextRange([index, index + 1], "")
Expand All @@ -96,37 +114,37 @@ function reporter(context, options = {}) {
betweenHanAndZen.filter(createFilter(text, 1)).forEach(reportMatch);
betweenZenAndHan.filter(createFilter(text, -1)).forEach(reportMatch);
};

// Always: アルファベットと全角の間はスペースを入れる
const needSpaceBetween = (node, text, options) => {
/**
* オプションを元に正規表現オプジェクトを生成する
* @param {Array} opt `space` オプション
* @param {boolean} btwHanAndZen=true 半角全角の間か全角半角の間か
* @returns {Object}
*/
/**
* オプションを元に正規表現オプジェクトを生成する
* @param {Array} opt `space` オプション
* @param {boolean} btwHanAndZen=true 半角全角の間か全角半角の間か
* @returns {Object}
*/
const generateRegExp = (opt, btwHanAndZen = true) => {
const alphabets = opt.alphabets ? 'A-Za-z' : '';
const numbers = opt.numbers ? '0-9' : '';

let expStr;
if (btwHanAndZen) {
expStr = `([${alphabets}${numbers}])(?:${ZenRegExpStr})`;
} else {
expStr = `(${ZenRegExpStr})[${alphabets}${numbers}]`;
}

return new RegExp(expStr);
const alphabets = opt.alphabets ? 'A-Za-z' : '';
const numbers = opt.numbers ? '0-9' : '';
let expStr;
if (btwHanAndZen) {
expStr = `([${alphabets}${numbers}])(?:${ZenRegExpStr})`;
} else {
expStr = `(${ZenRegExpStr})[${alphabets}${numbers}]`;
}
return new RegExp(expStr);
};

const betweenHanAndZenRegExp = generateRegExp(options);
const betweenZenAndHanRegExp = generateRegExp(options, false);
const errorMsg = '原則として、全角文字と半角文字の間にスペースを入れます。';

const betweenHanAndZen = matchCaptureGroupAll(text, betweenHanAndZenRegExp);
const betweenZenAndHan = matchCaptureGroupAll(text, betweenZenAndHanRegExp);
const reportMatch = (match) => {
const {index} = match;
const { index } = match;
report(node, new RuleError(errorMsg, {
index: match.index,
fix: fixer.replaceTextRange([index + 1, index + 1], " ")
Expand All @@ -136,12 +154,12 @@ function reporter(context, options = {}) {
betweenZenAndHan.filter(createFilter(text, 0)).forEach(reportMatch);
};
return {
[Syntax.Str](node){
[Syntax.Str](node) {
if (!lintStyledNode && !helper.isPlainStrNode(node)) {
return;
}
const text = getSource(node);

const noSpace = (key) => key === 'punctuation' ? true : !spaceOption[key];
if (Object.keys(spaceOption).every(noSpace)) {
noSpaceBetween(node, text);
Expand All @@ -151,6 +169,7 @@ function reporter(context, options = {}) {
}
}
}

module.exports = {
linter: reporter,
fixer: reporter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,35 @@ Pull Request、コミットのやりかたなどが書かれています。`,
options: {
space: ["alphabets", "punctuation"]
}
}
},
// allows,
{
text: "Eコーマス",
options: {
space: "always",
allows: [
"Eコーマス"
]
}
},
{
text: "これは A言語、B言語、C言語です。",
options: {
space: "always",
allows: [
"/(\\w)言語/"
]
}
},
{
text: "E コーマス",
options: {
space: "never",
allows: [
"E コーマス"
]
}
},
],
invalid: [
{
Expand Down
25 changes: 25 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1659,6 +1659,16 @@
resolved "https://registry.yarnpkg.com/@textlint/module-interop/-/module-interop-13.3.3.tgz#645b47b9e951030b2d656e2c9266b5587de2a17b"
integrity sha512-CwfVpRGAxbkhGY9vLLU06Q/dy/RMNnyzbmt6IS2WIyxqxvGaF7QZtFYpKEEm63aemVyUvzQ7WM3yVOoUg6P92w==

"@textlint/regexp-string-matcher@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@textlint/regexp-string-matcher/-/regexp-string-matcher-2.0.2.tgz#cef4d8353dac624086069e290d9631ca285df34d"
integrity sha512-OXLD9XRxMhd3S0LWuPHpiARQOI7z9tCOs0FsynccW2lmyZzHHFJ9/eR6kuK9xF459Qf+740qI5h+/0cx+NljzA==
dependencies:
escape-string-regexp "^4.0.0"
lodash.sortby "^4.7.0"
lodash.uniq "^4.5.0"
lodash.uniqwith "^4.5.0"

"@textlint/runtime-helper@^0.16.0":
version "0.16.0"
resolved "https://registry.yarnpkg.com/@textlint/runtime-helper/-/runtime-helper-0.16.0.tgz#b59967ac861cc873bf3e9cd69739f9bf098a2534"
Expand Down Expand Up @@ -4656,11 +4666,26 @@ lodash.ismatch@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37"
integrity sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==

lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==

lodash.truncate@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==

lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==

lodash.uniqwith@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz#7a0cbf65f43b5928625a9d4d0dc54b18cadc7ef3"
integrity sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q==

lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
Expand Down

0 comments on commit 76da132

Please sign in to comment.