From 5aea916df5fc4b7fedf434a61935454ef289eb36 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 25 Aug 2025 18:12:53 +0200 Subject: [PATCH 1/2] Linter: Allow self-closing tags in SVG (`html-no-self-closing`) --- .../linter/src/rules/html-no-self-closing.ts | 21 +++--- .../test/rules/html-no-self-closing.test.ts | 70 ++++++++++++++----- 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/javascript/packages/linter/src/rules/html-no-self-closing.ts b/javascript/packages/linter/src/rules/html-no-self-closing.ts index b6fffce40..410f4d6d0 100644 --- a/javascript/packages/linter/src/rules/html-no-self-closing.ts +++ b/javascript/packages/linter/src/rules/html-no-self-closing.ts @@ -1,25 +1,30 @@ import { ParserRule } from "../types.js" -import { BaseRuleVisitor, getTagName, isVoidElement } from "./rule-utils.js" +import { BaseRuleVisitor, isVoidElement } from "./rule-utils.js" +import { getTagName } from "@herb-tools/core" import type { LintContext, LintOffense } from "../types.js" -import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core" +import type { HTMLOpenTagNode, HTMLElementNode, ParseResult } from "@herb-tools/core" class NoSelfClosingVisitor extends BaseRuleVisitor { + visitHTMLElementNode(node: HTMLElementNode): void { + if (getTagName(node) === "svg") { + this.visit(node.open_tag) + } else { + this.visitChildNodes(node) + } + } + visitHTMLOpenTagNode(node: HTMLOpenTagNode): void { if (node.tag_closing?.value === "/>") { const tagName = getTagName(node) - - const shouldBeVoid = tagName ? isVoidElement(tagName) : false - const instead = shouldBeVoid ? `Use \`<${tagName}>\` instead.` : `Use \`<${tagName}>\` instead.` + const instead = isVoidElement(tagName) ? `<${tagName}>` : `<${tagName}>` this.addOffense( - `Self-closing syntax \`<${tagName} />\` is not allowed in HTML. ${instead}`, + `Use \`${instead}\` instead of self-closing \`<${tagName} />\` for HTML compatibility.`, node.location, "error" ) } - - super.visitHTMLOpenTagNode(node) } } diff --git a/javascript/packages/linter/test/rules/html-no-self-closing.test.ts b/javascript/packages/linter/test/rules/html-no-self-closing.test.ts index 8f39af888..c0d648cb5 100644 --- a/javascript/packages/linter/test/rules/html-no-self-closing.test.ts +++ b/javascript/packages/linter/test/rules/html-no-self-closing.test.ts @@ -32,20 +32,22 @@ describe("html-no-self-closing", () => {
+ `; const linter = new Linter(Herb, [HTMLNoSelfClosingRule]); const lintResult = linter.lint(html); - expect(lintResult.errors).toBe(4); - expect(lintResult.offenses).toHaveLength(4); + expect(lintResult.errors).toBe(5); + expect(lintResult.offenses).toHaveLength(5); expect(lintResult.offenses[0].rule).toBe("html-no-self-closing"); expect(lintResult.offenses[0].severity).toBe("error"); - expect(lintResult.offenses[0].message).toBe('Self-closing syntax `
` is not allowed in HTML. Use `
` instead.') - expect(lintResult.offenses[1].message).toBe('Self-closing syntax `` is not allowed in HTML. Use `` instead.') - expect(lintResult.offenses[2].message).toBe('Self-closing syntax `
` is not allowed in HTML. Use `
` instead.') - expect(lintResult.offenses[3].message).toBe('Self-closing syntax `` is not allowed in HTML. Use `` instead.') + expect(lintResult.offenses[0].message).toBe('Use `
` instead of self-closing `
` for HTML compatibility.') + expect(lintResult.offenses[1].message).toBe('Use `` instead of self-closing `` for HTML compatibility.') + expect(lintResult.offenses[2].message).toBe('Use `
` instead of self-closing `
` for HTML compatibility.') + expect(lintResult.offenses[3].message).toBe('Use `` instead of self-closing `` for HTML compatibility.') + expect(lintResult.offenses[4].message).toBe('Use `` instead of self-closing `` for HTML compatibility.') }); test("fails for self-closing void elements", () => { @@ -64,10 +66,10 @@ describe("html-no-self-closing", () => { expect(lintResult.offenses[0].rule).toBe("html-no-self-closing"); expect(lintResult.offenses[0].severity).toBe("error"); - expect(lintResult.offenses[0].message).toBe('Self-closing syntax `` is not allowed in HTML. Use `` instead.') - expect(lintResult.offenses[1].message).toBe('Self-closing syntax `` is not allowed in HTML. Use `` instead.') - expect(lintResult.offenses[2].message).toBe('Self-closing syntax `
` is not allowed in HTML. Use `
` instead.') - expect(lintResult.offenses[3].message).toBe('Self-closing syntax `
` is not allowed in HTML. Use `
` instead.') + expect(lintResult.offenses[0].message).toBe('Use `` instead of self-closing `` for HTML compatibility.') + expect(lintResult.offenses[1].message).toBe('Use `` instead of self-closing `` for HTML compatibility.') + expect(lintResult.offenses[2].message).toBe('Use `
` instead of self-closing `
` for HTML compatibility.') + expect(lintResult.offenses[3].message).toBe('Use `
` instead of self-closing `
` for HTML compatibility.') }); test("passes for mixed correct and incorrect tags", () => { @@ -83,8 +85,8 @@ describe("html-no-self-closing", () => { expect(lintResult.errors).toBe(2); expect(lintResult.offenses).toHaveLength(2); - expect(lintResult.offenses[0].message).toBe('Self-closing syntax `` is not allowed in HTML. Use `` instead.') - expect(lintResult.offenses[1].message).toBe('Self-closing syntax `` is not allowed in HTML. Use `` instead.') + expect(lintResult.offenses[0].message).toBe('Use `` instead of self-closing `` for HTML compatibility.') + expect(lintResult.offenses[1].message).toBe('Use `` instead of self-closing `` for HTML compatibility.') }); test("passes for nested non-self-closing tags", () => { @@ -114,8 +116,8 @@ describe("html-no-self-closing", () => { expect(lintResult.errors).toBe(2); expect(lintResult.offenses).toHaveLength(2); - expect(lintResult.offenses[0].message).toBe('Self-closing syntax `` is not allowed in HTML. Use `` instead.') - expect(lintResult.offenses[1].message).toBe('Self-closing syntax `
` is not allowed in HTML. Use `
` instead.') + expect(lintResult.offenses[0].message).toBe('Use `` instead of self-closing `` for HTML compatibility.') + expect(lintResult.offenses[1].message).toBe('Use `
` instead of self-closing `
` for HTML compatibility.') }); test("passes for custom elements without self-closing", () => { @@ -141,8 +143,8 @@ describe("html-no-self-closing", () => { expect(lintResult.errors).toBe(2); expect(lintResult.offenses).toHaveLength(2); - expect(lintResult.offenses[0].message).toBe('Self-closing syntax `` is not allowed in HTML. Use `` instead.') - expect(lintResult.offenses[1].message).toBe('Self-closing syntax `` is not allowed in HTML. Use `` instead.') + expect(lintResult.offenses[0].message).toBe('Use `` instead of self-closing `` for HTML compatibility.') + expect(lintResult.offenses[1].message).toBe('Use `` instead of self-closing `` for HTML compatibility.') }); test("passes for void elements without self-closing", () => { @@ -158,4 +160,40 @@ describe("html-no-self-closing", () => { expect(lintResult.errors).toBe(0); expect(lintResult.offenses).toHaveLength(0); }); + + test("passes for self-closing elements inside SVG", () => { + const html = ` +
+ + + + + +
+ `; + const linter = new Linter(Herb, [HTMLNoSelfClosingRule]); + const lintResult = linter.lint(html); + + expect(lintResult.errors).toBe(0); + expect(lintResult.offenses).toHaveLength(0); + }); + + test("fails for self-closing elements outside SVG but passes inside SVG", () => { + const html = ` +
+ + + + + + `; + const linter = new Linter(Herb, [HTMLNoSelfClosingRule]); + const lintResult = linter.lint(html); + + expect(lintResult.errors).toBe(2); + expect(lintResult.offenses).toHaveLength(2); + + expect(lintResult.offenses[0].message).toBe('Use `
` instead of self-closing `
` for HTML compatibility.'); + expect(lintResult.offenses[1].message).toBe('Use `` instead of self-closing `` for HTML compatibility.'); + }); }); From d7f3db177fb6f300546d80118a508f6c65e57ee3 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 25 Aug 2025 18:24:24 +0200 Subject: [PATCH 2/2] Update snapshot --- javascript/packages/linter/test/__snapshots__/cli.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap index d65715633..870d50e49 100644 --- a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap +++ b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap @@ -151,7 +151,7 @@ test/fixtures/multiple-rule-offenses.html.erb:10:4 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [10/11] ⎯⎯⎯⎯ -[error] Self-closing syntax \`\` is not allowed in HTML. Use \`\` instead. (html-no-self-closing) +[error] Use \`\` instead of self-closing \`\` for HTML compatibility. (html-no-self-closing) test/fixtures/multiple-rule-offenses.html.erb:4:2