Skip to content
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [template-no-nested-interactive](docs/rules/template-no-nested-interactive.md) | disallow nested interactive elements | | | |
| [template-no-nested-landmark](docs/rules/template-no-nested-landmark.md) | disallow nested landmark elements | | | |
| [template-no-pointer-down-event-binding](docs/rules/template-no-pointer-down-event-binding.md) | disallow pointer down event bindings | | | |
| [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") | | | |
| [template-require-aria-activedescendant-tabindex](docs/rules/template-require-aria-activedescendant-tabindex.md) | require non-interactive elements with aria-activedescendant to have tabindex | | 🔧 | |
| [template-require-iframe-title](docs/rules/template-require-iframe-title.md) | require iframe elements to have a title attribute | | | |
| [template-require-input-label](docs/rules/template-require-input-label.md) | require label for form input elements | | | |
Expand Down
63 changes: 63 additions & 0 deletions docs/rules/template-no-whitespace-within-word.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# ember/template-no-whitespace-within-word

<!-- end auto-generated rule header -->

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.

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).

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.

## Examples

This rule **forbids** the following:

```gjs
<template>
W e l c o m e
</template>
```

`W`**`&nbsp;`**`e`**`&nbsp;`**`l`**`&nbsp;`**`c`**`&nbsp;`**`o`**`&nbsp;`**`m`**`&nbsp;`**`e`

`Wel c o me`

`Wel`**`&nbsp;`**`c`**`&emsp;`**`o`**`&nbsp;`**`me`

```gjs
<template>
<div>W e l c o m e</div>

<div>Wel c o me</div>
</template>
```

This rule **allows** the following:

`Welcome`

`Yes`**`&nbsp;`**`I`**`&nbsp;`**`am`

`It is possible to get some examples of in-word emph a sis past this rule.`

`However, I do not want a rule that flags annoying false positives for correctly-used single-character words.`

```gjs
<template>
<div>Welcome</div>

<div>Yes&nbsp;I am.</div>
</template>
```

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).

## Migration

Use CSS to add letter-spacing to a word.

## References

- [F32: Using white space characters to create multiple columns in plain text content](https://www.w3.org/TR/WCAG20-TECHS/failures.html#F32)
- [WCAG Success Criterion 1.3.2: Meaningful Sequence](https://www.w3.org/TR/UNDERSTANDING-WCAG20/content-structure-separation-sequence.html)
- [C8: Using CSS letter-spacing to control spacing within a word](https://www.w3.org/WAI/WCAG21/Techniques/css/C8)
117 changes: 117 additions & 0 deletions lib/rules/template-no-whitespace-within-word.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
const WHITESPACE_ENTITY_LIST = [
'&#32;',
'&#160;',
'&nbsp;',
'&NonBreakingSpace;',
'&#8194;',
'&ensp;',
'&#8195;',
'&emsp;',
'&#8196;',
'&emsp13;',
'&#8197;',
'&emsp14;',
'&#8199;',
'&numsp;',
'&#8200;',
'&puncsp;',
'&#8201;',
'&thinsp;',
'&ThinSpace;',
'&#8202;',
'&hairsp;',
'&VeryThinSpace;',
'&ThickSpace;',
'&#8203;',
'&ZeroWidthSpace;',
'&NegativeVeryThinSpace;',
'&NegativeThinSpace;',
'&NegativeMediumSpace;',
'&NegativeThickSpace;',
'&#8204;',
'&zwnj;',
'&#8205;',
'&zwj;',
'&#8206;',
'&lrm;',
'&#8207;',
'&rlm;',
'&#8287;',
'&MediumSpace;',
'&ThickSpace;',
'&#8288;',
'&NoBreak;',
'&#8289;',
'&ApplyFunction;',
'&af;',
'&#8290;',
'&InvisibleTimes;',
'&it;',
'&#8291;',
'&InvisibleComma;',
'&ic;',
];

const CHARACTER_REGEX = '[a-zA-Z]';

// Build a regex that catches alternating non-whitespace/whitespace characters,
// for example, 'W e l c o m e'. The pattern requires 5 alternations to avoid
// false positives: (whitespace)(char)(whitespace)(char)(whitespace)
const whitespaceOrEntityRegex = `(?:\\s|${WHITESPACE_ENTITY_LIST.map(
(entity) => `\\${entity}`
).join('|')})+`;
const WHITESPACE_WITHIN_WORD_REGEX = new RegExp(
`${whitespaceOrEntityRegex}${CHARACTER_REGEX}${whitespaceOrEntityRegex}${CHARACTER_REGEX}${whitespaceOrEntityRegex}`
);

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'layout',
docs: {
description: 'disallow excess whitespace within words (e.g. "W e l c o m e")',
category: 'Accessibility',
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-whitespace-within-word.md',
templateMode: 'both',
},
schema: [],
messages: {
excessWhitespace: 'Excess whitespace in layout detected.',
},
originallyFrom: {
name: 'ember-template-lint',
rule: 'lib/rules/no-whitespace-within-word.js',
docs: 'docs/rule/no-whitespace-within-word.md',
tests: 'test/unit/rules/no-whitespace-within-word-test.js',
},
},

create(context) {
const sourceCode = context.getSourceCode();

return {
GlimmerTextNode(node) {
// Skip text inside attributes
let parent = node.parent;
while (parent) {
if (parent.type === 'GlimmerAttrNode') {
return;
}
// Skip text inside <style> elements
if (parent.type === 'GlimmerElementNode' && parent.tag === 'style') {
return;
}
parent = parent.parent;
}

const text = sourceCode.getText(node);
if (WHITESPACE_WITHIN_WORD_REGEX.test(text)) {
context.report({
node,
messageId: 'excessWhitespace',
});
}
},
};
},
};
93 changes: 93 additions & 0 deletions tests/lib/rules/template-no-whitespace-within-word.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const eslint = require('eslint');
const rule = require('../../../lib/rules/template-no-whitespace-within-word');

const { RuleTester } = eslint;

const validHbs = [
'Welcome',
'Hey - I like this!',
'Expected: 5-10 guests',
'Expected: 5 - 10 guests',
'It is possible to get some examples of in-word emph a sis past this rule.',
'However, I do not want a rule that flags annoying false positives for correctly-used single-character words.',
'<div>Welcome</div>',
'<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>',
`<style>
.my-custom-class > * {
border: 2px dotted red;
}
</style>`,
];

const invalidHbs = [
{
code: 'W e l c o m e',
output: null,
errors: [{ message: 'Excess whitespace in layout detected.' }],
},
{
code: 'W&nbsp;e&nbsp;l&nbsp;c&nbsp;o&nbsp;m&nbsp;e',
output: null,
errors: [{ message: 'Excess whitespace in layout detected.' }],
},
{
code: 'Wel c o me',
output: null,
errors: [{ message: 'Excess whitespace in layout detected.' }],
},
{
code: 'Wel&nbsp;c&emsp;o&nbsp;me',
output: null,
errors: [{ message: 'Excess whitespace in layout detected.' }],
},
{
code: '<div>W e l c o m e</div>',
output: null,
errors: [{ message: 'Excess whitespace in layout detected.' }],
},
{
code: '<div>Wel c o me</div>',
output: null,
errors: [{ message: 'Excess whitespace in layout detected.' }],
},
{
code: 'A B&nbsp;&nbsp; C ',
output: null,
errors: [{ message: 'Excess whitespace in layout detected.' }],
},
];

function wrapTemplate(entry) {
if (typeof entry === 'string') {
return `<template>${entry}</template>`;
}

return {
...entry,
code: `<template>${entry.code}</template>`,
output: entry.output ? `<template>${entry.output}</template>` : entry.output,
};
}

const gjsRuleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser'),
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
});

gjsRuleTester.run('template-no-whitespace-within-word', rule, {
valid: validHbs.map(wrapTemplate),
invalid: invalidHbs.map(wrapTemplate),
});

const hbsRuleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser/hbs'),
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
});

hbsRuleTester.run('template-no-whitespace-within-word', rule, {
valid: validHbs,
invalid: invalidHbs,
});
Loading