Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create padding-line-between-tags rule #1966

Merged
merged 44 commits into from Sep 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
2ba27ac
Create space-between-siblings rule
dev1437 Sep 10, 2022
b530e82
Fix lint
dev1437 Sep 10, 2022
36b4c2d
Change how options are initialised
dev1437 Sep 10, 2022
3567a1b
Fill in name
dev1437 Sep 10, 2022
2469a38
Remove block
dev1437 Sep 10, 2022
a597c4c
Update test
dev1437 Sep 10, 2022
aa94054
Tidy up test and examples
dev1437 Sep 10, 2022
dcc83e2
Change message
dev1437 Sep 10, 2022
07252a0
Add test for flat tag
dev1437 Sep 10, 2022
bb6f4f2
Add never functionality
dev1437 Sep 10, 2022
abbae10
Add tests for never and update previous tests for new schema
dev1437 Sep 10, 2022
4589a3d
Linting
dev1437 Sep 10, 2022
52fbe43
Update docs
dev1437 Sep 10, 2022
d417713
Rename to padding-line-between-tags
dev1437 Sep 10, 2022
74abc89
Change schema to array of objects
dev1437 Sep 11, 2022
84f793c
Change messages
dev1437 Sep 11, 2022
fb1899a
Allow for blank lines to be specified on each tag
dev1437 Sep 11, 2022
acbf506
Update tests
dev1437 Sep 11, 2022
4f84411
Linting
dev1437 Sep 11, 2022
adc23c0
Update docs
dev1437 Sep 11, 2022
3b885b0
Add another test
dev1437 Sep 11, 2022
a04b8a3
Lint
dev1437 Sep 11, 2022
6b0620d
Clean up doc
dev1437 Sep 11, 2022
e210bc9
Clean up tests
dev1437 Sep 11, 2022
95c563a
Change type
dev1437 Sep 11, 2022
9c57ee5
Fix doc
dev1437 Sep 11, 2022
bd9c128
Update docs/rules/padding-line-between-tags.md
dev1437 Sep 11, 2022
38fccaa
Ignore top level
dev1437 Sep 12, 2022
d17ec54
Remove testing stuff
dev1437 Sep 12, 2022
cfa916f
Simplify logic and make last configuration apply
dev1437 Sep 12, 2022
e152ceb
Add test for last configuration applying
dev1437 Sep 12, 2022
40be55c
Update docs
dev1437 Sep 12, 2022
b5fd572
Add newlines between siblings on same line
dev1437 Sep 12, 2022
e6094bd
Update docs
dev1437 Sep 12, 2022
8f9fb7f
Merge branch 'rule/space-between-siblings' of github.com:dev1437/esli…
dev1437 Sep 12, 2022
261ed4d
Lint
dev1437 Sep 12, 2022
5688564
Fix doc
dev1437 Sep 13, 2022
1c621c4
Fix spaces on line diff = 0
dev1437 Sep 13, 2022
a3f314f
Remove only space between tags
dev1437 Sep 13, 2022
b94e599
Append text backwards
dev1437 Sep 13, 2022
8bbbeed
Uncomment tests
dev1437 Sep 13, 2022
93ea378
Linting
dev1437 Sep 13, 2022
19d365b
Fix loop and add test
dev1437 Sep 15, 2022
b05e3ea
Add another test
dev1437 Sep 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -257,6 +257,7 @@ For example:
| [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | | :hammer: |
| [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: | :lipstick: |
| [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components | | :hammer: |
| [vue/padding-line-between-tags](./padding-line-between-tags.md) | Insert newlines between sibling tags in template | :wrench: | :warning: |
| [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: | :hammer: |
| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: | :lipstick: |
| [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: |
Expand Down
163 changes: 163 additions & 0 deletions docs/rules/padding-line-between-tags.md
@@ -0,0 +1,163 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/padding-line-between-tags
description: Require or disallow newlines between sibling tags in template
---
# vue/padding-line-between-tags

> Require or disallow newlines between sibling tags in template

- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
- :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.

## :book: Rule Details

This rule requires or disallows newlines between sibling HTML tags.

<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error']}">

```vue
<template>
<div>
<!-- ✓ GOOD: -->
<div></div>

<div>
</div>

<div />

<div />
<!-- ✗ BAD: -->
<div></div>
<div>
</div>
<div /><div />
</div>
</template>
```

</eslint-code-block>

## :wrench: Options

```json
{
"vue/padding-line-between-tags": ["error", [
{ "blankLine": "always", "prev": "*", "next": "*" }
]]
}
```

This rule requires blank lines between each sibling HTML tag by default.
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved

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.

- `blankLine` is one of the following:
- `always` requires one or more blank lines.
- `never` disallows blank lines.
- `prev` any tag name without brackets.
- `next` any tag name without brackets.

### Disallow blank lines between all tags

`{ blankLine: 'never', prev: '*', next: '*' }`

<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error', [
{ blankLine: 'never', prev: '*', next: '*' }
]]}">

```vue
<template>
<div>
<div></div>
<div>
</div>
<div />
</div>
</template>
```

</eslint-code-block>

### Require newlines after `<br>`

`{ blankLine: 'always', prev: 'br', next: '*' }`

<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error', [
{ blankLine: 'always', prev: 'br', next: '*' }
]]}">

```vue
<template>
<div>
<ul>
<li>
</li>
<br />

<li>
</li>
</ul>
</div>
</template>
```

</eslint-code-block>

### Require newlines before `<br>`

`{ blankLine: 'always', prev: '*', next: 'br' }`

<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error', [
{ blankLine: 'always', prev: '*', next: 'br' }
]]}">

```vue
<template>
<div>
<ul>
<li>
</li>

<br />
<li>
</li>
</ul>
</div>
</template>
```

</eslint-code-block>

### Require newlines between `<br>` and `<img>`

`{ blankLine: 'always', prev: 'br', next: 'img' }`

<eslint-code-block fix :rules="{'vue/padding-line-between-tags': ['error', [
{ blankLine: 'always', prev: 'br', next: 'img' }
]]}">

```vue
<template>
<div>
<ul>
<li>
</li>
<br />

<img />
<li>
</li>
</ul>
</div>
</template>
```

</eslint-code-block>

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/padding-line-between-tags.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/padding-line-between-tags.js)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -183,6 +183,7 @@ module.exports = {
'script-setup-uses-vars': require('./rules/script-setup-uses-vars'),
'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
'sort-keys': require('./rules/sort-keys'),
'padding-line-between-tags': require('./rules/padding-line-between-tags'),
'space-in-parens': require('./rules/space-in-parens'),
'space-infix-ops': require('./rules/space-infix-ops'),
'space-unary-ops': require('./rules/space-unary-ops'),
Expand Down
189 changes: 189 additions & 0 deletions lib/rules/padding-line-between-tags.js
@@ -0,0 +1,189 @@
/**
* @author dev1437
* See LICENSE file in root directory for full license.
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const utils = require('../utils')

/**
* Split the source code into multiple lines based on the line delimiters.
* Copied from padding-line-between-blocks
* @param {string} text Source code as a string.
* @returns {string[]} Array of source code lines.
*/
function splitLines(text) {
return text.split(/\r\n|[\r\n\u2028\u2029]/gu)
}

/**
* @param {RuleContext} context
* @param {VElement} tag
* @param {VElement} sibling
*/
function insertNewLine(context, tag, sibling) {
context.report({
messageId: 'always',
loc: sibling.loc,
// @ts-ignore
fix(fixer) {
return fixer.insertTextAfter(tag, '\n')
}
})
}

/**
* @param {RuleContext} context
* @param {VEndTag | VStartTag} endTag
* @param {VElement} sibling
*/
function removeExcessLines(context, endTag, sibling) {
context.report({
messageId: 'never',
loc: sibling.loc,
// @ts-ignore
fix(fixer) {
const start = endTag.range[1]
const end = sibling.range[0]
const paddingText = context.getSourceCode().text.slice(start, end)
const textBetween = splitLines(paddingText)
let newTextBetween = `\n${textBetween.pop()}`
for (let i = textBetween.length - 1; i >= 0; i--) {
if (!/^\s*$/.test(textBetween[i])) {
newTextBetween = `${i === 0 ? '' : '\n'}${
textBetween[i]
}${newTextBetween}`
}
}
return fixer.replaceTextRange([start, end], `${newTextBetween}`)
}
})
}

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

/**
* @param {RuleContext} context
*/
function checkNewline(context) {
/** @type {Array<{blankLine: "always" | "never", prev: string, next: string}>} */
const configureList = context.options[0] || [
{ blankLine: 'always', prev: '*', next: '*' }
]

/**
* @param {VElement} block
*/
return (block) => {
if (!block.parent.parent) {
return
}

const endTag = block.endTag || block.startTag
const lowerSiblings = block.parent.children
.filter(
(element) =>
element.type === 'VElement' && element.range !== block.range
)
.filter((sibling) => sibling.range[0] - endTag.range[1] >= 0)

if (lowerSiblings.length === 0) {
return
}

const closestSibling = /** @type {VElement} */ (lowerSiblings[0])

for (let i = configureList.length - 1; i >= 0; --i) {
const configure = configureList[i]
const matched =
(configure.prev === '*' || block.name === configure.prev) &&
(configure.next === '*' || closestSibling.name === configure.next)

if (matched) {
const lineDifference =
closestSibling.loc.start.line - endTag.loc.end.line
if (configure.blankLine === 'always') {
if (lineDifference === 1) {
insertNewLine(context, block, closestSibling)
} else if (lineDifference === 0) {
context.report({
messageId: 'always',
loc: closestSibling.loc,
// @ts-ignore
fix(fixer) {
const lastSpaces = /** @type {RegExpExecArray} */ (
/^\s*/.exec(
context.getSourceCode().lines[endTag.loc.start.line - 1]
)
)[0]

return fixer.insertTextAfter(endTag, `\n\n${lastSpaces}`)
}
})
}
} else {
if (lineDifference > 1) {
let hasOnlyTextBetween = true
for (
let i = endTag.loc.start.line;
i < closestSibling.loc.start.line - 1 && hasOnlyTextBetween;
i++
) {
hasOnlyTextBetween = !/^\s*$/.test(
context.getSourceCode().lines[i]
)
}
if (!hasOnlyTextBetween) {
removeExcessLines(context, endTag, closestSibling)
}
}
}
break
}
}
}
}

module.exports = {
meta: {
type: 'layout',
docs: {
description:
'require or disallow newlines between sibling tags in template',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/padding-line-between-tags.html'
},
fixable: 'whitespace',
schema: [
{
type: 'array',
items: {
type: 'object',
properties: {
blankLine: { enum: ['always', 'never'] },
prev: { type: 'string' },
next: { type: 'string' }
},
additionalProperties: false,
required: ['blankLine', 'prev', 'next']
}
}
],
messages: {
never: 'Unexpected blank line before this tag.',
always: 'Expected blank line before this tag.'
}
},
/** @param {RuleContext} context */
create(context) {
return utils.defineTemplateBodyVisitor(context, {
VElement: checkNewline(context)
})
}
}