Skip to content

Commit e53bfd3

Browse files
ota-meshimichalsnik
authored andcommitted
[New] Add vue/multiline-html-element-content-newline rule (#551)
* [New] Add `vue/multiline-html-element-content-newline` rule * Update `ignoreNames` -> `ignores` and report messages
1 parent bf7c2b7 commit e53bfd3

5 files changed

+745
-0
lines changed

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
232232
| | Rule ID | Description |
233233
|:---|:--------|:------------|
234234
| :wrench: | [vue/component-name-in-template-casing](./docs/rules/component-name-in-template-casing.md) | enforce specific casing for the component naming style in template |
235+
| :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 |
235236
| :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 |
236237
| :wrench: | [vue/script-indent](./docs/rules/script-indent.md) | enforce consistent indentation in `<script>` |
237238

Diff for: docs/rules/multiline-html-element-content-newline.md

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# require a line break before and after the contents of a multiline element (vue/multiline-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 multiline element.
8+
9+
10+
:-1: Examples of **incorrect** code:
11+
12+
```html
13+
<div>multiline
14+
content</div>
15+
16+
<div
17+
attr
18+
>multiline start tag</div>
19+
20+
<tr><td>multiline</td>
21+
<td>children</td></tr>
22+
23+
<div><!-- multiline
24+
comment --></div>
25+
26+
<div
27+
></div>
28+
```
29+
30+
:+1: Examples of **correct** code:
31+
32+
```html
33+
<div>
34+
multiline
35+
content
36+
</div>
37+
38+
<div
39+
attr
40+
>
41+
multiline start tag
42+
</div>
43+
44+
<tr>
45+
<td>multiline</td>
46+
<td>children</td>
47+
</tr>
48+
49+
<div>
50+
<!-- multiline
51+
comment -->
52+
</div>
53+
54+
<div
55+
>
56+
</div>
57+
58+
<div attr>singleline element</div>
59+
```
60+
61+
62+
## :wrench: Options
63+
64+
```json
65+
{
66+
"vue/multiline-html-element-content-newline": ["error", {
67+
"ignores": ["pre", "textarea"]
68+
}]
69+
}
70+
```
71+
72+
- `ignores` ... the configuration for element names to ignore line breaks style.
73+
default `["pre", "textarea"]`
74+
75+
76+
:+1: Examples of **correct** code:
77+
78+
```html
79+
/* eslint vue/multiline-html-element-content-newline: ["error", { "ignores": ["VueComponent", "pre", "textarea"]}] */
80+
81+
<VueComponent>multiline
82+
content</VueComponent>
83+
84+
<VueComponent><span
85+
class="bold">For example,</span>
86+
Defines the Vue component that accepts preformatted text.</VueComponent>
87+
```

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module.exports = {
1919
'html-self-closing': require('./rules/html-self-closing'),
2020
'jsx-uses-vars': require('./rules/jsx-uses-vars'),
2121
'max-attributes-per-line': require('./rules/max-attributes-per-line'),
22+
'multiline-html-element-content-newline': require('./rules/multiline-html-element-content-newline'),
2223
'mustache-interpolation-spacing': require('./rules/mustache-interpolation-spacing'),
2324
'name-property-casing': require('./rules/name-property-casing'),
2425
'no-async-in-computed-properties': require('./rules/no-async-in-computed-properties'),

Diff for: lib/rules/multiline-html-element-content-newline.js

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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 isMultilineElement (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+
}, options)
25+
}
26+
27+
function getPhrase (lineBreaks) {
28+
switch (lineBreaks) {
29+
case 0: return 'no'
30+
default: return `${lineBreaks}`
31+
}
32+
}
33+
/**
34+
* Check whether the given element is empty or not.
35+
* This ignores whitespaces, doesn't ignore comments.
36+
* @param {VElement} node The element node to check.
37+
* @param {SourceCode} sourceCode The source code object of the current context.
38+
* @returns {boolean} `true` if the element is empty.
39+
*/
40+
function isEmpty (node, sourceCode) {
41+
const start = node.startTag.range[1]
42+
const end = node.endTag.range[0]
43+
return sourceCode.text.slice(start, end).trim() === ''
44+
}
45+
46+
// ------------------------------------------------------------------------------
47+
// Rule Definition
48+
// ------------------------------------------------------------------------------
49+
50+
module.exports = {
51+
meta: {
52+
docs: {
53+
description: 'require a line break before and after the contents of a multiline element',
54+
category: undefined,
55+
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.2/docs/rules/multiline-html-element-content-newline.md'
56+
},
57+
fixable: 'whitespace',
58+
schema: [{
59+
type: 'object',
60+
properties: {
61+
'ignores': {
62+
type: 'array',
63+
items: { type: 'string' },
64+
uniqueItems: true,
65+
additionalItems: false
66+
}
67+
},
68+
additionalProperties: false
69+
}],
70+
messages: {
71+
unexpectedAfterClosingBracket: 'Expected 1 line break after opening tag (`<{{name}}>`), but {{actual}} line breaks found.',
72+
unexpectedBeforeOpeningBracket: 'Expected 1 line break before closing tag (`</{{name}}>`), but {{actual}} line breaks found.'
73+
}
74+
},
75+
76+
create (context) {
77+
const ignores = parseOptions(context.options[0]).ignores
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 (!isMultilineElement(node)) {
99+
return
100+
}
101+
102+
const getTokenOption = { includeComments: true, filter: (token) => token.type !== 'HTMLWhitespace' }
103+
const contentFirst = template.getTokenAfter(node.startTag, getTokenOption)
104+
const contentLast = template.getTokenBefore(node.endTag, getTokenOption)
105+
106+
const beforeLineBreaks = contentFirst.loc.start.line - node.startTag.loc.end.line
107+
const afterLineBreaks = node.endTag.loc.start.line - contentLast.loc.end.line
108+
if (beforeLineBreaks !== 1) {
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+
actual: getPhrase(beforeLineBreaks)
119+
},
120+
fix (fixer) {
121+
const range = [node.startTag.range[1], contentFirst.range[0]]
122+
return fixer.replaceTextRange(range, '\n')
123+
}
124+
})
125+
}
126+
127+
if (isEmpty(node, sourceCode)) {
128+
return
129+
}
130+
131+
if (afterLineBreaks !== 1) {
132+
context.report({
133+
node: template.getFirstToken(node.endTag),
134+
loc: {
135+
start: contentLast.loc.end,
136+
end: node.endTag.loc.start
137+
},
138+
messageId: 'unexpectedBeforeOpeningBracket',
139+
data: {
140+
name: node.name,
141+
actual: getPhrase(afterLineBreaks)
142+
},
143+
fix (fixer) {
144+
const range = [contentLast.range[1], node.endTag.range[0]]
145+
return fixer.replaceTextRange(range, '\n')
146+
}
147+
})
148+
}
149+
},
150+
'VElement:exit' (node) {
151+
if (inIgnoreElement === node) {
152+
inIgnoreElement = null
153+
}
154+
}
155+
})
156+
}
157+
}

0 commit comments

Comments
 (0)