From 5e6a04a837d9b2949677f06c89f9ff47087926e2 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Thu, 29 Mar 2018 19:24:29 +0900 Subject: [PATCH 1/6] [New] Add `vue/html-content-newline` --- lib/rules/html-content-newline.js | 127 +++++++++++++++ tests/lib/rules/html-content-newline.js | 197 ++++++++++++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 lib/rules/html-content-newline.js create mode 100644 tests/lib/rules/html-content-newline.js diff --git a/lib/rules/html-content-newline.js b/lib/rules/html-content-newline.js new file mode 100644 index 000000000..9fbc128e8 --- /dev/null +++ b/lib/rules/html-content-newline.js @@ -0,0 +1,127 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +function parseOptions (options) { + return Object.assign({ + 'singleline': 'ignore', + 'multiline': 'always', + 'ignoreNames': ['pre', 'textarea'], + + detectType (node) { + if (node.startTag.loc.start.line === node.startTag.loc.end.line && + node.endTag.loc.start.line === node.endTag.loc.end.line) { + return this.singleline + } + return this.multiline + } + }, options) +} + +function getBreakPhrase (breaks) { + return breaks ? 'line break' : 'no 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/v4.4.0/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 + } + if (options.ignoreNames.indexOf(node.name) >= 0) { + return + } + const type = options.detectType(node) + if (type === 'ignore') { + return + } + debugger// TODO + const getTokenOption = { includeComments: true, filter: (token) => token.type !== 'HTMLWhitespace' } + const contentFirst = template.getTokenAfter(node.startTag, getTokenOption) + const contentLast = template.getTokenBefore(node.endTag, getTokenOption) + const hasBeforeBreaks = node.startTag.loc.end.line < contentFirst.loc.start.line + const hasAfterBreaks = contentLast.loc.end.line < node.endTag.loc.start.line + const needBreaks = type === 'always' + if (needBreaks !== hasBeforeBreaks) { + context.report({ + node: template.getLastToken(node.startTag), + loc: { + start: node.startTag.loc.end, + end: contentFirst.loc.start + }, + message: 'Expected {{expected}} after closing bracket, but {{actual}} found.', + data: { + expected: getBreakPhrase(needBreaks), + actual: getBreakPhrase(hasBeforeBreaks) + }, + fix: type === 'always' + ? (fixer) => fixer.insertTextAfter(node.startTag, '\n') + : (fixer) => fixer.removeRange([node.startTag.range[1] + 1, contentFirst.range[0]]) + }) + } + + if (needBreaks !== hasAfterBreaks) { + context.report({ + node: template.getFirstToken(node.endTag), + loc: { + start: contentLast.loc.end, + end: node.endTag.loc.start + }, + message: 'Expected {{expected}} before open bracket, but {{actual}} found.', + data: { + expected: getBreakPhrase(needBreaks), + actual: getBreakPhrase(hasAfterBreaks) + }, + fix: type === 'always' + ? (fixer) => fixer.insertTextBefore(node.endTag, '\n') + : (fixer) => fixer.removeRange([contentLast.range[1] + 1, node.endTag.range[0]]) + }) + } + } + }) + } +} diff --git a/tests/lib/rules/html-content-newline.js b/tests/lib/rules/html-content-newline.js new file mode 100644 index 000000000..7a7d1fe40 --- /dev/null +++ b/tests/lib/rules/html-content-newline.js @@ -0,0 +1,197 @@ +/** + * @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' + }] + }, + { + code: ` + `, + options: [{ + singleline: 'always', + multiline: 'never' + }] + }, + // Ignore if no closing brackets + ` + + `, + output: ` + + `, + errors: [ + { + message: 'Expected line break after closing bracket, but no line breaks found.', + line: 5, + column: 12, + nodeType: 'HTMLTagClose', + endLine: 5, + endColumn: 12 + }, + { + message: 'Expected line break before open bracket, 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 line break after closing bracket, but no line breaks found.', + line: 3, + column: 30, + nodeType: 'HTMLTagClose', + endLine: 3, + endColumn: 30 + }, + { + message: 'Expected line break before open bracket, but no line breaks found.', + line: 3, + column: 35, + nodeType: 'HTMLEndTagOpen', + endLine: 3, + endColumn: 35 + }] + }, + { + code: ` + + `, + options: [{ + singleline: 'always', + multiline: 'never' + }], + output: ` + + `, + errors: [{ + message: 'Expected no line breaks after closing bracket, but line break found.', + line: 5, + column: 12, + nodeType: 'HTMLTagClose', + endLine: 6, + endColumn: 13 + }, + { + message: 'Expected no line breaks before open bracket, but line break found.', + line: 6, + column: 20, + nodeType: 'HTMLEndTagOpen', + endLine: 7, + endColumn: 11 + } + ] + } + ] +}) From f4aa12e66e40b6af16721926b5a1088c9dc7bb0c Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Thu, 29 Mar 2018 21:02:04 +0900 Subject: [PATCH 2/6] [update] add test case --- lib/rules/html-content-newline.js | 26 +- tests/lib/rules/html-content-newline.js | 347 ++++++++++++++++++++++-- 2 files changed, 343 insertions(+), 30 deletions(-) diff --git a/lib/rules/html-content-newline.js b/lib/rules/html-content-newline.js index 9fbc128e8..0fa23c937 100644 --- a/lib/rules/html-content-newline.js +++ b/lib/rules/html-content-newline.js @@ -18,9 +18,13 @@ function parseOptions (options) { return Object.assign({ 'singleline': 'ignore', 'multiline': 'always', + 'hasChildElements': null, 'ignoreNames': ['pre', 'textarea'], detectType (node) { + if (this.hasChildElements && node.children.filter(c => c.type === 'VElement').length) { + return this.hasChildElements + } if (node.startTag.loc.start.line === node.startTag.loc.end.line && node.endTag.loc.start.line === node.endTag.loc.end.line) { return this.singleline @@ -51,6 +55,7 @@ module.exports = { properties: { 'singleline': { enum: ['ignore', 'always', 'never'] }, 'multiline': { enum: ['ignore', 'always', 'never'] }, + 'hasChildElements': { enum: ['ignore', 'always', 'never'] }, 'ignoreNames': { type: 'array', items: { type: 'string' }, @@ -72,14 +77,19 @@ module.exports = { // self closing return } - if (options.ignoreNames.indexOf(node.name) >= 0) { - return + let target = node + while (target.type === 'VElement') { + if (options.ignoreNames.indexOf(target.name) >= 0) { + // ignore element name + return + } + target = target.parent } const type = options.detectType(node) if (type === 'ignore') { + // 'ignore' option return } - debugger// TODO const getTokenOption = { includeComments: true, filter: (token) => token.type !== 'HTMLWhitespace' } const contentFirst = template.getTokenAfter(node.startTag, getTokenOption) const contentLast = template.getTokenBefore(node.endTag, getTokenOption) @@ -93,14 +103,15 @@ module.exports = { start: node.startTag.loc.end, end: contentFirst.loc.start }, - message: 'Expected {{expected}} after closing bracket, but {{actual}} found.', + message: `Expected {{expected}} after closing bracket of the "{{name}}" element, but {{actual}} found.`, data: { + name: node.name, expected: getBreakPhrase(needBreaks), actual: getBreakPhrase(hasBeforeBreaks) }, fix: type === 'always' ? (fixer) => fixer.insertTextAfter(node.startTag, '\n') - : (fixer) => fixer.removeRange([node.startTag.range[1] + 1, contentFirst.range[0]]) + : (fixer) => fixer.removeRange([node.startTag.range[1], contentFirst.range[0]]) }) } @@ -111,14 +122,15 @@ module.exports = { start: contentLast.loc.end, end: node.endTag.loc.start }, - message: 'Expected {{expected}} before open bracket, but {{actual}} found.', + message: 'Expected {{expected}} before open bracket of the "{{name}}" element, but {{actual}} found.', data: { + name: node.name, expected: getBreakPhrase(needBreaks), actual: getBreakPhrase(hasAfterBreaks) }, fix: type === 'always' ? (fixer) => fixer.insertTextBefore(node.endTag, '\n') - : (fixer) => fixer.removeRange([contentLast.range[1] + 1, node.endTag.range[0]]) + : (fixer) => fixer.removeRange([contentLast.range[1], node.endTag.range[0]]) }) } } diff --git a/tests/lib/rules/html-content-newline.js b/tests/lib/rules/html-content-newline.js index 7a7d1fe40..546ab7a55 100644 --- a/tests/lib/rules/html-content-newline.js +++ b/tests/lib/rules/html-content-newline.js @@ -62,6 +62,17 @@ tester.run('html-content-newline', rule, { multiline: 'never' }] }, + // empty + ``, + { + code: ``, + options: [{ + singleline: 'never', + multiline: 'never' + }] + }, + // self closing { code: ` `, { code: ` - `, + + `, options: [{ singleline: 'always', multiline: 'never' @@ -52,11 +51,10 @@ tester.run('html-content-newline', rule, { }, { code: ` - `, + >content + `, options: [{ singleline: 'always', multiline: 'never' @@ -117,19 +115,17 @@ tester.run('html-content-newline', rule, { ignoreNames: ['ignore-tag'] }] }, - // hasChildElements - { - code: ` - + `, // Ignore if no closing brackets ` `, options: [{ singleline: 'always', multiline: 'never' }], output: ` - + >content `, errors: [ { message: 'Expected no line breaks after closing bracket of the "div" element, but 1 line break found.', - line: 5, + line: 4, column: 12, nodeType: 'HTMLTagClose', - endLine: 6, + endLine: 5, endColumn: 13 }, { message: 'Expected no line breaks before open bracket of the "div" element, but 1 line break found.', - line: 6, + line: 5, column: 20, nodeType: 'HTMLEndTagOpen', - endLine: 7, + endLine: 6, endColumn: 11 } ] @@ -293,31 +285,24 @@ content singleline: 'never' }], output: ` - + `, - errors: [{ - message: 'Expected no line breaks after closing bracket of the "template" element, but 1 line break found.', - line: 2, - column: 19 - }, - { - 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 open bracket of the "div" element, but 1 line break found.', - line: 4, - column: 25 + 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 open bracket of the "template" element, but 1 line break found.', - line: 5, - column: 17 + }, + { + message: 'Expected no line breaks before open bracket of the "div" element, but 1 line break found.', + line: 4, + column: 25 - }] + } + ] }, // one error { @@ -376,7 +361,8 @@ content `, options: [{ - singleline: 'never' + singleline: 'never', + multiline: 'ignore' }], output: ` @@ -392,7 +378,8 @@ content content `, options: [{ - singleline: 'never' + singleline: 'never', + multiline: 'ignore' }], output: ` @@ -403,42 +390,57 @@ content column: 24 }] }, - // hasChildElements + // multiline content { code: ` - + `, options: [{ - singleline: 'never', - hasChildElements: 'always' + 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 before open bracket of the "div" element, but no line breaks found.', - line: 2, - column: 56 - }, - { - message: 'Expected 1 line break before open bracket of the "template" element, but no line breaks found.', - line: 2, - column: 62 - }] + 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 open bracket of the "div" element, but no line breaks found.', + line: 3, + column: 16 + }, + { + message: 'Expected 1 line break before open bracket of the "div" element, but no line breaks found.', + line: 3, + column: 29 + }, + { + message: 'Expected 1 line break before open bracket of the "template" element, but no line breaks found.', + line: 3, + column: 35 + } + ] }, // empty { @@ -475,7 +477,8 @@ content
content
content `, options: [{ - singleline: 'never' + singleline: 'never', + multiline: 'ignore' }], output: ` From c36559d0ea066f454bc3fa03a99ebf4c09b0e4ee Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Thu, 29 Mar 2018 23:50:16 +0900 Subject: [PATCH 5/6] [update] --- README.md | 1 + docs/rules/html-content-newline.md | 86 +++++++++++++++++++++++++ lib/index.js | 1 + lib/rules/html-content-newline.js | 2 +- tests/lib/rules/html-content-newline.js | 28 ++++---- 5 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 docs/rules/html-content-newline.md diff --git a/README.md b/README.md index 2467f256e..48a910a17 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi |:---|:--------|:------------| | :wrench: | [vue/html-closing-bracket-newline](./docs/rules/html-closing-bracket-newline.md) | require or disallow a line break before tag's closing brackets | | :wrench: | [vue/html-closing-bracket-spacing](./docs/rules/html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets | +| :wrench: | [vue/html-content-newline](./docs/rules/html-content-newline.md) | require or disallow a line break before and after html contents | | :wrench: | [vue/prop-name-casing](./docs/rules/prop-name-casing.md) | enforce specific casing for the Prop name in Vue components | | :wrench: | [vue/script-indent](./docs/rules/script-indent.md) | enforce consistent indentation in `