text: /**
   * Not type-checking this file because it's mostly vendor code.
   */
  
  /*!
   * HTML Parser By John Resig (ejohn.org)
   * Modified by Juriy "kangax" Zaytsev
   * Original code by Erik Arvidsson (MPL-1.1 OR Apache-2.0 OR GPL-2.0-or-later)
   * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
   */
  
  import { makeMap, no } from 'shared/util'
  import { isNonPhrasingTag } from 'web/compiler/util'
  import { unicodeRegExp } from 'core/util/lang'
  
  // Regular Expressions for parsing tags and attributes
  // 这个表达式有五个捕获组，第一个捕获组用来匹配属性名，第二个捕获组用来匹配等于号，
  // 第三、第四、第五个捕获组都是用来匹配属性值的，同时 ? 表明第三、四、五个分组是可选的
  // html 标签中有4种写属性值的方式：
  // 1、使用双引号把值引起来：class="some-class"
  // 2、使用单引号把值引起来：class='some-class'
  // 3、不使用引号：class=some-class
  // 4、单独的属性名：disabled
  // 测试用例：
  // const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
  // console.log('class="some-class"'.match(attribute))  // 测试双引号
  // console.log("class='some-class'".match(attribute))  // 测试单引号
  // console.log('class=some-class'.match(attribute))  // 测试无引号
  // console.log('disabled'.match(attribute))  // 测试无属性值
  const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
  const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
  // 前提：一个合法的XML标签名应该是由 前缀、冒号(:) 以及 标签名称 组成的：<前缀:标签名称></前缀:标签名称>
  // ncname 的全称是 An XML name that does not contain a colon (:) 即：不包含冒号(:)的 XML 名称
  const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
  // qname 就是：<前缀:标签名称>，也就是合法的XML标签
  const qnameCapture = `((?:${ncname}\\:)?${ncname})`
  // 匹配开始标签的一部分
  const startTagOpen = new RegExp(`^<${qnameCapture}`)
  // 匹配开始标签结束部分
  const startTagClose = /^\s*(\/?)>/
  // 匹配结束标签
  const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
  const doctype = /^<!DOCTYPE [^>]+>/i
  // #7298: escape - to avoid being passed as HTML comment when inlined in page
  // 匹配注释节点
  const comment = /^<!\--/
  const conditionalComment = /^<!\[/
  
  // Special Elements (can contain anything)
  export const isPlainTextElement = makeMap('script,style,textarea', true)
  const reCache = {}
  
  const decodingMap = {
    '&lt;': '<',
    '&gt;': '>',
    '&quot;': '"',
    '&amp;': '&',
    '&#10;': '\n',
    '&#9;': '\t',
    '&#39;': "'"
  }
  const encodedAttr = /&(?:lt|gt|quot|amp|#39);/g
  const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g
  
  // #5992
  const isIgnoreNewlineTag = makeMap('pre,textarea', true)
  const shouldIgnoreFirstNewline = (tag, html) => tag && isIgnoreNewlineTag(tag) && html[0] === '\n'
  
  function decodeAttr (value, shouldDecodeNewlines) {
    const re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr
    return value.replace(re, match => decodingMap[match])
  }
  
  export function parseHTML (html, options) {
    // 检测非一元标签是否缺少结束标签的原理：
    // 例子：<article><section><div></section></article>
    // 在 parse 这个字符串的时候，首先会遇到 article 开始标签，
    // 并将该标签入栈(push 到 stack 数组)，然后会遇到 section 开始标签，
    // 并将该标签 push 到栈顶，接下来会遇到 div 开始标签，同样被压入栈顶
    // 再然后便会遇到 section 结束标签，我们知道：最先遇到的结束标签，其对应的开始标签应该最后被压入 stack 栈，
    // 也就是说此时 stack 栈顶的元素应该是 section，但是我们发现事实上 stack 栈顶并不是 section 而是 div，
    // 这说明 div 元素缺少闭合标签。
    // 这就是检测 html 字符串中是否缺少闭合标签的原理。
    const stack = [] // 用于检测非一元标签是否缺少结束标签
    const expectHTML = options.expectHTML
    const isUnaryTag = options.isUnaryTag || no // 用来检测一个标签是否是一元标签。
    const canBeLeftOpenTag = options.canBeLeftOpenTag || no // 用来检测一个标签是否是可以省略闭合标签的非一元标签
    let index = 0
    let last, lastTag
    while (html) {
      last = html
      // Make sure we're not in a plaintext content element like script/style
      // 确保即将 parse 的内容不是在纯文本标签里 (script,style,textarea)
      if (!lastTag || !isPlainTextElement(lastTag)) {
        let textEnd = html.indexOf('<')
        // textEnd === 0 的情况
        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)
              }
              // 将已经 parse 完毕的字符串剔除
              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) {
            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
        // textEnd >= 0 的情况
        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 text
            next = rest.indexOf('<', 1)
            if (next < 0) break
            textEnd += next
            rest = html.slice(textEnd)
          }
          text = html.substring(0, textEnd)
        }
  
        // textEnd < 0 的情况
        if (textEnd < 0) {
          text = html
        }
  
        if (text) {
          advance(text.length)
        }
  
        if (options.chars && text) {
          options.chars(text, index - text.length, index)
        }
      } else {
        let endTagLength = 0
        const stackedTag = lastTag.toLowerCase()
        const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
        const rest = html.replace(reStackedTag, function (all, text, endTag) {
          endTagLength = endTag.length
          if (!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.length
        html = rest
        parseEndTag(stackedTag, index - endTagLength, index)
      }
  
      // 将整个字符串作为文本对待
      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 tags
    parseEndTag()
  
    function advance (n) {
      index += n
      html = html.substring(n)
    }
  
    function parseStartTag () {
      const start = html.match(startTagOpen)
      if (start) {
        const match = {
          tagName: start[1],
          attrs: [],
          start: index
        }
        advance(start[0].length)
        let end, attr
        //两个条件，1、没有匹配到开始标签的结束部分 2、匹配到了属性
        while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
          attr.start = index
          advance(attr[0].length)
          attr.end = index
          match.attrs.push(attr)
        }
        if (end) {
          // end[1] 不为 undefined，那么说明该标签是一个一元标签
          match.unarySlash = end[1]
          advance(end[0].length)
          match.end = index
          return match
        }
      }
    }
  
    function handleStartTag (match) {
      const tagName = match.tagName
      const unarySlash = match.unarySlash
  
      if (expectHTML) {
        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)
      // 格式化 match.attrs 数组，并将格式化后的数据存储到常量 attrs 中
      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],
          value: decodeAttr(value, shouldDecodeNewlines)
        }
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          attrs[i].start = args.start + args[0].match(/^\s*/).length
          attrs[i].end = args.end
        }
      }
  
      // 如果开始标签是非一元标签，则将该开始标签的信息入栈，即 push 到 stack 数组中，并将 lastTag 的值设置为该标签名
      if (!unary) {
        stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
        lastTag = tagName
      }
  
      if (options.start) {
        options.start(tagName, attrs, unary, match.start, match.end)
      }
    }
     // 作用：
     // 检测是否缺少闭合标签
     // 处理 stack 栈中剩余的标签
     // 解析 </br> 与 </p> 标签，与浏览器的行为相同
     // parseEndTag 函数有三种使用方式
     // 第一种是处理普通的结束标签，此时 三个参数都传递
     // 第二种是只传递第一个参数
     // 第三种是 不传递参数，当不传递参数的时候，就是我们讲过的，这是在处理 stack 栈剩余未处理的标签。
    function parseEndTag (tagName, start, end) {
      let pos, lowerCasedTagName
      if (start == null) start = index
      if (end == null) end = index
  
      // Find the closest opened tag of the same type
      if (tagName) {
        lowerCasedTagName = tagName.toLowerCase()
        for (pos = stack.length - 1; pos >= 0; pos--) {
          if (stack[pos].lowerCasedTag === lowerCasedTagName) {
            break
          }
        }
      } else {
        // If no tag name is provided, clean shop
        pos = 0
      }
  
      if (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 }
            )
          }
          if (options.end) {
            options.end(stack[i].tag, start, end)
          }
        }
  
        // Remove the open elements from the stack
        stack.length = pos
        lastTag = pos && stack[pos - 1].tag
  
        // 浏览器是能够正常解析单个</br>和</p>，其中 </br> 标签被正常解析为 <br> 标签，而 </p> 标签被正常解析为 <p></p>。
        // 除了 br 与 p 其他任何标签如果你只写了结束标签那么浏览器都将会忽略。
        // 所以为了与浏览器的行为相同，parseEndTag 函数也需要专门处理 br 与 p 的结束标签
      } else if (lowerCasedTagName === 'br') {
        if (options.start) {
          options.start(tagName, [], true, start, end)
        }
      } else if (lowerCasedTagName === 'p') {
        if (options.start) {
          options.start(tagName, [], false, start, end)
        }
        if (options.end) {
          options.end(tagName, start, end)
        }
      }
    }
  }
  