src/language-handlebars/printer-glimmer.js/print
function print(path, options, print) {
  const node = path.getValue();

  /* istanbul ignore if*/
  if (!node) {
    return "";
  }

  if (hasPrettierIgnore(path)) {
    return options.originalText.slice(locStart(node), locEnd(node));
  }

  const favoriteQuote = options.singleQuote ? "'" : '"';

  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" &&
        isNextNodeOfSomeType(path, ["ElementNode"])
          ? softline
          : "";

      if (isVoid(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": {
      const pp = path.getParentNode(1);

      const isElseIf =
        pp &&
        pp.inverse &&
        pp.inverse.body.length === 1 &&
        pp.inverse.body[0] === node &&
        pp.inverse.body[0].path.parts[0] === "if";

      if (isElseIf) {
        return [
          printElseIfBlock(path, print),
          printProgram(path, print, options),
          printInverse(path, print, options),
        ];
      }

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

    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 isText = node.value.type === "TextNode";
      const isEmptyText = isText && node.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(node.value) === locEnd(node.value)) {
        return node.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(node.value.chars, favoriteQuote).quote
        : node.value.type === "ConcatStatement"
        ? getPreferredQuote(
            node.value.parts
              .filter((part) => part.type === "TextNode")
              .map((part) => part.chars)
              .join(""),
            favoriteQuote
          ).quote
        : "";

      const valueDoc = print("value");

      return [
        node.name,
        "=",
        quote,
        node.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": {
      /* 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.replace(/{{/g, "\\{{");

      const attrName = getCurrentAttributeName(path);

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

          let leadingSpace = false;
          let trailingSpace = false;

          if (isParentOfSomeType(path, ["ConcatStatement"])) {
            if (
              isPreviousNodeOfSomeType(path, ["MustacheStatement"]) &&
              /^\s/.test(text)
            ) {
              leadingSpace = true;
            }
            if (
              isNextNodeOfSomeType(path, ["MustacheStatement"]) &&
              /\s$/.test(text) &&
              formattedClasses !== ""
            ) {
              trailingSpace = true;
            }
          }

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

        return replaceTextEndOfLine(text);
      }

      const whitespacesOnlyRE = /^[\t\n\f\r ]*$/;
      const isWhitespaceOnly = whitespacesOnlyRE.test(text);
      const isFirstElement = !getPreviousNode(path);
      const isLastElement = !getNextNode(path);

      if (options.htmlWhitespaceSensitivity !== "ignore") {
        // https://infra.spec.whatwg.org/#ascii-whitespace
        const leadingWhitespacesRE = /^[\t\n\f\r ]*/;
        const trailingWhitespacesRE = /[\t\n\f\r ]*$/;

        // let's remove the file's final newline
        // https://github.com/ember-cli/ember-new-output/blob/1a04c67ddd02ccb35e0ff41bb5cbce34b31173ef/.editorconfig#L16
        const shouldTrimTrailingNewlines =
          isLastElement && isParentOfSomeType(path, ["Template"]);
        const shouldTrimLeadingNewlines =
          isFirstElement && isParentOfSomeType(path, ["Template"]);

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

          let breaks = [line];

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

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

          return breaks;
        }

        const [lead] = text.match(leadingWhitespacesRE);
        const [tail] = text.match(trailingWhitespacesRE);

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

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

          text = text.replace(leadingWhitespacesRE, "");
        }

        let trailBreaks = [];
        if (tail) {
          if (!shouldTrimTrailingNewlines) {
            trailBreaks = [line];

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

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

          text = text.replace(trailingWhitespacesRE, "");
        }

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

      const lineBreaksCount = countNewLines(text);

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

      if (
        (isFirstElement || isLastElement) &&
        isWhitespaceOnly &&
        isParentOfSomeType(path, ["Block", "ElementNode", "Template"])
      ) {
        return "";
      }

      if (isWhitespaceOnly && lineBreaksCount) {
        leadingLineBreaksCount = Math.min(
          lineBreaksCount,
          NEWLINES_TO_PRESERVE_MAX
        );
        trailingLineBreaksCount = 0;
      } else {
        if (isNextNodeOfSomeType(path, ["BlockStatement", "ElementNode"])) {
          trailingLineBreaksCount = Math.max(trailingLineBreaksCount, 1);
        }

        if (isPreviousNodeOfSomeType(path, ["BlockStatement", "ElementNode"])) {
          leadingLineBreaksCount = Math.max(leadingLineBreaksCount, 1);
        }
      }

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

      if (
        trailingLineBreaksCount === 0 &&
        isNextNodeOfSomeType(path, ["MustacheStatement"])
      ) {
        trailingSpace = " ";
      }

      if (
        leadingLineBreaksCount === 0 &&
        isPreviousNodeOfSomeType(path, ["MustacheStatement"])
      ) {
        leadingSpace = " ";
      }

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

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

      text = text
        .replace(/^[\t\n\f\r ]+/g, leadingSpace)
        .replace(/[\t\n\f\r ]+$/, 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 node.original;
    }
    case "BooleanLiteral": {
      return String(node.value);
    }
    case "CommentStatement": {
      return ["<!--", node.value, "-->"];
    }
    case "StringLiteral": {
      if (needsOppositeQuote(path)) {
        const printFavoriteQuote = !options.singleQuote ? "'" : '"';
        return printStringLiteral(node.value, printFavoriteQuote);
      }
      return printStringLiteral(node.value, favoriteQuote);
    }
    case "NumberLiteral": {
      return String(node.value);
    }
    case "UndefinedLiteral": {
      return "undefined";
    }
    case "NullLiteral": {
      return "null";
    }

    /* istanbul ignore next */
    default:
      throw new Error("unknown glimmer type: " + JSON.stringify(node.type));
  }
}