"use strict"; const htmlTagNames = require("html-tag-names"); const htmlElementAttributes = require("html-element-attributes"); const { getParserName, isFrontMatterNode } = require("../common/util"); const { CSS_DISPLAY_TAGS, CSS_DISPLAY_DEFAULT, CSS_WHITE_SPACE_TAGS, CSS_WHITE_SPACE_DEFAULT, } = require("./constants.evaluate"); const HTML_TAGS = arrayToMap(htmlTagNames); const HTML_ELEMENT_ATTRIBUTES = mapObject(htmlElementAttributes, arrayToMap); // https://infra.spec.whatwg.org/#ascii-whitespace const HTML_WHITESPACE = new Set(["\t", "\n", "\f", "\r", " "]); const htmlTrimStart = (string) => string.replace(/^[\t\n\f\r ]+/, ""); const htmlTrimEnd = (string) => string.replace(/[\t\n\f\r ]+$/, ""); const htmlTrim = (string) => htmlTrimStart(htmlTrimEnd(string)); const htmlTrimLeadingBlankLines = (string) => string.replace(/^[\t\f\r ]*?\n/g, ""); const htmlTrimPreserveIndentation = (string) => htmlTrimLeadingBlankLines(htmlTrimEnd(string)); const splitByHtmlWhitespace = (string) => string.split(/[\t\n\f\r ]+/); const getLeadingHtmlWhitespace = (string) => string.match(/^[\t\n\f\r ]*/)[0]; const getLeadingAndTrailingHtmlWhitespace = (string) => { const [, leadingWhitespace, text, trailingWhitespace] = string.match( /^([\t\n\f\r ]*)([\S\s]*?)([\t\n\f\r ]*)$/ ); return { leadingWhitespace, trailingWhitespace, text, }; }; const hasHtmlWhitespace = (string) => /[\t\n\f\r ]/.test(string); function arrayToMap(array) { const map = Object.create(null); for (const value of array) { map[value] = true; } return map; } function mapObject(object, fn) { const newObject = Object.create(null); for (const key of Object.keys(object)) { newObject[key] = fn(object[key], key); } return newObject; } function shouldPreserveContent(node, options) { // unterminated node in ie conditional comment // e.g. if ( node.type === "ieConditionalComment" && node.lastChild && !node.lastChild.isSelfClosing && !node.lastChild.endSourceSpan ) { return true; } // incomplete html in ie conditional comment // e.g. if (node.type === "ieConditionalComment" && !node.complete) { return true; } // TODO: handle non-text children in
  if (
    isPreLikeNode(node) &&
    node.children.some(
      (child) => child.type !== "text" && child.type !== "interpolation"
    )
  ) {
    return true;
  }

  if (
    isVueNonHtmlBlock(node, options) &&
    !isScriptLikeTag(node) &&
    node.type !== "interpolation"
  ) {
    return true;
  }

  return false;
}

function hasPrettierIgnore(node) {
  if (node.type === "attribute") {
    return false;
  }

  if (!node.parent) {
    return false;
  }

  if (typeof node.index !== "number" || node.index === 0) {
    return false;
  }

  const prevNode = node.parent.children[node.index - 1];
  return isPrettierIgnore(prevNode);
}

function isPrettierIgnore(node) {
  return node.type === "comment" && node.value.trim() === "prettier-ignore";
}

function getPrettierIgnoreAttributeCommentData(value) {
  const match = value.trim().match(/^prettier-ignore-attribute(?:\s+([^]+))?$/);

  if (!match) {
    return false;
  }

  if (!match[1]) {
    return true;
  }

  return match[1].split(/\s+/);
}

/** there's no opening/closing tag or it's considered not breakable */
function isTextLikeNode(node) {
  return node.type === "text" || node.type === "comment";
}

function isScriptLikeTag(node) {
  return (
    node.type === "element" &&
    (node.fullName === "script" ||
      node.fullName === "style" ||
      node.fullName === "svg:style" ||
      (isUnknownNamespace(node) &&
        (node.name === "script" || node.name === "style")))
  );
}

function canHaveInterpolation(node) {
  return node.children && !isScriptLikeTag(node);
}

function isWhitespaceSensitiveNode(node) {
  return (
    isScriptLikeTag(node) ||
    node.type === "interpolation" ||
    isIndentationSensitiveNode(node)
  );
}

function isIndentationSensitiveNode(node) {
  return getNodeCssStyleWhiteSpace(node).startsWith("pre");
}

function isLeadingSpaceSensitiveNode(node, options) {
  const isLeadingSpaceSensitive = _isLeadingSpaceSensitiveNode();

  if (
    isLeadingSpaceSensitive &&
    !node.prev &&
    node.parent &&
    node.parent.tagDefinition &&
    node.parent.tagDefinition.ignoreFirstLf
  ) {
    return node.type === "interpolation";
  }

  return isLeadingSpaceSensitive;

  function _isLeadingSpaceSensitiveNode() {
    if (isFrontMatterNode(node)) {
      return false;
    }

    if (
      (node.type === "text" || node.type === "interpolation") &&
      node.prev &&
      (node.prev.type === "text" || node.prev.type === "interpolation")
    ) {
      return true;
    }

    if (!node.parent || node.parent.cssDisplay === "none") {
      return false;
    }

    if (isPreLikeNode(node.parent)) {
      return true;
    }

    if (
      !node.prev &&
      (node.parent.type === "root" ||
        (isPreLikeNode(node) && node.parent) ||
        isScriptLikeTag(node.parent) ||
        isVueCustomBlock(node.parent, options) ||
        !isFirstChildLeadingSpaceSensitiveCssDisplay(node.parent.cssDisplay))
    ) {
      return false;
    }

    if (
      node.prev &&
      !isNextLeadingSpaceSensitiveCssDisplay(node.prev.cssDisplay)
    ) {
      return false;
    }

    return true;
  }
}

function isTrailingSpaceSensitiveNode(node, options) {
  if (isFrontMatterNode(node)) {
    return false;
  }

  if (
    (node.type === "text" || node.type === "interpolation") &&
    node.next &&
    (node.next.type === "text" || node.next.type === "interpolation")
  ) {
    return true;
  }

  if (!node.parent || node.parent.cssDisplay === "none") {
    return false;
  }

  if (isPreLikeNode(node.parent)) {
    return true;
  }

  if (
    !node.next &&
    (node.parent.type === "root" ||
      (isPreLikeNode(node) && node.parent) ||
      isScriptLikeTag(node.parent) ||
      isVueCustomBlock(node.parent, options) ||
      !isLastChildTrailingSpaceSensitiveCssDisplay(node.parent.cssDisplay))
  ) {
    return false;
  }

  if (
    node.next &&
    !isPrevTrailingSpaceSensitiveCssDisplay(node.next.cssDisplay)
  ) {
    return false;
  }

  return true;
}

function isDanglingSpaceSensitiveNode(node) {
  return (
    isDanglingSpaceSensitiveCssDisplay(node.cssDisplay) &&
    !isScriptLikeTag(node)
  );
}

function forceNextEmptyLine(node) {
  return (
    isFrontMatterNode(node) ||
    (node.next &&
      node.sourceSpan.end.line + 1 < node.next.sourceSpan.start.line)
  );
}

/** firstChild leadingSpaces and lastChild trailingSpaces */
function forceBreakContent(node) {
  return (
    forceBreakChildren(node) ||
    (node.type === "element" &&
      node.children.length !== 0 &&
      (["body", "script", "style"].includes(node.name) ||
        node.children.some((child) => hasNonTextChild(child)))) ||
    (node.firstChild &&
      node.firstChild === node.lastChild &&
      hasLeadingLineBreak(node.firstChild) &&
      (!node.lastChild.isTrailingSpaceSensitive ||
        hasTrailingLineBreak(node.lastChild)))
  );
}

/** spaces between children */
function forceBreakChildren(node) {
  return (
    node.type === "element" &&
    node.children.length !== 0 &&
    (["html", "head", "ul", "ol", "select"].includes(node.name) ||
      (node.cssDisplay.startsWith("table") && node.cssDisplay !== "table-cell"))
  );
}

function preferHardlineAsLeadingSpaces(node) {
  return (
    preferHardlineAsSurroundingSpaces(node) ||
    (node.prev && preferHardlineAsTrailingSpaces(node.prev)) ||
    hasSurroundingLineBreak(node)
  );
}

function preferHardlineAsTrailingSpaces(node) {
  return (
    preferHardlineAsSurroundingSpaces(node) ||
    (node.type === "element" && node.fullName === "br") ||
    hasSurroundingLineBreak(node)
  );
}

function hasSurroundingLineBreak(node) {
  return hasLeadingLineBreak(node) && hasTrailingLineBreak(node);
}

function hasLeadingLineBreak(node) {
  return (
    node.hasLeadingSpaces &&
    (node.prev
      ? node.prev.sourceSpan.end.line < node.sourceSpan.start.line
      : node.parent.type === "root" ||
        node.parent.startSourceSpan.end.line < node.sourceSpan.start.line)
  );
}

function hasTrailingLineBreak(node) {
  return (
    node.hasTrailingSpaces &&
    (node.next
      ? node.next.sourceSpan.start.line > node.sourceSpan.end.line
      : node.parent.type === "root" ||
        (node.parent.endSourceSpan &&
          node.parent.endSourceSpan.start.line > node.sourceSpan.end.line))
  );
}

function preferHardlineAsSurroundingSpaces(node) {
  switch (node.type) {
    case "ieConditionalComment":
    case "comment":
    case "directive":
      return true;
    case "element":
      return ["script", "select"].includes(node.name);
  }
  return false;
}

function getLastDescendant(node) {
  return node.lastChild ? getLastDescendant(node.lastChild) : node;
}

function hasNonTextChild(node) {
  return node.children && node.children.some((child) => child.type !== "text");
}

function _inferScriptParser(node) {
  const { type, lang } = node.attrMap;
  if (
    type === "module" ||
    type === "text/javascript" ||
    type === "text/babel" ||
    type === "application/javascript" ||
    lang === "jsx"
  ) {
    return "babel";
  }

  if (type === "application/x-typescript" || lang === "ts" || lang === "tsx") {
    return "typescript";
  }

  if (type === "text/markdown") {
    return "markdown";
  }

  if (type === "text/html") {
    return "html";
  }

  if (type && (type.endsWith("json") || type.endsWith("importmap"))) {
    return "json";
  }

  if (type === "text/x-handlebars-template") {
    return "glimmer";
  }
}

function inferStyleParser(node) {
  const { lang } = node.attrMap;
  if (lang === "postcss" || lang === "css") {
    return "css";
  }

  if (lang === "scss") {
    return "scss";
  }

  if (lang === "less") {
    return "less";
  }
}

function inferScriptParser(node, options) {
  if (node.name === "script" && !node.attrMap.src) {
    if (!node.attrMap.lang && !node.attrMap.type) {
      return "babel";
    }
    return _inferScriptParser(node);
  }

  if (node.name === "style") {
    return inferStyleParser(node) || "css";
  }

  if (options && isVueNonHtmlBlock(node, options)) {
    return (
      _inferScriptParser(node) ||
      inferStyleParser(node) ||
      getParserName(node.attrMap.lang, options)
    );
  }

  return null;
}

function isBlockLikeCssDisplay(cssDisplay) {
  return (
    cssDisplay === "block" ||
    cssDisplay === "list-item" ||
    cssDisplay.startsWith("table")
  );
}

function isFirstChildLeadingSpaceSensitiveCssDisplay(cssDisplay) {
  return !isBlockLikeCssDisplay(cssDisplay) && cssDisplay !== "inline-block";
}

function isLastChildTrailingSpaceSensitiveCssDisplay(cssDisplay) {
  return !isBlockLikeCssDisplay(cssDisplay) && cssDisplay !== "inline-block";
}

function isPrevTrailingSpaceSensitiveCssDisplay(cssDisplay) {
  return !isBlockLikeCssDisplay(cssDisplay);
}

function isNextLeadingSpaceSensitiveCssDisplay(cssDisplay) {
  return !isBlockLikeCssDisplay(cssDisplay);
}

function isDanglingSpaceSensitiveCssDisplay(cssDisplay) {
  return !isBlockLikeCssDisplay(cssDisplay) && cssDisplay !== "inline-block";
}

function isPreLikeNode(node) {
  return getNodeCssStyleWhiteSpace(node).startsWith("pre");
}

function countParents(path, predicate = () => true) {
  let counter = 0;
  for (let i = path.stack.length - 1; i >= 0; i--) {
    const value = path.stack[i];
    if (
      value &&
      typeof value === "object" &&
      !Array.isArray(value) &&
      predicate(value)
    ) {
      counter++;
    }
  }
  return counter;
}

function hasParent(node, fn) {
  let current = node;

  while (current) {
    if (fn(current)) {
      return true;
    }

    current = current.parent;
  }

  return false;
}

function getNodeCssStyleDisplay(node, options) {
  if (node.prev && node.prev.type === "comment") {
    // 
    const match = node.prev.value.match(/^\s*display:\s*([a-z]+)\s*$/);
    if (match) {
      return match[1];
    }
  }

  let isInSvgForeignObject = false;
  if (node.type === "element" && node.namespace === "svg") {
    if (hasParent(node, (parent) => parent.fullName === "svg:foreignObject")) {
      isInSvgForeignObject = true;
    } else {
      return node.name === "svg" ? "inline-block" : "block";
    }
  }

  switch (options.htmlWhitespaceSensitivity) {
    case "strict":
      return "inline";
    case "ignore":
      return "block";
    default: {
      // See https://github.com/prettier/prettier/issues/8151
      if (
        options.parser === "vue" &&
        node.parent &&
        node.parent.type === "root"
      ) {
        return "block";
      }
      return (
        (node.type === "element" &&
          (!node.namespace ||
            isInSvgForeignObject ||
            isUnknownNamespace(node)) &&
          CSS_DISPLAY_TAGS[node.name]) ||
        CSS_DISPLAY_DEFAULT
      );
    }
  }
}

function isUnknownNamespace(node) {
  return (
    node.type === "element" &&
    !node.hasExplicitNamespace &&
    !["html", "svg"].includes(node.namespace)
  );
}

function getNodeCssStyleWhiteSpace(node) {
  return (
    (node.type === "element" &&
      (!node.namespace || isUnknownNamespace(node)) &&
      CSS_WHITE_SPACE_TAGS[node.name]) ||
    CSS_WHITE_SPACE_DEFAULT
  );
}

function getMinIndentation(text) {
  let minIndentation = Infinity;

  for (const lineText of text.split("\n")) {
    if (lineText.length === 0) {
      continue;
    }

    if (!HTML_WHITESPACE.has(lineText[0])) {
      return 0;
    }

    const indentation = getLeadingHtmlWhitespace(lineText).length;

    if (lineText.length === indentation) {
      continue;
    }

    if (indentation < minIndentation) {
      minIndentation = indentation;
    }
  }

  return minIndentation === Infinity ? 0 : minIndentation;
}

function dedentString(text, minIndent = getMinIndentation(text)) {
  return minIndent === 0
    ? text
    : text
        .split("\n")
        .map((lineText) => lineText.slice(minIndent))
        .join("\n");
}

function identity(x) {
  return x;
}

function shouldNotPrintClosingTag(node, options) {
  return (
    !node.isSelfClosing &&
    !node.endSourceSpan &&
    (hasPrettierIgnore(node) || shouldPreserveContent(node.parent, options))
  );
}

function countChars(text, char) {
  let counter = 0;
  for (let i = 0; i < text.length; i++) {
    if (text[i] === char) {
      counter++;
    }
  }
  return counter;
}

function unescapeQuoteEntities(text) {
  return text.replace(/'/g, "'").replace(/"/g, '"');
}

// top-level elements (excluding