Skip to content

Commit fb812b8

Browse files
committed
New: html-self-closing-style rule (fixes #31)
1 parent f834a7e commit fb812b8

File tree

7 files changed

+518
-9
lines changed

7 files changed

+518
-9
lines changed

Diff for: docs/rules/html-self-closing-style.md

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Enforce self-closing style (html-self-closing-style)
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-style": ["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-style: "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-style: "error"*/
60+
61+
<template>
62+
<div></div>
63+
<img>
64+
<your-component />
65+
<svg><path d="" /></svg>
66+
</template>
67+
```

Diff for: 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) {

Diff for: lib/rules/html-self-closing-style.js

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

Diff for: 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())

Diff for: package-lock.json

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
},
4646
"dependencies": {
4747
"requireindex": "^1.1.0",
48-
"vue-eslint-parser": "^2.0.0-beta.3"
48+
"vue-eslint-parser": "2.0.0-beta.4"
4949
},
5050
"devDependencies": {
5151
"@types/node": "^4.2.16",

0 commit comments

Comments
 (0)