Skip to content

Commit 0e46f31

Browse files
mysticateamichalsnik
authored andcommitted
New: html-self-closing rule (fixes #31) (#129)
* New: `html-self-closing-style` rule (fixes #31) * add more tests. * Docs: add replacedBy property to html-no-self-closing * rename * update for review
1 parent 4d0c1d3 commit 0e46f31

File tree

6 files changed

+555
-6
lines changed

6 files changed

+555
-6
lines changed

docs/rules/html-self-closing.md

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Enforce self-closing style (html-self-closing)
2+
3+
In Vue.js template, we can use either two styles for elements which don't have their content.
4+
5+
1. `<your-component></your-component>`
6+
2. `<your-component />` (self-closing)
7+
8+
Self-closing is simple and shorter, but it's not supported in raw HTML.
9+
This rule helps you to unify the self-closing style.
10+
11+
## Rule Details
12+
13+
This rule has options which specify self-closing style for each context.
14+
15+
```json
16+
{
17+
"html-self-closing": ["error", {
18+
"html": {
19+
"normal": "never",
20+
"void": "never",
21+
"component": "always"
22+
},
23+
"svg": "always",
24+
"math": "always"
25+
}]
26+
}
27+
```
28+
29+
- `html.normal` (`"never"` by default) ... The style of well-known HTML elements except void elements.
30+
- `html.void` (`"never"` by default) ... The style of well-known HTML void elements.
31+
- `html.component` (`"always"` by default) ... The style of Vue.js custom components.
32+
- `svg`(`"always"` by default) .... The style of well-known SVG elements.
33+
- `math`(`"always"` by default) .... The style of well-known MathML elements.
34+
35+
Every option can be set to one of the following values:
36+
37+
- `"always"` ... Require self-closing at elements which don't have their content.
38+
- `"never"` ... Disallow self-closing.
39+
- `"any"` ... Don't enforce self-closing style.
40+
41+
----
42+
43+
:-1: Examples of **incorrect** code for this rule:
44+
45+
```html
46+
/*eslint html-self-closing: "error"*/
47+
48+
<template>
49+
<div />
50+
<img />
51+
<your-component></your-component>
52+
<svg><path d=""></path></svg>
53+
</template>
54+
```
55+
56+
:+1: Examples of **correct** code for this rule:
57+
58+
```html
59+
/*eslint html-self-closing: "error"*/
60+
61+
<template>
62+
<div></div>
63+
<img>
64+
<your-component />
65+
<svg><path d="" /></svg>
66+
</template>
67+
```

lib/rules/html-end-tags.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function create (context) {
2525
utils.registerTemplateBodyVisitor(context, {
2626
VElement (node) {
2727
const name = node.name
28-
const isVoid = utils.isVoidElementName(name)
28+
const isVoid = utils.isHtmlVoidElementName(name)
2929
const hasEndTag = node.endTag != null
3030

3131
if (isVoid && hasEndTag) {

lib/rules/html-no-self-closing.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ module.exports = {
5757
description: 'disallow self-closing elements.',
5858
category: 'Best Practices',
5959
recommended: false,
60-
replacedBy: []
60+
replacedBy: ['html-self-closing-style']
6161
},
6262
deprecated: true,
6363
fixable: 'code',

lib/rules/html-self-closing.js

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/**
2+
* @author Toru Nagashima
3+
* @copyright 2016 Toru Nagashima. All rights reserved.
4+
* See LICENSE file in root directory for full license.
5+
*/
6+
'use strict'
7+
8+
// ------------------------------------------------------------------------------
9+
// Requirements
10+
// ------------------------------------------------------------------------------
11+
12+
const utils = require('../utils')
13+
14+
// ------------------------------------------------------------------------------
15+
// Helpers
16+
// ------------------------------------------------------------------------------
17+
18+
/**
19+
* These strings wil be displayed in error messages.
20+
*/
21+
const ELEMENT_TYPE = Object.freeze({
22+
NORMAL: 'HTML elements',
23+
VOID: 'HTML void elements',
24+
COMPONENT: 'Vue.js custom components',
25+
SVG: 'SVG elements',
26+
MATH: 'MathML elements'
27+
})
28+
29+
/**
30+
* Normalize the given options.
31+
* @param {Object|undefined} options The raw options object.
32+
* @returns {Object} Normalized options.
33+
*/
34+
function parseOptions (options) {
35+
return {
36+
[ELEMENT_TYPE.NORMAL]: (options && options.html && options.html.normal) || 'never',
37+
[ELEMENT_TYPE.VOID]: (options && options.html && options.html.void) || 'never',
38+
[ELEMENT_TYPE.COMPONENT]: (options && options.html && options.html.component) || 'always',
39+
[ELEMENT_TYPE.SVG]: (options && options.svg) || 'always',
40+
[ELEMENT_TYPE.MATH]: (options && options.math) || 'always'
41+
}
42+
}
43+
44+
/**
45+
* Get the elementType of the given element.
46+
* @param {VElement} node The element node to get.
47+
* @returns {string} The elementType of the element.
48+
*/
49+
function getElementType (node) {
50+
if (utils.isCustomComponent(node)) {
51+
return ELEMENT_TYPE.COMPONENT
52+
}
53+
if (utils.isHtmlElementNode(node)) {
54+
if (utils.isHtmlVoidElementName(node.name)) {
55+
return ELEMENT_TYPE.VOID
56+
}
57+
return ELEMENT_TYPE.NORMAL
58+
}
59+
if (utils.isSvgElementNode(node)) {
60+
return ELEMENT_TYPE.SVG
61+
}
62+
if (utils.isMathMLElementNode(node)) {
63+
return ELEMENT_TYPE.MATH
64+
}
65+
return 'unknown elements'
66+
}
67+
68+
/**
69+
* Check whether the given element is empty or not.
70+
* This ignores whitespaces, doesn't ignore comments.
71+
* @param {VElement} node The element node to check.
72+
* @param {SourceCode} sourceCode The source code object of the current context.
73+
* @returns {boolean} `true` if the element is empty.
74+
*/
75+
function isEmpty (node, sourceCode) {
76+
const start = node.startTag.range[1]
77+
const end = (node.endTag != null) ? node.endTag.range[0] : node.range[1]
78+
79+
return sourceCode.text.slice(start, end).trim() === ''
80+
}
81+
82+
/**
83+
* Creates AST event handlers for html-self-closing.
84+
*
85+
* @param {RuleContext} context - The rule context.
86+
* @returns {object} AST event handlers.
87+
*/
88+
function create (context) {
89+
const sourceCode = context.getSourceCode()
90+
const options = parseOptions(context.options[0])
91+
92+
utils.registerTemplateBodyVisitor(context, {
93+
'VElement' (node) {
94+
const elementType = getElementType(node)
95+
const mode = options[elementType]
96+
97+
if (mode === 'always' && !node.startTag.selfClosing && isEmpty(node, sourceCode)) {
98+
context.report({
99+
node,
100+
loc: node.loc,
101+
message: 'Require self-closing on {{elementType}} (<{{name}}>).',
102+
data: { elementType, name: node.rawName },
103+
fix: (fixer) => {
104+
const tokens = context.parserServices.getTemplateBodyTokenStore()
105+
const close = tokens.getLastToken(node.startTag)
106+
if (close.type !== 'HTMLTagClose') {
107+
return null
108+
}
109+
return fixer.replaceTextRange([close.range[0], node.range[1]], '/>')
110+
}
111+
})
112+
}
113+
114+
if (mode === 'never' && node.startTag.selfClosing) {
115+
context.report({
116+
node,
117+
loc: node.loc,
118+
message: 'Disallow self-closing on {{elementType}} (<{{name}}/>).',
119+
data: { elementType, name: node.rawName },
120+
fix: (fixer) => {
121+
const tokens = context.parserServices.getTemplateBodyTokenStore()
122+
const close = tokens.getLastToken(node.startTag)
123+
if (close.type !== 'HTMLSelfClosingTagClose') {
124+
return null
125+
}
126+
if (elementType === ELEMENT_TYPE.VOID) {
127+
return fixer.replaceText(close, '>')
128+
}
129+
return fixer.replaceText(close, `></${node.rawName}>`)
130+
}
131+
})
132+
}
133+
}
134+
})
135+
136+
return {}
137+
}
138+
139+
// ------------------------------------------------------------------------------
140+
// Rule Definition
141+
// ------------------------------------------------------------------------------
142+
143+
module.exports = {
144+
create,
145+
meta: {
146+
docs: {
147+
description: 'enforce self-closing style.',
148+
category: 'Stylistic Issues',
149+
recommended: false
150+
},
151+
fixable: 'code',
152+
schema: {
153+
definitions: {
154+
optionValue: {
155+
enum: ['always', 'never', 'any']
156+
}
157+
},
158+
type: 'array',
159+
items: [{
160+
type: 'object',
161+
properties: {
162+
html: {
163+
type: 'object',
164+
properties: {
165+
normal: { $ref: '#/definitions/optionValue' },
166+
void: { $ref: '#/definitions/optionValue' },
167+
component: { $ref: '#/definitions/optionValue' }
168+
},
169+
additionalProperties: false
170+
},
171+
svg: { $ref: '#/definitions/optionValue' },
172+
math: { $ref: '#/definitions/optionValue' }
173+
},
174+
additionalProperties: false
175+
}],
176+
maxItems: 1
177+
}
178+
}
179+
}

lib/utils/index.js

+15-4
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ module.exports = {
183183
assert(node && node.type === 'VElement')
184184

185185
return (
186-
!(this.isKnownHtmlElementNode(node) || this.isSvgElementNode(node) || this.isMathMLElementNode(node)) ||
186+
(this.isHtmlElementNode(node) && !this.isHtmlWellKnownElementName(node.name)) ||
187187
this.hasAttribute(node, 'is') ||
188188
this.hasDirective(node, 'bind', 'is')
189189
)
@@ -194,10 +194,10 @@ module.exports = {
194194
* @param {ASTNode} node The node to check.
195195
* @returns {boolean} `true` if the node is a HTML element.
196196
*/
197-
isKnownHtmlElementNode (node) {
197+
isHtmlElementNode (node) {
198198
assert(node && node.type === 'VElement')
199199

200-
return node.namespace === vueEslintParser.AST.NS.HTML && HTML_ELEMENT_NAMES.has(node.name.toLowerCase())
200+
return node.namespace === vueEslintParser.AST.NS.HTML
201201
},
202202

203203
/**
@@ -222,12 +222,23 @@ module.exports = {
222222
return node.namespace === vueEslintParser.AST.NS.MathML
223223
},
224224

225+
/**
226+
* Check whether the given name is an well-known element or not.
227+
* @param {string} name The name to check.
228+
* @returns {boolean} `true` if the name is an well-known element name.
229+
*/
230+
isHtmlWellKnownElementName (name) {
231+
assert(typeof name === 'string')
232+
233+
return HTML_ELEMENT_NAMES.has(name.toLowerCase())
234+
},
235+
225236
/**
226237
* Check whether the given name is a void element name or not.
227238
* @param {string} name The name to check.
228239
* @returns {boolean} `true` if the name is a void element name.
229240
*/
230-
isVoidElementName (name) {
241+
isHtmlVoidElementName (name) {
231242
assert(typeof name === 'string')
232243

233244
return VOID_ELEMENT_NAMES.has(name.toLowerCase())

0 commit comments

Comments
 (0)