Skip to content

Commit e53bfd3

Browse files
ota-meshimichalsnik
authored andcommittedAug 13, 2018
[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
 

‎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

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+
```

‎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'),
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,499 @@
1+
/**
2+
* @author Yosuke Ota
3+
*/
4+
'use strict'
5+
6+
// ------------------------------------------------------------------------------
7+
// Requirements
8+
// ------------------------------------------------------------------------------
9+
10+
const rule = require('../../../lib/rules/multiline-html-element-content-newline')
11+
const RuleTester = require('eslint').RuleTester
12+
13+
// ------------------------------------------------------------------------------
14+
// Tests
15+
// ------------------------------------------------------------------------------
16+
17+
const tester = new RuleTester({
18+
parser: 'vue-eslint-parser',
19+
parserOptions: {
20+
ecmaVersion: 2015
21+
}
22+
})
23+
24+
tester.run('multiline-html-element-content-newline', rule, {
25+
valid: [
26+
`<template><div class="panel">content</div></template>`,
27+
`<template><div class="panel"><div></div></div></template>`,
28+
`<template><div class="panel"><!-- comment --></div></template>`,
29+
`
30+
<template>
31+
<div class="panel">
32+
content
33+
</div>
34+
</template>`,
35+
`
36+
<template>
37+
<div
38+
class="panel"
39+
>
40+
content
41+
</div>
42+
</template>`,
43+
`
44+
<template>
45+
<div>
46+
<div>
47+
content
48+
content
49+
</div>
50+
</div>
51+
</template>`,
52+
`<div>multiline end tag</div
53+
>`,
54+
// empty
55+
`<template><div class="panel"></div></template>`,
56+
`
57+
<template>
58+
<div
59+
class="panel">
60+
</div>
61+
</template>`,
62+
// self closing
63+
`
64+
<template>
65+
<self-closing />
66+
</template>`,
67+
// ignores
68+
`
69+
<template>
70+
<pre>content</pre>
71+
<pre
72+
id="test-pre"
73+
>content</pre>
74+
<pre><div
75+
>content</div></pre>
76+
<pre>content
77+
content</pre>
78+
<textarea>content</textarea>
79+
<textarea
80+
id="test-textarea"
81+
>content</textarea>
82+
<textarea>content
83+
content</textarea>
84+
</template>`,
85+
{
86+
code: `
87+
<template>
88+
<ignore-tag>content</ignore-tag>
89+
<ignore-tag
90+
id="test-pre"
91+
>content</ignore-tag>
92+
<ignore-tag><div
93+
>content</div></ignore-tag>
94+
<ignore-tag>>content
95+
content</ignore-tag>
96+
</template>`,
97+
options: [{
98+
ignores: ['ignore-tag']
99+
}]
100+
},
101+
// Ignore if no closing brackets
102+
`
103+
<template>
104+
<div
105+
id=
106+
""
107+
`
108+
],
109+
invalid: [
110+
{
111+
code: `
112+
<template>
113+
<div
114+
class="panel"
115+
>content</div>
116+
</template>
117+
`,
118+
output: `
119+
<template>
120+
<div
121+
class="panel"
122+
>
123+
content
124+
</div>
125+
</template>
126+
`,
127+
errors: [
128+
{
129+
message: 'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
130+
line: 5,
131+
column: 12,
132+
nodeType: 'HTMLTagClose',
133+
endLine: 5,
134+
endColumn: 12
135+
},
136+
{
137+
message: 'Expected 1 line break before closing tag (`</div>`), but no line breaks found.',
138+
line: 5,
139+
column: 19,
140+
nodeType: 'HTMLEndTagOpen',
141+
endLine: 5,
142+
endColumn: 19
143+
}
144+
]
145+
},
146+
// spaces
147+
{
148+
code: `
149+
<template>
150+
<div
151+
class="panel"
152+
> content</div>
153+
</template>
154+
`,
155+
output: `
156+
<template>
157+
<div
158+
class="panel"
159+
>
160+
content
161+
</div>
162+
</template>
163+
`,
164+
errors: [
165+
{
166+
message: 'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
167+
line: 5,
168+
column: 12,
169+
endLine: 5,
170+
endColumn: 15
171+
},
172+
{
173+
message: 'Expected 1 line break before closing tag (`</div>`), but no line breaks found.',
174+
line: 5,
175+
column: 22,
176+
endLine: 5,
177+
endColumn: 22
178+
}
179+
]
180+
},
181+
// elements
182+
{
183+
code: `
184+
<template>
185+
<div><div></div>
186+
<div></div></div>
187+
</template>
188+
`,
189+
output: `
190+
<template>
191+
<div>
192+
<div></div>
193+
<div></div>
194+
</div>
195+
</template>
196+
`,
197+
errors: [
198+
{
199+
message: 'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
200+
line: 3,
201+
column: 16,
202+
endLine: 3,
203+
endColumn: 16
204+
},
205+
{
206+
message: 'Expected 1 line break before closing tag (`</div>`), but no line breaks found.',
207+
line: 4,
208+
column: 22,
209+
endLine: 4,
210+
endColumn: 22
211+
}
212+
]
213+
},
214+
// contents
215+
{
216+
code: `
217+
<template>
218+
<div>multiline
219+
content</div>
220+
</template>`,
221+
output: `
222+
<template>
223+
<div>
224+
multiline
225+
content
226+
</div>
227+
</template>`,
228+
errors: [
229+
'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
230+
'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
231+
]
232+
},
233+
{
234+
code: `
235+
<template>
236+
<div>multiline content
237+
</div>
238+
</template>`,
239+
output: `
240+
<template>
241+
<div>
242+
multiline content
243+
</div>
244+
</template>`,
245+
errors: [
246+
'Expected 1 line break after opening tag (`<div>`), but no line breaks found.'
247+
]
248+
},
249+
{
250+
code: `
251+
<template>
252+
<div>
253+
multiline content</div>
254+
</template>`,
255+
output: `
256+
<template>
257+
<div>
258+
multiline content
259+
</div>
260+
</template>`,
261+
errors: [
262+
'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
263+
]
264+
},
265+
// comments
266+
{
267+
code: `
268+
<template>
269+
<div><!--comment-->
270+
<!--comment--></div>
271+
</template>
272+
`,
273+
output: `
274+
<template>
275+
<div>
276+
<!--comment-->
277+
<!--comment-->
278+
</div>
279+
</template>
280+
`,
281+
errors: [
282+
'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
283+
'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
284+
]
285+
},
286+
{
287+
code: `
288+
<template>
289+
<div><!--comment
290+
comment--></div>
291+
</template>
292+
`,
293+
output: `
294+
<template>
295+
<div>
296+
<!--comment
297+
comment-->
298+
</div>
299+
</template>
300+
`,
301+
errors: [
302+
'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
303+
'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
304+
]
305+
},
306+
// one error
307+
{
308+
code: `
309+
<template>
310+
<div>content
311+
content
312+
</div>
313+
</template>
314+
`,
315+
output: `
316+
<template>
317+
<div>
318+
content
319+
content
320+
</div>
321+
</template>
322+
`,
323+
errors: [
324+
'Expected 1 line break after opening tag (`<div>`), but no line breaks found.'
325+
]
326+
},
327+
{
328+
code: `
329+
<template>
330+
<div>
331+
content
332+
content</div>
333+
</template>
334+
`,
335+
output: `
336+
<template>
337+
<div>
338+
content
339+
content
340+
</div>
341+
</template>
342+
`,
343+
errors: [
344+
'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
345+
]
346+
},
347+
// multi
348+
{
349+
code: `
350+
<template><div>content<div>content
351+
content</div>content</div></template>
352+
`,
353+
output: `
354+
<template>
355+
<div>
356+
content<div>
357+
content
358+
content
359+
</div>content
360+
</div>
361+
</template>
362+
`,
363+
errors: [
364+
'Expected 1 line break after opening tag (`<template>`), but no line breaks found.',
365+
'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
366+
'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
367+
'Expected 1 line break before closing tag (`</div>`), but no line breaks found.',
368+
'Expected 1 line break before closing tag (`</div>`), but no line breaks found.',
369+
'Expected 1 line break before closing tag (`</template>`), but no line breaks found.'
370+
]
371+
},
372+
// multi line breaks
373+
{
374+
code: `
375+
<template>
376+
<div>
377+
378+
content
379+
content
380+
381+
</div>
382+
</template>
383+
`,
384+
output: `
385+
<template>
386+
<div>
387+
content
388+
content
389+
</div>
390+
</template>
391+
`,
392+
errors: [
393+
'Expected 1 line break after opening tag (`<div>`), but 2 line breaks found.',
394+
'Expected 1 line break before closing tag (`</div>`), but 2 line breaks found.'
395+
]
396+
},
397+
// mustache
398+
{
399+
code: `
400+
<template>
401+
<div>{{content}}
402+
{{content2}}</div>
403+
</template>
404+
`,
405+
output: `
406+
<template>
407+
<div>
408+
{{content}}
409+
{{content2}}
410+
</div>
411+
</template>
412+
`,
413+
errors: [
414+
'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
415+
'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
416+
]
417+
},
418+
// mix
419+
{
420+
code: `
421+
<template>
422+
<div>content
423+
<child></child>
424+
<!-- comment --></div>
425+
</template>
426+
`,
427+
output: `
428+
<template>
429+
<div>
430+
content
431+
<child></child>
432+
<!-- comment -->
433+
</div>
434+
</template>
435+
`,
436+
errors: [
437+
'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
438+
'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
439+
]
440+
},
441+
// start tag
442+
{
443+
code: `
444+
<template>
445+
<div
446+
>content</div>
447+
</template>
448+
`,
449+
output: `
450+
<template>
451+
<div
452+
>
453+
content
454+
</div>
455+
</template>
456+
`,
457+
errors: [
458+
'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
459+
'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
460+
]
461+
},
462+
{
463+
code: `
464+
<template>
465+
<div
466+
attr>content</div>
467+
</template>
468+
`,
469+
output: `
470+
<template>
471+
<div
472+
attr>
473+
content
474+
</div>
475+
</template>
476+
`,
477+
errors: [
478+
'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
479+
'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
480+
]
481+
},
482+
{
483+
code: `
484+
<template>
485+
<div
486+
></div>
487+
</template>
488+
`,
489+
output: `
490+
<template>
491+
<div
492+
>
493+
</div>
494+
</template>
495+
`,
496+
errors: ['Expected 1 line break after opening tag (`<div>`), but no line breaks found.']
497+
}
498+
]
499+
})

0 commit comments

Comments
 (0)
Please sign in to comment.