function printJsxElementInternal(path, options, print) {
  const { node } = path;

  if (node.type === "JSXElement" && isEmptyJsxElement(node)) {
    return [print("openingElement"), print("closingElement")];
  }

  const openingLines =
    node.type === "JSXElement"
      ? print("openingElement")
      : print("openingFragment");
  const closingLines =
    node.type === "JSXElement"
      ? print("closingElement")
      : print("closingFragment");

  if (
    node.children.length === 1 &&
    node.children[0].type === "JSXExpressionContainer" &&
    (node.children[0].expression.type === "TemplateLiteral" ||
      node.children[0].expression.type === "TaggedTemplateExpression")
  ) {
    return [openingLines, ...path.map(print, "children"), closingLines];
  }

  // Convert `{" "}` to text nodes containing a space.
  // This makes it easy to turn them into `jsxWhitespace` which
  // can then print as either a space or `{" "}` when breaking.
  node.children = node.children.map((child) => {
    if (isJsxWhitespaceExpression(child)) {
      return {
        type: "JSXText",
        value: " ",
        raw: " ",
      };
    }
    return child;
  });

  const containsTag = node.children.some(isJsxElement);
  const containsMultipleExpressions =
    node.children.filter((child) => child.type === "JSXExpressionContainer")
      .length > 1;
  const containsMultipleAttributes =
    node.type === "JSXElement" && node.openingElement.attributes.length > 1;

  // Record any breaks. Should never go from true to false, only false to true.
  let forcedBreak =
    willBreak(openingLines) ||
    containsTag ||
    containsMultipleAttributes ||
    containsMultipleExpressions;

  const isMdxBlock = path.parent.rootMarker === "mdx";

  const rawJsxWhitespace = options.singleQuote ? "{' '}" : '{" "}';
  const jsxWhitespace = isMdxBlock
    ? line
    : ifBreak([rawJsxWhitespace, softline], " ");

  const isFacebookTranslationTag = node.openingElement?.name?.name === "fbt";

  const children = printJsxChildren(
    path,
    options,
    print,
    jsxWhitespace,
    isFacebookTranslationTag,
  );

  const containsText = node.children.some((child) =>
    isMeaningfulJsxText(child),
  );

  // We can end up we multiple whitespace elements with empty string
  // content between them.
  // We need to remove empty whitespace and softlines before JSX whitespace
  // to get the correct output.
  for (let i = children.length - 2; i >= 0; i--) {
    const isPairOfEmptyStrings = children[i] === "" && children[i + 1] === "";
    const isPairOfHardlines =
      children[i] === hardline &&
      children[i + 1] === "" &&
      children[i + 2] === hardline;
    const isLineFollowedByJsxWhitespace =
      (children[i] === softline || children[i] === hardline) &&
      children[i + 1] === "" &&
      children[i + 2] === jsxWhitespace;
    const isJsxWhitespaceFollowedByLine =
      children[i] === jsxWhitespace &&
      children[i + 1] === "" &&
      (children[i + 2] === softline || children[i + 2] === hardline);
    const isDoubleJsxWhitespace =
      children[i] === jsxWhitespace &&
      children[i + 1] === "" &&
      children[i + 2] === jsxWhitespace;
    const isPairOfHardOrSoftLines =
      (children[i] === softline &&
        children[i + 1] === "" &&
        children[i + 2] === hardline) ||
      (children[i] === hardline &&
        children[i + 1] === "" &&
        children[i + 2] === softline);

    if (
      (isPairOfHardlines && containsText) ||
      isPairOfEmptyStrings ||
      isLineFollowedByJsxWhitespace ||
      isDoubleJsxWhitespace ||
      isPairOfHardOrSoftLines
    ) {
      children.splice(i, 2);
    } else if (isJsxWhitespaceFollowedByLine) {
      children.splice(i + 1, 2);
    }
  }

  // Trim trailing lines (or empty strings)
  while (children.length > 0 && isEmptyStringOrAnyLine(children.at(-1))) {
    children.pop();
  }

  // Trim leading lines (or empty strings)
  while (
    children.length > 1 &&
    isEmptyStringOrAnyLine(children[0]) &&
    isEmptyStringOrAnyLine(children[1])
  ) {
    children.shift();
    children.shift();
  }

  /*
   * Tweak how we format children if outputting this element over multiple lines.
   * Also detect whether we will force this element to output over multiple lines.
   *
   * Moreover, we need to ensure that we always have line-like doc at odd index, that is rule of fill().
   * Assuming that parts.length is always odd, satisfying the above can be straightforwardly done by:
   * - if we push line-like doc, we push empty string after it
   * - if we push non-line-like doc, push [parts.pop(), doc] instead
   */
  /** @type {Doc[]} */
  const multilineChildren = [""];
  for (const [i, child] of children.entries()) {
    // There are a number of situations where we need to ensure we display
    // whitespace as `{" "}` when outputting this element over multiple lines.
    if (child === jsxWhitespace) {
      if (i === 1 && isEmptyDoc(children[i - 1])) {
        if (children.length === 2) {
          // Solitary whitespace
          multilineChildren.push([multilineChildren.pop(), rawJsxWhitespace]);
          continue;
        }
        // Leading whitespace
        multilineChildren.push([rawJsxWhitespace, hardline], "");
        continue;
      } else if (i === children.length - 1) {
        // Trailing whitespace
        multilineChildren.push([multilineChildren.pop(), rawJsxWhitespace]);
        continue;
      } else if (children[i - 1] === "" && children[i - 2] === hardline) {
        // Whitespace after line break
        multilineChildren.push([multilineChildren.pop(), rawJsxWhitespace]);
        continue;
      }
    }

    // Note that children always satisfy the rule of fill() content.
    // - printJsxChildren always returns valid fill() content
    // - we always remove even number (containing zero) of leading items from children.
    if (i % 2 === 0) {
      // non-line-like
      multilineChildren.push([multilineChildren.pop(), child]);
    } else {
      // line-like
      multilineChildren.push(child, "");
    }

    if (willBreak(child)) {
      forcedBreak = true;
    }
  }

  // If there is text we use `fill` to fit as much onto each line as possible.
  // When there is no text (just tags and expressions) we use `group`
  // to output each on a separate line.
  /** @type {Doc} */
  let content = containsText
    ? fill(multilineChildren)
    : group(multilineChildren, { shouldBreak: true });

  /*
  `printJsxChildren` won't call `print` on `JSXText`, so when the cursorNode,
  nodeBeforeCursor, or nodeAfterCursor is inside, `cursor` won't get printed.
  This logic fixes that:
  */
  if (
    options.cursorNode?.type === "JSXText" &&
    node.children.includes(options.cursorNode)
  ) {
    content = [cursor, content, cursor];
  } else if (
    options.nodeBeforeCursor?.type === "JSXText" &&
    node.children.includes(options.nodeBeforeCursor)
  ) {
    content = [cursor, content];
  } else if (
    options.nodeAfterCursor?.type === "JSXText" &&
    node.children.includes(options.nodeAfterCursor)
  ) {
    content = [content, cursor];
  }

  if (isMdxBlock) {
    return content;
  }

  const multiLineElem = group([
    openingLines,
    indent([hardline, content]),
    hardline,
    closingLines,
  ]);

  if (forcedBreak) {
    return multiLineElem;
  }

  return conditionalGroup([
    group([openingLines, ...children, closingLines]),
    multiLineElem,
  ]);
}
@import AstPath from "../../common/ast-path.js"
@import {Node, JSXElement} from "../types/estree.js"
@import {Doc} from "../../document/builders.js"