Skip to content

Commit bf9b95c

Browse files
dev1437FloEdelmann
andauthored
Create padding-line-between-tags rule (#1966)
* Create space-between-siblings rule * Fix lint * Change how options are initialised * Fill in name * Remove block * Update test * Tidy up test and examples * Change message * Add test for flat tag * Add never functionality * Add tests for never and update previous tests for new schema * Linting * Update docs * Rename to padding-line-between-tags * Change schema to array of objects * Change messages * Allow for blank lines to be specified on each tag * Update tests * Linting * Update docs * Add another test * Lint * Clean up doc * Clean up tests * Change type * Fix doc * Update docs/rules/padding-line-between-tags.md Co-authored-by: Flo Edelmann <[email protected]> * Ignore top level * Remove testing stuff * Simplify logic and make last configuration apply * Add test for last configuration applying * Update docs * Add newlines between siblings on same line * Update docs * Lint * Fix doc * Fix spaces on line diff = 0 * Remove only space between tags * Append text backwards * Uncomment tests * Linting * Fix loop and add test * Add another test Co-authored-by: Flo Edelmann <[email protected]>
1 parent faa067e commit bf9b95c

File tree

5 files changed

+1386
-0
lines changed

5 files changed

+1386
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ For example:
258258
| [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | | :hammer: |
259259
| [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: | :lipstick: |
260260
| [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components | | :hammer: |
261+
| [vue/padding-line-between-tags](./padding-line-between-tags.md) | Insert newlines between sibling tags in template | :wrench: | :warning: |
261262
| [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: | :hammer: |
262263
| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: | :lipstick: |
263264
| [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: | :hammer: |
+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/padding-line-between-tags
5+
description: Require or disallow newlines between sibling tags in template
6+
---
7+
# vue/padding-line-between-tags
8+
9+
> Require or disallow newlines between sibling tags in template
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :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.
13+
14+
## :book: Rule Details
15+
16+
This rule requires or disallows newlines between sibling HTML tags.
17+
18+
<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error']}">
19+
20+
```vue
21+
<template>
22+
<div>
23+
<!-- ✓ GOOD: -->
24+
<div></div>
25+
26+
<div>
27+
</div>
28+
29+
<div />
30+
31+
<div />
32+
<!-- ✗ BAD: -->
33+
<div></div>
34+
<div>
35+
</div>
36+
<div /><div />
37+
</div>
38+
</template>
39+
```
40+
41+
</eslint-code-block>
42+
43+
## :wrench: Options
44+
45+
```json
46+
{
47+
"vue/padding-line-between-tags": ["error", [
48+
{ "blankLine": "always", "prev": "*", "next": "*" }
49+
]]
50+
}
51+
```
52+
53+
This rule requires blank lines between each sibling HTML tag by default.
54+
55+
A configuration is an object which has 3 properties; blankLine, prev and next. For example, { blankLine: "always", prev: "br", next: "div" } means “one or more blank lines are required between a br tag and a div tag.” You can supply any number of configurations. If a tag pair matches multiple configurations, the last matched configuration will be used.
56+
57+
- `blankLine` is one of the following:
58+
- `always` requires one or more blank lines.
59+
- `never` disallows blank lines.
60+
- `prev` any tag name without brackets.
61+
- `next` any tag name without brackets.
62+
63+
### Disallow blank lines between all tags
64+
65+
`{ blankLine: 'never', prev: '*', next: '*' }`
66+
67+
<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error', [
68+
{ blankLine: 'never', prev: '*', next: '*' }
69+
]]}">
70+
71+
```vue
72+
<template>
73+
<div>
74+
<div></div>
75+
<div>
76+
</div>
77+
<div />
78+
</div>
79+
</template>
80+
```
81+
82+
</eslint-code-block>
83+
84+
### Require newlines after `<br>`
85+
86+
`{ blankLine: 'always', prev: 'br', next: '*' }`
87+
88+
<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error', [
89+
{ blankLine: 'always', prev: 'br', next: '*' }
90+
]]}">
91+
92+
```vue
93+
<template>
94+
<div>
95+
<ul>
96+
<li>
97+
</li>
98+
<br />
99+
100+
<li>
101+
</li>
102+
</ul>
103+
</div>
104+
</template>
105+
```
106+
107+
</eslint-code-block>
108+
109+
### Require newlines before `<br>`
110+
111+
`{ blankLine: 'always', prev: '*', next: 'br' }`
112+
113+
<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error', [
114+
{ blankLine: 'always', prev: '*', next: 'br' }
115+
]]}">
116+
117+
```vue
118+
<template>
119+
<div>
120+
<ul>
121+
<li>
122+
</li>
123+
124+
<br />
125+
<li>
126+
</li>
127+
</ul>
128+
</div>
129+
</template>
130+
```
131+
132+
</eslint-code-block>
133+
134+
### Require newlines between `<br>` and `<img>`
135+
136+
`{ blankLine: 'always', prev: 'br', next: 'img' }`
137+
138+
<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error', [
139+
{ blankLine: 'always', prev: 'br', next: 'img' }
140+
]]}">
141+
142+
```vue
143+
<template>
144+
<div>
145+
<ul>
146+
<li>
147+
</li>
148+
<br />
149+
150+
<img />
151+
<li>
152+
</li>
153+
</ul>
154+
</div>
155+
</template>
156+
```
157+
158+
</eslint-code-block>
159+
160+
## :mag: Implementation
161+
162+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/padding-line-between-tags.js)
163+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/padding-line-between-tags.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ module.exports = {
184184
'script-setup-uses-vars': require('./rules/script-setup-uses-vars'),
185185
'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
186186
'sort-keys': require('./rules/sort-keys'),
187+
'padding-line-between-tags': require('./rules/padding-line-between-tags'),
187188
'space-in-parens': require('./rules/space-in-parens'),
188189
'space-infix-ops': require('./rules/space-infix-ops'),
189190
'space-unary-ops': require('./rules/space-unary-ops'),
+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* @author dev1437
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+
* Split the source code into multiple lines based on the line delimiters.
15+
* Copied from padding-line-between-blocks
16+
* @param {string} text Source code as a string.
17+
* @returns {string[]} Array of source code lines.
18+
*/
19+
function splitLines(text) {
20+
return text.split(/\r\n|[\r\n\u2028\u2029]/gu)
21+
}
22+
23+
/**
24+
* @param {RuleContext} context
25+
* @param {VElement} tag
26+
* @param {VElement} sibling
27+
*/
28+
function insertNewLine(context, tag, sibling) {
29+
context.report({
30+
messageId: 'always',
31+
loc: sibling.loc,
32+
// @ts-ignore
33+
fix(fixer) {
34+
return fixer.insertTextAfter(tag, '\n')
35+
}
36+
})
37+
}
38+
39+
/**
40+
* @param {RuleContext} context
41+
* @param {VEndTag | VStartTag} endTag
42+
* @param {VElement} sibling
43+
*/
44+
function removeExcessLines(context, endTag, sibling) {
45+
context.report({
46+
messageId: 'never',
47+
loc: sibling.loc,
48+
// @ts-ignore
49+
fix(fixer) {
50+
const start = endTag.range[1]
51+
const end = sibling.range[0]
52+
const paddingText = context.getSourceCode().text.slice(start, end)
53+
const textBetween = splitLines(paddingText)
54+
let newTextBetween = `\n${textBetween.pop()}`
55+
for (let i = textBetween.length - 1; i >= 0; i--) {
56+
if (!/^\s*$/.test(textBetween[i])) {
57+
newTextBetween = `${i === 0 ? '' : '\n'}${
58+
textBetween[i]
59+
}${newTextBetween}`
60+
}
61+
}
62+
return fixer.replaceTextRange([start, end], `${newTextBetween}`)
63+
}
64+
})
65+
}
66+
67+
// ------------------------------------------------------------------------------
68+
// Rule Definition
69+
// ------------------------------------------------------------------------------
70+
71+
/**
72+
* @param {RuleContext} context
73+
*/
74+
function checkNewline(context) {
75+
/** @type {Array<{blankLine: "always" | "never", prev: string, next: string}>} */
76+
const configureList = context.options[0] || [
77+
{ blankLine: 'always', prev: '*', next: '*' }
78+
]
79+
80+
/**
81+
* @param {VElement} block
82+
*/
83+
return (block) => {
84+
if (!block.parent.parent) {
85+
return
86+
}
87+
88+
const endTag = block.endTag || block.startTag
89+
const lowerSiblings = block.parent.children
90+
.filter(
91+
(element) =>
92+
element.type === 'VElement' && element.range !== block.range
93+
)
94+
.filter((sibling) => sibling.range[0] - endTag.range[1] >= 0)
95+
96+
if (lowerSiblings.length === 0) {
97+
return
98+
}
99+
100+
const closestSibling = /** @type {VElement} */ (lowerSiblings[0])
101+
102+
for (let i = configureList.length - 1; i >= 0; --i) {
103+
const configure = configureList[i]
104+
const matched =
105+
(configure.prev === '*' || block.name === configure.prev) &&
106+
(configure.next === '*' || closestSibling.name === configure.next)
107+
108+
if (matched) {
109+
const lineDifference =
110+
closestSibling.loc.start.line - endTag.loc.end.line
111+
if (configure.blankLine === 'always') {
112+
if (lineDifference === 1) {
113+
insertNewLine(context, block, closestSibling)
114+
} else if (lineDifference === 0) {
115+
context.report({
116+
messageId: 'always',
117+
loc: closestSibling.loc,
118+
// @ts-ignore
119+
fix(fixer) {
120+
const lastSpaces = /** @type {RegExpExecArray} */ (
121+
/^\s*/.exec(
122+
context.getSourceCode().lines[endTag.loc.start.line - 1]
123+
)
124+
)[0]
125+
126+
return fixer.insertTextAfter(endTag, `\n\n${lastSpaces}`)
127+
}
128+
})
129+
}
130+
} else {
131+
if (lineDifference > 1) {
132+
let hasOnlyTextBetween = true
133+
for (
134+
let i = endTag.loc.start.line;
135+
i < closestSibling.loc.start.line - 1 && hasOnlyTextBetween;
136+
i++
137+
) {
138+
hasOnlyTextBetween = !/^\s*$/.test(
139+
context.getSourceCode().lines[i]
140+
)
141+
}
142+
if (!hasOnlyTextBetween) {
143+
removeExcessLines(context, endTag, closestSibling)
144+
}
145+
}
146+
}
147+
break
148+
}
149+
}
150+
}
151+
}
152+
153+
module.exports = {
154+
meta: {
155+
type: 'layout',
156+
docs: {
157+
description:
158+
'require or disallow newlines between sibling tags in template',
159+
categories: undefined,
160+
url: 'https://eslint.vuejs.org/rules/padding-line-between-tags.html'
161+
},
162+
fixable: 'whitespace',
163+
schema: [
164+
{
165+
type: 'array',
166+
items: {
167+
type: 'object',
168+
properties: {
169+
blankLine: { enum: ['always', 'never'] },
170+
prev: { type: 'string' },
171+
next: { type: 'string' }
172+
},
173+
additionalProperties: false,
174+
required: ['blankLine', 'prev', 'next']
175+
}
176+
}
177+
],
178+
messages: {
179+
never: 'Unexpected blank line before this tag.',
180+
always: 'Expected blank line before this tag.'
181+
}
182+
},
183+
/** @param {RuleContext} context */
184+
create(context) {
185+
return utils.defineTemplateBodyVisitor(context, {
186+
VElement: checkNewline(context)
187+
})
188+
}
189+
}

0 commit comments

Comments
 (0)