-
-
Notifications
You must be signed in to change notification settings - Fork 679
New: html-self-closing
rule (fixes #31)
#129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# Enforce self-closing style (html-self-closing-style) | ||
|
||
In Vue.js template, we can use either two styles for elements which don't have their content. | ||
|
||
1. `<your-component></your-component>` | ||
2. `<your-component />` (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-style": ["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-style: "error"*/ | ||
|
||
<template> | ||
<div /> | ||
<img /> | ||
<your-component></your-component> | ||
<svg><path d=""></path></svg> | ||
</template> | ||
``` | ||
|
||
:+1: Examples of **correct** code for this rule: | ||
|
||
```html | ||
/*eslint html-self-closing-style: "error"*/ | ||
|
||
<template> | ||
<div></div> | ||
<img> | ||
<your-component /> | ||
<svg><path d="" /></svg> | ||
</template> | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
// ------------------------------------------------------------------------------ | ||
|
||
/** | ||
* Kind strings. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove this comment, it sounds like |
||
* This strings wil be displayed in error messages. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
*/ | ||
const KIND = 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 { | ||
[KIND.NORMAL]: (options && options.html && options.html.normal) || 'never', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like we could use https://github.com/letsgetrandy/brototype 😂 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or sindresorhus' dot-prop. However, I'm not sure if this is enough worth adding a new dependency. This may be not stylish, but I think enough readable. |
||
[KIND.VOID]: (options && options.html && options.html.void) || 'never', | ||
[KIND.COMPONENT]: (options && options.html && options.html.component) || 'always', | ||
[KIND.SVG]: (options && options.svg) || 'always', | ||
[KIND.MATH]: (options && options.math) || 'always' | ||
} | ||
} | ||
|
||
/** | ||
* Get the kind of the given element. | ||
* @param {VElement} node The element node to get. | ||
* @returns {string} The kind of the element. | ||
*/ | ||
function getKind (node) { | ||
if (utils.isCustomComponent(node)) { | ||
return KIND.COMPONENT | ||
} | ||
if (utils.isHtmlElementNode(node)) { | ||
if (utils.isHtmlVoidElementName(node.name)) { | ||
return KIND.VOID | ||
} | ||
return KIND.NORMAL | ||
} | ||
if (utils.isSvgElementNode(node)) { | ||
return KIND.SVG | ||
} | ||
if (utils.isMathMLElementNode(node)) { | ||
return KIND.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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You missed the second parameter here |
||
* @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] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I love |
||
|
||
return sourceCode.text.slice(start, end).trim() === '' | ||
} | ||
|
||
/** | ||
* Creates AST event handlers for html-self-closing-style. | ||
* | ||
* @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 kind = getKind(node) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd keep Btw. I think we should rename There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Hmm, I'm not sure if it increases maintainability/readability.
It's nice. My English skill is poor. |
||
const mode = options[kind] | ||
|
||
if (mode === 'always' && !node.startTag.selfClosing && isEmpty(node, sourceCode)) { | ||
context.report({ | ||
node, | ||
loc: node.loc, | ||
message: 'Require self-closing on {{kind}}.', | ||
data: { kind }, | ||
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 {{kind}}.', | ||
data: { kind }, | ||
fix: (fixer) => { | ||
const tokens = context.parserServices.getTemplateBodyTokenStore() | ||
const close = tokens.getLastToken(node.startTag) | ||
if (close.type !== 'HTMLSelfClosingTagClose') { | ||
return null | ||
} | ||
if (kind === KIND.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 | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can shorten the name to just
html-self-closing
?