From 0f5b9d5e7898548949a83bb207c49be5d1947269 Mon Sep 17 00:00:00 2001 From: RobertAKARobin Date: Thu, 29 Aug 2024 12:39:02 -0500 Subject: [PATCH 1/7] add no-extra-spacing-text --- docs/rules/no-extra-spacing-text.md | 69 ++++++++++ packages/eslint-plugin/lib/rules/index.js | 2 + .../lib/rules/no-extra-spacing-text.js | 115 +++++++++++++++++ packages/eslint-plugin/lib/types.d.ts | 8 ++ .../tests/rules/no-extra-spacing-text.test.js | 122 ++++++++++++++++++ 5 files changed, 316 insertions(+) create mode 100644 docs/rules/no-extra-spacing-text.md create mode 100644 packages/eslint-plugin/lib/rules/no-extra-spacing-text.js create mode 100644 packages/eslint-plugin/tests/rules/no-extra-spacing-text.test.js diff --git a/docs/rules/no-extra-spacing-text.md b/docs/rules/no-extra-spacing-text.md new file mode 100644 index 00000000..19297f77 --- /dev/null +++ b/docs/rules/no-extra-spacing-text.md @@ -0,0 +1,69 @@ +# 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 sequences of whitespace in text. + +When used with `--fix`, the rule will replace invalid whitespace with a single space. + +Note: + +- This rule ignores whitespace at the start and end of lines/strings so as not to conflict with indentation rules. See [./indent](@html-eslint/indent). +- This rule does **not** affect whitespace around attributes. See [./no-extra-spacing-attrs](@html-eslint/no-extra-spacing-attrs). + +Examples of **incorrect** code for this rule: + +```html,incorrect +
+ foo bar +
+``` + +Examples of **correct** code for this rule: + +```html,correct +
+ foo bar +
+``` + +### Options + +This rule has an object option: + +- `"skip"`: skips whitespace-checking within the specified elements. + +```ts +//... +"@html-eslint/element-newline": ["error", { + "skip": Array +}] +``` + +#### 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: + + +```html +
+ Only short whitespace here. + +
    Any    kind    of   whitespace    here!    
+
+``` diff --git a/packages/eslint-plugin/lib/rules/index.js b/packages/eslint-plugin/lib/rules/index.js index cc40ac0c..bea5420a 100644 --- a/packages/eslint-plugin/lib/rules/index.js +++ b/packages/eslint-plugin/lib/rules/index.js @@ -6,6 +6,7 @@ const noDuplicateId = require("./no-duplicate-id"); const noInlineStyles = require("./no-inline-styles"); const noMultipleH1 = require("./no-multiple-h1"); const noExtraSpacingAttrs = require("./no-extra-spacing-attrs"); +const noExtraSpacingText = require("./no-extra-spacing-text"); const attrsNewline = require("./attrs-newline"); const elementNewLine = require("./element-newline"); const noSkipHeadingLevels = require("./no-skip-heading-levels"); @@ -46,6 +47,7 @@ module.exports = { "no-inline-styles": noInlineStyles, "no-multiple-h1": noMultipleH1, "no-extra-spacing-attrs": noExtraSpacingAttrs, + "no-extra-spacing-text": noExtraSpacingText, "attrs-newline": attrsNewline, "element-newline": elementNewLine, "no-skip-heading-levels": noSkipHeadingLevels, diff --git a/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js b/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js new file mode 100644 index 00000000..2bb71a9c --- /dev/null +++ b/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js @@ -0,0 +1,115 @@ +/** + * @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]: "Multiple consecutive spaces not allowed here", + }, + }, + + create(context) { + const options = context.options[0] || {}; + const skipTags = options.skip || []; + const sourceCode = context.getSourceCode(); + + /** + * @param {Array} 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]{2,})($|[^\n \t])/g; + + while (true) { + // eslint-disable-line no-constant-condition + const offender = matcher.exec(text); + if (offender === null) { + break; + } + + const space = offender[2]; + const spaceAfter = offender[3]; + const indexStart = + node.range[0] + matcher.lastIndex - space.length - spaceAfter.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], ` `); + }, + }); + } + } + }, +}; diff --git a/packages/eslint-plugin/lib/types.d.ts b/packages/eslint-plugin/lib/types.d.ts index fd634026..25a25d25 100644 --- a/packages/eslint-plugin/lib/types.d.ts +++ b/packages/eslint-plugin/lib/types.d.ts @@ -267,3 +267,11 @@ export type ChildType = T extends ProgramNode : T extends TagNode ? T["children"][number] : never; + +export type ContentNode = + | CommentNode + | DoctypeNode + | ScriptTagNode + | StyleTagNode + | TagNode + | TextNode; diff --git a/packages/eslint-plugin/tests/rules/no-extra-spacing-text.test.js b/packages/eslint-plugin/tests/rules/no-extra-spacing-text.test.js new file mode 100644 index 00000000..b32539bc --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-extra-spacing-text.test.js @@ -0,0 +1,122 @@ +const createRuleTester = require("../rule-tester"); +const rule = require("../../lib/rules/no-extra-spacing-text"); + +/** + * @param {Array>} positions + */ +function errorsAt(...positions) { + return positions.map(([line, column, length]) => ({ + messageId: `unexpected`, + line, + column, + endLine: line, + endColumn: column + length, + })); +} + +const ruleTester = createRuleTester(); + +ruleTester.run("no-extra-spacing-text", rule, { + valid: [ + { + code: `
foo
`, + }, + + { + code: `
\tfoo\tbar\t
`, + }, + + { + code: ` +\t
+ \t
+ foo + bar + \t\t
+\t
+`, + }, + + { + code: `
   foo   bar   
`, + options: [ + { + skip: [`pre`], + }, + ], + }, + + { + code: ` +
+ Only short whitespace here. + +
    Any    kind    of   whitespace    here!    
+
+`, + options: [ + { + skip: [`pre`], + }, + ], + }, + ], + + invalid: [ + { + code: `foo bar`, + output: `foo bar`, + errors: errorsAt([1, 4, 3]), + }, + + { + code: `
\tfoo \t
`, + output: `
\tfoo
`, + errors: errorsAt([1, 10, 2]), + }, + + { + code: `
foo
`, + output: `
foo
`, + errors: errorsAt([1, 6, 2], [1, 11, 3]), + }, + + { + code: ` +
\t\tfoo \t\t
+`, + output: ` +
foo
+`, + errors: errorsAt([2, 10, 3], [2, 16, 3]), + }, + + { + code: ` +
+ foo bar +
+`, + output: ` +
+ foo bar +
+`, + errors: errorsAt([3, 6, 5]), + }, + + { + code: `\n\n \n\n`, + output: `\n\n \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] + ), + }, + ], +}); From c2f10a6d07448a8046c5ba039cd4f59f746fa882 Mon Sep 17 00:00:00 2001 From: RobertAKARobin Date: Thu, 29 Aug 2024 12:40:54 -0500 Subject: [PATCH 2/7] Add to doc index --- docs/rules.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/rules.md b/docs/rules.md index aac5a14f..becaf253 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -58,6 +58,7 @@ | [indent](rules/indent) | Enforce consistent indentation | ⭐🔧 | | [lowercase](rules/lowercase) | Enforce to use lowercase for tag and attribute names. | 🔧 | | [no-extra-spacing-attrs](rules/no-extra-spacing-attrs) | Disallow an extra spacing around attributes | ⭐🔧 | +| [no-extra-spacing-text](rules/no-extra-spacing-text) | Disallow extra spacing in text | 🔧 | | [no-multiple-empty-lines](rules/no-multiple-empty-lines) | Disallow multiple empty lines | 🔧 | | [no-trailing-spaces](rules/no-trailing-spaces) | Disallow trailing whitespace at the end of lines | 🔧 | | [quotes](rules/quotes) | Enforce consistent quoting attributes with double(") or single(') | ⭐🔧 | From 90ab772a65ad2c3e66c05dc7935f891c2ee15e0e Mon Sep 17 00:00:00 2001 From: RobertAKARobin Date: Thu, 29 Aug 2024 12:51:15 -0500 Subject: [PATCH 3/7] fix lint --- packages/eslint-plugin/lib/rules/no-extra-spacing-text.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js b/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js index 2bb71a9c..ce794e67 100644 --- a/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js +++ b/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js @@ -85,8 +85,8 @@ module.exports = { const text = node.value; const matcher = /(^|[^\n \t])([ \t]{2,})($|[^\n \t])/g; + // eslint-disable-next-line no-constant-condition while (true) { - // eslint-disable-line no-constant-condition const offender = matcher.exec(text); if (offender === null) { break; From d788facef27ea97c649f9e41f85527d5da856016 Mon Sep 17 00:00:00 2001 From: RobertAKARobin Date: Tue, 3 Sep 2024 12:50:56 -0500 Subject: [PATCH 4/7] Strip trailing spaces --- .vscode/settings.json | 4 +- docs/rules/no-extra-spacing-text.md | 7 +- .../lib/rules/no-extra-spacing-text.js | 13 ++-- .../tests/rules/no-extra-spacing-text.test.js | 75 ++++++++++++++----- 4 files changed, 71 insertions(+), 28 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6171e827..1e741cbb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,7 @@ "eslint.validate": [ "javascript", // ... "html" // Add "html" to enable linting `.html` files. - ] + ], + + "files.trimTrailingWhitespace": false } diff --git a/docs/rules/no-extra-spacing-text.md b/docs/rules/no-extra-spacing-text.md index 19297f77..8bdec5e2 100644 --- a/docs/rules/no-extra-spacing-text.md +++ b/docs/rules/no-extra-spacing-text.md @@ -14,13 +14,16 @@ module.exports = { ## 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 sequences of whitespace in text. +[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 whitepsace character When used with `--fix`, the rule will replace invalid whitespace with a single space. Note: -- This rule ignores whitespace at the start and end of lines/strings so as not to conflict with indentation rules. See [./indent](@html-eslint/indent). +- This rule ignores whitespace at the start of lines in order to not conflict with indentation rules. See [./indent](@html-eslint/indent). - This rule does **not** affect whitespace around attributes. See [./no-extra-spacing-attrs](@html-eslint/no-extra-spacing-attrs). Examples of **incorrect** code for this rule: diff --git a/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js b/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js index ce794e67..6698b7eb 100644 --- a/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js +++ b/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js @@ -41,7 +41,7 @@ module.exports = { }, ], messages: { - [MESSAGE_IDS.UNEXPECTED]: "Multiple consecutive spaces not allowed here", + [MESSAGE_IDS.UNEXPECTED]: "Tabs and/or multiple consecutive spaces not allowed here", }, }, @@ -83,7 +83,7 @@ module.exports = { */ function stripConsecutiveSpaces(node) { const text = node.value; - const matcher = /(^|[^\n \t])([ \t]{2,})($|[^\n \t])/g; + const matcher = /(^|[^\n \t])([ \t]+\n|\t[\t ]*|[ \t]{2,})/g; // eslint-disable-next-line no-constant-condition while (true) { @@ -93,9 +93,7 @@ module.exports = { } const space = offender[2]; - const spaceAfter = offender[3]; - const indexStart = - node.range[0] + matcher.lastIndex - space.length - spaceAfter.length; + const indexStart = node.range[0] + matcher.lastIndex - space.length; const indexEnd = indexStart + space.length; context.report({ @@ -106,7 +104,10 @@ module.exports = { }, messageId: MESSAGE_IDS.UNEXPECTED, fix(fixer) { - return fixer.replaceTextRange([indexStart, indexEnd], ` `); + return fixer.replaceTextRange( + [indexStart, indexEnd], + space.endsWith(`\n`) ? `\n` : ` ` + ); }, }); } diff --git a/packages/eslint-plugin/tests/rules/no-extra-spacing-text.test.js b/packages/eslint-plugin/tests/rules/no-extra-spacing-text.test.js index b32539bc..10b41552 100644 --- a/packages/eslint-plugin/tests/rules/no-extra-spacing-text.test.js +++ b/packages/eslint-plugin/tests/rules/no-extra-spacing-text.test.js @@ -1,17 +1,29 @@ const createRuleTester = require("../rule-tester"); const rule = require("../../lib/rules/no-extra-spacing-text"); -/** - * @param {Array>} positions - */ + function errorsAt(...positions) { - return positions.map(([line, column, length]) => ({ - messageId: `unexpected`, - line, - column, - endLine: line, - endColumn: column + length, - })); + 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(); @@ -23,7 +35,7 @@ ruleTester.run("no-extra-spacing-text", rule, { }, { - code: `
\tfoo\tbar\t
`, + code: `
foo bar
`, }, { @@ -38,7 +50,7 @@ ruleTester.run("no-extra-spacing-text", rule, { }, { - code: `
   foo   bar   
`, + code: `
   foo\t\t\tbar   
`, options: [ { skip: [`pre`], @@ -64,15 +76,15 @@ ruleTester.run("no-extra-spacing-text", rule, { invalid: [ { - code: `foo bar`, - output: `foo bar`, - errors: errorsAt([1, 4, 3]), + code: `foo bar `, + output: `foo bar `, + errors: errorsAt([1, 4, 3], [1, 10, 3]), }, { code: `
\tfoo \t
`, - output: `
\tfoo
`, - errors: errorsAt([1, 10, 2]), + output: `
foo
`, + errors: errorsAt([1, 6, 1], [1, 10, 2]), }, { @@ -81,6 +93,28 @@ ruleTester.run("no-extra-spacing-text", rule, { errors: errorsAt([1, 6, 2], [1, 11, 3]), }, + { + code: `
foo \n
`, + output: `
foo\n
`, + errors: errorsAt([1, 9, 2, 1]), + }, + + { + code: `
foo\t\n
`, + output: `
foo\n
`, + errors: errorsAt([1, 9, 2, 1]), + }, + + { + code: `
\n\tfoo \n
\n
\n\tbar\t\n
`, + output: `
\n\tfoo\n
\n
\n\tbar\n
`, + errors: errorsAt( + [2, 5, 3, 1], + [3, 7, 4, 1], + [5, 5, 6, 1], + ), + }, + { code: `
\t\tfoo \t\t
@@ -94,7 +128,7 @@ ruleTester.run("no-extra-spacing-text", rule, { { code: `
- foo bar + foo bar
`, output: ` @@ -102,7 +136,10 @@ ruleTester.run("no-extra-spacing-text", rule, { foo bar `, - errors: errorsAt([3, 6, 5]), + errors: errorsAt( + [3, 6, 5], + [3, 14, 4, 1], + ), }, { From 31699c2e7f28de3b4fd3043003d82d5eb26c0226 Mon Sep 17 00:00:00 2001 From: RobertAKARobin Date: Tue, 3 Sep 2024 12:53:23 -0500 Subject: [PATCH 5/7] prettier --- docs/rules/no-extra-spacing-text.md | 2 +- .../lib/rules/no-extra-spacing-text.js | 3 ++- .../tests/rules/no-extra-spacing-text.test.js | 14 +++----------- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/docs/rules/no-extra-spacing-text.md b/docs/rules/no-extra-spacing-text.md index 8bdec5e2..9121e9d7 100644 --- a/docs/rules/no-extra-spacing-text.md +++ b/docs/rules/no-extra-spacing-text.md @@ -17,7 +17,7 @@ module.exports = { [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 whitepsace character +- Sequences of more than 1 whitespace character When used with `--fix`, the rule will replace invalid whitespace with a single space. diff --git a/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js b/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js index 6698b7eb..7dc384d2 100644 --- a/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js +++ b/packages/eslint-plugin/lib/rules/no-extra-spacing-text.js @@ -41,7 +41,8 @@ module.exports = { }, ], messages: { - [MESSAGE_IDS.UNEXPECTED]: "Tabs and/or multiple consecutive spaces not allowed here", + [MESSAGE_IDS.UNEXPECTED]: + "Tabs and/or multiple consecutive spaces not allowed here", }, }, diff --git a/packages/eslint-plugin/tests/rules/no-extra-spacing-text.test.js b/packages/eslint-plugin/tests/rules/no-extra-spacing-text.test.js index 10b41552..5e9daec5 100644 --- a/packages/eslint-plugin/tests/rules/no-extra-spacing-text.test.js +++ b/packages/eslint-plugin/tests/rules/no-extra-spacing-text.test.js @@ -1,9 +1,8 @@ const createRuleTester = require("../rule-tester"); const rule = require("../../lib/rules/no-extra-spacing-text"); - function errorsAt(...positions) { - return positions.map(input => { + return positions.map((input) => { const [line, column, length] = input; if (input.length === 3) { return { @@ -108,11 +107,7 @@ ruleTester.run("no-extra-spacing-text", rule, { { code: `
\n\tfoo \n
\n
\n\tbar\t\n
`, output: `
\n\tfoo\n
\n
\n\tbar\n
`, - errors: errorsAt( - [2, 5, 3, 1], - [3, 7, 4, 1], - [5, 5, 6, 1], - ), + errors: errorsAt([2, 5, 3, 1], [3, 7, 4, 1], [5, 5, 6, 1]), }, { @@ -136,10 +131,7 @@ ruleTester.run("no-extra-spacing-text", rule, { foo bar `, - errors: errorsAt( - [3, 6, 5], - [3, 14, 4, 1], - ), + errors: errorsAt([3, 6, 5], [3, 14, 4, 1]), }, { From 11e7a51b843d3cc1078b497eb8c88a962f8b6482 Mon Sep 17 00:00:00 2001 From: RobertAKARobin Date: Tue, 3 Sep 2024 12:58:20 -0500 Subject: [PATCH 6/7] undo vscode trimTrailingWhitespace --- .vscode/settings.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 1e741cbb..6171e827 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,5 @@ "eslint.validate": [ "javascript", // ... "html" // Add "html" to enable linting `.html` files. - ], - - "files.trimTrailingWhitespace": false + ] } From 77332420ee12c2bd463e38016f09fc008fcf1b66 Mon Sep 17 00:00:00 2001 From: RobertAKARobin Date: Tue, 3 Sep 2024 21:29:20 -0500 Subject: [PATCH 7/7] Add trailing space doc --- docs/rules/no-extra-spacing-text.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/rules/no-extra-spacing-text.md b/docs/rules/no-extra-spacing-text.md index 9121e9d7..18ff40e1 100644 --- a/docs/rules/no-extra-spacing-text.md +++ b/docs/rules/no-extra-spacing-text.md @@ -18,13 +18,15 @@ module.exports = { - 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 [./indent](@html-eslint/indent). -- This rule does **not** affect whitespace around attributes. See [./no-extra-spacing-attrs](@html-eslint/no-extra-spacing-attrs). +- 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: