src\compiler\parser\html-parser.js
// 匹配属性,兼容 class="some-class"/class='some-class'/class=some-class/disable 四种写法const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/// 动态属性值(如 @/:/v- 等)const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/// 不包含前缀名的 tag 名称const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`// 包含前缀的 tag 名称const qnameCapture = `((?:${ncname}\\:)?${ncname})`// 开始 tagconst startTagOpen = new RegExp(`^<${qnameCapture}`)// tag 结束前的内容const startTagClose = /^\s*(\/?)>/// 结束标签const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)// DOCTYPE 标签const doctype = /^<!DOCTYPE [^>]+>/i// 注释节点const comment = /^<!\--/// 条件注释节点const conditionalComment = /^<!\[/// 纯文本标签export const isPlainTextElement = makeMap('script,style,textarea', true)const reCache = {}// html 中特殊字符的 decodeconst decodingMap = {'<': '<','>': '>','"': '"','&': '&',' ': '\n','	': '\t',''': "'"}// 匹配上面被转义的特殊字符const encodedAttr = /&(?:lt|gt|quot|amp|#39);/gconst encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g// 是否保留 html 的换行在特殊的标签内const isIgnoreNewlineTag = makeMap('pre,textarea', true)const shouldIgnoreFirstNewline = (tag, html) => tag && isIgnoreNewlineTag(tag) && html[0] === '\n'// 将被转义的特殊字符转义回来function decodeAttr (value, shouldDecodeNewlines) {const re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttrreturn value.replace(re, match => decodingMap[match])}
function parseHTML (html, options) {// 用于存放 tag 的堆栈const stack = []// 传入的 options 之一const expectHTML = options.expectHTML// 传入的 options 之一,用于判断是否是一元标签const isUnaryTag = options.isUnaryTag || no// 传入的 options 之一,用于判断标签是否可以时自闭和标签const canBeLeftOpenTag = options.canBeLeftOpenTag || no// 当前字符流读入的位置let index = 0// 尚未 parse 的 html 字符串、stack 栈顶的标签元素let last, lastTag// 当 html 被 parse 完了则退出循环while (html) {// last 用于存放尚未被 parse 的 htmllast = html// Make sure we're not in a plaintext content element like script/styleif (!lastTag || !isPlainTextElement(lastTag)) {// 如果内容不是在纯文本标签里(script、style、textarea)// 获取第一个 < 出现的位置let textEnd = html.indexOf('<')// 如果 < 出现在第一个位置if (textEnd === 0) {// Comment:// 判断是不是注释节点if (comment.test(html)) {// 确认是否是完整的注释节点const commentEnd = html.indexOf('-->')// 如果是,则根据配置要求处理if (commentEnd >= 0) {if (options.shouldKeepComment) {options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)}// 跳转到注释结束advance(commentEnd + 3)// 结束当前循环continue}}// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment// 如果是条件注释节点if (conditionalComment.test(html)) {// 确认是否是条件注释节点const conditionalEnd = html.indexOf(']>')// 确认是if (conditionalEnd >= 0) {// 跳转时注释结束advance(conditionalEnd + 2)// 结束当前循环continue}}// Doctype:// 如果是 Doctype 节点const doctypeMatch = html.match(doctype)if (doctypeMatch) {// 跳转时 Doctype 结束advance(doctypeMatch[0].length)// 结束当前循环continue}// End tag:// 结束标签const endTagMatch = html.match(endTag)// 如果是结束标签if (endTagMatch) {// 标记开始位置const curIndex = index// 跳到结束标签后面advance(endTagMatch[0].length)// 解析结束标签parseEndTag(endTagMatch[1], curIndex, index)continue}// Start tag:// 开始标签const startTagMatch = parseStartTag()// 如果有对应的匹配结果,说明是开始标签if (startTagMatch) {// 处理并分析匹配的结果handleStartTag(startTagMatch)if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {advance(1)}continue}}let text, rest, next// 一直循环跳到非文本的 < 位置if (textEnd >= 0) {rest = html.slice(textEnd)while (!endTag.test(rest) &&!startTagOpen.test(rest) &&!comment.test(rest) &&!conditionalComment.test(rest)) {// < in plain text, be forgiving and treat it as textnext = rest.indexOf('<', 1)if (next < 0) breaktextEnd += nextrest = html.slice(textEnd)}text = html.substring(0, textEnd)}// 说明剩余的都是文本if (textEnd < 0) {text = html}// 跳过文本内容if (text) {advance(text.length)}if (options.chars && text) {options.chars(text, index - text.length, index)}} else {// 如果是纯文本 tag,则将内容视作文本处理let endTagLength = 0// 小写的 tag 名称const stackedTag = lastTag.toLowerCase()// 正则 reStackedTag 的作用是用来匹配纯文本标签的内容以及结束标签的const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))const rest = html.replace(reStackedTag, function (all, text, endTag) {endTagLength = endTag.lengthif (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {text = text.replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')}if (shouldIgnoreFirstNewline(stackedTag, text)) {text = text.slice(1)}if (options.chars) {options.chars(text)}return ''})index += html.length - rest.lengthhtml = restparseEndTag(stackedTag, index - endTagLength, index)}// 如果先前的 parse 没有发生任何改变,则将 html 视作纯文本来对待if (html === last) {options.chars && options.chars(html)if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {options.warn(`Mal-formatted tag at end of template: "${html}"`, { start: index + html.length })}break}}// Clean up any remaining tagsparseEndTag()// 跳过指定字符长度function advance (n) {index += nhtml = html.substring(n)}// 解析开始标签function parseStartTag () {// 使用正则开始解析const start = html.match(startTagOpen)// 如果解析到了if (start) {const match = {// 解析到的 tag 名称tagName: start[1],// tag 对应的参数名称attrs: [],// tag 开始的位置start: index}// 跳到开始标签的后面advance(start[0].length)let end, attr// 如果没有解析到起始标签的结束,并且能匹配到参数while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {// 属性的起点标记attr.start = index// 跳转到属性的后面advance(attr[0].length)// 标记属性的结尾位置attr.end = index// push 到 attrs 中match.attrs.push(attr)}// 如果已经没有属性能匹配的同时,还没有到最后一位if (end) {// end[1] 如果有值说明是一元标签match.unarySlash = end[1]// 跳到标签的结束advance(end[0].length)// 标记开始标签的结束为止match.end = indexreturn match}}}// 处理开始标签相关信息function handleStartTag (match) {// tag 名const tagName = match.tagName// 是否是一元标签const unarySlash = match.unarySlash// 根据配置判断合法 htmlif (expectHTML) {// 如果上一个标签是 p,同时自身不是流式内容的标签,则直接结束 p 标签if (lastTag === 'p' && isNonPhrasingTag(tagName)) {parseEndTag(lastTag)}// 如果当前解析的标签是一个可以省略结束标签的标签,并且与上一个解析到的开始标签相同时,则会立刻关闭当前标签if (canBeLeftOpenTag(tagName) && lastTag === tagName) {parseEndTag(tagName)}}// 判断当前标签是否为一元标签const unary = isUnaryTag(tagName) || !!unarySlash// 标签属性的长度const l = match.attrs.length// 创建一个跟标签属性同等长度的数组const attrs = new Array(l)// 遍历属性for (let i = 0; i < l; i++) {// 取出属性匹配结果const args = match.attrs[i]// 拿出属性对应的值const value = args[3] || args[4] || args[5] || ''// 根据实际情况拿取相关换行的配置const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'? options.shouldDecodeNewlinesForHref: options.shouldDecodeNewlines// 注入到准备好的参数数组中attrs[i] = {// 参数名name: args[1],// 进行过 decode 的值value: decodeAttr(value, shouldDecodeNewlines)}// 开发所需 sourcemapif (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {attrs[i].start = args.start + args[0].match(/^\s*/).lengthattrs[i].end = args.end}}// 如果不是一元标签if (!unary) {// 则将当前标签信息入栈stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })// 设置栈顶信息lastTag = tagName}// 调用 start 钩子函数if (options.start) {options.start(tagName, attrs, unary, match.start, match.end)}}// 解析结束标签// 检测是否缺少闭合标签// 处理 stack 栈中剩余的标签// 解析 </br> 与 </p> 标签,与浏览器的行为相同function parseEndTag (tagName, start, end) {// pos 用于判断 html 字符串是否缺少结束标签// lowerCasedTagName 用于存储小写的 tag 名称let pos, lowerCasedTagName// 空处理if (start == null) start = indexif (end == null) end = index// Find the closest opened tag of the same type// 如果有 tag 名称if (tagName) {// 获取小写的 tag 名称lowerCasedTagName = tagName.toLowerCase()// 以倒叙遍历堆栈的方式查找当前 tag 的开始标枪在堆栈里的位置for (pos = stack.length - 1; pos >= 0; pos--) {if (stack[pos].lowerCasedTag === lowerCasedTagName) {break}}} else {// If no tag name is provided, clean shop// 反之位置为 0pos = 0}// 如果在堆栈里找到了对应的开始位置,或者干脆当前没有对应 tagNameif (pos >= 0) {// Close all the open elements, up the stack// 倒叙遍历堆栈,如果开始标签后有标签,在开发环境会报出对应的错误for (let i = stack.length - 1; i >= pos; i--) {if (process.env.NODE_ENV !== 'production' &&(i > pos || !tagName) &&options.warn) {options.warn(`tag <${stack[i].tag}> has no matching end tag.`,{ start: stack[i].start, end: stack[i].end })}// 闭合该 tagif (options.end) {options.end(stack[i].tag, start, end)}}// Remove the open elements from the stack// 更新堆栈和栈顶信息stack.length = poslastTag = pos && stack[pos - 1].tag} else if (lowerCasedTagName === 'br') {// 如果结束标签为 br,同时没有找到开始标签,则在此之前就地添加开始标签if (options.start) {options.start(tagName, [], true, start, end)}} else if (lowerCasedTagName === 'p') {// 如果结束标签为 p,同时没有找到开始标签,则在此之前就地添加开始标签if (options.start) {options.start(tagName, [], false, start, end)}if (options.end) {options.end(tagName, start, end)}}}}
