html-comments.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. /**
  2. * @typedef { { exceptions?: string[] } } CommentParserConfig
  3. * @typedef { (comment: ParsedHTMLComment) => void } HTMLCommentVisitor
  4. * @typedef { { includeDirectives?: boolean } } CommentVisitorOption
  5. *
  6. * @typedef { Token & { type: 'HTMLCommentOpen' } } HTMLCommentOpen
  7. * @typedef { Token & { type: 'HTMLCommentOpenDecoration' } } HTMLCommentOpenDecoration
  8. * @typedef { Token & { type: 'HTMLCommentValue' } } HTMLCommentValue
  9. * @typedef { Token & { type: 'HTMLCommentClose' } } HTMLCommentClose
  10. * @typedef { Token & { type: 'HTMLCommentCloseDecoration' } } HTMLCommentCloseDecoration
  11. * @typedef { { open: HTMLCommentOpen, openDecoration: HTMLCommentOpenDecoration | null, value: HTMLCommentValue | null, closeDecoration: HTMLCommentCloseDecoration | null, close: HTMLCommentClose } } ParsedHTMLComment
  12. */
  13. const utils = require('./')
  14. const COMMENT_DIRECTIVE = /^\s*eslint-(?:en|dis)able/
  15. const IE_CONDITIONAL_IF = /^\[if\s+/
  16. const IE_CONDITIONAL_ENDIF = /\[endif]$/
  17. /** @type { 'HTMLCommentOpen' } */
  18. const TYPE_HTML_COMMENT_OPEN = 'HTMLCommentOpen'
  19. /** @type { 'HTMLCommentOpenDecoration' } */
  20. const TYPE_HTML_COMMENT_OPEN_DECORATION = 'HTMLCommentOpenDecoration'
  21. /** @type { 'HTMLCommentValue' } */
  22. const TYPE_HTML_COMMENT_VALUE = 'HTMLCommentValue'
  23. /** @type { 'HTMLCommentClose' } */
  24. const TYPE_HTML_COMMENT_CLOSE = 'HTMLCommentClose'
  25. /** @type { 'HTMLCommentCloseDecoration' } */
  26. const TYPE_HTML_COMMENT_CLOSE_DECORATION = 'HTMLCommentCloseDecoration'
  27. /**
  28. * @param {HTMLComment} comment
  29. * @returns {boolean}
  30. */
  31. function isCommentDirective(comment) {
  32. return COMMENT_DIRECTIVE.test(comment.value)
  33. }
  34. /**
  35. * @param {HTMLComment} comment
  36. * @returns {boolean}
  37. */
  38. function isIEConditionalComment(comment) {
  39. return (
  40. IE_CONDITIONAL_IF.test(comment.value) ||
  41. IE_CONDITIONAL_ENDIF.test(comment.value)
  42. )
  43. }
  44. /**
  45. * Define HTML comment parser
  46. *
  47. * @param {SourceCode} sourceCode The source code instance.
  48. * @param {CommentParserConfig | null} config The config.
  49. * @returns { (node: Token) => (ParsedHTMLComment | null) } HTML comment parser.
  50. */
  51. function defineParser(sourceCode, config) {
  52. config = config || {}
  53. const exceptions = config.exceptions || []
  54. /**
  55. * Get a open decoration string from comment contents.
  56. * @param {string} contents comment contents
  57. * @returns {string} decoration string
  58. */
  59. function getOpenDecoration(contents) {
  60. let decoration = ''
  61. for (const exception of exceptions) {
  62. const length = exception.length
  63. let index = 0
  64. while (contents.startsWith(exception, index)) {
  65. index += length
  66. }
  67. const exceptionLength = index
  68. if (decoration.length < exceptionLength) {
  69. decoration = contents.slice(0, exceptionLength)
  70. }
  71. }
  72. return decoration
  73. }
  74. /**
  75. * Get a close decoration string from comment contents.
  76. * @param {string} contents comment contents
  77. * @returns {string} decoration string
  78. */
  79. function getCloseDecoration(contents) {
  80. let decoration = ''
  81. for (const exception of exceptions) {
  82. const length = exception.length
  83. let index = contents.length
  84. while (contents.endsWith(exception, index)) {
  85. index -= length
  86. }
  87. const exceptionLength = contents.length - index
  88. if (decoration.length < exceptionLength) {
  89. decoration = contents.slice(index)
  90. }
  91. }
  92. return decoration
  93. }
  94. /**
  95. * Parse HTMLComment.
  96. * @param {Token} node a comment token
  97. * @returns {ParsedHTMLComment | null} the result of HTMLComment tokens.
  98. */
  99. return function parseHTMLComment(node) {
  100. if (node.type !== 'HTMLComment') {
  101. // Is not HTMLComment
  102. return null
  103. }
  104. const htmlCommentText = sourceCode.getText(node)
  105. if (
  106. !htmlCommentText.startsWith('<!--') ||
  107. !htmlCommentText.endsWith('-->')
  108. ) {
  109. // Is not normal HTML Comment
  110. // e.g. Error Code: "abrupt-closing-of-empty-comment", "incorrectly-closed-comment"
  111. return null
  112. }
  113. let valueText = htmlCommentText.slice(4, -3)
  114. const openDecorationText = getOpenDecoration(valueText)
  115. valueText = valueText.slice(openDecorationText.length)
  116. const firstCharIndex = valueText.search(/\S/)
  117. const beforeSpace =
  118. firstCharIndex >= 0 ? valueText.slice(0, firstCharIndex) : valueText
  119. valueText = valueText.slice(beforeSpace.length)
  120. const closeDecorationText = getCloseDecoration(valueText)
  121. if (closeDecorationText) {
  122. valueText = valueText.slice(0, -closeDecorationText.length)
  123. }
  124. const lastCharIndex = valueText.search(/\S\s*$/)
  125. const afterSpace =
  126. lastCharIndex >= 0 ? valueText.slice(lastCharIndex + 1) : valueText
  127. if (afterSpace) {
  128. valueText = valueText.slice(0, -afterSpace.length)
  129. }
  130. let tokenIndex = node.range[0]
  131. /**
  132. * @param {string} type
  133. * @param {string} value
  134. * @returns {any}
  135. */
  136. const createToken = (type, value) => {
  137. /** @type {Range} */
  138. const range = [tokenIndex, tokenIndex + value.length]
  139. tokenIndex = range[1]
  140. /** @type {SourceLocation} */
  141. let loc
  142. return {
  143. type,
  144. value,
  145. range,
  146. get loc() {
  147. if (loc) {
  148. return loc
  149. }
  150. return (loc = {
  151. start: sourceCode.getLocFromIndex(range[0]),
  152. end: sourceCode.getLocFromIndex(range[1])
  153. })
  154. }
  155. }
  156. }
  157. /** @type {HTMLCommentOpen} */
  158. const open = createToken(TYPE_HTML_COMMENT_OPEN, '<!--')
  159. /** @type {HTMLCommentOpenDecoration | null} */
  160. const openDecoration = openDecorationText
  161. ? createToken(TYPE_HTML_COMMENT_OPEN_DECORATION, openDecorationText)
  162. : null
  163. tokenIndex += beforeSpace.length
  164. /** @type {HTMLCommentValue | null} */
  165. const value = valueText
  166. ? createToken(TYPE_HTML_COMMENT_VALUE, valueText)
  167. : null
  168. tokenIndex += afterSpace.length
  169. /** @type {HTMLCommentCloseDecoration | null} */
  170. const closeDecoration = closeDecorationText
  171. ? createToken(TYPE_HTML_COMMENT_CLOSE_DECORATION, closeDecorationText)
  172. : null
  173. /** @type {HTMLCommentClose} */
  174. const close = createToken(TYPE_HTML_COMMENT_CLOSE, '-->')
  175. return {
  176. /** HTML comment open (`<!--`) */
  177. open,
  178. /** decoration of the start of HTML comments. (`*****` when `<!--*****`) */
  179. openDecoration,
  180. /** value of HTML comment. whitespaces and other tokens are not included. */
  181. value,
  182. /** decoration of the end of HTML comments. (`*****` when `*****-->`) */
  183. closeDecoration,
  184. /** HTML comment close (`-->`) */
  185. close
  186. }
  187. }
  188. }
  189. /**
  190. * Define HTML comment visitor
  191. *
  192. * @param {RuleContext} context The rule context.
  193. * @param {CommentParserConfig | null} config The config.
  194. * @param {HTMLCommentVisitor} visitHTMLComment The HTML comment visitor.
  195. * @param {CommentVisitorOption} [visitorOption] The option for visitor.
  196. * @returns {RuleListener} HTML comment visitor.
  197. */
  198. function defineVisitor(context, config, visitHTMLComment, visitorOption) {
  199. return {
  200. Program(node) {
  201. visitorOption = visitorOption || {}
  202. if (utils.hasInvalidEOF(node)) {
  203. return
  204. }
  205. if (!node.templateBody) {
  206. return
  207. }
  208. const parse = defineParser(context.getSourceCode(), config)
  209. for (const comment of node.templateBody.comments) {
  210. if (comment.type !== 'HTMLComment') {
  211. continue
  212. }
  213. if (!visitorOption.includeDirectives && isCommentDirective(comment)) {
  214. // ignore directives
  215. continue
  216. }
  217. if (isIEConditionalComment(comment)) {
  218. // ignore IE conditional
  219. continue
  220. }
  221. const tokens = parse(comment)
  222. if (tokens) {
  223. visitHTMLComment(tokens)
  224. }
  225. }
  226. }
  227. }
  228. }
  229. module.exports = {
  230. defineVisitor
  231. }