Skip to content

Commit 85053a1

Browse files
authored
feat: add config option for foreign elements in svelte/html-self-closing rule (#841)
This PR adds separate configuration option for foreign (SVG and MathML) elements to `svelte/html-self-closing` rule. According to HTML spec (https://html.spec.whatwg.org/multipage/syntax.html#elements-2): > Raw text, escapable raw text, and normal elements have a start tag to indicate where they begin, and an end tag to indicate where they end. The start and end tags of certain normal elements can be omitted, as described below in the section on optional tags. Those that cannot be omitted must not be omitted. Void elements only have a start tag; end tags must not be specified for void elements. Foreign elements must either have a start tag and an end tag, or a start tag that is marked as self-closing, in which case they must not have an end tag. This means that `<div/>` is invalid, while `<path/>` (which is a SVG element) is OK. This configuration option would allow users to take advantage of the terser SVG syntax, while still using correct HTML syntax for normal elements (like `div`). Closes #837.
1 parent d117d7f commit 85053a1

18 files changed

+215
-25
lines changed

.changeset/rotten-news-swim.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': minor
3+
---
4+
5+
feat: add config option for foreign elements in `svelte/html-self-closing` rule

docs/rules/html-self-closing.md

+2
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ config object:
7070
{
7171
"void": "always", // or "never" or "ignore"
7272
"normal": "always", // or "never" or "ignore"
73+
"foreign": "always", // or "never" or "ignore"
7374
"component": "always", // or "never" or "ignore"
7475
"svelte": "always" // or "never" or "ignore"
7576
}
@@ -86,6 +87,7 @@ presets:
8687
config object:
8788

8889
- `void` (`"always"` in default preset)... Style of HTML void elements
90+
- `foreign` (`"always"` in default preset)... Style of foreign elements (SVG and MathML)
8991
- `component` (`"always"` in default preset)... Style of svelte components
9092
- `svelte` (`"always"` in default preset)... Style of svelte special elements (`<svelte:head>`, `<svelte:self>`)
9193
- `normal` (`"always"` in default preset)... Style of other elements

packages/eslint-plugin-svelte/src/rule-types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ type SvelteHtmlQuotes = []|[{
379379
type SvelteHtmlSelfClosing = []|[({
380380
void?: ("never" | "always" | "ignore")
381381
normal?: ("never" | "always" | "ignore")
382+
foreign?: ("never" | "always" | "ignore")
382383
component?: ("never" | "always" | "ignore")
383384
svelte?: ("never" | "always" | "ignore")
384385
} | ("all" | "html" | "none"))]

packages/eslint-plugin-svelte/src/rules/html-self-closing.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import type { AST } from 'svelte-eslint-parser';
22
import { createRule } from '../utils';
3-
import { getNodeName, isVoidHtmlElement } from '../utils/ast-utils';
3+
import { getNodeName, isVoidHtmlElement, isForeignElement } from '../utils/ast-utils';
44
import { getSourceCode } from '../utils/compat';
55

66
const TYPE_MESSAGES = {
77
normal: 'HTML elements',
88
void: 'HTML void elements',
9+
foreign: 'foreign (SVG or MathML) elements',
910
component: 'Svelte custom components',
1011
svelte: 'Svelte special elements'
1112
};
1213

13-
type ElementTypes = 'normal' | 'void' | 'component' | 'svelte';
14+
type ElementTypes = 'normal' | 'void' | 'foreign' | 'component' | 'svelte';
1415

1516
export default createRule('html-self-closing', {
1617
meta: {
@@ -37,6 +38,9 @@ export default createRule('html-self-closing', {
3738
normal: {
3839
enum: ['never', 'always', 'ignore']
3940
},
41+
foreign: {
42+
enum: ['never', 'always', 'ignore']
43+
},
4044
component: {
4145
enum: ['never', 'always', 'ignore']
4246
},
@@ -57,6 +61,7 @@ export default createRule('html-self-closing', {
5761
let options = {
5862
void: 'always',
5963
normal: 'always',
64+
foreign: 'always',
6065
component: 'always',
6166
svelte: 'always'
6267
};
@@ -67,6 +72,7 @@ export default createRule('html-self-closing', {
6772
options = {
6873
void: 'never',
6974
normal: 'never',
75+
foreign: 'never',
7076
component: 'never',
7177
svelte: 'never'
7278
};
@@ -75,6 +81,7 @@ export default createRule('html-self-closing', {
7581
options = {
7682
void: 'always',
7783
normal: 'never',
84+
foreign: 'always',
7885
component: 'never',
7986
svelte: 'always'
8087
};
@@ -101,6 +108,7 @@ export default createRule('html-self-closing', {
101108
if (node.kind === 'component') return 'component';
102109
if (node.kind === 'special') return 'svelte';
103110
if (isVoidHtmlElement(node)) return 'void';
111+
if (isForeignElement(node)) return 'foreign';
104112
return 'normal';
105113
}
106114

packages/eslint-plugin-svelte/src/utils/ast-utils.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { TSESTree } from '@typescript-eslint/types';
33
import type { Scope, Variable } from '@typescript-eslint/scope-manager';
44
import type { AST as SvAST } from 'svelte-eslint-parser';
55
import * as eslintUtils from '@eslint-community/eslint-utils';
6-
import voidElements from './void-elements';
6+
import { voidElements, svgElements, mathmlElements } from './element-types';
77
import { getSourceCode } from './compat';
88

99
/**
@@ -560,6 +560,13 @@ export function isVoidHtmlElement(node: SvAST.SvelteElement): boolean {
560560
return voidElements.includes(getNodeName(node));
561561
}
562562

563+
/**
564+
* Returns true if element is known foreign (SVG or MathML) element
565+
*/
566+
export function isForeignElement(node: SvAST.SvelteElement): boolean {
567+
return svgElements.includes(getNodeName(node)) || mathmlElements.includes(getNodeName(node));
568+
}
569+
563570
/** Checks whether the given identifier node is used as an expression. */
564571
export function isExpressionIdentifier(node: TSESTree.Identifier): boolean {
565572
const parent = node.parent;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
export const voidElements = [
2+
'area',
3+
'base',
4+
'br',
5+
'col',
6+
'embed',
7+
'hr',
8+
'img',
9+
'input',
10+
'keygen',
11+
'link',
12+
'menuitem',
13+
'meta',
14+
'param',
15+
'source',
16+
'track',
17+
'wbr'
18+
];
19+
20+
export const svgElements = [
21+
'altGlyph',
22+
'altGlyphDef',
23+
'altGlyphItem',
24+
'animate',
25+
'animateColor',
26+
'animateMotion',
27+
'animateTransform',
28+
'circle',
29+
'clipPath',
30+
'color-profile',
31+
'cursor',
32+
'defs',
33+
'desc',
34+
'discard',
35+
'ellipse',
36+
'feBlend',
37+
'feColorMatrix',
38+
'feComponentTransfer',
39+
'feComposite',
40+
'feConvolveMatrix',
41+
'feDiffuseLighting',
42+
'feDisplacementMap',
43+
'feDistantLight',
44+
'feDropShadow',
45+
'feFlood',
46+
'feFuncA',
47+
'feFuncB',
48+
'feFuncG',
49+
'feFuncR',
50+
'feGaussianBlur',
51+
'feImage',
52+
'feMerge',
53+
'feMergeNode',
54+
'feMorphology',
55+
'feOffset',
56+
'fePointLight',
57+
'feSpecularLighting',
58+
'feSpotLight',
59+
'feTile',
60+
'feTurbulence',
61+
'filter',
62+
'font',
63+
'font-face',
64+
'font-face-format',
65+
'font-face-name',
66+
'font-face-src',
67+
'font-face-uri',
68+
'foreignObject',
69+
'g',
70+
'glyph',
71+
'glyphRef',
72+
'hatch',
73+
'hatchpath',
74+
'hkern',
75+
'image',
76+
'line',
77+
'linearGradient',
78+
'marker',
79+
'mask',
80+
'mesh',
81+
'meshgradient',
82+
'meshpatch',
83+
'meshrow',
84+
'metadata',
85+
'missing-glyph',
86+
'mpath',
87+
'path',
88+
'pattern',
89+
'polygon',
90+
'polyline',
91+
'radialGradient',
92+
'rect',
93+
'set',
94+
'solidcolor',
95+
'stop',
96+
'svg',
97+
'switch',
98+
'symbol',
99+
'text',
100+
'textPath',
101+
'tref',
102+
'tspan',
103+
'unknown',
104+
'use',
105+
'view',
106+
'vkern'
107+
];
108+
109+
export const mathmlElements = [
110+
'annotation',
111+
'annotation-xml',
112+
'maction',
113+
'math',
114+
'merror',
115+
'mfrac',
116+
'mi',
117+
'mmultiscripts',
118+
'mn',
119+
'mo',
120+
'mover',
121+
'mpadded',
122+
'mphantom',
123+
'mprescripts',
124+
'mroot',
125+
'mrow',
126+
'ms',
127+
'mspace',
128+
'msqrt',
129+
'mstyle',
130+
'msub',
131+
'msubsup',
132+
'msup',
133+
'mtable',
134+
'mtd',
135+
'mtext',
136+
'mtr',
137+
'munder',
138+
'munderover',
139+
'semantics'
140+
];

packages/eslint-plugin-svelte/src/utils/void-elements.ts

-20
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"options": [
3+
{
4+
"foreign": "never"
5+
}
6+
]
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
- message: Disallow self-closing on foreign (SVG or MathML) elements.
2+
line: 2
3+
column: 12
4+
suggestions: null
5+
- message: Disallow self-closing on foreign (SVG or MathML) elements.
6+
line: 3
7+
column: 13
8+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<!-- prettier-ignore -->
2+
<svg><path /></svg>
3+
<math><msup /></math>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<!-- prettier-ignore -->
2+
<svg><path ></path></svg>
3+
<math><msup ></msup></math>

packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-errors.yaml

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@
1010
line: 5
1111
column: 18
1212
suggestions: null
13+
- message: Require self-closing on foreign (SVG or MathML) elements.
14+
line: 6
15+
column: 13
16+
suggestions: null
17+
- message: Require self-closing on foreign (SVG or MathML) elements.
18+
line: 7
19+
column: 14
20+
suggestions: null
1321
- message: Require self-closing on Svelte special elements.
14-
line: 8
22+
line: 10
1523
column: 13
1624
suggestions: null

packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-input.svelte

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
<div />
44
<img>
55
<TestComponent />
6+
<svg><path></path></svg>
7+
<math><msup></msup></math>
68
</div>
79
<!-- prettier-ignore -->
810
<svelte:head></svelte:head>

packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/html/preset-html-output.svelte

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
<div ></div>
44
<img/>
55
<TestComponent ></TestComponent>
6+
<svg><path/></svg>
7+
<math><msup/></math>
68
</div>
79
<!-- prettier-ignore -->
810
<svelte:head/>

packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-errors.yaml

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@
1010
line: 5
1111
column: 8
1212
suggestions: null
13+
- message: Disallow self-closing on foreign (SVG or MathML) elements.
14+
line: 6
15+
column: 14
16+
suggestions: null
17+
- message: Disallow self-closing on foreign (SVG or MathML) elements.
18+
line: 7
19+
column: 15
20+
suggestions: null
1321
- message: Disallow self-closing on Svelte special elements.
14-
line: 8
22+
line: 10
1523
column: 14
1624
suggestions: null

packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-input.svelte

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
<div />
44
<TestComponent />
55
<img />
6+
<svg><path /></svg>
7+
<math><msup /></math>
68
</div>
79
<!-- prettier-ignore -->
810
<svelte:head />

packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/invalid/presets/none/preset-none-output.svelte

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
<div ></div>
44
<TestComponent ></TestComponent>
55
<img >
6+
<svg><path ></path></svg>
7+
<math><msup ></msup></math>
68
</div>
79
<!-- prettier-ignore -->
810
<svelte:head ></svelte:head>

packages/eslint-plugin-svelte/tests/fixtures/rules/html-self-closing/valid/test01-input.svelte

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
<div />
33
<div>hello</div>
44
<img />
5+
<svg><path /></svg>
6+
<math><msup /></math>
57
{#if true}
68
<svelte:self />
79
{/if}

0 commit comments

Comments
 (0)