html-closing-bracket-newline.js 2.8 KB

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