Skip to content

Commit 40f4c0b

Browse files
Merge pull request #2617 from NullVoxPopuli/nvp/template-lint-extract-rule-template-self-closing-void-elements
Extract rule: template-self-closing-void-elements
2 parents c98dafb + 9ddf20f commit 40f4c0b

4 files changed

Lines changed: 506 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ rules in templates can be disabled with eslint directives with mustache or html
245245
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
246246
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
247247
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
248+
| [template-self-closing-void-elements](docs/rules/template-self-closing-void-elements.md) | require self-closing on void elements | | 🔧 | |
248249
| [template-simple-modifiers](docs/rules/template-simple-modifiers.md) | require simple modifier syntax | | | |
249250
| [template-simple-unless](docs/rules/template-simple-unless.md) | require simple conditions in unless blocks | | | |
250251
| [template-splat-attributes-only](docs/rules/template-splat-attributes-only.md) | disallow ...spread other than ...attributes | | | |
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# ember/template-self-closing-void-elements
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Disallow or require self-closing void elements.
8+
9+
HTML has no self-closing tags. The HTML5 parser will ignore a self-closing marker on
10+
[void elements](https://html.spec.whatwg.org/#void-elements) (elements that should not
11+
have a closing tag), but it is unnecessary and can be confusing when mixed with
12+
SVG/XML-like syntax.
13+
14+
## Examples
15+
16+
This rule **forbids** the following:
17+
18+
```gjs
19+
<template>
20+
<img src="http://emberjs.com/images/ember-logo.svg" alt="ember" />
21+
<hr />
22+
</template>
23+
```
24+
25+
This rule **allows** the following:
26+
27+
```gjs
28+
<template>
29+
<img src="http://emberjs.com/images/ember-logo.svg" alt="ember">
30+
<hr>
31+
</template>
32+
```
33+
34+
There may be cases where a self-closing tag is preferred for void elements. In those
35+
cases, pass the string `"require"` to require the self-closing form instead.
36+
37+
## Configuration
38+
39+
The following values are valid configuration:
40+
41+
- boolean -- `true` for enabled / `false` for disabled
42+
- string -- `"require"` to mandate the use of self-closing tags
43+
44+
## References
45+
46+
- [HTML spec/void elements](https://html.spec.whatwg.org/#void-elements)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'suggestion',
5+
docs: {
6+
description: 'require self-closing on void elements',
7+
category: 'Best Practices',
8+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-self-closing-void-elements.md',
9+
templateMode: 'both',
10+
},
11+
fixable: 'code',
12+
schema: [
13+
{
14+
oneOf: [{ type: 'boolean' }, { type: 'string', enum: ['require'] }],
15+
},
16+
],
17+
messages: {
18+
redundantSelfClosing: 'Self-closing a void element is redundant',
19+
requireSelfClosing: 'Self-closing a void element is required',
20+
},
21+
originallyFrom: {
22+
name: 'ember-template-lint',
23+
rule: 'lib/rules/self-closing-void-elements.js',
24+
docs: 'docs/rule/self-closing-void-elements.md',
25+
tests: 'test/unit/rules/self-closing-void-elements-test.js',
26+
},
27+
},
28+
29+
create(context) {
30+
const VOID_ELEMENTS = new Set([
31+
'area',
32+
'base',
33+
'br',
34+
'col',
35+
'command',
36+
'embed',
37+
'hr',
38+
'img',
39+
'input',
40+
'keygen',
41+
'link',
42+
'meta',
43+
'param',
44+
'source',
45+
'track',
46+
'wbr',
47+
]);
48+
49+
const sourceCode = context.sourceCode;
50+
const config = context.options[0] ?? true;
51+
52+
if (config === false) {
53+
return {};
54+
}
55+
56+
const requireSelfClosing = config === 'require';
57+
58+
return {
59+
GlimmerElementNode(node) {
60+
if (!VOID_ELEMENTS.has(node.tag)) {
61+
return;
62+
}
63+
64+
if (requireSelfClosing) {
65+
if (!node.selfClosing) {
66+
const source = sourceCode.getText(node).trim();
67+
68+
context.report({
69+
node,
70+
messageId: 'requireSelfClosing',
71+
fix(fixer) {
72+
return fixer.replaceText(node, source.replace(/>$/, '/>'));
73+
},
74+
});
75+
}
76+
} else {
77+
if (node.selfClosing) {
78+
const source = sourceCode.getText(node).trim();
79+
80+
context.report({
81+
node,
82+
messageId: 'redundantSelfClosing',
83+
fix(fixer) {
84+
const replacement = node.blockParams?.length
85+
? source.replace(/\/>$/, '>')
86+
: source.replace(/\s*\/>$/, '>');
87+
88+
return fixer.replaceText(node, replacement);
89+
},
90+
});
91+
}
92+
}
93+
},
94+
};
95+
},
96+
};

0 commit comments

Comments
 (0)