html-closing-bracket-newline.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. /**
  2. * @author Toru Nagashima
  3. * @copyright 2016 Toru Nagashima. All rights reserved.
  4. * See LICENSE file in root directory for full license.
  5. */
  6. 'use strict'
  7. // ------------------------------------------------------------------------------
  8. // Requirements
  9. // ------------------------------------------------------------------------------
  10. const utils = require('../utils')
  11. // ------------------------------------------------------------------------------
  12. // Helpers
  13. // ------------------------------------------------------------------------------
  14. /**
  15. * @param {number} lineBreaks
  16. */
  17. function getPhrase(lineBreaks) {
  18. switch (lineBreaks) {
  19. case 0:
  20. return 'no line breaks'
  21. case 1:
  22. return '1 line break'
  23. default:
  24. return `${lineBreaks} line breaks`
  25. }
  26. }
  27. // ------------------------------------------------------------------------------
  28. // Rule Definition
  29. // ------------------------------------------------------------------------------
  30. module.exports = {
  31. meta: {
  32. type: 'layout',
  33. docs: {
  34. description:
  35. "require or disallow a line break before tag's closing brackets",
  36. categories: ['vue3-strongly-recommended', 'strongly-recommended'],
  37. url: 'https://eslint.vuejs.org/rules/html-closing-bracket-newline.html'
  38. },
  39. fixable: 'whitespace',
  40. schema: [
  41. {
  42. type: 'object',
  43. properties: {
  44. singleline: { enum: ['always', 'never'] },
  45. multiline: { enum: ['always', 'never'] }
  46. },
  47. additionalProperties: false
  48. }
  49. ]
  50. },
  51. /** @param {RuleContext} context */
  52. create(context) {
  53. const options = Object.assign(
  54. {},
  55. {
  56. singleline: 'never',
  57. multiline: 'always'
  58. },
  59. context.options[0] || {}
  60. )
  61. const template =
  62. context.parserServices.getTemplateBodyTokenStore &&
  63. context.parserServices.getTemplateBodyTokenStore()
  64. return utils.defineTemplateBodyVisitor(context, {
  65. /** @param {VStartTag | VEndTag} node */
  66. 'VStartTag, VEndTag'(node) {
  67. const closingBracketToken = template.getLastToken(node)
  68. if (
  69. closingBracketToken.type !== 'HTMLSelfClosingTagClose' &&
  70. closingBracketToken.type !== 'HTMLTagClose'
  71. ) {
  72. return
  73. }
  74. const prevToken = template.getTokenBefore(closingBracketToken)
  75. const type =
  76. node.loc.start.line === prevToken.loc.end.line
  77. ? 'singleline'
  78. : 'multiline'
  79. const expectedLineBreaks = options[type] === 'always' ? 1 : 0
  80. const actualLineBreaks =
  81. closingBracketToken.loc.start.line - prevToken.loc.end.line
  82. if (actualLineBreaks !== expectedLineBreaks) {
  83. context.report({
  84. node,
  85. loc: {
  86. start: prevToken.loc.end,
  87. end: closingBracketToken.loc.start
  88. },
  89. message:
  90. 'Expected {{expected}} before closing bracket, but {{actual}} found.',
  91. data: {
  92. expected: getPhrase(expectedLineBreaks),
  93. actual: getPhrase(actualLineBreaks)
  94. },
  95. fix(fixer) {
  96. /** @type {Range} */
  97. const range = [prevToken.range[1], closingBracketToken.range[0]]
  98. const text = '\n'.repeat(expectedLineBreaks)
  99. return fixer.replaceTextRange(range, text)
  100. }
  101. })
  102. }
  103. }
  104. })
  105. }
  106. }