singleline-html-element-content-newline.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. /**
  2. * @author Yosuke Ota
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. // ------------------------------------------------------------------------------
  7. // Requirements
  8. // ------------------------------------------------------------------------------
  9. const utils = require('../utils')
  10. const casing = require('../utils/casing')
  11. const INLINE_ELEMENTS = require('../utils/inline-non-void-elements.json')
  12. // ------------------------------------------------------------------------------
  13. // Helpers
  14. // ------------------------------------------------------------------------------
  15. function isSinglelineElement (element) {
  16. return element.loc.start.line === element.endTag.loc.start.line
  17. }
  18. function parseOptions (options) {
  19. return Object.assign({
  20. ignores: ['pre', 'textarea'].concat(INLINE_ELEMENTS),
  21. ignoreWhenNoAttributes: true,
  22. ignoreWhenEmpty: true
  23. }, options)
  24. }
  25. /**
  26. * Check whether the given element is empty or not.
  27. * This ignores whitespaces, doesn't ignore comments.
  28. * @param {VElement} node The element node to check.
  29. * @param {SourceCode} sourceCode The source code object of the current context.
  30. * @returns {boolean} `true` if the element is empty.
  31. */
  32. function isEmpty (node, sourceCode) {
  33. const start = node.startTag.range[1]
  34. const end = node.endTag.range[0]
  35. return sourceCode.text.slice(start, end).trim() === ''
  36. }
  37. // ------------------------------------------------------------------------------
  38. // Rule Definition
  39. // ------------------------------------------------------------------------------
  40. module.exports = {
  41. meta: {
  42. type: 'layout',
  43. docs: {
  44. description: 'require a line break before and after the contents of a singleline element',
  45. category: 'strongly-recommended',
  46. url: 'https://eslint.vuejs.org/rules/singleline-html-element-content-newline.html'
  47. },
  48. fixable: 'whitespace',
  49. schema: [{
  50. type: 'object',
  51. properties: {
  52. ignoreWhenNoAttributes: {
  53. type: 'boolean'
  54. },
  55. ignoreWhenEmpty: {
  56. type: 'boolean'
  57. },
  58. ignores: {
  59. type: 'array',
  60. items: { type: 'string' },
  61. uniqueItems: true,
  62. additionalItems: false
  63. }
  64. },
  65. additionalProperties: false
  66. }],
  67. messages: {
  68. unexpectedAfterClosingBracket: 'Expected 1 line break after opening tag (`<{{name}}>`), but no line breaks found.',
  69. unexpectedBeforeOpeningBracket: 'Expected 1 line break before closing tag (`</{{name}}>`), but no line breaks found.'
  70. }
  71. },
  72. create (context) {
  73. const options = parseOptions(context.options[0])
  74. const ignores = options.ignores
  75. const ignoreWhenNoAttributes = options.ignoreWhenNoAttributes
  76. const ignoreWhenEmpty = options.ignoreWhenEmpty
  77. const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
  78. const sourceCode = context.getSourceCode()
  79. let inIgnoreElement
  80. function isIgnoredElement (node) {
  81. return ignores.includes(node.name) ||
  82. ignores.includes(casing.pascalCase(node.rawName)) ||
  83. ignores.includes(casing.kebabCase(node.rawName))
  84. }
  85. return utils.defineTemplateBodyVisitor(context, {
  86. 'VElement' (node) {
  87. if (inIgnoreElement) {
  88. return
  89. }
  90. if (isIgnoredElement(node)) {
  91. // ignore element name
  92. inIgnoreElement = node
  93. return
  94. }
  95. if (node.startTag.selfClosing || !node.endTag) {
  96. // self closing
  97. return
  98. }
  99. if (!isSinglelineElement(node)) {
  100. return
  101. }
  102. if (ignoreWhenNoAttributes && node.startTag.attributes.length === 0) {
  103. return
  104. }
  105. const getTokenOption = { includeComments: true, filter: (token) => token.type !== 'HTMLWhitespace' }
  106. if (
  107. ignoreWhenEmpty &&
  108. node.children.length === 0 &&
  109. template.getFirstTokensBetween(node.startTag, node.endTag, getTokenOption).length === 0
  110. ) {
  111. return
  112. }
  113. const contentFirst = template.getTokenAfter(node.startTag, getTokenOption)
  114. const contentLast = template.getTokenBefore(node.endTag, getTokenOption)
  115. context.report({
  116. node: template.getLastToken(node.startTag),
  117. loc: {
  118. start: node.startTag.loc.end,
  119. end: contentFirst.loc.start
  120. },
  121. messageId: 'unexpectedAfterClosingBracket',
  122. data: {
  123. name: node.rawName
  124. },
  125. fix (fixer) {
  126. const range = [node.startTag.range[1], contentFirst.range[0]]
  127. return fixer.replaceTextRange(range, '\n')
  128. }
  129. })
  130. if (isEmpty(node, sourceCode)) {
  131. return
  132. }
  133. context.report({
  134. node: template.getFirstToken(node.endTag),
  135. loc: {
  136. start: contentLast.loc.end,
  137. end: node.endTag.loc.start
  138. },
  139. messageId: 'unexpectedBeforeOpeningBracket',
  140. data: {
  141. name: node.rawName
  142. },
  143. fix (fixer) {
  144. const range = [contentLast.range[1], node.endTag.range[0]]
  145. return fixer.replaceTextRange(range, '\n')
  146. }
  147. })
  148. },
  149. 'VElement:exit' (node) {
  150. if (inIgnoreElement === node) {
  151. inIgnoreElement = null
  152. }
  153. }
  154. })
  155. }
  156. }