-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adds no-extra-spacing-text (#215)
* add no-extra-spacing-text * Add to doc index * fix lint * Strip trailing spaces * prettier * undo vscode trimTrailingWhitespace * Add trailing space doc
- Loading branch information
1 parent
402d7fa
commit 8adb2a5
Showing
6 changed files
with
353 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
# no-extra-spacing-text | ||
|
||
This rule disallows multiple consecutive spaces or tabs in text and comments. | ||
|
||
## How to use | ||
|
||
```js,.eslintrc.js | ||
module.exports = { | ||
rules: { | ||
"@html-eslint/no-extra-spacing-text": "error", | ||
}, | ||
}; | ||
``` | ||
|
||
## Rule Details | ||
|
||
[Whitespace in HTML is largely ignored](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace), so the purpose of this rule is to prevent unnecessary whitespace in text, such as: | ||
|
||
- Tab characters | ||
- Sequences of more than 1 whitespace character | ||
- Whitespace at the end of a line | ||
|
||
When used with `--fix`, the rule will replace invalid whitespace with a single space. | ||
|
||
Note: | ||
|
||
- This rule ignores whitespace at the start of lines in order to not conflict with indentation rules. See [@html-eslint/indent](./indent). | ||
- This rule strips whitespace from the end of lines, as does [@html-eslint/no-trailing-spaces](./no-trailing-spaces). | ||
- This rule does **not** affect whitespace around attributes. See [@html-eslint/no-extra-spacing-attrs](./no-extra-spacing-attrs). | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```html,incorrect | ||
<div foo = " bar " > | ||
foo bar | ||
</div> | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```html,correct | ||
<div foo="bar"> | ||
foo bar | ||
</div> | ||
``` | ||
|
||
### Options | ||
|
||
This rule has an object option: | ||
|
||
- `"skip"`: skips whitespace-checking within the specified elements. | ||
|
||
```ts | ||
//... | ||
"@html-eslint/element-newline": ["error", { | ||
"skip": Array<string> | ||
}] | ||
``` | ||
|
||
#### skip | ||
|
||
You can specify a list of tag names in the `skip` option. | ||
Whitespace-checking is not performed on children of the specified tags. | ||
|
||
Examples of **correct** code for the `{ "skip": ["pre"] }` option: | ||
|
||
<!-- prettier-ignore --> | ||
```html | ||
<div> | ||
Only short whitespace here. | ||
|
||
<pre> Any kind of whitespace here! </pre> | ||
</div> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
117 changes: 117 additions & 0 deletions
117
packages/eslint-plugin/lib/rules/no-extra-spacing-text.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
/** | ||
* @typedef { import("../types").RuleModule } RuleModule | ||
* @typedef { import("../types").ProgramNode } ProgramNode | ||
* @typedef { import("es-html-parser").CommentContentNode } CommentContentNode | ||
* @typedef { import("../types").ContentNode } ContentNode | ||
* @typedef { import("../types").TextNode } TextNode | ||
*/ | ||
|
||
const { RULE_CATEGORY } = require("../constants"); | ||
|
||
const MESSAGE_IDS = { | ||
UNEXPECTED: "unexpected", | ||
}; | ||
|
||
/** | ||
* @type {RuleModule} | ||
*/ | ||
module.exports = { | ||
meta: { | ||
type: "code", | ||
|
||
docs: { | ||
description: "Disallow unnecessary consecutive spaces", | ||
category: RULE_CATEGORY.BEST_PRACTICE, | ||
recommended: false, | ||
}, | ||
|
||
fixable: true, | ||
schema: [ | ||
{ | ||
type: "object", | ||
properties: { | ||
skip: { | ||
type: "array", | ||
items: { | ||
type: "string", | ||
}, | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
messages: { | ||
[MESSAGE_IDS.UNEXPECTED]: | ||
"Tabs and/or multiple consecutive spaces not allowed here", | ||
}, | ||
}, | ||
|
||
create(context) { | ||
const options = context.options[0] || {}; | ||
const skipTags = options.skip || []; | ||
const sourceCode = context.getSourceCode(); | ||
|
||
/** | ||
* @param {Array<ContentNode>} siblings | ||
*/ | ||
function checkSiblings(siblings) { | ||
for ( | ||
let length = siblings.length, index = 0; | ||
index < length; | ||
index += 1 | ||
) { | ||
const node = siblings[index]; | ||
|
||
if (node.type === `Tag` && skipTags.includes(node.name) === false) { | ||
checkSiblings(node.children); | ||
} else if (node.type === `Text`) { | ||
stripConsecutiveSpaces(node); | ||
} else if (node.type === `Comment`) { | ||
stripConsecutiveSpaces(node.value); | ||
} | ||
} | ||
} | ||
|
||
return { | ||
Program(node) { | ||
// @ts-ignore | ||
checkSiblings(node.body); | ||
}, | ||
}; | ||
|
||
/** | ||
* @param {TextNode | CommentContentNode} node | ||
*/ | ||
function stripConsecutiveSpaces(node) { | ||
const text = node.value; | ||
const matcher = /(^|[^\n \t])([ \t]+\n|\t[\t ]*|[ \t]{2,})/g; | ||
|
||
// eslint-disable-next-line no-constant-condition | ||
while (true) { | ||
const offender = matcher.exec(text); | ||
if (offender === null) { | ||
break; | ||
} | ||
|
||
const space = offender[2]; | ||
const indexStart = node.range[0] + matcher.lastIndex - space.length; | ||
const indexEnd = indexStart + space.length; | ||
|
||
context.report({ | ||
node: node, | ||
loc: { | ||
start: sourceCode.getLocFromIndex(indexStart), | ||
end: sourceCode.getLocFromIndex(indexEnd), | ||
}, | ||
messageId: MESSAGE_IDS.UNEXPECTED, | ||
fix(fixer) { | ||
return fixer.replaceTextRange( | ||
[indexStart, indexEnd], | ||
space.endsWith(`\n`) ? `\n` : ` ` | ||
); | ||
}, | ||
}); | ||
} | ||
} | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
151 changes: 151 additions & 0 deletions
151
packages/eslint-plugin/tests/rules/no-extra-spacing-text.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
const createRuleTester = require("../rule-tester"); | ||
const rule = require("../../lib/rules/no-extra-spacing-text"); | ||
|
||
function errorsAt(...positions) { | ||
return positions.map((input) => { | ||
const [line, column, length] = input; | ||
if (input.length === 3) { | ||
return { | ||
messageId: `unexpected`, | ||
line, | ||
column, | ||
endLine: line, | ||
endColumn: column + length, | ||
}; | ||
} else { | ||
const [line, column, endLine, endColumn] = input; | ||
return { | ||
messageId: `unexpected`, | ||
line, | ||
column, | ||
endLine, | ||
endColumn, | ||
}; | ||
} | ||
}); | ||
} | ||
|
||
const ruleTester = createRuleTester(); | ||
|
||
ruleTester.run("no-extra-spacing-text", rule, { | ||
valid: [ | ||
{ | ||
code: `<div> foo </div>`, | ||
}, | ||
|
||
{ | ||
code: `<div> foo bar </div>`, | ||
}, | ||
|
||
{ | ||
code: ` | ||
\t <div> | ||
\t <div> | ||
foo | ||
bar | ||
\t\t </div> | ||
\t </div> | ||
`, | ||
}, | ||
|
||
{ | ||
code: `<pre> foo\t\t\tbar </pre><script> const foo = 'bar' </script><style> .foo { bar } </style>`, | ||
options: [ | ||
{ | ||
skip: [`pre`], | ||
}, | ||
], | ||
}, | ||
|
||
{ | ||
code: ` | ||
<div foo = "bar"> | ||
Only short whitespace here. | ||
<pre> Any kind of whitespace here! </pre> | ||
</div> | ||
`, | ||
options: [ | ||
{ | ||
skip: [`pre`], | ||
}, | ||
], | ||
}, | ||
], | ||
|
||
invalid: [ | ||
{ | ||
code: `foo bar `, | ||
output: `foo bar `, | ||
errors: errorsAt([1, 4, 3], [1, 10, 3]), | ||
}, | ||
|
||
{ | ||
code: `<div>\tfoo \t</div>`, | ||
output: `<div> foo </div>`, | ||
errors: errorsAt([1, 6, 1], [1, 10, 2]), | ||
}, | ||
|
||
{ | ||
code: `<div> foo </div>`, | ||
output: `<div> foo </div>`, | ||
errors: errorsAt([1, 6, 2], [1, 11, 3]), | ||
}, | ||
|
||
{ | ||
code: `<div>foo \n</div>`, | ||
output: `<div>foo\n</div>`, | ||
errors: errorsAt([1, 9, 2, 1]), | ||
}, | ||
|
||
{ | ||
code: `<div>foo\t\n</div>`, | ||
output: `<div>foo\n</div>`, | ||
errors: errorsAt([1, 9, 2, 1]), | ||
}, | ||
|
||
{ | ||
code: `<div>\n\tfoo \n</div> \n<div>\n\tbar\t\n</div>`, | ||
output: `<div>\n\tfoo\n</div>\n<div>\n\tbar\n</div>`, | ||
errors: errorsAt([2, 5, 3, 1], [3, 7, 4, 1], [5, 5, 6, 1]), | ||
}, | ||
|
||
{ | ||
code: ` | ||
<div> \t\tfoo \t\t</div> | ||
`, | ||
output: ` | ||
<div> foo </div> | ||
`, | ||
errors: errorsAt([2, 10, 3], [2, 16, 3]), | ||
}, | ||
|
||
{ | ||
code: ` | ||
<div> | ||
foo bar | ||
</div> | ||
`, | ||
output: ` | ||
<div> | ||
foo bar | ||
</div> | ||
`, | ||
errors: errorsAt([3, 6, 5], [3, 14, 4, 1]), | ||
}, | ||
|
||
{ | ||
code: `\n\n <div> <a> <!-- foo bar --> </a> </div>\n\n`, | ||
output: `\n\n <div> <a> <!-- foo bar --> </a> </div>\n\n`, | ||
errors: errorsAt( | ||
[3, 10, 3], | ||
[3, 16, 3], | ||
[3, 23, 3], | ||
[3, 29, 3], | ||
[3, 35, 3], | ||
[3, 41, 3], | ||
[3, 48, 3] | ||
), | ||
}, | ||
], | ||
}); |