src\compiler\parser\html-parser.js

    1. // 匹配属性,兼容 class="some-class"/class='some-class'/class=some-class/disable 四种写法
    2. const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
    3. // 动态属性值(如 @/:/v- 等)
    4. const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
    5. // 不包含前缀名的 tag 名称
    6. const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
    7. // 包含前缀的 tag 名称
    8. const qnameCapture = `((?:${ncname}\\:)?${ncname})`
    9. // 开始 tag
    10. const startTagOpen = new RegExp(`^<${qnameCapture}`)
    11. // tag 结束前的内容
    12. const startTagClose = /^\s*(\/?)>/
    13. // 结束标签
    14. const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
    15. // DOCTYPE 标签
    16. const doctype = /^<!DOCTYPE [^>]+>/i
    17. // 注释节点
    18. const comment = /^<!\--/
    19. // 条件注释节点
    20. const conditionalComment = /^<!\[/
    21. // 纯文本标签
    22. export const isPlainTextElement = makeMap('script,style,textarea', true)
    23. const reCache = {}
    24. // html 中特殊字符的 decode
    25. const decodingMap = {
    26. '&lt;': '<',
    27. '&gt;': '>',
    28. '&quot;': '"',
    29. '&amp;': '&',
    30. '&#10;': '\n',
    31. '&#9;': '\t',
    32. '&#39;': "'"
    33. }
    34. // 匹配上面被转义的特殊字符
    35. const encodedAttr = /&(?:lt|gt|quot|amp|#39);/g
    36. const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g
    37. // 是否保留 html 的换行在特殊的标签内
    38. const isIgnoreNewlineTag = makeMap('pre,textarea', true)
    39. const shouldIgnoreFirstNewline = (tag, html) => tag && isIgnoreNewlineTag(tag) && html[0] === '\n'
    40. // 将被转义的特殊字符转义回来
    41. function decodeAttr (value, shouldDecodeNewlines) {
    42. const re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr
    43. return value.replace(re, match => decodingMap[match])
    44. }
    1. function parseHTML (html, options) {
    2. // 用于存放 tag 的堆栈
    3. const stack = []
    4. // 传入的 options 之一
    5. const expectHTML = options.expectHTML
    6. // 传入的 options 之一,用于判断是否是一元标签
    7. const isUnaryTag = options.isUnaryTag || no
    8. // 传入的 options 之一,用于判断标签是否可以时自闭和标签
    9. const canBeLeftOpenTag = options.canBeLeftOpenTag || no
    10. // 当前字符流读入的位置
    11. let index = 0
    12. // 尚未 parse 的 html 字符串、stack 栈顶的标签元素
    13. let last, lastTag
    14. // 当 html 被 parse 完了则退出循环
    15. while (html) {
    16. // last 用于存放尚未被 parse 的 html
    17. last = html
    18. // Make sure we're not in a plaintext content element like script/style
    19. if (!lastTag || !isPlainTextElement(lastTag)) {
    20. // 如果内容不是在纯文本标签里(script、style、textarea)
    21. // 获取第一个 < 出现的位置
    22. let textEnd = html.indexOf('<')
    23. // 如果 < 出现在第一个位置
    24. if (textEnd === 0) {
    25. // Comment:
    26. // 判断是不是注释节点
    27. if (comment.test(html)) {
    28. // 确认是否是完整的注释节点
    29. const commentEnd = html.indexOf('-->')
    30. // 如果是,则根据配置要求处理
    31. if (commentEnd >= 0) {
    32. if (options.shouldKeepComment) {
    33. options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
    34. }
    35. // 跳转到注释结束
    36. advance(commentEnd + 3)
    37. // 结束当前循环
    38. continue
    39. }
    40. }
    41. // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
    42. // 如果是条件注释节点
    43. if (conditionalComment.test(html)) {
    44. // 确认是否是条件注释节点
    45. const conditionalEnd = html.indexOf(']>')
    46. // 确认是
    47. if (conditionalEnd >= 0) {
    48. // 跳转时注释结束
    49. advance(conditionalEnd + 2)
    50. // 结束当前循环
    51. continue
    52. }
    53. }
    54. // Doctype:
    55. // 如果是 Doctype 节点
    56. const doctypeMatch = html.match(doctype)
    57. if (doctypeMatch) {
    58. // 跳转时 Doctype 结束
    59. advance(doctypeMatch[0].length)
    60. // 结束当前循环
    61. continue
    62. }
    63. // End tag:
    64. // 结束标签
    65. const endTagMatch = html.match(endTag)
    66. // 如果是结束标签
    67. if (endTagMatch) {
    68. // 标记开始位置
    69. const curIndex = index
    70. // 跳到结束标签后面
    71. advance(endTagMatch[0].length)
    72. // 解析结束标签
    73. parseEndTag(endTagMatch[1], curIndex, index)
    74. continue
    75. }
    76. // Start tag:
    77. // 开始标签
    78. const startTagMatch = parseStartTag()
    79. // 如果有对应的匹配结果,说明是开始标签
    80. if (startTagMatch) {
    81. // 处理并分析匹配的结果
    82. handleStartTag(startTagMatch)
    83. if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
    84. advance(1)
    85. }
    86. continue
    87. }
    88. }
    89. let text, rest, next
    90. // 一直循环跳到非文本的 < 位置
    91. if (textEnd >= 0) {
    92. rest = html.slice(textEnd)
    93. while (
    94. !endTag.test(rest) &&
    95. !startTagOpen.test(rest) &&
    96. !comment.test(rest) &&
    97. !conditionalComment.test(rest)
    98. ) {
    99. // < in plain text, be forgiving and treat it as text
    100. next = rest.indexOf('<', 1)
    101. if (next < 0) break
    102. textEnd += next
    103. rest = html.slice(textEnd)
    104. }
    105. text = html.substring(0, textEnd)
    106. }
    107. // 说明剩余的都是文本
    108. if (textEnd < 0) {
    109. text = html
    110. }
    111. // 跳过文本内容
    112. if (text) {
    113. advance(text.length)
    114. }
    115. if (options.chars && text) {
    116. options.chars(text, index - text.length, index)
    117. }
    118. } else {
    119. // 如果是纯文本 tag,则将内容视作文本处理
    120. let endTagLength = 0
    121. // 小写的 tag 名称
    122. const stackedTag = lastTag.toLowerCase()
    123. // 正则 reStackedTag 的作用是用来匹配纯文本标签的内容以及结束标签的
    124. const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
    125. const rest = html.replace(reStackedTag, function (all, text, endTag) {
    126. endTagLength = endTag.length
    127. if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
    128. text = text
    129. .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
    130. .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
    131. }
    132. if (shouldIgnoreFirstNewline(stackedTag, text)) {
    133. text = text.slice(1)
    134. }
    135. if (options.chars) {
    136. options.chars(text)
    137. }
    138. return ''
    139. })
    140. index += html.length - rest.length
    141. html = rest
    142. parseEndTag(stackedTag, index - endTagLength, index)
    143. }
    144. // 如果先前的 parse 没有发生任何改变,则将 html 视作纯文本来对待
    145. if (html === last) {
    146. options.chars && options.chars(html)
    147. if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {
    148. options.warn(`Mal-formatted tag at end of template: "${html}"`, { start: index + html.length })
    149. }
    150. break
    151. }
    152. }
    153. // Clean up any remaining tags
    154. parseEndTag()
    155. // 跳过指定字符长度
    156. function advance (n) {
    157. index += n
    158. html = html.substring(n)
    159. }
    160. // 解析开始标签
    161. function parseStartTag () {
    162. // 使用正则开始解析
    163. const start = html.match(startTagOpen)
    164. // 如果解析到了
    165. if (start) {
    166. const match = {
    167. // 解析到的 tag 名称
    168. tagName: start[1],
    169. // tag 对应的参数名称
    170. attrs: [],
    171. // tag 开始的位置
    172. start: index
    173. }
    174. // 跳到开始标签的后面
    175. advance(start[0].length)
    176. let end, attr
    177. // 如果没有解析到起始标签的结束,并且能匹配到参数
    178. while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
    179. // 属性的起点标记
    180. attr.start = index
    181. // 跳转到属性的后面
    182. advance(attr[0].length)
    183. // 标记属性的结尾位置
    184. attr.end = index
    185. // push 到 attrs 中
    186. match.attrs.push(attr)
    187. }
    188. // 如果已经没有属性能匹配的同时,还没有到最后一位
    189. if (end) {
    190. // end[1] 如果有值说明是一元标签
    191. match.unarySlash = end[1]
    192. // 跳到标签的结束
    193. advance(end[0].length)
    194. // 标记开始标签的结束为止
    195. match.end = index
    196. return match
    197. }
    198. }
    199. }
    200. // 处理开始标签相关信息
    201. function handleStartTag (match) {
    202. // tag 名
    203. const tagName = match.tagName
    204. // 是否是一元标签
    205. const unarySlash = match.unarySlash
    206. // 根据配置判断合法 html
    207. if (expectHTML) {
    208. // 如果上一个标签是 p,同时自身不是流式内容的标签,则直接结束 p 标签
    209. if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
    210. parseEndTag(lastTag)
    211. }
    212. // 如果当前解析的标签是一个可以省略结束标签的标签,并且与上一个解析到的开始标签相同时,则会立刻关闭当前标签
    213. if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
    214. parseEndTag(tagName)
    215. }
    216. }
    217. // 判断当前标签是否为一元标签
    218. const unary = isUnaryTag(tagName) || !!unarySlash
    219. // 标签属性的长度
    220. const l = match.attrs.length
    221. // 创建一个跟标签属性同等长度的数组
    222. const attrs = new Array(l)
    223. // 遍历属性
    224. for (let i = 0; i < l; i++) {
    225. // 取出属性匹配结果
    226. const args = match.attrs[i]
    227. // 拿出属性对应的值
    228. const value = args[3] || args[4] || args[5] || ''
    229. // 根据实际情况拿取相关换行的配置
    230. const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
    231. ? options.shouldDecodeNewlinesForHref
    232. : options.shouldDecodeNewlines
    233. // 注入到准备好的参数数组中
    234. attrs[i] = {
    235. // 参数名
    236. name: args[1],
    237. // 进行过 decode 的值
    238. value: decodeAttr(value, shouldDecodeNewlines)
    239. }
    240. // 开发所需 sourcemap
    241. if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
    242. attrs[i].start = args.start + args[0].match(/^\s*/).length
    243. attrs[i].end = args.end
    244. }
    245. }
    246. // 如果不是一元标签
    247. if (!unary) {
    248. // 则将当前标签信息入栈
    249. stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
    250. // 设置栈顶信息
    251. lastTag = tagName
    252. }
    253. // 调用 start 钩子函数
    254. if (options.start) {
    255. options.start(tagName, attrs, unary, match.start, match.end)
    256. }
    257. }
    258. // 解析结束标签
    259. // 检测是否缺少闭合标签
    260. // 处理 stack 栈中剩余的标签
    261. // 解析 </br> 与 </p> 标签,与浏览器的行为相同
    262. function parseEndTag (tagName, start, end) {
    263. // pos 用于判断 html 字符串是否缺少结束标签
    264. // lowerCasedTagName 用于存储小写的 tag 名称
    265. let pos, lowerCasedTagName
    266. // 空处理
    267. if (start == null) start = index
    268. if (end == null) end = index
    269. // Find the closest opened tag of the same type
    270. // 如果有 tag 名称
    271. if (tagName) {
    272. // 获取小写的 tag 名称
    273. lowerCasedTagName = tagName.toLowerCase()
    274. // 以倒叙遍历堆栈的方式查找当前 tag 的开始标枪在堆栈里的位置
    275. for (pos = stack.length - 1; pos >= 0; pos--) {
    276. if (stack[pos].lowerCasedTag === lowerCasedTagName) {
    277. break
    278. }
    279. }
    280. } else {
    281. // If no tag name is provided, clean shop
    282. // 反之位置为 0
    283. pos = 0
    284. }
    285. // 如果在堆栈里找到了对应的开始位置,或者干脆当前没有对应 tagName
    286. if (pos >= 0) {
    287. // Close all the open elements, up the stack
    288. // 倒叙遍历堆栈,如果开始标签后有标签,在开发环境会报出对应的错误
    289. for (let i = stack.length - 1; i >= pos; i--) {
    290. if (process.env.NODE_ENV !== 'production' &&
    291. (i > pos || !tagName) &&
    292. options.warn
    293. ) {
    294. options.warn(
    295. `tag <${stack[i].tag}> has no matching end tag.`,
    296. { start: stack[i].start, end: stack[i].end }
    297. )
    298. }
    299. // 闭合该 tag
    300. if (options.end) {
    301. options.end(stack[i].tag, start, end)
    302. }
    303. }
    304. // Remove the open elements from the stack
    305. // 更新堆栈和栈顶信息
    306. stack.length = pos
    307. lastTag = pos && stack[pos - 1].tag
    308. } else if (lowerCasedTagName === 'br') {
    309. // 如果结束标签为 br,同时没有找到开始标签,则在此之前就地添加开始标签
    310. if (options.start) {
    311. options.start(tagName, [], true, start, end)
    312. }
    313. } else if (lowerCasedTagName === 'p') {
    314. // 如果结束标签为 p,同时没有找到开始标签,则在此之前就地添加开始标签
    315. if (options.start) {
    316. options.start(tagName, [], false, start, end)
    317. }
    318. if (options.end) {
    319. options.end(tagName, start, end)
    320. }
    321. }
    322. }
    323. }