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

  switch (node.type) {
    case "Block":
    case "Program":
    case "Template":
      return group(path.map(print, "body"));

    case "ElementNode": {
      const startingTag = group(printStartingTag(path, print));

      const escapeNextElementNode =
        options.htmlWhitespaceSensitivity === "ignore" &&
        path.next?.type === "ElementNode"
          ? softline
          : "";

      if (isVoidElement(node)) {
        return [startingTag, escapeNextElementNode];
      }

      const endingTag = ["</", node.tag, ">"];

      if (node.children.length === 0) {
        return [startingTag, indent(endingTag), escapeNextElementNode];
      }

      if (options.htmlWhitespaceSensitivity === "ignore") {
        return [
          startingTag,
          indent(printChildren(path, options, print)),
          hardline,
          indent(endingTag),
          escapeNextElementNode,
        ];
      }

      return [
        startingTag,
        indent(group(printChildren(path, options, print))),
        indent(endingTag),
        escapeNextElementNode,
      ];
    }

    case "BlockStatement":
      if (isElseIfLike(path)) {
        return [
          printElseIfLikeBlock(path, print),
          printProgram(path, options, print),
          printInverse(path, options, print),
        ];
      }

      return [
        printOpenBlock(path, print),
        group([
          printProgram(path, options, print),
          printInverse(path, options, print),
          printCloseBlock(path, options, print),
        ]),
      ];

    case "ElementModifierStatement":
      return group(["{{", printPathAndParams(path, print), "}}"]);

    case "MustacheStatement":
      return group([
        printOpeningMustache(node),
        printPathAndParams(path, print),
        printClosingMustache(node),
      ]);

    case "SubExpression":
      return group([
        "(",
        printSubExpressionPathAndParams(path, print),
        softline,
        ")",
      ]);

    case "AttrNode": {
      const { name, value } = node;
      const isText = value.type === "TextNode";
      const isEmptyText = isText && value.chars === "";

      // If the text is empty and the value's loc start and end offsets are the
      // same, there is no value for this AttrNode and it should be printed
      // without the `=""`. Example: `<img data-test>` -> `<img data-test>`
      if (isEmptyText && locStart(value) === locEnd(value)) {
        return name;
      }

      // Let's assume quotes inside the content of text nodes are already
      // properly escaped with entities, otherwise the parse wouldn't have parsed them.
      const quote = isText
        ? getPreferredQuote(value.chars, options.singleQuote)
        : value.type === "ConcatStatement"
          ? getPreferredQuote(
              value.parts
                .map((part) => (part.type === "TextNode" ? part.chars : ""))
                .join(""),
              options.singleQuote,
            )
          : "";

      const valueDoc = print("value");

      return [
        name,
        "=",
        quote,
        name === "class" && quote ? group(indent(valueDoc)) : valueDoc,
        quote,
      ];
    }

    case "ConcatStatement":
      return path.map(print, "parts");

    case "Hash":
      return join(line, path.map(print, "pairs"));

    case "HashPair":
      return [node.key, "=", print("value")];

    case "TextNode": {
      // Don't format content:
      // 1. in `<pre>`,
      // 2. in `<style>`

      if (path.parent.tag === "pre" || path.parent.tag === "style") {
        return node.chars;
      }

      /* if `{{my-component}}` (or any text containing "{{")
       * makes it to the TextNode, it means it was escaped,
       * so let's print it escaped, ie.; `\{{my-component}}` */
      let text = node.chars.replaceAll("{{", String.raw`\{{`);

      const attrName = getCurrentAttributeName(path);

      if (attrName) {
        // TODO: format style and srcset attributes
        if (attrName === "class") {
          const formattedClasses = text.trim().split(/\s+/u).join(" ");

          let leadingSpace = false;
          let trailingSpace = false;

          if (path.parent.type === "ConcatStatement") {
            if (
              path.previous?.type === "MustacheStatement" &&
              /^\s/u.test(text)
            ) {
              leadingSpace = true;
            }
            if (
              path.next?.type === "MustacheStatement" &&
              /\s$/u.test(text) &&
              formattedClasses !== ""
            ) {
              trailingSpace = true;
            }
          }

          return [
            leadingSpace ? line : "",
            formattedClasses,
            trailingSpace ? line : "",
          ];
        }

        return replaceEndOfLine(text);
      }

      const isWhitespaceOnly = htmlWhitespaceUtils.isWhitespaceOnly(text);
      const { isFirst, isLast } = path;

      if (options.htmlWhitespaceSensitivity !== "ignore") {
        // let's remove the file's final newline
        // https://github.com/ember-cli/ember-new-output/blob/1a04c67ddd02ccb35e0ff41bb5cbce34b31173ef/.editorconfig#L16
        const shouldTrimTrailingNewlines =
          isLast && path.parent.type === "Template";
        const shouldTrimLeadingNewlines =
          isFirst && path.parent.type === "Template";

        if (isWhitespaceOnly) {
          if (shouldTrimLeadingNewlines || shouldTrimTrailingNewlines) {
            return "";
          }

          let breaks = [line];

          const newlines = countNewLines(text);
          if (newlines) {
            breaks = generateHardlines(newlines);
          }

          if (isLast) {
            breaks = breaks.map((newline) => dedent(newline));
          }

          return breaks;
        }

        const leadingWhitespace =
          htmlWhitespaceUtils.getLeadingWhitespace(text);

        let leadBreaks = [];
        if (leadingWhitespace) {
          leadBreaks = [line];

          const leadingNewlines = countNewLines(leadingWhitespace);
          if (leadingNewlines) {
            leadBreaks = generateHardlines(leadingNewlines);
          }

          text = text.slice(leadingWhitespace.length);
        }

        const tailingWhitespace =
          htmlWhitespaceUtils.getTrailingWhitespace(text);
        let trailBreaks = [];
        if (tailingWhitespace) {
          if (!shouldTrimTrailingNewlines) {
            trailBreaks = [line];

            const trailingNewlines = countNewLines(tailingWhitespace);
            if (trailingNewlines) {
              trailBreaks = generateHardlines(trailingNewlines);
            }

            if (isLast) {
              trailBreaks = trailBreaks.map((hardline) => dedent(hardline));
            }
          }

          text = text.slice(0, -tailingWhitespace.length);
        }

        return [...leadBreaks, fill(getTextValueParts(text)), ...trailBreaks];
      }

      const lineBreaksCount = countNewLines(text);

      let leadingLineBreaksCount = countLeadingNewLines(text);
      let trailingLineBreaksCount = countTrailingNewLines(text);

      if (
        (isFirst || isLast) &&
        isWhitespaceOnly &&
        (path.parent.type === "Block" ||
          path.parent.type === "ElementNode" ||
          path.parent.type === "Template")
      ) {
        return "";
      }

      if (isWhitespaceOnly && lineBreaksCount) {
        leadingLineBreaksCount = Math.min(
          lineBreaksCount,
          NEWLINES_TO_PRESERVE_MAX,
        );
        trailingLineBreaksCount = 0;
      } else {
        if (
          path.next?.type === "BlockStatement" ||
          path.next?.type === "ElementNode"
        ) {
          trailingLineBreaksCount = Math.max(trailingLineBreaksCount, 1);
        }

        if (
          path.previous?.type === "BlockStatement" ||
          path.previous?.type === "ElementNode"
        ) {
          leadingLineBreaksCount = Math.max(leadingLineBreaksCount, 1);
        }
      }

      let leadingSpace = "";
      let trailingSpace = "";

      if (
        trailingLineBreaksCount === 0 &&
        path.next?.type === "MustacheStatement"
      ) {
        trailingSpace = " ";
      }

      if (
        leadingLineBreaksCount === 0 &&
        path.previous?.type === "MustacheStatement"
      ) {
        leadingSpace = " ";
      }

      if (isFirst) {
        leadingLineBreaksCount = 0;
        leadingSpace = "";
      }

      if (isLast) {
        trailingLineBreaksCount = 0;
        trailingSpace = "";
      }

      if (htmlWhitespaceUtils.hasLeadingWhitespace(text)) {
        text = leadingSpace + htmlWhitespaceUtils.trimStart(text);
      }

      if (htmlWhitespaceUtils.hasTrailingWhitespace(text)) {
        text = htmlWhitespaceUtils.trimEnd(text) + trailingSpace;
      }

      return [
        ...generateHardlines(leadingLineBreaksCount),
        fill(getTextValueParts(text)),
        ...generateHardlines(trailingLineBreaksCount),
      ];
    }
    case "MustacheCommentStatement": {
      const start = locStart(node);
      const end = locEnd(node);
      // Starts with `{{~`
      const isLeftWhiteSpaceSensitive =
        options.originalText.charAt(start + 2) === "~";
      // Ends with `{{~`
      const isRightWhitespaceSensitive =
        options.originalText.charAt(end - 3) === "~";

      const dashes = node.value.includes("}}") ? "--" : "";
      return [
        "{{",
        isLeftWhiteSpaceSensitive ? "~" : "",
        "!",
        dashes,
        node.value,
        dashes,
        isRightWhitespaceSensitive ? "~" : "",
        "}}",
      ];
    }
    case "PathExpression":
      return printPathExpression(node);

    case "BooleanLiteral":
      return String(node.value);

    case "CommentStatement":
      return ["<!--", node.value, "-->"];

    case "StringLiteral":
      return printStringLiteral(path, options);

    case "NumberLiteral":
      return String(node.value);

    case "UndefinedLiteral":
      return "undefined";

    case "NullLiteral":
      return "null";

    case "AtHead": // Handled in `printPathExpression`
    case "VarHead": // Handled in `printPathExpression`
    case "ThisHead": // Handled in `printPathExpression`
    default:
      /* c8 ignore next */
      throw new UnexpectedNodeError(node, "Handlebars");
  }
}
@import {Doc} from "../document/builders.js"