Skip to content

Commit 96a830a

Browse files
authored
[New] Add vue/html-content-newline (#4)
1 parent faaa69c commit 96a830a

File tree

5 files changed

+800
-0
lines changed

5 files changed

+800
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
209209
|:---|:--------|:------------|
210210
| :wrench: | [vue/html-closing-bracket-newline](./docs/rules/html-closing-bracket-newline.md) | require or disallow a line break before tag's closing brackets |
211211
| :wrench: | [vue/html-closing-bracket-spacing](./docs/rules/html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets |
212+
| :wrench: | [vue/html-content-newline](./docs/rules/html-content-newline.md) | require or disallow a line break before and after html contents |
212213
| :wrench: | [vue/prop-name-casing](./docs/rules/prop-name-casing.md) | enforce specific casing for the Prop name in Vue components |
213214
| :wrench: | [vue/script-indent](./docs/rules/script-indent.md) | enforce consistent indentation in `<script>` |
214215

docs/rules/html-content-newline.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# require or disallow a line break before and after html contents (vue/html-content-newline)
2+
3+
- :wrench: The `--fix` option on the [command line](http://eslint.org/docs/user-guide/command-line-interface#fix) can automatically fix some of the problems reported by this rule.
4+
5+
## :book: Rule Details
6+
7+
This rule enforces a line break (or no line break) before and after html contents.
8+
9+
10+
:-1: Examples of **incorrect** code:
11+
12+
```html
13+
<div
14+
class="panel"
15+
>content</div>
16+
```
17+
18+
:+1: Examples of **correct** code:
19+
20+
```html
21+
<div class="panel">content</div>
22+
23+
<div class="panel">
24+
content
25+
</div>
26+
27+
<div
28+
class="panel"
29+
>
30+
content
31+
</div>
32+
```
33+
34+
35+
## :wrench: Options
36+
37+
```json
38+
{
39+
"vue/html-content-newline": ["error", {
40+
"singleline": "ignore",
41+
"multiline": "always",
42+
"ignoreNames": ["pre", "textarea"]
43+
}]
44+
}
45+
```
46+
47+
- `singleline` ... the configuration for single-line elements. It's a single-line element if startTag, endTag and contents are single-line.
48+
- `"ignore"` ... Don't enforce line breaks style before and after the contents. This is the default.
49+
- `"never"` ... disallow line breaks before and after the contents.
50+
- `"always"` ... require one line break before and after the contents.
51+
- `multiline` ... the configuration for multiline elements. It's a multiline element if startTag, endTag or contents are multiline.
52+
- `"ignore"` ... Don't enforce line breaks style before and after the contents.
53+
- `"never"` ... disallow line breaks before and after the contents.
54+
- `"always"` ... require one line break before and after the contents. This is the default.
55+
- `ignoreNames` ... the configuration for element names to ignore line breaks style.
56+
default `["pre", "textarea"]`
57+
58+
59+
:-1: Examples of **incorrect** code:
60+
61+
```html
62+
/*eslint vue/html-content-newline: ["error", { "singleline": "always", "multiline": "never"}] */
63+
64+
<div class="panel">content</div>
65+
66+
<div
67+
class="panel"
68+
>
69+
content
70+
</div>
71+
```
72+
73+
:+1: Examples of **correct** code:
74+
75+
```html
76+
/*eslint vue/html-content-newline: ["error", { "singleline": "always", "multiline": "never"}] */
77+
78+
<div class="panel">
79+
content
80+
</div>
81+
82+
<div
83+
class="panel"
84+
>content</div>
85+
```
86+

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module.exports = {
1212
'comment-directive': require('./rules/comment-directive'),
1313
'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
1414
'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'),
15+
'html-content-newline': require('./rules/html-content-newline'),
1516
'html-end-tags': require('./rules/html-end-tags'),
1617
'html-indent': require('./rules/html-indent'),
1718
'html-quotes': require('./rules/html-quotes'),

lib/rules/html-content-newline.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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 isMultiline (node, contentFirst, contentLast) {
18+
if (node.startTag.loc.start.line !== node.startTag.loc.end.line ||
19+
node.endTag.loc.start.line !== node.endTag.loc.end.line) {
20+
// multiline tag
21+
return true
22+
}
23+
if (contentFirst.loc.start.line < contentLast.loc.end.line) {
24+
// multiline contents
25+
return true
26+
}
27+
return false
28+
}
29+
30+
function parseOptions (options) {
31+
return Object.assign({
32+
'singleline': 'ignore',
33+
'multiline': 'always',
34+
'ignoreNames': ['pre', 'textarea']
35+
}, options)
36+
}
37+
38+
function getPhrase (lineBreaks) {
39+
switch (lineBreaks) {
40+
case 0: return 'no line breaks'
41+
case 1: return '1 line break'
42+
default: return `${lineBreaks} line breaks`
43+
}
44+
}
45+
46+
// ------------------------------------------------------------------------------
47+
// Rule Definition
48+
// ------------------------------------------------------------------------------
49+
50+
module.exports = {
51+
meta: {
52+
docs: {
53+
description: 'require or disallow a line break before and after html contents',
54+
category: undefined,
55+
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.4.0/docs/rules/html-content-newline.md'
56+
},
57+
fixable: 'whitespace',
58+
schema: [{
59+
type: 'object',
60+
properties: {
61+
'singleline': { enum: ['ignore', 'always', 'never'] },
62+
'multiline': { enum: ['ignore', 'always', 'never'] },
63+
'ignoreNames': {
64+
type: 'array',
65+
items: { type: 'string' },
66+
uniqueItems: true,
67+
additionalItems: false
68+
}
69+
},
70+
additionalProperties: false
71+
}]
72+
},
73+
74+
create (context) {
75+
const options = parseOptions(context.options[0])
76+
const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
77+
78+
return utils.defineTemplateBodyVisitor(context, {
79+
'VElement' (node) {
80+
if (node.startTag.selfClosing || !node.endTag) {
81+
// self closing
82+
return
83+
}
84+
let target = node
85+
while (target.type === 'VElement') {
86+
if (options.ignoreNames.indexOf(target.name) >= 0) {
87+
// ignore element name
88+
return
89+
}
90+
target = target.parent
91+
}
92+
const getTokenOption = { includeComments: true, filter: (token) => token.type !== 'HTMLWhitespace' }
93+
const contentFirst = template.getTokenAfter(node.startTag, getTokenOption)
94+
const contentLast = template.getTokenBefore(node.endTag, getTokenOption)
95+
const type = isMultiline(node, contentFirst, contentLast) ? options.multiline : options.singleline
96+
if (type === 'ignore') {
97+
// 'ignore' option
98+
return
99+
}
100+
const beforeLineBreaks = contentFirst.loc.start.line - node.startTag.loc.end.line
101+
const afterLineBreaks = node.endTag.loc.start.line - contentLast.loc.end.line
102+
const expectedLineBreaks = type === 'always' ? 1 : 0
103+
if (expectedLineBreaks !== beforeLineBreaks) {
104+
context.report({
105+
node: template.getLastToken(node.startTag),
106+
loc: {
107+
start: node.startTag.loc.end,
108+
end: contentFirst.loc.start
109+
},
110+
message: `Expected {{expected}} after closing bracket of the "{{name}}" element, but {{actual}} found.`,
111+
data: {
112+
name: node.name,
113+
expected: getPhrase(expectedLineBreaks),
114+
actual: getPhrase(beforeLineBreaks)
115+
},
116+
fix (fixer) {
117+
const range = [node.startTag.range[1], contentFirst.range[0]]
118+
const text = '\n'.repeat(expectedLineBreaks)
119+
return fixer.replaceTextRange(range, text)
120+
}
121+
})
122+
}
123+
124+
if (expectedLineBreaks !== afterLineBreaks) {
125+
context.report({
126+
node: template.getFirstToken(node.endTag),
127+
loc: {
128+
start: contentLast.loc.end,
129+
end: node.endTag.loc.start
130+
},
131+
message: 'Expected {{expected}} before opening bracket of the "{{name}}" element, but {{actual}} found.',
132+
data: {
133+
name: node.name,
134+
expected: getPhrase(expectedLineBreaks),
135+
actual: getPhrase(afterLineBreaks)
136+
},
137+
fix (fixer) {
138+
const range = [contentLast.range[1], node.endTag.range[0]]
139+
const text = '\n'.repeat(expectedLineBreaks)
140+
return fixer.replaceTextRange(range, text)
141+
}
142+
})
143+
}
144+
}
145+
})
146+
}
147+
}

0 commit comments

Comments
 (0)