html-comments.js 8.0 KB

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