diff --git a/docs/rules/html-self-closing.md b/docs/rules/html-self-closing.md
new file mode 100644
index 000000000..c7ac3822f
--- /dev/null
+++ b/docs/rules/html-self-closing.md
@@ -0,0 +1,67 @@
+# Enforce self-closing style (html-self-closing)
+
+In Vue.js template, we can use either two styles for elements which don't have their content.
+
+1. ``
+2. `` (self-closing)
+
+Self-closing is simple and shorter, but it's not supported in raw HTML.
+This rule helps you to unify the self-closing style.
+
+## Rule Details
+
+This rule has options which specify self-closing style for each context.
+
+```json
+{
+ "html-self-closing": ["error", {
+ "html": {
+ "normal": "never",
+ "void": "never",
+ "component": "always"
+ },
+ "svg": "always",
+ "math": "always"
+ }]
+}
+```
+
+- `html.normal` (`"never"` by default) ... The style of well-known HTML elements except void elements.
+- `html.void` (`"never"` by default) ... The style of well-known HTML void elements.
+- `html.component` (`"always"` by default) ... The style of Vue.js custom components.
+- `svg`(`"always"` by default) .... The style of well-known SVG elements.
+- `math`(`"always"` by default) .... The style of well-known MathML elements.
+
+Every option can be set to one of the following values:
+
+- `"always"` ... Require self-closing at elements which don't have their content.
+- `"never"` ... Disallow self-closing.
+- `"any"` ... Don't enforce self-closing style.
+
+----
+
+:-1: Examples of **incorrect** code for this rule:
+
+```html
+/*eslint html-self-closing: "error"*/
+
+
+
+
+
+
+
+```
+
+:+1: Examples of **correct** code for this rule:
+
+```html
+/*eslint html-self-closing: "error"*/
+
+
+
+
+
+
+
+```
diff --git a/lib/rules/html-end-tags.js b/lib/rules/html-end-tags.js
index 4238cca1d..511351095 100644
--- a/lib/rules/html-end-tags.js
+++ b/lib/rules/html-end-tags.js
@@ -25,7 +25,7 @@ function create (context) {
utils.registerTemplateBodyVisitor(context, {
VElement (node) {
const name = node.name
- const isVoid = utils.isVoidElementName(name)
+ const isVoid = utils.isHtmlVoidElementName(name)
const hasEndTag = node.endTag != null
if (isVoid && hasEndTag) {
diff --git a/lib/rules/html-no-self-closing.js b/lib/rules/html-no-self-closing.js
index 02e74b505..b85ab12fd 100644
--- a/lib/rules/html-no-self-closing.js
+++ b/lib/rules/html-no-self-closing.js
@@ -57,7 +57,7 @@ module.exports = {
description: 'disallow self-closing elements.',
category: 'Best Practices',
recommended: false,
- replacedBy: []
+ replacedBy: ['html-self-closing-style']
},
deprecated: true,
fixable: 'code',
diff --git a/lib/rules/html-self-closing.js b/lib/rules/html-self-closing.js
new file mode 100644
index 000000000..7dc2bb28b
--- /dev/null
+++ b/lib/rules/html-self-closing.js
@@ -0,0 +1,179 @@
+/**
+ * @author Toru Nagashima
+ * @copyright 2016 Toru Nagashima. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * These strings wil be displayed in error messages.
+ */
+const ELEMENT_TYPE = Object.freeze({
+ NORMAL: 'HTML elements',
+ VOID: 'HTML void elements',
+ COMPONENT: 'Vue.js custom components',
+ SVG: 'SVG elements',
+ MATH: 'MathML elements'
+})
+
+/**
+ * Normalize the given options.
+ * @param {Object|undefined} options The raw options object.
+ * @returns {Object} Normalized options.
+ */
+function parseOptions (options) {
+ return {
+ [ELEMENT_TYPE.NORMAL]: (options && options.html && options.html.normal) || 'never',
+ [ELEMENT_TYPE.VOID]: (options && options.html && options.html.void) || 'never',
+ [ELEMENT_TYPE.COMPONENT]: (options && options.html && options.html.component) || 'always',
+ [ELEMENT_TYPE.SVG]: (options && options.svg) || 'always',
+ [ELEMENT_TYPE.MATH]: (options && options.math) || 'always'
+ }
+}
+
+/**
+ * Get the elementType of the given element.
+ * @param {VElement} node The element node to get.
+ * @returns {string} The elementType of the element.
+ */
+function getElementType (node) {
+ if (utils.isCustomComponent(node)) {
+ return ELEMENT_TYPE.COMPONENT
+ }
+ if (utils.isHtmlElementNode(node)) {
+ if (utils.isHtmlVoidElementName(node.name)) {
+ return ELEMENT_TYPE.VOID
+ }
+ return ELEMENT_TYPE.NORMAL
+ }
+ if (utils.isSvgElementNode(node)) {
+ return ELEMENT_TYPE.SVG
+ }
+ if (utils.isMathMLElementNode(node)) {
+ return ELEMENT_TYPE.MATH
+ }
+ return 'unknown elements'
+}
+
+/**
+ * Check whether the given element is empty or not.
+ * This ignores whitespaces, doesn't ignore comments.
+ * @param {VElement} node The element node to check.
+ * @param {SourceCode} sourceCode The source code object of the current context.
+ * @returns {boolean} `true` if the element is empty.
+ */
+function isEmpty (node, sourceCode) {
+ const start = node.startTag.range[1]
+ const end = (node.endTag != null) ? node.endTag.range[0] : node.range[1]
+
+ return sourceCode.text.slice(start, end).trim() === ''
+}
+
+/**
+ * Creates AST event handlers for html-self-closing.
+ *
+ * @param {RuleContext} context - The rule context.
+ * @returns {object} AST event handlers.
+ */
+function create (context) {
+ const sourceCode = context.getSourceCode()
+ const options = parseOptions(context.options[0])
+
+ utils.registerTemplateBodyVisitor(context, {
+ 'VElement' (node) {
+ const elementType = getElementType(node)
+ const mode = options[elementType]
+
+ if (mode === 'always' && !node.startTag.selfClosing && isEmpty(node, sourceCode)) {
+ context.report({
+ node,
+ loc: node.loc,
+ message: 'Require self-closing on {{elementType}} (<{{name}}>).',
+ data: { elementType, name: node.rawName },
+ fix: (fixer) => {
+ const tokens = context.parserServices.getTemplateBodyTokenStore()
+ const close = tokens.getLastToken(node.startTag)
+ if (close.type !== 'HTMLTagClose') {
+ return null
+ }
+ return fixer.replaceTextRange([close.range[0], node.range[1]], '/>')
+ }
+ })
+ }
+
+ if (mode === 'never' && node.startTag.selfClosing) {
+ context.report({
+ node,
+ loc: node.loc,
+ message: 'Disallow self-closing on {{elementType}} (<{{name}}/>).',
+ data: { elementType, name: node.rawName },
+ fix: (fixer) => {
+ const tokens = context.parserServices.getTemplateBodyTokenStore()
+ const close = tokens.getLastToken(node.startTag)
+ if (close.type !== 'HTMLSelfClosingTagClose') {
+ return null
+ }
+ if (elementType === ELEMENT_TYPE.VOID) {
+ return fixer.replaceText(close, '>')
+ }
+ return fixer.replaceText(close, `>${node.rawName}>`)
+ }
+ })
+ }
+ }
+ })
+
+ return {}
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ create,
+ meta: {
+ docs: {
+ description: 'enforce self-closing style.',
+ category: 'Stylistic Issues',
+ recommended: false
+ },
+ fixable: 'code',
+ schema: {
+ definitions: {
+ optionValue: {
+ enum: ['always', 'never', 'any']
+ }
+ },
+ type: 'array',
+ items: [{
+ type: 'object',
+ properties: {
+ html: {
+ type: 'object',
+ properties: {
+ normal: { $ref: '#/definitions/optionValue' },
+ void: { $ref: '#/definitions/optionValue' },
+ component: { $ref: '#/definitions/optionValue' }
+ },
+ additionalProperties: false
+ },
+ svg: { $ref: '#/definitions/optionValue' },
+ math: { $ref: '#/definitions/optionValue' }
+ },
+ additionalProperties: false
+ }],
+ maxItems: 1
+ }
+ }
+}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 1e741703b..36036d602 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -183,7 +183,7 @@ module.exports = {
assert(node && node.type === 'VElement')
return (
- !(this.isKnownHtmlElementNode(node) || this.isSvgElementNode(node) || this.isMathMLElementNode(node)) ||
+ (this.isHtmlElementNode(node) && !this.isHtmlWellKnownElementName(node.name)) ||
this.hasAttribute(node, 'is') ||
this.hasDirective(node, 'bind', 'is')
)
@@ -194,10 +194,10 @@ module.exports = {
* @param {ASTNode} node The node to check.
* @returns {boolean} `true` if the node is a HTML element.
*/
- isKnownHtmlElementNode (node) {
+ isHtmlElementNode (node) {
assert(node && node.type === 'VElement')
- return node.namespace === vueEslintParser.AST.NS.HTML && HTML_ELEMENT_NAMES.has(node.name.toLowerCase())
+ return node.namespace === vueEslintParser.AST.NS.HTML
},
/**
@@ -222,12 +222,23 @@ module.exports = {
return node.namespace === vueEslintParser.AST.NS.MathML
},
+ /**
+ * Check whether the given name is an well-known element or not.
+ * @param {string} name The name to check.
+ * @returns {boolean} `true` if the name is an well-known element name.
+ */
+ isHtmlWellKnownElementName (name) {
+ assert(typeof name === 'string')
+
+ return HTML_ELEMENT_NAMES.has(name.toLowerCase())
+ },
+
/**
* Check whether the given name is a void element name or not.
* @param {string} name The name to check.
* @returns {boolean} `true` if the name is a void element name.
*/
- isVoidElementName (name) {
+ isHtmlVoidElementName (name) {
assert(typeof name === 'string')
return VOID_ELEMENT_NAMES.has(name.toLowerCase())
diff --git a/tests/lib/rules/html-self-closing.js b/tests/lib/rules/html-self-closing.js
new file mode 100644
index 000000000..49ee3c43f
--- /dev/null
+++ b/tests/lib/rules/html-self-closing.js
@@ -0,0 +1,292 @@
+/**
+ * @author Toru Nagashima
+ * @copyright 2016 Toru Nagashima. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/html-self-closing')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+ parser: 'vue-eslint-parser'
+})
+
+const ALL_CODE = `
+
+
+
+
+
+
+
+
+
+
+`
+
+const anyWith = (opts) => Object.assign(
+ {
+ svg: 'any',
+ math: 'any'
+ },
+ opts,
+ {
+ html: Object.assign(
+ {
+ normal: 'any',
+ void: 'any',
+ component: 'any'
+ },
+ opts.html || {}
+ )
+ }
+)
+
+tester.run('html-self-closing', rule, {
+ valid: [
+ // default
+ '',
+ '
',
+ '',
+ '',
+ '',
+
+ // Don't error if there are comments in their content.
+ {
+ code: '',
+ output: null,
+ options: [{ html: { normal: 'always' }}]
+ }
+
+ // other cases are in `invalid` tests.
+ ],
+ invalid: [
+ // default
+ {
+ code: '',
+ output: '',
+ errors: ['Disallow self-closing on HTML elements (
).']
+ },
+ {
+ code: '
',
+ output: '
',
+ errors: ['Disallow self-closing on HTML void elements (
).']
+ },
+ {
+ code: '',
+ output: '',
+ errors: ['Require self-closing on Vue.js custom components ().']
+ },
+ {
+ code: '',
+ output: '',
+ errors: ['Require self-closing on SVG elements ().']
+ },
+ {
+ code: '',
+ output: '',
+ errors: ['Require self-closing on MathML elements ().']
+ },
+
+ // others
+ {
+ code: ALL_CODE,
+ output: `
+
+
+
+
+
+
+
+
+
+
+`,
+ options: [anyWith({ html: { normal: 'always' }})],
+ errors: [
+ { message: 'Require self-closing on HTML elements ().', line: 2 }
+ ]
+ },
+ {
+ code: ALL_CODE,
+ output: `
+
+
+
+
+
+
+
+
+
+
+`,
+ options: [anyWith({ html: { normal: 'never' }})],
+ errors: [
+ { message: 'Disallow self-closing on HTML elements (
).', line: 3 }
+ ]
+ },
+ {
+ code: ALL_CODE,
+ output: `
+
+
+
+
+
+
+
+
+
+
+`,
+ options: [anyWith({ html: { void: 'always' }})],
+ errors: [
+ { message: 'Require self-closing on HTML void elements (
![]()
).', line: 4 }
+ ]
+ },
+ {
+ code: ALL_CODE,
+ output: `
+
+
+
+
+
+
+
+
+
+
+`,
+ options: [anyWith({ html: { void: 'never' }})],
+ errors: [
+ { message: 'Disallow self-closing on HTML void elements (
![]()
).', line: 5 }
+ ]
+ },
+ {
+ code: ALL_CODE,
+ output: `
+
+
+
+
+
+
+
+
+
+
+`,
+ options: [anyWith({ html: { component: 'always' }})],
+ errors: [
+ { message: 'Require self-closing on Vue.js custom components (
).', line: 6 }
+ ]
+ },
+ {
+ code: ALL_CODE,
+ output: `
+
+
+
+
+
+
+
+
+
+
+`,
+ options: [anyWith({ html: { component: 'never' }})],
+ errors: [
+ { message: 'Disallow self-closing on Vue.js custom components ().', line: 7 }
+ ]
+ },
+ {
+ code: ALL_CODE,
+ output: `
+
+
+
+
+
+
+
+
+
+
+`,
+ options: [anyWith({ svg: 'always' })],
+ errors: [
+ { message: 'Require self-closing on SVG elements ().', line: 8 }
+ ]
+ },
+ {
+ code: ALL_CODE,
+ output: `
+
+
+
+
+
+
+
+
+
+
+`,
+ options: [anyWith({ svg: 'never' })],
+ errors: [
+ { message: 'Disallow self-closing on SVG elements ().', line: 9 }
+ ]
+ },
+ {
+ code: ALL_CODE,
+ output: `
+
+
+
+
+
+
+
+
+
+
+`,
+ options: [anyWith({ math: 'always' })],
+ errors: [
+ { message: 'Require self-closing on MathML elements ().', line: 10 }
+ ]
+ },
+ {
+ code: ALL_CODE,
+ output: `
+
+
+
+
+
+
+
+
+
+
+`,
+ options: [anyWith({ math: 'never' })],
+ errors: [
+ { message: 'Disallow self-closing on MathML elements ().', line: 11 }
+ ]
+ }
+ ]
+})