Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ja-space-between-half-and-full-width): add allows option #64

Merged
merged 2 commits into from
Apr 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading