html-closing-bracket-spacing.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. /**
  2. * @author Toru Nagashima <https://github.com/mysticatea>
  3. */
  4. 'use strict'
  5. // -----------------------------------------------------------------------------
  6. // Requirements
  7. // -----------------------------------------------------------------------------
  8. const utils = require('../utils')
  9. // -----------------------------------------------------------------------------
  10. // Helpers
  11. // -----------------------------------------------------------------------------
  12. /**
  13. * @typedef { {startTag?:"always"|"never",endTag?:"always"|"never",selfClosingTag?:"always"|"never"} } Options
  14. */
  15. /**
  16. * Normalize options.
  17. * @param {Options} options The options user configured.
  18. * @param {ParserServices.TokenStore} tokens The token store of template body.
  19. * @returns {Options & { detectType: (node: VStartTag | VEndTag) => 'never' | 'always' | null }} The normalized options.
  20. */
  21. function parseOptions(options, tokens) {
  22. const opts = Object.assign(
  23. {
  24. startTag: 'never',
  25. endTag: 'never',
  26. selfClosingTag: 'always'
  27. },
  28. options
  29. )
  30. return Object.assign(opts, {
  31. /**
  32. * @param {VStartTag | VEndTag} node
  33. * @returns {'never' | 'always' | null}
  34. */
  35. detectType(node) {
  36. const openType = tokens.getFirstToken(node).type
  37. const closeType = tokens.getLastToken(node).type
  38. if (openType === 'HTMLEndTagOpen' && closeType === 'HTMLTagClose') {
  39. return opts.endTag
  40. }
  41. if (openType === 'HTMLTagOpen' && closeType === 'HTMLTagClose') {
  42. return opts.startTag
  43. }
  44. if (
  45. openType === 'HTMLTagOpen' &&
  46. closeType === 'HTMLSelfClosingTagClose'
  47. ) {
  48. return opts.selfClosingTag
  49. }
  50. return null
  51. }
  52. })
  53. }
  54. // -----------------------------------------------------------------------------
  55. // Rule Definition
  56. // -----------------------------------------------------------------------------
  57. module.exports = {
  58. meta: {
  59. type: 'layout',
  60. docs: {
  61. description: "require or disallow a space before tag's closing brackets",
  62. categories: ['vue3-strongly-recommended', 'strongly-recommended'],
  63. url: 'https://eslint.vuejs.org/rules/html-closing-bracket-spacing.html'
  64. },
  65. schema: [
  66. {
  67. type: 'object',
  68. properties: {
  69. startTag: { enum: ['always', 'never'] },
  70. endTag: { enum: ['always', 'never'] },
  71. selfClosingTag: { enum: ['always', 'never'] }
  72. },
  73. additionalProperties: false
  74. }
  75. ],
  76. fixable: 'whitespace'
  77. },
  78. /** @param {RuleContext} context */
  79. create(context) {
  80. const sourceCode = context.getSourceCode()
  81. const tokens =
  82. context.parserServices.getTemplateBodyTokenStore &&
  83. context.parserServices.getTemplateBodyTokenStore()
  84. const options = parseOptions(context.options[0], tokens)
  85. return utils.defineTemplateBodyVisitor(context, {
  86. /** @param {VStartTag | VEndTag} node */
  87. 'VStartTag, VEndTag'(node) {
  88. const type = options.detectType(node)
  89. const lastToken = tokens.getLastToken(node)
  90. const prevToken = tokens.getLastToken(node, 1)
  91. // Skip if EOF exists in the tag or linebreak exists before `>`.
  92. if (
  93. type == null ||
  94. prevToken == null ||
  95. prevToken.loc.end.line !== lastToken.loc.start.line
  96. ) {
  97. return
  98. }
  99. // Check and report.
  100. const hasSpace = prevToken.range[1] !== lastToken.range[0]
  101. if (type === 'always' && !hasSpace) {
  102. context.report({
  103. node,
  104. loc: lastToken.loc,
  105. message: "Expected a space before '{{bracket}}', but not found.",
  106. data: { bracket: sourceCode.getText(lastToken) },
  107. fix: (fixer) => fixer.insertTextBefore(lastToken, ' ')
  108. })
  109. } else if (type === 'never' && hasSpace) {
  110. context.report({
  111. node,
  112. loc: {
  113. start: prevToken.loc.end,
  114. end: lastToken.loc.end
  115. },
  116. message: "Expected no space before '{{bracket}}', but found.",
  117. data: { bracket: sourceCode.getText(lastToken) },
  118. fix: (fixer) =>
  119. fixer.removeRange([prevToken.range[1], lastToken.range[0]])
  120. })
  121. }
  122. }
  123. })
  124. }
  125. }