eslint-plugin-smarthr 1.10.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +0 -1
- package/package.json +4 -4
- package/rules/a11y-anchor-has-href-attribute/index.js +44 -80
- package/rules/a11y-clickable-element-has-text/index.js +9 -76
- package/rules/a11y-image-has-alt-attribute/index.js +15 -51
- package/rules/a11y-input-has-name-attribute/index.js +34 -70
- package/rules/a11y-input-in-form-control/index.js +3 -0
- package/rules/a11y-prohibit-input-maxlength-attribute/index.js +5 -11
- package/rules/a11y-prohibit-input-placeholder/index.js +31 -52
- package/test/a11y-clickable-element-has-text.js +0 -4
- package/test/a11y-image-has-alt-attribute.js +0 -1
- package/test/a11y-input-in-form-control.js +2 -0
- package/rules/a11y-replace-unreadable-symbol/README.md +0 -38
- package/rules/a11y-replace-unreadable-symbol/index.js +0 -31
- package/test/a11y-replace-unreadable-symbol.js +0 -35
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [2.0.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.11.0...eslint-plugin-smarthr-v2.0.0) (2025-10-16)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* eslint rule の smarthr/a11y-replace-unreadable-symbolを削除 ([#850](https://github.com/kufu/tamatebako/issues/850))
|
|
11
|
+
|
|
12
|
+
### Code Refactoring
|
|
13
|
+
|
|
14
|
+
* eslint rule の smarthr/a11y-replace-unreadable-symbolを削除 ([#850](https://github.com/kufu/tamatebako/issues/850)) ([68336f8](https://github.com/kufu/tamatebako/commit/68336f8df42fcfe0c3a8375671377cc62b2be711))
|
|
15
|
+
|
|
16
|
+
## [1.11.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.10.0...eslint-plugin-smarthr-v1.11.0) (2025-10-01)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* a11y-input-in-form-controlのaria-label,aria-labelledbyが設定されている場合、FormControlでラップしなくてもよしとする ([#795](https://github.com/kufu/tamatebako/issues/795)) ([443892f](https://github.com/kufu/tamatebako/commit/443892ffb39732157d77a179484da3f529e43885))
|
|
22
|
+
|
|
5
23
|
## [1.10.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v1.9.0...eslint-plugin-smarthr-v1.10.0) (2025-10-01)
|
|
6
24
|
|
|
7
25
|
|
package/README.md
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
- [a11y-prohibit-input-placeholder](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-prohibit-input-placeholder)
|
|
15
15
|
- [a11y-prohibit-sectioning-content-in-form](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-prohibit-sectioning-content-in-form)
|
|
16
16
|
- [a11y-prohibit-useless-sectioning-fragment](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-prohibit-useless-sectioning-fragment)
|
|
17
|
-
- [a11y-replace-unreadable-symbol](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-replace-unreadable-symbol)
|
|
18
17
|
- [a11y-required-layout-as-attribute](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-required-layout-as-attribute)
|
|
19
18
|
- [a11y-trigger-has-button](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/a11y-trigger-has-button)
|
|
20
19
|
- [best-practice-for-async-current-target](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/best-practice-for-async-current-target)
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-smarthr",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"author": "SmartHR",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A sharable ESLint plugin for SmartHR",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"engines": {
|
|
9
|
-
"node": ">=22.
|
|
9
|
+
"node": ">=22.20.0"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "jest"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"json5": "^2.2.3"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"typescript-eslint": "^8.
|
|
29
|
+
"typescript-eslint": "^8.46.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"eslint": "^9"
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"eslintplugin",
|
|
38
38
|
"smarthr"
|
|
39
39
|
],
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "dd35a6760dd9022d4b28747996c2016022e94dc6"
|
|
41
41
|
}
|
|
@@ -4,90 +4,52 @@ const fs = require('fs')
|
|
|
4
4
|
const OPTION = (() => {
|
|
5
5
|
const file = `${process.cwd()}/package.json`
|
|
6
6
|
|
|
7
|
-
if (
|
|
8
|
-
|
|
9
|
-
}
|
|
7
|
+
if (fs.existsSync(file)) {
|
|
8
|
+
const json = JSON5.parse(fs.readFileSync(file))
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
...(json.dependencies ? Object.keys(json.dependencies) : []),
|
|
14
|
-
...(json.devDependencies ? Object.keys(json.devDependencies) : []),
|
|
15
|
-
]
|
|
10
|
+
if (json.dependencies){
|
|
11
|
+
const dependencies = Object.keys(json.dependencies)
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
let nextjs = false
|
|
14
|
+
let react_router = false
|
|
15
|
+
const result = () => ({
|
|
16
|
+
nextjs,
|
|
17
|
+
react_router,
|
|
18
|
+
})
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
for (let i = 0; i < dependencies.length; i++) {
|
|
21
|
+
switch (dependencies[i]) {
|
|
22
|
+
case 'next':
|
|
23
|
+
nextjs = true
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
if (react_router) {
|
|
26
|
+
return result()
|
|
27
|
+
}
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
break
|
|
30
|
+
case 'react-router':
|
|
31
|
+
react_router = true
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
if (nextjs) {
|
|
34
|
+
return result()
|
|
35
|
+
}
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
break
|
|
38
|
+
}
|
|
39
|
+
}
|
|
42
40
|
}
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
return
|
|
43
|
+
return {}
|
|
46
44
|
})()
|
|
47
45
|
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
const baseCheck = (node, checkType) => {
|
|
55
|
-
const nodeName = node.name.name || ''
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
REGEX_TARGET.test(nodeName) &&
|
|
59
|
-
checkExistAttribute(node, findHrefAttribute) &&
|
|
60
|
-
(checkType !== 'allow-spread-attributes' || !node.attributes.some(findSpreadAttr))
|
|
61
|
-
) {
|
|
62
|
-
return nodeName
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return false
|
|
66
|
-
}
|
|
67
|
-
const nextCheck = (node, checkType) => {
|
|
68
|
-
// HINT: next/link で `Link>a` という構造がありえるので直上のJSXElementを調べる
|
|
69
|
-
const target = node.parent.parent.openingElement
|
|
70
|
-
|
|
71
|
-
return target ? baseCheck(target, checkType) : false
|
|
72
|
-
}
|
|
73
|
-
const reactRouterCheck = (node) => checkExistAttribute(node, findToAttribute)
|
|
46
|
+
const ANCHOR_ELEMENT = 'JSXOpeningElement[name.name=/(Anchor|Link|^a)$/]'
|
|
47
|
+
const HREF_ATTRIBUTE = `JSXAttribute[name.name=${OPTION.react_router ? '/^(href|to)$/' : '"href"'}]`
|
|
48
|
+
const NEXT_LINK_REGEX = /Link$/
|
|
49
|
+
// HINT: next/link で `Link>a` という構造がありえるので直上のJSXElementを調べる
|
|
50
|
+
const nextCheck = (node) => ((node.parent.parent.openingElement.name.name || '').test(NEXT_LINK_REGEX))
|
|
74
51
|
|
|
75
|
-
const
|
|
76
|
-
const attr = node.attributes.find(find)?.value
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
!attr ||
|
|
80
|
-
isNullTextHref(attr) ||
|
|
81
|
-
(attr.type === 'JSXExpressionContainer' && isNullTextHref(attr.expression))
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
const isNullTextHref = (attr) => attr.type === 'Literal' && (attr.value === '' || attr.value === '#')
|
|
85
|
-
const findSpreadAttr = (a) => a.type === 'JSXSpreadAttribute'
|
|
86
|
-
|
|
87
|
-
const findHrefAttribute = (a) => a.name?.name == 'href'
|
|
88
|
-
const findToAttribute = (a) => a.name?.name == 'to'
|
|
89
|
-
|
|
90
|
-
const MESSAGE_SUFFIX = ` に href 属性を正しく設定してください
|
|
52
|
+
const MESSAGE_SUFFIX = ` に href${OPTION.react_router ? '、もしくはto' : ''} 属性を正しく設定してください
|
|
91
53
|
- onClickなどでページ遷移する場合でもhref属性に遷移先のURIを設定してください
|
|
92
54
|
- Cmd + clickなどのキーボードショートカットに対応出来ます
|
|
93
55
|
- onClickなどの動作がURLの変更を行わない場合、button要素でマークアップすることを検討してください
|
|
@@ -115,19 +77,21 @@ module.exports = {
|
|
|
115
77
|
},
|
|
116
78
|
create(context) {
|
|
117
79
|
const option = context.options[0] || {}
|
|
118
|
-
const
|
|
80
|
+
const spreadAttributeSelector = option.checkType === 'allow-spread-attributes' ? ':not(:has(JSXSpreadAttribute))' : ''
|
|
81
|
+
const reporter = (node) => {
|
|
82
|
+
context.report({
|
|
83
|
+
node,
|
|
84
|
+
message: `${node.name.name}${MESSAGE_SUFFIX}`,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
119
87
|
|
|
120
88
|
return {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (nodeName) {
|
|
125
|
-
context.report({
|
|
126
|
-
node,
|
|
127
|
-
message: `${nodeName}${MESSAGE_SUFFIX}`,
|
|
128
|
-
})
|
|
89
|
+
[`${ANCHOR_ELEMENT}:not(:has(${HREF_ATTRIBUTE}))${spreadAttributeSelector}`]: (node) => {
|
|
90
|
+
if (!OPTION.nextjs || !nextCheck(node)) {
|
|
91
|
+
reporter(node)
|
|
129
92
|
}
|
|
130
93
|
},
|
|
94
|
+
[`${ANCHOR_ELEMENT}:has(${HREF_ATTRIBUTE}:matches([value=null],:has(Literal[value=/^(|#)$/])))`]: reporter,
|
|
131
95
|
}
|
|
132
96
|
},
|
|
133
97
|
}
|
|
@@ -8,14 +8,8 @@ const SCHEMA = [
|
|
|
8
8
|
}
|
|
9
9
|
]
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const REGEX_SMARTHR_LOGO = /SmartHRLogo$/
|
|
14
|
-
const REGEX_TEXT_COMPONENT = /(Text|Message)$/
|
|
15
|
-
const REGEX_JSX_TYPE = /^(JSXText|JSXExpressionContainer)$/
|
|
16
|
-
|
|
17
|
-
const filterFalsyJSXText = (cs) => cs.filter(checkFalsyJSXText)
|
|
18
|
-
const checkFalsyJSXText = (c) => c.type !== 'JSXText' || !REGEX_NLSP.test(c.value)
|
|
11
|
+
const CLICKABLE_ELEMENT = 'JSXElement[openingElement.name.name=/((^b|B)utton|Anchor|Link|^a)/]:has(JSXClosingElement)'
|
|
12
|
+
const TEXT_LIKE_ATTRIBUTE = 'JSXAttribute[name.name=/^(text|alt|aria-label(ledby)?)$/]:not(:matches([value=null],[value.value=""])))'
|
|
19
13
|
|
|
20
14
|
const message = `a, buttonなどのクリッカブルな要素内にはテキストを設定してください
|
|
21
15
|
- 要素内にアイコン、画像のみを設置する場合はaltなどの代替テキスト用属性を指定してください
|
|
@@ -33,76 +27,15 @@ module.exports = {
|
|
|
33
27
|
create(context) {
|
|
34
28
|
const option = context.options[0] || {}
|
|
35
29
|
const componentsWithText = option.componentsWithText || []
|
|
30
|
+
// HINT: SmartHRLogo コンポーネントは内部でaltを持っているため対象外にする
|
|
31
|
+
const elementWithText = `JSXOpeningElement[name.name=/(SmartHRLogo|Text|Message|^(${componentsWithText.join('|')}))$/]`
|
|
36
32
|
|
|
37
33
|
return {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const node = parentNode.openingElement
|
|
45
|
-
|
|
46
|
-
if (!node.name.name || !REGEX_CLICKABLE_ELEMENT.test(node.name.name)) {
|
|
47
|
-
return
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const recursiveSearch = (c) => {
|
|
51
|
-
if (REGEX_JSX_TYPE.test(c.type)) {
|
|
52
|
-
return true
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
switch (c.type) {
|
|
56
|
-
case 'JSXFragment': {
|
|
57
|
-
return c.children && filterFalsyJSXText(c.children).some(recursiveSearch)
|
|
58
|
-
}
|
|
59
|
-
case 'JSXElement': {
|
|
60
|
-
// // HINT: SmartHRLogo コンポーネントは内部でaltを持っているため対象外にする
|
|
61
|
-
if (REGEX_SMARTHR_LOGO.test(c.openingElement.name.name)) {
|
|
62
|
-
return true
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const tagName = c.openingElement.name.name
|
|
66
|
-
|
|
67
|
-
if (REGEX_TEXT_COMPONENT.test(tagName) || componentsWithText.includes(tagName)) {
|
|
68
|
-
return true
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// HINT: role & aria-label を同時に設定されている場合か、text属性が設定されている場合許可
|
|
72
|
-
let existRole = false
|
|
73
|
-
let existAriaLabel = false
|
|
74
|
-
const result = c.openingElement.attributes.reduce((prev, a) => {
|
|
75
|
-
const n = a.name?.name
|
|
76
|
-
|
|
77
|
-
if (prev || n === 'text') {
|
|
78
|
-
return true
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const v = a.value?.value
|
|
82
|
-
|
|
83
|
-
existRole = existRole || (n === 'role' && v === 'img')
|
|
84
|
-
existAriaLabel = existAriaLabel || n === 'aria-label'
|
|
85
|
-
|
|
86
|
-
if (existRole && existAriaLabel) {
|
|
87
|
-
return true
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return n === 'alt' && (v || a.value.type === 'JSXExpressionContainer') || false
|
|
91
|
-
}, null)
|
|
92
|
-
|
|
93
|
-
return result || (c.children && filterFalsyJSXText(c.children).some(recursiveSearch))
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return false
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (!filterFalsyJSXText(parentNode.children).find(recursiveSearch)) {
|
|
101
|
-
context.report({
|
|
102
|
-
node,
|
|
103
|
-
message,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
34
|
+
[`${CLICKABLE_ELEMENT}:not(:has(:matches(${TEXT_LIKE_ATTRIBUTE},JSXText,JSXExpressionContainer,${elementWithText}))`]: (node) => {
|
|
35
|
+
context.report({
|
|
36
|
+
node,
|
|
37
|
+
message,
|
|
38
|
+
});
|
|
106
39
|
},
|
|
107
40
|
}
|
|
108
41
|
},
|
|
@@ -1,18 +1,6 @@
|
|
|
1
|
-
const REGEX_IMG = /(
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const findAriaDescribedbyAttr = (a) => a.name?.name === 'aria-describedby'
|
|
5
|
-
const findSpreadAttr = (a) => a.type === 'JSXSpreadAttribute'
|
|
6
|
-
const isWithinSvgJsxElement = (node) => {
|
|
7
|
-
if (
|
|
8
|
-
node.type === 'JSXElement' &&
|
|
9
|
-
node.openingElement.name?.name === 'svg'
|
|
10
|
-
) {
|
|
11
|
-
return true
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return node.parent ? isWithinSvgJsxElement(node.parent) : false
|
|
15
|
-
}
|
|
1
|
+
const REGEX_IMG = /((i|I)mg|Image)$/ // HINT: Iconは別途テキストが存在する場合が多いためチェックの対象外とする
|
|
2
|
+
const IMG_ELEMENT = 'JSXOpeningElement[name.name=/((i|I)mg|Image)$/]'
|
|
3
|
+
const ALT_LIKE_ATTRIBUTE = 'JSXAttribute[name.name=/^(alt|aria-describedby)$/]'
|
|
16
4
|
|
|
17
5
|
const MESSAGE_NOT_EXIST_ALT = `画像にはalt属性を指定してください。
|
|
18
6
|
- コンポーネントが画像ではない場合、img or image を末尾に持たない名称に変更してください。
|
|
@@ -43,44 +31,20 @@ module.exports = {
|
|
|
43
31
|
},
|
|
44
32
|
create(context) {
|
|
45
33
|
const option = context.options[0] || {}
|
|
46
|
-
const
|
|
34
|
+
const notHasSpreadAttr = option.checkType === 'allow-spread-attributes' ? ':not(:has(JSXSpreadAttribute))' : ''
|
|
47
35
|
|
|
48
36
|
return {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
!node.attributes.find(findAriaDescribedbyAttr) &&
|
|
61
|
-
(
|
|
62
|
-
matcher.input !== 'image' ||
|
|
63
|
-
!isWithinSvgJsxElement(node.parent)
|
|
64
|
-
) &&
|
|
65
|
-
(
|
|
66
|
-
checkType !== 'allow-spread-attributes' ||
|
|
67
|
-
!node.attributes.some(findSpreadAttr)
|
|
68
|
-
)
|
|
69
|
-
) {
|
|
70
|
-
message = MESSAGE_NOT_EXIST_ALT
|
|
71
|
-
}
|
|
72
|
-
} else if (alt.value.value === '') {
|
|
73
|
-
message = MESSAGE_NULL_ALT
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (message) {
|
|
77
|
-
context.report({
|
|
78
|
-
node,
|
|
79
|
-
message,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
37
|
+
[`${IMG_ELEMENT}:not(:has(${ALT_LIKE_ATTRIBUTE}))${notHasSpreadAttr}`]: (node) => {
|
|
38
|
+
context.report({
|
|
39
|
+
node,
|
|
40
|
+
message: MESSAGE_NOT_EXIST_ALT,
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
[`${IMG_ELEMENT}:has(${ALT_LIKE_ATTRIBUTE}:matches([value.value=""],[value=null]))${notHasSpreadAttr}`]: (node) => {
|
|
44
|
+
context.report({
|
|
45
|
+
node,
|
|
46
|
+
message: MESSAGE_NULL_ALT,
|
|
47
|
+
});
|
|
84
48
|
},
|
|
85
49
|
}
|
|
86
50
|
},
|
|
@@ -5,31 +5,26 @@ const JSON5 = require('json5')
|
|
|
5
5
|
const OPTION = (() => {
|
|
6
6
|
const file = `${process.cwd()}/package.json`
|
|
7
7
|
|
|
8
|
-
if (
|
|
9
|
-
return {
|
|
8
|
+
if (fs.existsSync(file)) {
|
|
9
|
+
return {
|
|
10
|
+
react_hook_form: Object.keys(JSON5.parse(fs.readFileSync(file)).dependencies).includes('react-hook-form'),
|
|
11
|
+
}
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
const dependencies = [...Object.keys(json.dependencies || {}), ...Object.keys(json.devDependencies || {})]
|
|
14
|
-
|
|
15
|
-
return {
|
|
16
|
-
react_hook_form: dependencies.includes('react-hook-form'),
|
|
17
|
-
}
|
|
14
|
+
return {}
|
|
18
15
|
})()
|
|
19
16
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
17
|
+
const INPUT_ELEMENT = 'JSXOpeningElement[name.name=/((I|^i)nput|(T|^t)extarea|(S|^s)elect|InputFile|RadioButton(Panel)?|(Check|Combo)(B|b)ox|(Date|Wareki|Time)Picker|DropZone)$/]'
|
|
18
|
+
const INPUT_ELEMENT_WITHOUT_RADIO = 'JSXOpeningElement[name.name=/((I|^i)nput|(T|^t)extarea|(S|^s)elect|InputFile|(Check|Combo)(B|b)ox|(Date|Wareki|Time)Picker|DropZone)$/]'
|
|
19
|
+
const RADIO_ELEMENT = 'JSXOpeningElement:matches([name.name=/RadioButton(Panel)?$/],[name.name=/(I|^i)nput?$/]:has(JSXAttribute[name.name="type"][value.value="radio"]))'
|
|
20
|
+
const NAME_ATTRIBUTE = 'JSXAttribute[name.name="name"]'
|
|
21
|
+
|
|
22
|
+
const INPUT_NAME_REGEX = /^[a-zA-Z0-9_\[\]]+$/.toString()
|
|
24
23
|
|
|
25
24
|
const MESSAGE_PART_FORMAT = `"${INPUT_NAME_REGEX.toString()}"にmatchするフォーマットで命名してください`
|
|
26
25
|
const MESSAGE_UNDEFINED_NAME_PART = `
|
|
27
26
|
- ブラウザの自動補完が有効化されるなどのメリットがあります
|
|
28
27
|
- より多くのブラウザが自動補完を行える可能性を上げるため、${MESSAGE_PART_FORMAT}`
|
|
29
|
-
const MESSAGE_UNDEFINED_FOR_RADIO = `にグループとなる他のinput[radio]と同じname属性を指定してください
|
|
30
|
-
- 適切に指定することで同じname属性を指定したinput[radio]とグループが確立され、適切なキーボード操作を行えるようになります${MESSAGE_UNDEFINED_NAME_PART}`
|
|
31
|
-
const MESSAGE_UNDEFINED_FOR_NOT_RADIO = `にname属性を指定してください${MESSAGE_UNDEFINED_NAME_PART}`
|
|
32
|
-
const MESSAGE_NAME_FORMAT_SUFFIX = `はブラウザの自動補完が適切に行えない可能性があるため${MESSAGE_PART_FORMAT}`
|
|
33
28
|
|
|
34
29
|
const SCHEMA = [
|
|
35
30
|
{
|
|
@@ -53,61 +48,30 @@ module.exports = {
|
|
|
53
48
|
const option = context.options[0] || {}
|
|
54
49
|
const checkType = option.checkType || 'always'
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (TARGET_TAG_NAME_REGEX.test(nodeName)) {
|
|
62
|
-
let nameAttr = null
|
|
63
|
-
let hasSpreadAttr = false
|
|
64
|
-
let hasReactHookFormRegisterSpreadAttr = false
|
|
65
|
-
let hasRadioInput = false
|
|
51
|
+
const notHasSpreadAttribute =
|
|
52
|
+
option.checkType == 'allow-spread-attributes'
|
|
53
|
+
? ':not(:has(JSXSpreadAttribute))'
|
|
54
|
+
: OPTION.react_hook_form ? ':not(:has(JSXSpreadAttribute CallExpression[callee.name="register"]))' : ''
|
|
66
55
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
if (nameAttr) {
|
|
91
|
-
const nameValue = nameAttr.value?.value
|
|
92
|
-
|
|
93
|
-
if (nameValue && !INPUT_NAME_REGEX.test(nameValue)) {
|
|
94
|
-
context.report({
|
|
95
|
-
node,
|
|
96
|
-
message: `${nodeName} のname属性の値(${nameValue})${MESSAGE_NAME_FORMAT_SUFFIX}`,
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
} else if (
|
|
100
|
-
!(OPTION.react_hook_form && hasReactHookFormRegisterSpreadAttr) &&
|
|
101
|
-
(attributes.length === 0 || checkType !== 'allow-spread-attributes' || !hasSpreadAttr)
|
|
102
|
-
) {
|
|
103
|
-
const isRadio = RADIO_BUTTON_REGEX.test(nodeName) || INPUT_TAG_REGEX.test(nodeName) && hasRadioInput
|
|
104
|
-
|
|
105
|
-
context.report({
|
|
106
|
-
node,
|
|
107
|
-
message: `${nodeName} ${isRadio ? MESSAGE_UNDEFINED_FOR_RADIO : MESSAGE_UNDEFINED_FOR_NOT_RADIO}`,
|
|
108
|
-
})
|
|
109
|
-
}
|
|
110
|
-
}
|
|
56
|
+
return {
|
|
57
|
+
[`${INPUT_ELEMENT_WITHOUT_RADIO}:not(:has(:matches(${NAME_ATTRIBUTE},JSXAttribute[name.name="type"][value.value="radio"])))${notHasSpreadAttribute}`]: (node) => {
|
|
58
|
+
context.report({
|
|
59
|
+
node,
|
|
60
|
+
message: `${node.name.name} にname属性を指定してください${MESSAGE_UNDEFINED_NAME_PART}`,
|
|
61
|
+
})
|
|
62
|
+
},
|
|
63
|
+
[`${RADIO_ELEMENT}:not(:has(${NAME_ATTRIBUTE}))${notHasSpreadAttribute}`]: (node) => {
|
|
64
|
+
context.report({
|
|
65
|
+
node,
|
|
66
|
+
message: `${node.name.name} にグループとなる他のinput[radio]と同じname属性を指定してください
|
|
67
|
+
- 適切に指定することで同じname属性を指定したinput[radio]とグループが確立され、適切なキーボード操作を行えるようになります${MESSAGE_UNDEFINED_NAME_PART}`,
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
[`${INPUT_ELEMENT}${notHasSpreadAttribute} ${NAME_ATTRIBUTE}:not([value.value=${INPUT_NAME_REGEX}])`]: (node) => {
|
|
71
|
+
context.report({
|
|
72
|
+
node,
|
|
73
|
+
message: `${node.parent.name.name} のname属性の値(${node.value.value})はブラウザの自動補完が適切に行えない可能性があるため${MESSAGE_PART_FORMAT}`,
|
|
74
|
+
})
|
|
111
75
|
},
|
|
112
76
|
}
|
|
113
77
|
},
|
|
@@ -77,8 +77,11 @@ module.exports = {
|
|
|
77
77
|
for (const i of node.attributes) {
|
|
78
78
|
if (i.name) {
|
|
79
79
|
// HINT: idが設定されている場合、htmlForでlabelと紐づく可能性が高いため無視する
|
|
80
|
+
// aria-label, aria-labelledbyが設定されている場合は疑似ラベルが設定されているため許容する
|
|
80
81
|
switch (i.name.name) {
|
|
81
82
|
case 'id':
|
|
83
|
+
case 'aria-label':
|
|
84
|
+
case 'aria-labelledby':
|
|
82
85
|
isPseudoLabel = true
|
|
83
86
|
break
|
|
84
87
|
case 'type':
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
const INPUT_COMPONENT_NAMES = /((I|^i)nput|(T|^t)extarea)$/
|
|
2
|
-
|
|
3
1
|
const SCHEMA = []
|
|
4
2
|
|
|
5
|
-
const checkHasMaxLength = (attr) => attr.name?.name === 'maxLength'
|
|
6
|
-
|
|
7
3
|
/**
|
|
8
4
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
9
5
|
*/
|
|
@@ -14,17 +10,15 @@ module.exports = {
|
|
|
14
10
|
},
|
|
15
11
|
create(context) {
|
|
16
12
|
return {
|
|
17
|
-
JSXOpeningElement: (node) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
message: `${node.name.name}にmaxLength属性を設定しないでください。
|
|
13
|
+
[`JSXOpeningElement[name.name=/((I|^i)nput|(T|^t)extarea)$/]:has(JSXAttribute[name.name="maxLength"])`]: (node) => {
|
|
14
|
+
context.report({
|
|
15
|
+
node,
|
|
16
|
+
message: `${node.name.name}にmaxLength属性を設定しないでください。
|
|
22
17
|
- maxLength属性がついた要素に、テキストをペーストすると、maxLength属性の値を超えた範囲が意図せず切り捨てられてしまう場合があります
|
|
23
18
|
- 以下のいずれかの方法で修正をおこなってください
|
|
24
19
|
- 方法1: pattern属性とtitle属性を組み合わせ、form要素でラップする
|
|
25
20
|
- 方法2: JavaScriptを用いたバリデーションを実装する`,
|
|
26
|
-
|
|
27
|
-
}
|
|
21
|
+
})
|
|
28
22
|
},
|
|
29
23
|
}
|
|
30
24
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
1
|
+
const PLACEHOLDER_ATTRIBUTE = 'JSXAttribute[name.name="placeholder"]'
|
|
2
|
+
const COMBOBOX_ELEMENT = 'JSXOpeningElement[name.name=/Combo(B|b)ox$/]'
|
|
3
|
+
const DEFAULT_ITEM_ATTRIBUTE = 'JSXAttribute[name.name="defaultItem"]'
|
|
4
|
+
const SEARCH_INPUT_NAME = '[name.name=/SearchInput$/]'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
@@ -12,59 +13,37 @@ module.exports = {
|
|
|
12
13
|
},
|
|
13
14
|
create(context) {
|
|
14
15
|
return {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const tooltipMessage = node.attributes.find((a) => a.name?.name === 'tooltipMessage')
|
|
24
|
-
|
|
25
|
-
if (!tooltipMessage) {
|
|
26
|
-
context.report({
|
|
27
|
-
node: placeholder,
|
|
28
|
-
message: `${name} にはplaceholder属性を単独で利用せず、tooltipMessageオプションのみ、もしくはplaceholderとtooltipMessageの併用を検討してください。 (例: '<${name} tooltipMessage="ヒント" />', '<${name} tooltipMessage={hint} placeholder={hint} />')`,
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
} else if (COMBOBOX_REGEX.test(name)) {
|
|
32
|
-
let defaultItem
|
|
33
|
-
let dropdownHelpMessage
|
|
34
|
-
|
|
35
|
-
node.attributes.forEach((a) => {
|
|
36
|
-
switch(a.name?.name) {
|
|
37
|
-
case 'defaultItem':
|
|
38
|
-
defaultItem = a
|
|
39
|
-
break
|
|
40
|
-
case 'dropdownHelpMessage':
|
|
41
|
-
dropdownHelpMessage = a
|
|
42
|
-
break
|
|
43
|
-
}
|
|
44
|
-
})
|
|
16
|
+
[`${COMBOBOX_ELEMENT}:has(${DEFAULT_ITEM_ATTRIBUTE}) ${PLACEHOLDER_ATTRIBUTE}`]: (node) => {
|
|
17
|
+
context.report({
|
|
18
|
+
node,
|
|
19
|
+
message: `${node.parent.name.name} にはdefaultItemが設定されているため、placeholder属性を閲覧出来ません。削除してください。`,
|
|
20
|
+
})
|
|
21
|
+
},
|
|
22
|
+
[`${COMBOBOX_ELEMENT}:not(:has(${DEFAULT_ITEM_ATTRIBUTE}, JSXAttribute[name.name="dropdownHelpMessage"])) ${PLACEHOLDER_ATTRIBUTE}`]: (node) => {
|
|
23
|
+
const name = node.parent.name.name
|
|
45
24
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
message: `${name} にはdefaultItemが設定されているため、placeholder属性を閲覧出来ません。削除してください。`,
|
|
50
|
-
})
|
|
51
|
-
} else if (!dropdownHelpMessage) {
|
|
52
|
-
context.report({
|
|
53
|
-
node: placeholder,
|
|
54
|
-
message: `${name} にはplaceholder属性は設定せず、以下のいずれか、もしくは組み合わせての対応を検討してください。
|
|
25
|
+
context.report({
|
|
26
|
+
node,
|
|
27
|
+
message: `${name} にはplaceholder属性は設定せず、以下のいずれか、もしくは組み合わせての対応を検討してください。
|
|
55
28
|
- 選択肢をどんな値で絞り込めるかの説明をしたい場合は dropdownHelpMessage 属性に変更してください。
|
|
56
29
|
- 空の値の説明のためにplaceholderを利用している場合は defaultItem 属性に変更してください。
|
|
57
30
|
- 上記以外の説明を行いたい場合、ヒント用要素を設置してください。(例: '<div><${name} /><Hint>ヒント</Hint></div>')`,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
31
|
+
})
|
|
32
|
+
},
|
|
33
|
+
[`JSXOpeningElement${SEARCH_INPUT_NAME}:not(:has(JSXAttribute[name.name="tooltipMessage"])) ${PLACEHOLDER_ATTRIBUTE}`]: (node) => {
|
|
34
|
+
const inputName = node.parent.name.name
|
|
35
|
+
context.report({
|
|
36
|
+
node,
|
|
37
|
+
message: `${inputName} にはplaceholder属性を単独で利用せず、tooltipMessageオプションのみ、もしくはplaceholderとtooltipMessageの併用を検討してください。 (例: '<${inputName} tooltipMessage="ヒント" />', '<${inputName} tooltipMessage={hint} placeholder={hint} />')`,
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
[`JSXOpeningElement[name.name=/((I|^i)nput|(T|^t)extarea|FieldSet|(Date|Wareki|Time)Picker)$/]:not(${SEARCH_INPUT_NAME}) ${PLACEHOLDER_ATTRIBUTE}`]: (node) => {
|
|
41
|
+
const name = node.parent.name.name
|
|
42
|
+
|
|
43
|
+
context.report({
|
|
44
|
+
node,
|
|
45
|
+
message: `${name} にはplaceholder属性は設定せず、別途ヒント用要素の利用を検討してください。(例: '<div><${name} /><Hint>ヒント</Hint></div>')`,
|
|
46
|
+
})
|
|
68
47
|
},
|
|
69
48
|
}
|
|
70
49
|
},
|
|
@@ -142,10 +142,6 @@ ruleTester.run('a11y-clickable-element-has-text', rule, {
|
|
|
142
142
|
code: `<button><SmartHRLogoSuffix /></button>`,
|
|
143
143
|
errors: [{ message: defaultErrorMessage }]
|
|
144
144
|
},
|
|
145
|
-
{
|
|
146
|
-
code: `<a><div role="article" aria-label="hoge" /></a>`,
|
|
147
|
-
errors: [{ message: defaultErrorMessage }]
|
|
148
|
-
},
|
|
149
145
|
{
|
|
150
146
|
code: `<a><TextWithHoge /></a>`,
|
|
151
147
|
errors: [{ message: defaultErrorMessage }]
|
|
@@ -33,7 +33,6 @@ ruleTester.run('a11y-image-has-alt-attribute', rule, {
|
|
|
33
33
|
invalid: [
|
|
34
34
|
{ code: '<img />', errors: [ { message: messageNotExistAlt } ] },
|
|
35
35
|
{ code: '<HogeImage alt="" />', errors: [ { message: messageNullAlt } ] },
|
|
36
|
-
{ code: '<hoge><image /></hoge>', errors: [ { message: messageNotExistAlt } ] },
|
|
37
36
|
{ code: '<AnyImg {...hoge} />', errors: [ { message: messageNotExistAlt } ] },
|
|
38
37
|
{ code: '<AnyImg {...hoge} />', options: [{ checkType: 'always' }], errors: [ { message: messageNotExistAlt } ] },
|
|
39
38
|
]
|
|
@@ -111,6 +111,8 @@ ruleTester.run('a11y-input-in-form-control', rule, {
|
|
|
111
111
|
{ code: '<Fieldset><HogeCheckBoxs /></Fieldset>' },
|
|
112
112
|
{ code: '<Fieldset><HogeCheckBoxes /></Fieldset>' },
|
|
113
113
|
{ code: '<HogeFormControl>{ dateInput ? <DateInput /> : <Input /> }</HogeFormControl>'},
|
|
114
|
+
{ code: '<Input aria-label="hoge" />' },
|
|
115
|
+
{ code: '<Select aria-labelledby="hoge" />' },
|
|
114
116
|
],
|
|
115
117
|
invalid: [
|
|
116
118
|
{ code: `<input />`, errors: [ { message: noLabeledInput('input') } ] },
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# smarthr/a11y-replace-unreadable-symbol
|
|
2
|
-
|
|
3
|
-
- 一部記号はスクリーンリーダーで読み上げられない、もしくは記号名そのままで読み上げられてしまい、意図が正しく伝えられない場合があります
|
|
4
|
-
- それらの記号を適切に読み上げられるコンポーネントに置き換えることを促すルールです
|
|
5
|
-
|
|
6
|
-
## rules
|
|
7
|
-
|
|
8
|
-
```js
|
|
9
|
-
{
|
|
10
|
-
rules: {
|
|
11
|
-
'smarthr/a11y-replace-unreadable-symbol': 'error', // 'warn', 'off',
|
|
12
|
-
},
|
|
13
|
-
}
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## ❌ Incorrect
|
|
17
|
-
|
|
18
|
-
```jsx
|
|
19
|
-
<>XXXX年YY月ZZ日 〜 XXXX年YY月ZZ日</>
|
|
20
|
-
// スクリーンリーダーは "XXXX年YY月ZZ日XXXX年YY月ZZ日" と読み上げる場合があります
|
|
21
|
-
|
|
22
|
-
<p>選択できる数値の範囲は 0 ~ 9999 です</p>
|
|
23
|
-
// スクリーンリーダーは "選択できる数値の範囲は09999です" と読み上げる場合があります
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## ✅ Correct
|
|
27
|
-
|
|
28
|
-
```jsx
|
|
29
|
-
//
|
|
30
|
-
<>XXXX年YY月ZZ日 <RangeSeparator /> XXXX年YY月ZZ日</>
|
|
31
|
-
// スクリーンリーダーは "XXXX年YY月ZZ日からXXXX年YY月ZZ日" と読み上げます
|
|
32
|
-
|
|
33
|
-
<p>選択できる数値の範囲は 0 <RangeSeparator /> 9999 です</p>
|
|
34
|
-
// スクリーンリーダーは "選択できる数値の範囲は0から9999です" と読み上げます
|
|
35
|
-
|
|
36
|
-
<p>入力できる記号は <RangeSeparator decorators={{ text: '~', visuallyHiddenText: '半角チルダ' }} /> です</p>
|
|
37
|
-
// スクリーンリーダーは "入力できる記号は半角チルダです" と読み上げます
|
|
38
|
-
```
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
const TILDE_REGEX = /([~〜])/
|
|
2
|
-
|
|
3
|
-
const SCHEMA = []
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
7
|
-
*/
|
|
8
|
-
module.exports = {
|
|
9
|
-
meta: {
|
|
10
|
-
type: 'problem',
|
|
11
|
-
schema: SCHEMA,
|
|
12
|
-
},
|
|
13
|
-
create(context) {
|
|
14
|
-
return {
|
|
15
|
-
JSXText: (node) => {
|
|
16
|
-
const matcher = node.value.match(TILDE_REGEX)
|
|
17
|
-
|
|
18
|
-
if (matcher) {
|
|
19
|
-
context.report({
|
|
20
|
-
node,
|
|
21
|
-
message: `"${matcher[1]}"はスクリーンリーダーが正しく読み上げることができない場合があるため、smarthr-ui/RangeSeparatorに置き換えてください。
|
|
22
|
-
- エラー表示されている行に"${matcher[1]}"が存在しない場合、改行文字を含む関係で行番号がずれている場合があります。数行下の範囲を確認してください
|
|
23
|
-
- smarthr-ui/RangeSeparatorに置き換えることでスクリーンリーダーが "から" と読み上げることができます
|
|
24
|
-
- 前後の文脈などで "から" と読まれることが不適切な場合 \`<RangeSeparator decorators={{ visuallyHiddenText: () => "ANY" }} />\` のようにdecoratorsを指定してください`,
|
|
25
|
-
})
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
}
|
|
31
|
-
module.exports.schema = SCHEMA
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
const rule = require('../rules/a11y-replace-unreadable-symbol')
|
|
2
|
-
const RuleTester = require('eslint').RuleTester
|
|
3
|
-
|
|
4
|
-
const generateErrorText = (symbol, replaced, read) => `"${symbol}"はスクリーンリーダーが正しく読み上げることができない場合があるため、smarthr-ui/${replaced}に置き換えてください。
|
|
5
|
-
- エラー表示されている行に"${symbol}"が存在しない場合、改行文字を含む関係で行番号がずれている場合があります。数行下の範囲を確認してください
|
|
6
|
-
- smarthr-ui/${replaced}に置き換えることでスクリーンリーダーが "${read}" と読み上げることができます
|
|
7
|
-
- 前後の文脈などで "${read}" と読まれることが不適切な場合 \`<RangeSeparator decorators={{ visuallyHiddenText: () => "ANY" }} />\` のようにdecoratorsを指定してください`
|
|
8
|
-
|
|
9
|
-
const ruleTester = new RuleTester({
|
|
10
|
-
languageOptions: {
|
|
11
|
-
parserOptions: {
|
|
12
|
-
ecmaFeatures: {
|
|
13
|
-
jsx: true,
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
ruleTester.run('a11y-replace-unreadable-symbol', rule, {
|
|
20
|
-
valid: [
|
|
21
|
-
{ code: `<>ほげふが</>` },
|
|
22
|
-
{ code: `<RangeSeparator />` },
|
|
23
|
-
{ code: `<p>ほげ<RangeSeparator />ふが</p>` },
|
|
24
|
-
{ code: `<p>
|
|
25
|
-
ほげ
|
|
26
|
-
<RangeSeparator />
|
|
27
|
-
ふが
|
|
28
|
-
</p>` },
|
|
29
|
-
],
|
|
30
|
-
invalid: [
|
|
31
|
-
{ code: `<>~</>`, errors: [ { message: generateErrorText('~', 'RangeSeparator', 'から') } ] },
|
|
32
|
-
{ code: `<>ほげ~ふが</>`, errors: [ { message: generateErrorText('~', 'RangeSeparator', 'から') } ] },
|
|
33
|
-
{ code: `<p>ほげ〜ふが</p>`, errors: [ { message: generateErrorText('〜', 'RangeSeparator', 'から') } ] },
|
|
34
|
-
]
|
|
35
|
-
})
|