Skip to content

Commit cae7d51

Browse files
ota-meshimichalsnik
authored andcommitted
[New] Add vue/singleline-html-element-content-newline rule (#552)
* [New] Add `vue/singleline-html-element-content-newline` rule * Update `strict` -> `ignoreWhenNoAttributes`, `ignoreNames` -> `ignores` and report messages
1 parent a7d0b3c commit cae7d51

5 files changed

+694
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
235235
| :wrench: | [vue/multiline-html-element-content-newline](./docs/rules/multiline-html-element-content-newline.md) | require a line break before and after the contents of a multiline element |
236236
| :wrench: | [vue/no-spaces-around-equal-signs-in-attribute](./docs/rules/no-spaces-around-equal-signs-in-attribute.md) | disallow spaces around equal signs in attribute |
237237
| :wrench: | [vue/script-indent](./docs/rules/script-indent.md) | enforce consistent indentation in `<script>` |
238+
| :wrench: | [vue/singleline-html-element-content-newline](./docs/rules/singleline-html-element-content-newline.md) | require a line break before and after the contents of a singleline element |
238239

239240
### Deprecated
240241

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# require a line break before and after the contents of a singleline element (vue/singleline-html-element-content-newline)
2+
3+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
4+
5+
## :book: Rule Details
6+
7+
This rule enforces a line break before and after the contents of a singleline element.
8+
9+
10+
:-1: Examples of **incorrect** code:
11+
12+
```html
13+
<div attr>content</div>
14+
15+
<tr attr><td>{{ data1 }}</td><td>{{ data2 }}</td></tr>
16+
17+
<div attr><!-- comment --></div>
18+
```
19+
20+
:+1: Examples of **correct** code:
21+
22+
```html
23+
<div attr>
24+
content
25+
</div>
26+
27+
<tr attr>
28+
<td>
29+
{{ data1 }}
30+
</td>
31+
<td>
32+
{{ data2 }}
33+
</td>
34+
</tr>
35+
36+
<div attr>
37+
<!-- comment -->
38+
</div>
39+
```
40+
41+
## :wrench: Options
42+
43+
```json
44+
{
45+
"vue/singleline-html-element-content-newline": ["error", {
46+
"ignoreWhenNoAttributes": true,
47+
"ignores": ["pre", "textarea"]
48+
}]
49+
}
50+
```
51+
52+
- `ignoreWhenNoAttributes` ... allows having contents in one line, when given element has no attributes.
53+
default `true`
54+
- `ignores` ... the configuration for element names to ignore line breaks style.
55+
default `["pre", "textarea"]`
56+
57+
:-1: Examples of **incorrect** code for `{ignoreWhenNoAttributes: false}`:
58+
59+
```html
60+
/* eslint vue/singleline-html-element-content-newline: ["error", { "ignoreWhenNoAttributes": false}] */
61+
62+
<div>content</div>
63+
64+
<tr><td>{{ data1 }}</td><td>{{ data2 }}</td></tr>
65+
66+
<div><!-- comment --></div>
67+
```
68+
69+
:+1: Examples of **correct** code for `{ignoreWhenNoAttributes: true}` (default):
70+
71+
```html
72+
/* eslint vue/singleline-html-element-content-newline: ["error", { "ignoreWhenNoAttributes": true}] */
73+
74+
<div>content</div>
75+
76+
<tr><td>{{ data1 }}</td><td>{{ data2 }}</td></tr>
77+
78+
<div><!-- comment --></div>
79+
```
80+
81+
:-1: Examples of **incorrect** code for `{ignoreWhenNoAttributes: true}` (default):
82+
83+
```html
84+
/* eslint vue/singleline-html-element-content-newline: ["error", { "ignoreWhenNoAttributes": true}] */
85+
86+
<div attr>content</div>
87+
88+
<tr attr><td>{{ data1 }}</td><td>{{ data2 }}</td></tr>
89+
90+
<div attr><!-- comment --></div>
91+
```
92+
93+
:+1: Examples of **correct** code for `ignores`:
94+
95+
```html
96+
/* eslint vue/singleline-html-element-content-newline: ["error", { "ignores": ["VueComponent", "pre", "textarea"]}] */
97+
98+
<VueComponent>content</VueComponent>
99+
100+
<VueComponent attr><span>content</span></VueComponent>
101+
```

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ module.exports = {
5050
'require-valid-default-prop': require('./rules/require-valid-default-prop'),
5151
'return-in-computed-property': require('./rules/return-in-computed-property'),
5252
'script-indent': require('./rules/script-indent'),
53+
'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
5354
'this-in-template': require('./rules/this-in-template'),
5455
'v-bind-style': require('./rules/v-bind-style'),
5556
'v-on-style': require('./rules/v-on-style'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
13+
// ------------------------------------------------------------------------------
14+
// Helpers
15+
// ------------------------------------------------------------------------------
16+
17+
function isSinglelineElement (element) {
18+
return element.loc.start.line === element.endTag.loc.start.line
19+
}
20+
21+
function parseOptions (options) {
22+
return Object.assign({
23+
'ignores': ['pre', 'textarea'],
24+
'ignoreWhenNoAttributes': true
25+
}, options)
26+
}
27+
28+
/**
29+
* Check whether the given element is empty or not.
30+
* This ignores whitespaces, doesn't ignore comments.
31+
* @param {VElement} node The element node to check.
32+
* @param {SourceCode} sourceCode The source code object of the current context.
33+
* @returns {boolean} `true` if the element is empty.
34+
*/
35+
function isEmpty (node, sourceCode) {
36+
const start = node.startTag.range[1]
37+
const end = node.endTag.range[0]
38+
return sourceCode.text.slice(start, end).trim() === ''
39+
}
40+
41+
// ------------------------------------------------------------------------------
42+
// Rule Definition
43+
// ------------------------------------------------------------------------------
44+
45+
module.exports = {
46+
meta: {
47+
docs: {
48+
description: 'require a line break before and after the contents of a singleline element',
49+
category: undefined,
50+
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/singleline-html-element-content-newline.md'
51+
},
52+
fixable: 'whitespace',
53+
schema: [{
54+
type: 'object',
55+
properties: {
56+
'ignoreWhenNoAttributes': {
57+
type: 'boolean'
58+
},
59+
'ignores': {
60+
type: 'array',
61+
items: { type: 'string' },
62+
uniqueItems: true,
63+
additionalItems: false
64+
}
65+
},
66+
additionalProperties: false
67+
}],
68+
messages: {
69+
unexpectedAfterClosingBracket: 'Expected 1 line break after opening tag (`<{{name}}>`), but no line breaks found.',
70+
unexpectedBeforeOpeningBracket: 'Expected 1 line break before closing tag (`</{{name}}>`), but no line breaks found.'
71+
}
72+
},
73+
74+
create (context) {
75+
const options = parseOptions(context.options[0])
76+
const ignores = options.ignores
77+
const ignoreWhenNoAttributes = options.ignoreWhenNoAttributes
78+
const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
79+
const sourceCode = context.getSourceCode()
80+
81+
let inIgnoreElement
82+
83+
return utils.defineTemplateBodyVisitor(context, {
84+
'VElement' (node) {
85+
if (inIgnoreElement) {
86+
return
87+
}
88+
if (ignores.indexOf(node.name) >= 0) {
89+
// ignore element name
90+
inIgnoreElement = node
91+
return
92+
}
93+
if (node.startTag.selfClosing || !node.endTag) {
94+
// self closing
95+
return
96+
}
97+
98+
if (!isSinglelineElement(node)) {
99+
return
100+
}
101+
if (ignoreWhenNoAttributes && node.startTag.attributes.length === 0) {
102+
return
103+
}
104+
105+
const getTokenOption = { includeComments: true, filter: (token) => token.type !== 'HTMLWhitespace' }
106+
const contentFirst = template.getTokenAfter(node.startTag, getTokenOption)
107+
const contentLast = template.getTokenBefore(node.endTag, getTokenOption)
108+
109+
context.report({
110+
node: template.getLastToken(node.startTag),
111+
loc: {
112+
start: node.startTag.loc.end,
113+
end: contentFirst.loc.start
114+
},
115+
messageId: 'unexpectedAfterClosingBracket',
116+
data: {
117+
name: node.name
118+
},
119+
fix (fixer) {
120+
const range = [node.startTag.range[1], contentFirst.range[0]]
121+
return fixer.replaceTextRange(range, '\n')
122+
}
123+
})
124+
125+
if (isEmpty(node, sourceCode)) {
126+
return
127+
}
128+
129+
context.report({
130+
node: template.getFirstToken(node.endTag),
131+
loc: {
132+
start: contentLast.loc.end,
133+
end: node.endTag.loc.start
134+
},
135+
messageId: 'unexpectedBeforeOpeningBracket',
136+
data: {
137+
name: node.name
138+
},
139+
fix (fixer) {
140+
const range = [contentLast.range[1], node.endTag.range[0]]
141+
return fixer.replaceTextRange(range, '\n')
142+
}
143+
})
144+
},
145+
'VElement:exit' (node) {
146+
if (inIgnoreElement === node) {
147+
inIgnoreElement = null
148+
}
149+
}
150+
})
151+
}
152+
}

0 commit comments

Comments
 (0)