diff --git a/packages/textlint-rule-ja-space-between-half-and-full-width/README.md b/packages/textlint-rule-ja-space-between-half-and-full-width/README.md index 326f445..9ac7d81 100644 --- a/packages/textlint-rule-ja-space-between-half-and-full-width/README.md +++ b/packages/textlint-rule-ja-space-between-half-and-full-width/README.md @@ -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 { @@ -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 diff --git a/packages/textlint-rule-ja-space-between-half-and-full-width/package.json b/packages/textlint-rule-ja-space-between-half-and-full-width/package.json index cd8fd1d..dc922cd 100644 --- a/packages/textlint-rule-ja-space-between-half-and-full-width/package.json +++ b/packages/textlint-rule-ja-space-between-half-and-full-width/package.json @@ -29,6 +29,7 @@ "textlintrule" ], "devDependencies": { + "@textlint/regexp-string-matcher": "^2.0.2", "textlint-scripts": "^13.3.3" }, "dependencies": { diff --git a/packages/textlint-rule-ja-space-between-half-and-full-width/src/index.js b/packages/textlint-rule-ja-space-between-half-and-full-width/src/index.js index ed13028..24fc4ae 100644 --- a/packages/textlint-rule-ja-space-between-half-and-full-width/src/index.js +++ b/packages/textlint-rule-ja-space-between-half-and-full-width/src/index.js @@ -4,61 +4,71 @@ 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 テスト対象のテキスト全体 @@ -66,6 +76,7 @@ function reporter(context, options = {}) { * @returns {function(*, *)} */ const createFilter = (text, padding) => { + const allowedPatterns = allows.length > 0 ? matchPatterns(text, allows) : []; /** * `PunctuationRegExp`で指定された例外を取り除く * @param {Object} match @@ -79,7 +90,14 @@ 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: アルファベットと全角の間はスペースを入れない @@ -87,7 +105,7 @@ function reporter(context, options = {}) { 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], "") @@ -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], " ") @@ -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); @@ -151,6 +169,7 @@ function reporter(context, options = {}) { } } } + module.exports = { linter: reporter, fixer: reporter diff --git a/packages/textlint-rule-ja-space-between-half-and-full-width/test/index-test.js b/packages/textlint-rule-ja-space-between-half-and-full-width/test/index-test.js index 039e34d..d8f4766 100644 --- a/packages/textlint-rule-ja-space-between-half-and-full-width/test/index-test.js +++ b/packages/textlint-rule-ja-space-between-half-and-full-width/test/index-test.js @@ -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: [ { diff --git a/yarn.lock b/yarn.lock index d2a3e3e..e42bc1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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"