Skip to content

Commit

Permalink
Create padding-line-between-tags rule (#1966)
Browse files Browse the repository at this point in the history
* 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 <florian-edelmann@online.de>

* 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 <florian-edelmann@online.de>
  • Loading branch information
dev1437 and FloEdelmann committed Sep 16, 2022
1 parent faa067e commit bf9b95c
Show file tree
Hide file tree
Showing 5 changed files with 1,386 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -258,6 +258,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.

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 @@ -184,6 +184,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)
})
}
}

0 comments on commit bf9b95c

Please sign in to comment.