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

⭐️New: Add vue/html-content-newline #445

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions docs/rules/html-content-newline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# require or disallow a line break before and after html contents (vue/html-content-newline)

- :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.

## :book: Rule Details

This rule enforces a line break (or no line break) before and after html contents.


:-1: Examples of **incorrect** code:

```html
<div
class="panel"
>content</div>
```

:+1: Examples of **correct** code:

```html
<div class="panel">content</div>

<div class="panel">
content
</div>

<div
class="panel"
>
content
</div>
```


## :wrench: Options

```json
{
"vue/html-content-newline": ["error", {
"singleline": "ignore",
"multiline": "always",
"ignoreNames": ["pre", "textarea"]
}]
}
```

- `singleline` ... the configuration for single-line elements. It's a single-line element if startTag, endTag and contents are single-line.
- `"ignore"` ... Don't enforce line breaks style before and after the contents. This is the default.
- `"never"` ... disallow line breaks before and after the contents.
- `"always"` ... require one line break before and after the contents.
- `multiline` ... the configuration for multiline elements. It's a multiline element if startTag, endTag or contents are multiline.
- `"ignore"` ... Don't enforce line breaks style before and after the contents.
- `"never"` ... disallow line breaks before and after the contents.
- `"always"` ... require one line break before and after the contents. This is the default.
- `ignoreNames` ... the configuration for element names to ignore line breaks style.
default `["pre", "textarea"]`


:-1: Examples of **incorrect** code:

```html
/*eslint vue/html-content-newline: ["error", { "singleline": "always", "multiline": "never"}] */

<div class="panel">content</div>

<div
class="panel"
>
content
</div>
```

:+1: Examples of **correct** code:

```html
/*eslint vue/html-content-newline: ["error", { "singleline": "always", "multiline": "never"}] */

<div class="panel">
content
</div>

<div
class="panel"
>content</div>
```

1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
'comment-directive': require('./rules/comment-directive'),
'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'),
'html-content-newline': require('./rules/html-content-newline'),
'html-end-tags': require('./rules/html-end-tags'),
'html-indent': require('./rules/html-indent'),
'html-quotes': require('./rules/html-quotes'),
Expand Down
147 changes: 147 additions & 0 deletions lib/rules/html-content-newline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'

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

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

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

function isMultiline (node, contentFirst, contentLast) {
if (node.startTag.loc.start.line !== node.startTag.loc.end.line ||
node.endTag.loc.start.line !== node.endTag.loc.end.line) {
// multiline tag
return true
}
if (contentFirst.loc.start.line < contentLast.loc.end.line) {
// multiline contents
return true
}
return false
}

function parseOptions (options) {
return Object.assign({
'singleline': 'ignore',
'multiline': 'always',
'ignoreNames': ['pre', 'textarea']
}, options)
}

function getPhrase (lineBreaks) {
switch (lineBreaks) {
case 0: return 'no line breaks'
case 1: return '1 line break'
default: return `${lineBreaks} line breaks`
}
}

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

module.exports = {
meta: {
docs: {
description: 'require or disallow a line break before and after html contents',
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/html-content-newline.md'
},
fixable: 'whitespace',
schema: [{
type: 'object',
properties: {
'singleline': { enum: ['ignore', 'always', 'never'] },
'multiline': { enum: ['ignore', 'always', 'never'] },
'ignoreNames': {
type: 'array',
items: { type: 'string' },
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}]
},

create (context) {
const options = parseOptions(context.options[0])
const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()

return utils.defineTemplateBodyVisitor(context, {
'VElement' (node) {
if (node.startTag.selfClosing || !node.endTag) {
// self closing
return
}
let target = node
while (target.type === 'VElement') {
if (options.ignoreNames.indexOf(target.name) >= 0) {
// ignore element name
return
}
target = target.parent
}
const getTokenOption = { includeComments: true, filter: (token) => token.type !== 'HTMLWhitespace' }
const contentFirst = template.getTokenAfter(node.startTag, getTokenOption)
const contentLast = template.getTokenBefore(node.endTag, getTokenOption)
const type = isMultiline(node, contentFirst, contentLast) ? options.multiline : options.singleline
if (type === 'ignore') {
// 'ignore' option
return
}
const beforeLineBreaks = contentFirst.loc.start.line - node.startTag.loc.end.line
const afterLineBreaks = node.endTag.loc.start.line - contentLast.loc.end.line
const expectedLineBreaks = type === 'always' ? 1 : 0
if (expectedLineBreaks !== beforeLineBreaks) {
context.report({
node: template.getLastToken(node.startTag),
loc: {
start: node.startTag.loc.end,
end: contentFirst.loc.start
},
message: `Expected {{expected}} after closing bracket of the "{{name}}" element, but {{actual}} found.`,
data: {
name: node.name,
expected: getPhrase(expectedLineBreaks),
actual: getPhrase(beforeLineBreaks)
},
fix (fixer) {
const range = [node.startTag.range[1], contentFirst.range[0]]
const text = '\n'.repeat(expectedLineBreaks)
return fixer.replaceTextRange(range, text)
}
})
}

if (expectedLineBreaks !== afterLineBreaks) {
context.report({
node: template.getFirstToken(node.endTag),
loc: {
start: contentLast.loc.end,
end: node.endTag.loc.start
},
message: 'Expected {{expected}} before opening bracket of the "{{name}}" element, but {{actual}} found.',
data: {
name: node.name,
expected: getPhrase(expectedLineBreaks),
actual: getPhrase(afterLineBreaks)
},
fix (fixer) {
const range = [contentLast.range[1], node.endTag.range[0]]
const text = '\n'.repeat(expectedLineBreaks)
return fixer.replaceTextRange(range, text)
}
})
}
}
})
}
}
Loading