diff --git a/.changeset/rotten-news-swim.md b/.changeset/rotten-news-swim.md new file mode 100644 index 000000000..337185239 --- /dev/null +++ b/.changeset/rotten-news-swim.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': minor +--- + +feat: add config option for foreign elements in `svelte/html-self-closing` rule diff --git a/docs/rules/html-self-closing.md b/docs/rules/html-self-closing.md index c7d268b3c..5f9cc366e 100644 --- a/docs/rules/html-self-closing.md +++ b/docs/rules/html-self-closing.md @@ -70,6 +70,7 @@ config object: { "void": "always", // or "never" or "ignore" "normal": "always", // or "never" or "ignore" + "foreign": "always", // or "never" or "ignore" "component": "always", // or "never" or "ignore" "svelte": "always" // or "never" or "ignore" } @@ -86,6 +87,7 @@ presets: config object: - `void` (`"always"` in default preset)... Style of HTML void elements +- `foreign` (`"always"` in default preset)... Style of foreign elements (SVG and MathML) - `component` (`"always"` in default preset)... Style of svelte components - `svelte` (`"always"` in default preset)... Style of svelte special elements (``, ``) - `normal` (`"always"` in default preset)... Style of other elements diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index 77d3c4ca4..c6b683f45 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -379,6 +379,7 @@ type SvelteHtmlQuotes = []|[{ type SvelteHtmlSelfClosing = []|[({ void?: ("never" | "always" | "ignore") normal?: ("never" | "always" | "ignore") + foreign?: ("never" | "always" | "ignore") component?: ("never" | "always" | "ignore") svelte?: ("never" | "always" | "ignore") } | ("all" | "html" | "none"))] diff --git a/packages/eslint-plugin-svelte/src/rules/html-self-closing.ts b/packages/eslint-plugin-svelte/src/rules/html-self-closing.ts index d4bf01b40..a929bdd12 100644 --- a/packages/eslint-plugin-svelte/src/rules/html-self-closing.ts +++ b/packages/eslint-plugin-svelte/src/rules/html-self-closing.ts @@ -1,16 +1,17 @@ import type { AST } from 'svelte-eslint-parser'; import { createRule } from '../utils'; -import { getNodeName, isVoidHtmlElement } from '../utils/ast-utils'; +import { getNodeName, isVoidHtmlElement, isForeignElement } from '../utils/ast-utils'; import { getSourceCode } from '../utils/compat'; const TYPE_MESSAGES = { normal: 'HTML elements', void: 'HTML void elements', + foreign: 'foreign (SVG or MathML) elements', component: 'Svelte custom components', svelte: 'Svelte special elements' }; -type ElementTypes = 'normal' | 'void' | 'component' | 'svelte'; +type ElementTypes = 'normal' | 'void' | 'foreign' | 'component' | 'svelte'; export default createRule('html-self-closing', { meta: { @@ -37,6 +38,9 @@ export default createRule('html-self-closing', { normal: { enum: ['never', 'always', 'ignore'] }, + foreign: { + enum: ['never', 'always', 'ignore'] + }, component: { enum: ['never', 'always', 'ignore'] }, @@ -57,6 +61,7 @@ export default createRule('html-self-closing', { let options = { void: 'always', normal: 'always', + foreign: 'always', component: 'always', svelte: 'always' }; @@ -67,6 +72,7 @@ export default createRule('html-self-closing', { options = { void: 'never', normal: 'never', + foreign: 'never', component: 'never', svelte: 'never' }; @@ -75,6 +81,7 @@ export default createRule('html-self-closing', { options = { void: 'always', normal: 'never', + foreign: 'always', component: 'never', svelte: 'always' }; @@ -101,6 +108,7 @@ export default createRule('html-self-closing', { if (node.kind === 'component') return 'component'; if (node.kind === 'special') return 'svelte'; if (isVoidHtmlElement(node)) return 'void'; + if (isForeignElement(node)) return 'foreign'; return 'normal'; } diff --git a/packages/eslint-plugin-svelte/src/utils/ast-utils.ts b/packages/eslint-plugin-svelte/src/utils/ast-utils.ts index 401102032..212e91038 100644 --- a/packages/eslint-plugin-svelte/src/utils/ast-utils.ts +++ b/packages/eslint-plugin-svelte/src/utils/ast-utils.ts @@ -3,7 +3,7 @@ import type { TSESTree } from '@typescript-eslint/types'; import type { Scope, Variable } from '@typescript-eslint/scope-manager'; import type { AST as SvAST } from 'svelte-eslint-parser'; import * as eslintUtils from '@eslint-community/eslint-utils'; -import voidElements from './void-elements'; +import { voidElements, svgElements, mathmlElements } from './element-types'; import { getSourceCode } from './compat'; /** @@ -560,6 +560,13 @@ export function isVoidHtmlElement(node: SvAST.SvelteElement): boolean { return voidElements.includes(getNodeName(node)); } +/** + * Returns true if element is known foreign (SVG or MathML) element + */ +export function isForeignElement(node: SvAST.SvelteElement): boolean { + return svgElements.includes(getNodeName(node)) || mathmlElements.includes(getNodeName(node)); +} + /** Checks whether the given identifier node is used as an expression. */ export function isExpressionIdentifier(node: TSESTree.Identifier): boolean { const parent = node.parent; diff --git a/packages/eslint-plugin-svelte/src/utils/element-types.ts b/packages/eslint-plugin-svelte/src/utils/element-types.ts new file mode 100644 index 000000000..398afd644 --- /dev/null +++ b/packages/eslint-plugin-svelte/src/utils/element-types.ts @@ -0,0 +1,140 @@ +export const voidElements = [ + 'area', + 'base', + 'br', + 'col', + 'embed', + 'hr', + 'img', + 'input', + 'keygen', + 'link', + 'menuitem', + 'meta', + 'param', + 'source', + 'track', + 'wbr' +]; + +export const svgElements = [ + 'altGlyph', + 'altGlyphDef', + 'altGlyphItem', + 'animate', + 'animateColor', + 'animateMotion', + 'animateTransform', + 'circle', + 'clipPath', + 'color-profile', + 'cursor', + 'defs', + 'desc', + 'discard', + 'ellipse', + 'feBlend', + 'feColorMatrix', + 'feComponentTransfer', + 'feComposite', + 'feConvolveMatrix', + 'feDiffuseLighting', + 'feDisplacementMap', + 'feDistantLight', + 'feDropShadow', + 'feFlood', + 'feFuncA', + 'feFuncB', + 'feFuncG', + 'feFuncR', + 'feGaussianBlur', + 'feImage', + 'feMerge', + 'feMergeNode', + 'feMorphology', + 'feOffset', + 'fePointLight', + 'feSpecularLighting', + 'feSpotLight', + 'feTile', + 'feTurbulence', + 'filter', + 'font', + 'font-face', + 'font-face-format', + 'font-face-name', + 'font-face-src', + 'font-face-uri', + 'foreignObject', + 'g', + 'glyph', + 'glyphRef', + 'hatch', + 'hatchpath', + 'hkern', + 'image', + 'line', + 'linearGradient', + 'marker', + 'mask', + 'mesh', + 'meshgradient', + 'meshpatch', + 'meshrow', + 'metadata', + 'missing-glyph', + 'mpath', + 'path', + 'pattern', + 'polygon', + 'polyline', + 'radialGradient', + 'rect', + 'set', + 'solidcolor', + 'stop', + 'svg', + 'switch', + 'symbol', + 'text', + 'textPath', + 'tref', + 'tspan', + 'unknown', + 'use', + 'view', + 'vkern' +]; + +export const mathmlElements = [ + 'annotation', + 'annotation-xml', + 'maction', + 'math', + 'merror', + 'mfrac', + 'mi', + 'mmultiscripts', + 'mn', + 'mo', + 'mover', + 'mpadded', + 'mphantom', + 'mprescripts', + 'mroot', + 'mrow', + 'ms', + 'mspace', + 'msqrt', + 'mstyle', + 'msub', + 'msubsup', + 'msup', + 'mtable', + 'mtd', + 'mtext', + 'mtr', + 'munder', + 'munderover', + 'semantics' +]; diff --git a/packages/eslint-plugin-svelte/src/utils/void-elements.ts b/packages/eslint-plugin-svelte/src/utils/void-elements.ts deleted file mode 100644 index 8e490d163..000000000 --- a/packages/eslint-plugin-svelte/src/utils/void-elements.ts +++ /dev/null @@ -1,20 +0,0 @@ -const voidElements = [ - 'area', - 'base', - 'br', - 'col', - 'embed', - 'hr', - 'img', - 'input', - 'keygen', - 'link', - 'menuitem', - 'meta', - 'param', - 'source', - 'track', - 'wbr' -]; - -export default voidElements; diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/foreign-never/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/foreign-never/_config.json new file mode 100644 index 000000000..6081aab9f --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/foreign-never/_config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "foreign": "never" + } + ] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/foreign-never/svelte-never-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/foreign-never/svelte-never-errors.yaml new file mode 100644 index 000000000..a2e0ece09 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/foreign-never/svelte-never-errors.yaml @@ -0,0 +1,8 @@ +- message: Disallow self-closing on foreign (SVG or MathML) elements. + line: 2 + column: 12 + suggestions: null +- message: Disallow self-closing on foreign (SVG or MathML) elements. + line: 3 + column: 13 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/foreign-never/svelte-never-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/foreign-never/svelte-never-input.svelte new file mode 100644 index 000000000..2e6a96029 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/foreign-never/svelte-never-input.svelte @@ -0,0 +1,3 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/foreign-never/svelte-never-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/foreign-never/svelte-never-output.svelte new file mode 100644 index 000000000..76223b771 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/foreign-never/svelte-never-output.svelte @@ -0,0 +1,3 @@ + + + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-errors.yaml index b2883996e..ed068bc5f 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-errors.yaml @@ -10,7 +10,15 @@ line: 5 column: 18 suggestions: null +- message: Require self-closing on foreign (SVG or MathML) elements. + line: 6 + column: 13 + suggestions: null +- message: Require self-closing on foreign (SVG or MathML) elements. + line: 7 + column: 14 + suggestions: null - message: Require self-closing on Svelte special elements. - line: 8 + line: 10 column: 13 suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-input.svelte index 35dc286b6..e9b5f1794 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-input.svelte @@ -3,6 +3,8 @@
+ +
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-output.svelte index c93424600..d11d92be5 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-output.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-output.svelte @@ -3,6 +3,8 @@
+ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-errors.yaml index b15a06363..f12f4c329 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-errors.yaml +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-errors.yaml @@ -10,7 +10,15 @@ line: 5 column: 8 suggestions: null +- message: Disallow self-closing on foreign (SVG or MathML) elements. + line: 6 + column: 14 + suggestions: null +- message: Disallow self-closing on foreign (SVG or MathML) elements. + line: 7 + column: 15 + suggestions: null - message: Disallow self-closing on Svelte special elements. - line: 8 + line: 10 column: 14 suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-input.svelte index 61675d719..5a6369cdf 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-input.svelte @@ -3,6 +3,8 @@
+ +
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-output.svelte index 42d0a6c5c..4246e6ca5 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-output.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-output.svelte @@ -3,6 +3,8 @@
+ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/valid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/valid/test01-input.svelte index c974189c0..082bc477c 100644 --- a/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/valid/test01-input.svelte +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/valid/test01-input.svelte @@ -2,6 +2,8 @@
hello
+ + {#if true} {/if}