diff --git a/docs/rules/html-content-newline.md b/docs/rules/html-content-newline.md new file mode 100644 index 000000000..64e05616e --- /dev/null +++ b/docs/rules/html-content-newline.md @@ -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 +
content
+``` + +:+1: Examples of **correct** code: + +```html +
content
+ +
+ content +
+ +
+ content +
+``` + + +## :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"}] */ + +
content
+ +
+ content +
+``` + +:+1: Examples of **correct** code: + +```html +/*eslint vue/html-content-newline: ["error", { "singleline": "always", "multiline": "never"}] */ + +
+ content +
+ +
content
+``` + diff --git a/lib/index.js b/lib/index.js index 79769f2a3..174ff1f1c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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'), diff --git a/lib/rules/html-content-newline.js b/lib/rules/html-content-newline.js new file mode 100644 index 000000000..44417f555 --- /dev/null +++ b/lib/rules/html-content-newline.js @@ -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) + } + }) + } + } + }) + } +} diff --git a/tests/lib/rules/html-content-newline.js b/tests/lib/rules/html-content-newline.js new file mode 100644 index 000000000..c8c5894f3 --- /dev/null +++ b/tests/lib/rules/html-content-newline.js @@ -0,0 +1,593 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/html-content-newline') +const RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: 'vue-eslint-parser', + parserOptions: { + ecmaVersion: 2015 + } +}) + +tester.run('html-content-newline', rule, { + valid: [ + ``, + ` + `, + ` + `, + { + code: ` + + `, + options: [{ + singleline: 'always', + multiline: 'never' + }] + }, + { + code: ` + + `, + options: [{ + singleline: 'always', + multiline: 'never' + }] + }, + // empty + ``, + { + code: ``, + options: [{ + singleline: 'never', + multiline: 'never' + }] + }, + // self closing + { + code: ` + `, + options: [{ + singleline: 'always', + multiline: 'never' + }] + }, + // ignores + { + code: ` + `, + options: [{ + singleline: 'always', + multiline: 'always' + }] + }, + { + code: ` + `, + options: [{ + singleline: 'always', + multiline: 'always', + ignoreNames: ['ignore-tag'] + }] + }, + // multiline contents + ` + + `, + // Ignore if no closing brackets + ` + + `, + output: ` + + `, + errors: [ + { + message: 'Expected 1 line break after closing bracket of the "div" element, but no line breaks found.', + line: 5, + column: 12, + nodeType: 'HTMLTagClose', + endLine: 5, + endColumn: 12 + }, + { + message: 'Expected 1 line break before opening bracket of the "div" element, but no line breaks found.', + line: 5, + column: 19, + nodeType: 'HTMLEndTagOpen', + endLine: 5, + endColumn: 19 + } + ] + }, + { + code: ` + + `, + options: [{ + singleline: 'always', + multiline: 'never' + }], + output: ` + + `, + errors: [ + { + message: 'Expected 1 line break after closing bracket of the "div" element, but no line breaks found.', + line: 3, + column: 30, + nodeType: 'HTMLTagClose', + endLine: 3, + endColumn: 30 + }, + { + message: 'Expected 1 line break before opening bracket of the "div" element, but no line breaks found.', + line: 3, + column: 37, + nodeType: 'HTMLEndTagOpen', + endLine: 3, + endColumn: 37 + } + ] + }, + { + code: ` + + `, + options: [{ + singleline: 'always', + multiline: 'never' + }], + output: ` + + `, + errors: [ + { + message: 'Expected no line breaks after closing bracket of the "div" element, but 1 line break found.', + line: 4, + column: 12, + nodeType: 'HTMLTagClose', + endLine: 5, + endColumn: 13 + }, + { + message: 'Expected no line breaks before opening bracket of the "div" element, but 1 line break found.', + line: 5, + column: 20, + nodeType: 'HTMLEndTagOpen', + endLine: 6, + endColumn: 11 + } + ] + }, + // comments + { + code: ` + + `, + options: [{ + singleline: 'always' + }], + output: ` + + `, + errors: [ + { + message: 'Expected 1 line break after closing bracket of the "div" element, but no line breaks found.', + line: 3, + column: 16 + }, + { + message: 'Expected 1 line break before opening bracket of the "div" element, but no line breaks found.', + line: 3, + column: 30 + } + ] + }, + { + code: ` + + `, + options: [{ + singleline: 'never' + }], + output: ` + + `, + errors: [ + { + message: 'Expected no line breaks after closing bracket of the "div" element, but 1 line break found.', + line: 3, + column: 16 + + }, + { + message: 'Expected no line breaks before opening bracket of the "div" element, but 1 line break found.', + line: 4, + column: 25 + + } + ] + }, + // one error + { + code: ` + + `, + options: [{ + singleline: 'always' + }], + output: ` + + `, + errors: [ + { + message: 'Expected 1 line break after closing bracket of the "div" element, but no line breaks found.', + line: 3, + column: 16 + } + ] + }, + { + code: ` + + `, + options: [{ + singleline: 'always' + }], + output: ` + + `, + errors: [ + { + message: 'Expected 1 line break before opening bracket of the "div" element, but no line breaks found.', + line: 4, + column: 18 + } + ] + }, + { + code: ` + + `, + options: [{ + singleline: 'never', + multiline: 'ignore' + }], + output: ` + + `, + errors: [{ + message: 'Expected no line breaks before opening bracket of the "div" element, but 1 line break found.', + line: 2, + column: 31 }] + }, + { + code: ` + + `, + options: [{ + singleline: 'never', + multiline: 'ignore' + }], + output: ` + + `, + errors: [{ + message: 'Expected no line breaks after closing bracket of the "div" element, but 1 line break found.', + line: 2, + column: 24 + }] + }, + // multiline content + { + code: ` + + `, + options: [{ + singleline: 'never' + }], + output: ` + + `, + errors: [ + { + message: 'Expected 1 line break after closing bracket of the "template" element, but no line breaks found.', + line: 2, + column: 19 + }, + { + message: 'Expected 1 line break after closing bracket of the "div" element, but no line breaks found.', + line: 2, + column: 24 + }, + { + message: 'Expected 1 line break after closing bracket of the "div" element, but no line breaks found.', + line: 2, + column: 36 + }, + { + message: 'Expected 1 line break before opening bracket of the "div" element, but no line breaks found.', + line: 3, + column: 16 + }, + { + message: 'Expected 1 line break before opening bracket of the "div" element, but no line breaks found.', + line: 3, + column: 29 + }, + { + message: 'Expected 1 line break before opening bracket of the "template" element, but no line breaks found.', + line: 3, + column: 35 + } + ] + }, + // empty + { + code: ` + + `, + options: [{ + singleline: 'always' + }], + output: ` + + `, + errors: [ + { + message: 'Expected 1 line break after closing bracket of the "div" element, but no line breaks found.', + line: 3, + column: 16 + }, + { + message: 'Expected 1 line break before opening bracket of the "div" element, but no line breaks found.', + line: 3, + column: 16 + } + ] + }, + { + code: ` + + `, + options: [{ + singleline: 'never', + multiline: 'ignore' + }], + output: ` + + `, + errors: [ + { + message: 'Expected no line breaks after closing bracket of the "div" element, but 1 line break found.', + line: 2, + column: 24 + }, + { + message: 'Expected no line breaks before opening bracket of the "div" element, but 1 line break found.', + line: 2, + column: 24 + + } + ] + }, + // multi line breaks + { + code: ` + + `, + options: [{ + singleline: 'always' + }], + output: ` + + `, + errors: [ + { + message: 'Expected 1 line break after closing bracket of the "div" element, but 2 line breaks found.', + line: 3, + column: 16 + }, + { + message: 'Expected 1 line break before opening bracket of the "div" element, but 2 line breaks found.', + line: 5, + column: 20 + } + ] + }, + // mustache + { + code: ` + + `, + options: [{ + singleline: 'always' + }], + output: ` + + `, + errors: [ + { + message: 'Expected 1 line break after closing bracket of the "div" element, but no line breaks found.', + line: 3, + column: 16 + }, + { + message: 'Expected 1 line break before opening bracket of the "div" element, but no line breaks found.', + line: 3, + column: 27 + } + ] + }, + // multiline end tag + { + code: ` + + `, + output: ` + + `, + errors: [ + { + message: 'Expected 1 line break after closing bracket of the "div" element, but no line breaks found.', + line: 3, + column: 16 + }, + { + message: 'Expected 1 line break before opening bracket of the "div" element, but no line breaks found.', + line: 3, + column: 23 + }] + } + ] +})