Skip to content

Commit

Permalink
Add no-whitespace-within-word rule (ember-template-lint#848)
Browse files Browse the repository at this point in the history
  • Loading branch information
MelSumner authored and rwjblue committed Oct 31, 2019
1 parent e867616 commit 1ad7534
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 0 deletions.
55 changes: 55 additions & 0 deletions docs/rule/no-whitespace-within-word.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
## no-whitespace-within-word

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 `no-whitespace-within-word` rule operates on the assumption that artifically-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:

```hbs
W e l c o m e
```

`W`**` `**`e`**` `**`l`**` `**`c`**` `**`o`**` `**`m`**` `**`e`

`Wel c o me`

`Wel`**` `**`c`**` `**`o`**` `**`me`

```hbs
<div>W e l c o m e</div>
<div>Wel c o me</div>
```

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

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

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)
1 change: 1 addition & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
* [no-unnecessary-concat](rule/no-unnecessary-concat.md)
* [no-unused-block-params](rule/no-unused-block-params.md)
* [no-whitespace-for-layout](rule/no-whitespace-for-layout.md)
* [no-whitespace-within-word](rule/no-whitespace-within-word.md)
* [quotes](rule/quotes.md)
* [require-iframe-title](rule/require-iframe-title.md)
* [require-valid-alt-text](rule/require-valid-alt-text.md)
Expand Down
1 change: 1 addition & 0 deletions lib/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ module.exports = {
'no-unnecessary-concat': require('./lint-no-unnecessary-concat'),
'no-unused-block-params': require('./lint-no-unused-block-params'),
'no-whitespace-for-layout': require('./lint-no-whitespace-for-layout'),
'no-whitespace-within-word': require('./lint-no-whitespace-within-word'),
quotes: require('./lint-quotes'),
'require-iframe-title': require('./lint-require-iframe-title'),
'require-valid-alt-text': require('./lint-require-valid-alt-text'),
Expand Down
143 changes: 143 additions & 0 deletions lib/rules/lint-no-whitespace-within-word.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
'use strict';
const Rule = require('./base');

const ERROR_MESSAGE = 'Excess whitespace in layout detected.';

const blackList = [
'&#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;',
];

function isWhitespace(char) {
return blackList.includes(char);
}

function splitTextByEntity(input) {
let result = [];

for (let i = 0; i < input.length; i++) {
let current = input[i];

if (current === '&') {
let possibleEndIndex = input.indexOf(';', i);

// this is a stand alone `&`
if (possibleEndIndex === -1) {
result.push(current);
}

// now we know we have an "entity like thing"
let possibleEntity = input.substring(i, possibleEndIndex + 1);
if (blackList.includes(possibleEntity)) {
result.push(possibleEntity);
i += possibleEntity.length - 1;
} else {
result.push(current);
}
} else {
result.push(current);
}
}

return result;
}

// The goal here is to catch alternating non-whitespace/whitespace
// characters, for example, in 'W e l c o m e'.
//
// So the final pattern boils down to this:
//
// (whitespace)(non-whitespace)(whitespace)(non-whitespace)(whitespace)
//
// Specifically using this "5 alternations" rule since any less than this
// will return false positives and any more than this should not be
// necessary in 99.99% of cases
module.exports = class NoWhitespaceWithinWord extends Rule {
visitor() {
return {
TextNode(node) {
let alternationCount = 0;
let source = this.sourceForNode(node);
let characters = splitTextByEntity(source);

for (let i = 0; i < characters.length; i++) {
let currentChar = characters[i];
let previousChar = i > 0 ? characters[i - 1] : undefined;

if (
(isWhitespace(currentChar) && !isWhitespace(previousChar)) ||
(!isWhitespace(currentChar) && isWhitespace(previousChar))
) {
alternationCount++;
} else {
alternationCount = 0;
}

if (alternationCount >= 5) {
this.log({
message: ERROR_MESSAGE,
line: node.loc && node.loc.start.line,
column: node.loc && node.loc.start.column,
source,
});

// no need to keep parsing, we've already reported
return;
}
}
},
};
}
};

module.exports.ERROR_MESSAGE = ERROR_MESSAGE;
82 changes: 82 additions & 0 deletions test/unit/rules/lint-no-whitespace-within-word-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// no-whitespace-within-word-test.js

'use strict';

const generateRuleTests = require('../../helpers/rule-test-harness');
const ERROR_MESSAGE = require('../../../lib/rules/lint-no-whitespace-within-word').ERROR_MESSAGE;

generateRuleTests({
name: 'no-whitespace-within-word',
config: true,

good: [
'Welcome',
`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>',
],

bad: [
{
template: 'W e l c o m e',

result: {
moduleId: 'layout.hbs',
message: ERROR_MESSAGE,
line: 1,
column: 0,
source: 'W e l c o m e',
},
},
{
template: 'W&nbsp;e&nbsp;l&nbsp;c&nbsp;o&nbsp;m&nbsp;e',
result: {
moduleId: 'layout.hbs',
message: ERROR_MESSAGE,
line: 1,
column: 0,
source: 'W&nbsp;e&nbsp;l&nbsp;c&nbsp;o&nbsp;m&nbsp;e',
},
},
{
template: 'Wel c o me',
result: {
moduleId: 'layout.hbs',
message: ERROR_MESSAGE,
line: 1,
column: 0,
source: 'Wel c o me',
},
},
{
template: 'Wel&nbsp;c&emsp;o&nbsp;me',
result: {
moduleId: 'layout.hbs',
message: ERROR_MESSAGE,
line: 1,
column: 0,
source: 'Wel&nbsp;c&emsp;o&nbsp;me',
},
},
{
template: '<div>W e l c o m e</div>',
result: {
moduleId: 'layout.hbs',
message: ERROR_MESSAGE,
line: 1,
column: 5,
source: 'W e l c o m e',
},
},
{
template: '<div>Wel c o me</div>',
result: {
moduleId: 'layout.hbs',
message: ERROR_MESSAGE,
line: 1,
column: 5,
source: 'Wel c o me',
},
},
],
});

0 comments on commit 1ad7534

Please sign in to comment.