@@ -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+
911const PunctuationRegExp = / [ 。 、 ] / ;
1012const ZenRegExpStr = '[、。]|[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]|[\uD840-\uD87F][\uDC00-\uDFFF]|[ぁ-んァ-ヶ]' ;
1113const defaultSpaceOptions = {
1214 alphabets : false ,
1315 numbers : false ,
14- punctuation : false
16+ punctuation : false ,
1517} ;
1618const 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+
2029function 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+
154173module . exports = {
155174 linter : reporter ,
156175 fixer : reporter
0 commit comments