Skip to content

Commit 9801f32

Browse files
Merge pull request #2593 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-whitespace-within-word
Extract rule: template-no-whitespace-within-word
2 parents 102db31 + ce53f5c commit 9801f32

4 files changed

Lines changed: 274 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ rules in templates can be disabled with eslint directives with mustache or html
197197
| [template-no-nested-interactive](docs/rules/template-no-nested-interactive.md) | disallow nested interactive elements | | | |
198198
| [template-no-nested-landmark](docs/rules/template-no-nested-landmark.md) | disallow nested landmark elements | | | |
199199
| [template-no-pointer-down-event-binding](docs/rules/template-no-pointer-down-event-binding.md) | disallow pointer down event bindings | | | |
200+
| [template-no-whitespace-within-word](docs/rules/template-no-whitespace-within-word.md) | disallow excess whitespace within words (e.g. "W e l c o m e") | | | |
200201
| [template-require-aria-activedescendant-tabindex](docs/rules/template-require-aria-activedescendant-tabindex.md) | require non-interactive elements with aria-activedescendant to have tabindex | | 🔧 | |
201202
| [template-require-iframe-title](docs/rules/template-require-iframe-title.md) | require iframe elements to have a title attribute | | | |
202203
| [template-require-input-label](docs/rules/template-require-input-label.md) | require label for form input elements | | | |
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# ember/template-no-whitespace-within-word
2+
3+
<!-- end auto-generated rule header -->
4+
5+
In practice, the predominant issue raised by inline whitespace styling is that the resultant text "formatting" is entirely visual in nature; the ability to discern the correct manner in which to read the text, and therefore, to correctly comprehend its meaning, is restricted to sighted users.
6+
7+
Using in-line whitespace word formatting produces results that are explicitly mentioned in [WCAG's list of common sources of web accessibility failures](https://www.w3.org/TR/WCAG20-TECHS/failures.html). Specifically, this common whitespace-within-word-induced web accessibility issue fails to successfully achieve [WCAG Success Criterion 1.3.2: Meaningful Sequence](https://www.w3.org/TR/UNDERSTANDING-WCAG20/content-structure-separation-sequence.html).
8+
9+
The `template-no-whitespace-within-word` rule operates on the assumption that artificially-spaced English words in rendered text content contain, at a minimum, two word characters fencepost-delimited by three whitespace characters (`space-char-space-char-space`) so it should be avoided.
10+
11+
## Examples
12+
13+
This rule **forbids** the following:
14+
15+
```gjs
16+
<template>
17+
W e l c o m e
18+
</template>
19+
```
20+
21+
`W`**`&nbsp;`**`e`**`&nbsp;`**`l`**`&nbsp;`**`c`**`&nbsp;`**`o`**`&nbsp;`**`m`**`&nbsp;`**`e`
22+
23+
`Wel c o me`
24+
25+
`Wel`**`&nbsp;`**`c`**`&emsp;`**`o`**`&nbsp;`**`me`
26+
27+
```gjs
28+
<template>
29+
<div>W e l c o m e</div>
30+
31+
<div>Wel c o me</div>
32+
</template>
33+
```
34+
35+
This rule **allows** the following:
36+
37+
`Welcome`
38+
39+
`Yes`**`&nbsp;`**`I`**`&nbsp;`**`am`
40+
41+
`It is possible to get some examples of in-word emph a sis past this rule.`
42+
43+
`However, I do not want a rule that flags annoying false positives for correctly-used single-character words.`
44+
45+
```gjs
46+
<template>
47+
<div>Welcome</div>
48+
49+
<div>Yes&nbsp;I am.</div>
50+
</template>
51+
```
52+
53+
This rule uses the heuristic of letter, whitespace character, letter, whitespace character, letter which makes it a good candidate for most use cases, but not ideal for some languages (such as Japanese).
54+
55+
## Migration
56+
57+
Use CSS to add letter-spacing to a word.
58+
59+
## References
60+
61+
- [F32: Using white space characters to create multiple columns in plain text content](https://www.w3.org/TR/WCAG20-TECHS/failures.html#F32)
62+
- [WCAG Success Criterion 1.3.2: Meaningful Sequence](https://www.w3.org/TR/UNDERSTANDING-WCAG20/content-structure-separation-sequence.html)
63+
- [C8: Using CSS letter-spacing to control spacing within a word](https://www.w3.org/WAI/WCAG21/Techniques/css/C8)
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
const WHITESPACE_ENTITY_LIST = [
2+
'&#32;',
3+
'&#160;',
4+
'&nbsp;',
5+
'&NonBreakingSpace;',
6+
'&#8194;',
7+
'&ensp;',
8+
'&#8195;',
9+
'&emsp;',
10+
'&#8196;',
11+
'&emsp13;',
12+
'&#8197;',
13+
'&emsp14;',
14+
'&#8199;',
15+
'&numsp;',
16+
'&#8200;',
17+
'&puncsp;',
18+
'&#8201;',
19+
'&thinsp;',
20+
'&ThinSpace;',
21+
'&#8202;',
22+
'&hairsp;',
23+
'&VeryThinSpace;',
24+
'&ThickSpace;',
25+
'&#8203;',
26+
'&ZeroWidthSpace;',
27+
'&NegativeVeryThinSpace;',
28+
'&NegativeThinSpace;',
29+
'&NegativeMediumSpace;',
30+
'&NegativeThickSpace;',
31+
'&#8204;',
32+
'&zwnj;',
33+
'&#8205;',
34+
'&zwj;',
35+
'&#8206;',
36+
'&lrm;',
37+
'&#8207;',
38+
'&rlm;',
39+
'&#8287;',
40+
'&MediumSpace;',
41+
'&ThickSpace;',
42+
'&#8288;',
43+
'&NoBreak;',
44+
'&#8289;',
45+
'&ApplyFunction;',
46+
'&af;',
47+
'&#8290;',
48+
'&InvisibleTimes;',
49+
'&it;',
50+
'&#8291;',
51+
'&InvisibleComma;',
52+
'&ic;',
53+
];
54+
55+
const CHARACTER_REGEX = '[a-zA-Z]';
56+
57+
// Build a regex that catches alternating non-whitespace/whitespace characters,
58+
// for example, 'W e l c o m e'. The pattern requires 5 alternations to avoid
59+
// false positives: (whitespace)(char)(whitespace)(char)(whitespace)
60+
const whitespaceOrEntityRegex = `(?:\\s|${WHITESPACE_ENTITY_LIST.map(
61+
(entity) => `\\${entity}`
62+
).join('|')})+`;
63+
const WHITESPACE_WITHIN_WORD_REGEX = new RegExp(
64+
`${whitespaceOrEntityRegex}${CHARACTER_REGEX}${whitespaceOrEntityRegex}${CHARACTER_REGEX}${whitespaceOrEntityRegex}`
65+
);
66+
67+
/** @type {import('eslint').Rule.RuleModule} */
68+
module.exports = {
69+
meta: {
70+
type: 'layout',
71+
docs: {
72+
description: 'disallow excess whitespace within words (e.g. "W e l c o m e")',
73+
category: 'Accessibility',
74+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-whitespace-within-word.md',
75+
templateMode: 'both',
76+
},
77+
schema: [],
78+
messages: {
79+
excessWhitespace: 'Excess whitespace in layout detected.',
80+
},
81+
originallyFrom: {
82+
name: 'ember-template-lint',
83+
rule: 'lib/rules/no-whitespace-within-word.js',
84+
docs: 'docs/rule/no-whitespace-within-word.md',
85+
tests: 'test/unit/rules/no-whitespace-within-word-test.js',
86+
},
87+
},
88+
89+
create(context) {
90+
const sourceCode = context.getSourceCode();
91+
92+
return {
93+
GlimmerTextNode(node) {
94+
// Skip text inside attributes
95+
let parent = node.parent;
96+
while (parent) {
97+
if (parent.type === 'GlimmerAttrNode') {
98+
return;
99+
}
100+
// Skip text inside <style> elements
101+
if (parent.type === 'GlimmerElementNode' && parent.tag === 'style') {
102+
return;
103+
}
104+
parent = parent.parent;
105+
}
106+
107+
const text = sourceCode.getText(node);
108+
if (WHITESPACE_WITHIN_WORD_REGEX.test(text)) {
109+
context.report({
110+
node,
111+
messageId: 'excessWhitespace',
112+
});
113+
}
114+
},
115+
};
116+
},
117+
};
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
const eslint = require('eslint');
2+
const rule = require('../../../lib/rules/template-no-whitespace-within-word');
3+
4+
const { RuleTester } = eslint;
5+
6+
const validHbs = [
7+
'Welcome',
8+
'Hey - I like this!',
9+
'Expected: 5-10 guests',
10+
'Expected: 5 - 10 guests',
11+
'It is possible to get some examples of in-word emph a sis past this rule.',
12+
'However, I do not want a rule that flags annoying false positives for correctly-used single-character words.',
13+
'<div>Welcome</div>',
14+
'<div enable-background="a b c d e f g h i j k l m">We want to ignore values of HTML attributes</div>',
15+
`<style>
16+
.my-custom-class > * {
17+
border: 2px dotted red;
18+
}
19+
</style>`,
20+
];
21+
22+
const invalidHbs = [
23+
{
24+
code: 'W e l c o m e',
25+
output: null,
26+
errors: [{ message: 'Excess whitespace in layout detected.' }],
27+
},
28+
{
29+
code: 'W&nbsp;e&nbsp;l&nbsp;c&nbsp;o&nbsp;m&nbsp;e',
30+
output: null,
31+
errors: [{ message: 'Excess whitespace in layout detected.' }],
32+
},
33+
{
34+
code: 'Wel c o me',
35+
output: null,
36+
errors: [{ message: 'Excess whitespace in layout detected.' }],
37+
},
38+
{
39+
code: 'Wel&nbsp;c&emsp;o&nbsp;me',
40+
output: null,
41+
errors: [{ message: 'Excess whitespace in layout detected.' }],
42+
},
43+
{
44+
code: '<div>W e l c o m e</div>',
45+
output: null,
46+
errors: [{ message: 'Excess whitespace in layout detected.' }],
47+
},
48+
{
49+
code: '<div>Wel c o me</div>',
50+
output: null,
51+
errors: [{ message: 'Excess whitespace in layout detected.' }],
52+
},
53+
{
54+
code: 'A B&nbsp;&nbsp; C ',
55+
output: null,
56+
errors: [{ message: 'Excess whitespace in layout detected.' }],
57+
},
58+
];
59+
60+
function wrapTemplate(entry) {
61+
if (typeof entry === 'string') {
62+
return `<template>${entry}</template>`;
63+
}
64+
65+
return {
66+
...entry,
67+
code: `<template>${entry.code}</template>`,
68+
output: entry.output ? `<template>${entry.output}</template>` : entry.output,
69+
};
70+
}
71+
72+
const gjsRuleTester = new RuleTester({
73+
parser: require.resolve('ember-eslint-parser'),
74+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
75+
});
76+
77+
gjsRuleTester.run('template-no-whitespace-within-word', rule, {
78+
valid: validHbs.map(wrapTemplate),
79+
invalid: invalidHbs.map(wrapTemplate),
80+
});
81+
82+
const hbsRuleTester = new RuleTester({
83+
parser: require.resolve('ember-eslint-parser/hbs'),
84+
parserOptions: {
85+
ecmaVersion: 2022,
86+
sourceType: 'module',
87+
},
88+
});
89+
90+
hbsRuleTester.run('template-no-whitespace-within-word', rule, {
91+
valid: validHbs,
92+
invalid: invalidHbs,
93+
});

0 commit comments

Comments
 (0)