html-closing-bracket-newline.js 2.9 KB

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