src/language-html/parser-html.js/doSecondParse
function ngHtmlParser(
  input,
  {
    recognizeSelfClosing,
    normalizeTagName,
    normalizeAttributeName,
    allowHtmComponentClosingTags,
    isTagNameCaseSensitive,
    getTagContentType,
  },
  options
) {
  const parser = require("angular-html-parser");
  const {
    RecursiveVisitor,
    visitAll,
    Attribute,
    CDATA,
    Comment,
    DocType,
    Element,
    Text,
  } = require("angular-html-parser/lib/compiler/src/ml_parser/ast");
  const {
    ParseSourceSpan,
  } = require("angular-html-parser/lib/compiler/src/parse_util");
  const {
    getHtmlTagDefinition,
  } = require("angular-html-parser/lib/compiler/src/ml_parser/html_tags");

  let { rootNodes, errors } = parser.parse(input, {
    canSelfClose: recognizeSelfClosing,
    allowHtmComponentClosingTags,
    isTagNameCaseSensitive,
    getTagContentType,
  });

  const isVueHtml =
    options.parser === "vue" &&
    rootNodes.some(
      (node) =>
        (node instanceof DocType && node.value === "html") ||
        (node instanceof Element && node.name.toLowerCase() === "html")
    );

  if (options.parser === "vue" && !isVueHtml) {
    const shouldParseAsHTML = (node) => {
      if (!node) {
        return false;
      }
      if (node.name !== "template") {
        return false;
      }
      const langAttr = node.attrs.find((attr) => attr.name === "lang");
      const langValue = langAttr && langAttr.value;
      return langValue == null || getParserName(langValue, options) === "html";
    };
    if (rootNodes.some(shouldParseAsHTML)) {
      let secondParseResult;
      const doSecondParse = () =>
        parser.parse(input, {
          canSelfClose: recognizeSelfClosing,
          allowHtmComponentClosingTags,
          isTagNameCaseSensitive,
        });
      const getSecondParse = () =>
        secondParseResult || (secondParseResult = doSecondParse());
      const getSameLocationNode = (node) =>
        getSecondParse().rootNodes.find(
          ({ startSourceSpan }) =>
            startSourceSpan &&
            startSourceSpan.start.offset === node.startSourceSpan.start.offset
        );
      for (let i = 0; i < rootNodes.length; i++) {
        const node = rootNodes[i];
        const { endSourceSpan, startSourceSpan } = node;
        const isUnclosedNode = endSourceSpan === null;
        if (isUnclosedNode) {
          const result = getSecondParse();
          errors = result.errors;
          rootNodes[i] = getSameLocationNode(node) || node;
        } else if (shouldParseAsHTML(node)) {
          const result = getSecondParse();
          const startOffset = startSourceSpan.end.offset;
          const endOffset = endSourceSpan.start.offset;
          for (const error of result.errors) {
            const { offset } = error.span.start;
            if (startOffset < offset && offset < endOffset) {
              errors = [error];
              break;
            }
          }
          rootNodes[i] = getSameLocationNode(node) || node;
        }
      }
    }
  } else if (isVueHtml) {
    // If not Vue SFC, treat as html
    recognizeSelfClosing = true;
    normalizeTagName = true;
    normalizeAttributeName = true;
    allowHtmComponentClosingTags = true;
    isTagNameCaseSensitive = false;
    const htmlParseResult = parser.parse(input, {
      canSelfClose: recognizeSelfClosing,
      allowHtmComponentClosingTags,
      isTagNameCaseSensitive,
    });

    rootNodes = htmlParseResult.rootNodes;
    errors = htmlParseResult.errors;
  }

  if (errors.length !== 0) {
    const { msg, span } = errors[0];
    const { line, col } = span.start;
    throw createError(msg, { start: { line: line + 1, column: col + 1 } });
  }

  const addType = (node) => {
    if (node instanceof Attribute) {
      node.type = "attribute";
    } else if (node instanceof CDATA) {
      node.type = "cdata";
    } else if (node instanceof Comment) {
      node.type = "comment";
    } else if (node instanceof DocType) {
      node.type = "docType";
    } else if (node instanceof Element) {
      node.type = "element";
    } else if (node instanceof Text) {
      node.type = "text";
    } else {
      throw new Error(`Unexpected node ${JSON.stringify(node)}`);
    }
  };

  const restoreName = (node) => {
    const namespace = node.name.startsWith(":")
      ? node.name.slice(1).split(":")[0]
      : null;
    const rawName = node.nameSpan.toString();
    const hasExplicitNamespace = rawName.startsWith(`${namespace}:`);
    const name = hasExplicitNamespace
      ? rawName.slice(namespace.length + 1)
      : rawName;

    node.name = name;
    node.namespace = namespace;
    node.hasExplicitNamespace = hasExplicitNamespace;
  };

  const restoreNameAndValue = (node) => {
    if (node instanceof Element) {
      restoreName(node);
      node.attrs.forEach((attr) => {
        restoreName(attr);
        if (!attr.valueSpan) {
          attr.value = null;
        } else {
          attr.value = attr.valueSpan.toString();
          if (/["']/.test(attr.value[0])) {
            attr.value = attr.value.slice(1, -1);
          }
        }
      });
    } else if (node instanceof Comment) {
      node.value = node.sourceSpan
        .toString()
        .slice("<!--".length, -"-->".length);
    } else if (node instanceof Text) {
      node.value = node.sourceSpan.toString();
    }
  };

  const lowerCaseIfFn = (text, fn) => {
    const lowerCasedText = text.toLowerCase();
    return fn(lowerCasedText) ? lowerCasedText : text;
  };
  const normalizeName = (node) => {
    if (node instanceof Element) {
      if (
        normalizeTagName &&
        (!node.namespace ||
          node.namespace === node.tagDefinition.implicitNamespacePrefix ||
          isUnknownNamespace(node))
      ) {
        node.name = lowerCaseIfFn(
          node.name,
          (lowerCasedName) => lowerCasedName in HTML_TAGS
        );
      }

      if (normalizeAttributeName) {
        const CURRENT_HTML_ELEMENT_ATTRIBUTES =
          HTML_ELEMENT_ATTRIBUTES[node.name] || Object.create(null);
        node.attrs.forEach((attr) => {
          if (!attr.namespace) {
            attr.name = lowerCaseIfFn(
              attr.name,
              (lowerCasedAttrName) =>
                node.name in HTML_ELEMENT_ATTRIBUTES &&
                (lowerCasedAttrName in HTML_ELEMENT_ATTRIBUTES["*"] ||
                  lowerCasedAttrName in CURRENT_HTML_ELEMENT_ATTRIBUTES)
            );
          }
        });
      }
    }
  };

  const fixSourceSpan = (node) => {
    if (node.sourceSpan && node.endSourceSpan) {
      node.sourceSpan = new ParseSourceSpan(
        node.sourceSpan.start,
        node.endSourceSpan.end
      );
    }
  };

  const addTagDefinition = (node) => {
    if (node instanceof Element) {
      const tagDefinition = getHtmlTagDefinition(
        isTagNameCaseSensitive ? node.name : node.name.toLowerCase()
      );
      if (
        !node.namespace ||
        node.namespace === tagDefinition.implicitNamespacePrefix ||
        isUnknownNamespace(node)
      ) {
        node.tagDefinition = tagDefinition;
      } else {
        node.tagDefinition = getHtmlTagDefinition(""); // the default one
      }
    }
  };

  visitAll(
    new (class extends RecursiveVisitor {
      visit(node) {
        addType(node);
        restoreNameAndValue(node);
        addTagDefinition(node);
        normalizeName(node);
        fixSourceSpan(node);
      }
    })(),
    rootNodes
  );

  return rootNodes;
}