Skip to content

Commit f7c847f

Browse files
committed
Extract rule: template-no-whitespace-within-word
1 parent a1378e5 commit f7c847f

4 files changed

Lines changed: 311 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

201202
### Best Practices
202203

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# ember/template-no-whitespace-within-word
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallow excess whitespace within words (e.g. "W e l c o m e").
6+
7+
## Rule Details
8+
9+
This rule detects text content where letters are separated by whitespace or whitespace HTML entities, producing a "spaced out" word effect like `W e l c o m e`. This pattern is an accessibility concern because screen readers will read each letter individually instead of reading the word.
10+
11+
The rule checks `GlimmerTextNode` content (excluding text inside attributes and `<style>` elements) for patterns matching alternating whitespace and letter characters.
12+
13+
## Examples
14+
15+
Examples of **incorrect** code for this rule:
16+
17+
```gjs
18+
<template>
19+
<span>W e l c o m e</span>
20+
</template>
21+
```
22+
23+
```gjs
24+
<template>
25+
<h1>H&nbsp;E&nbsp;L&nbsp;L&nbsp;O</h1>
26+
</template>
27+
```
28+
29+
Examples of **correct** code for this rule:
30+
31+
```gjs
32+
<template>
33+
<span>Welcome</span>
34+
</template>
35+
```
36+
37+
```gjs
38+
<template>
39+
<h1>Hello World</h1>
40+
</template>
41+
```
42+
43+
## Migration
44+
45+
Use CSS to add letter-spacing to a word.
46+
47+
## References
48+
49+
- [WCAG - Meaningful Sequence](https://www.w3.org/WAI/WCAG21/Understanding/meaningful-sequence.html)
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: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
const eslint = require('eslint');
2+
const rule = require('../../../lib/rules/template-no-whitespace-within-word');
3+
4+
const { RuleTester } = eslint;
5+
6+
const ruleTester = new RuleTester({
7+
parser: require.resolve('ember-eslint-parser'),
8+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
9+
});
10+
11+
ruleTester.run('template-no-whitespace-within-word', rule, {
12+
valid: [
13+
// No whitespace
14+
'<template>{{value}}</template>',
15+
'<template>{{this.property}}</template>',
16+
'<template>{{@arg}}</template>',
17+
'<template>{{#if condition}}content{{/if}}</template>',
18+
19+
'<template>Welcome</template>',
20+
'<template>Hey - I like this!</template>',
21+
'<template>Expected: 5-10 guests</template>',
22+
'<template>Expected: 5 - 10 guests</template>',
23+
'<template>It is possible to get some examples of in-word emph a sis past this rule.</template>',
24+
'<template>However, I do not want a rule that flags annoying false positives for correctly-used single-character words.</template>',
25+
'<template><div>Welcome</div></template>',
26+
'<template><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></template>',
27+
`<template><style>
28+
.my-custom-class > * {
29+
border: 2px dotted red;
30+
}
31+
</style></template>`,
32+
],
33+
invalid: [
34+
{
35+
code: '<template><div>W e l c o m e</div></template>',
36+
output: null,
37+
errors: [{ messageId: 'excessWhitespace' }],
38+
},
39+
{
40+
code: '<template><span>H e l l o</span></template>',
41+
output: null,
42+
errors: [{ messageId: 'excessWhitespace' }],
43+
},
44+
{
45+
code: '<template><p>C l i c k</p></template>',
46+
output: null,
47+
errors: [{ messageId: 'excessWhitespace' }],
48+
},
49+
50+
{
51+
code: '<template>W e l c o m e</template>',
52+
output: null,
53+
errors: [{ messageId: 'excessWhitespace' }],
54+
},
55+
{
56+
code: '<template>W&nbsp;e&nbsp;l&nbsp;c&nbsp;o&nbsp;m&nbsp;e</template>',
57+
output: null,
58+
errors: [{ messageId: 'excessWhitespace' }],
59+
},
60+
{
61+
code: '<template>Wel c o me</template>',
62+
output: null,
63+
errors: [{ messageId: 'excessWhitespace' }],
64+
},
65+
{
66+
code: '<template>Wel&nbsp;c&emsp;o&nbsp;me</template>',
67+
output: null,
68+
errors: [{ messageId: 'excessWhitespace' }],
69+
},
70+
{
71+
code: '<template><div>Wel c o me</div></template>',
72+
output: null,
73+
errors: [{ messageId: 'excessWhitespace' }],
74+
},
75+
{
76+
code: '<template>A B&nbsp;&nbsp; C </template>',
77+
output: null,
78+
errors: [{ messageId: 'excessWhitespace' }],
79+
},
80+
],
81+
});
82+
83+
const hbsRuleTester = new RuleTester({
84+
parser: require.resolve('ember-eslint-parser/hbs'),
85+
parserOptions: {
86+
ecmaVersion: 2022,
87+
sourceType: 'module',
88+
},
89+
});
90+
91+
hbsRuleTester.run('template-no-whitespace-within-word', rule, {
92+
valid: [
93+
'Welcome',
94+
'Hey - I like this!',
95+
'Expected: 5-10 guests',
96+
'Expected: 5 - 10 guests',
97+
'It is possible to get some examples of in-word emph a sis past this rule.',
98+
'However, I do not want a rule that flags annoying false positives for correctly-used single-character words.',
99+
'<div>Welcome</div>',
100+
'<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>',
101+
`<style>
102+
.my-custom-class > * {
103+
border: 2px dotted red;
104+
}
105+
</style>`,
106+
],
107+
invalid: [
108+
{
109+
code: 'W e l c o m e',
110+
output: null,
111+
errors: [{ message: 'Excess whitespace in layout detected.' }],
112+
},
113+
{
114+
code: 'W&nbsp;e&nbsp;l&nbsp;c&nbsp;o&nbsp;m&nbsp;e',
115+
output: null,
116+
errors: [{ message: 'Excess whitespace in layout detected.' }],
117+
},
118+
{
119+
code: 'Wel c o me',
120+
output: null,
121+
errors: [{ message: 'Excess whitespace in layout detected.' }],
122+
},
123+
{
124+
code: 'Wel&nbsp;c&emsp;o&nbsp;me',
125+
output: null,
126+
errors: [{ message: 'Excess whitespace in layout detected.' }],
127+
},
128+
{
129+
code: '<div>W e l c o m e</div>',
130+
output: null,
131+
errors: [{ message: 'Excess whitespace in layout detected.' }],
132+
},
133+
{
134+
code: '<div>Wel c o me</div>',
135+
output: null,
136+
errors: [{ message: 'Excess whitespace in layout detected.' }],
137+
},
138+
{
139+
code: 'A B&nbsp;&nbsp; C ',
140+
output: null,
141+
errors: [{ message: 'Excess whitespace in layout detected.' }],
142+
},
143+
],
144+
});

0 commit comments

Comments
 (0)