src/language-yaml/printer-yaml.js/printNode
function printNode(node, parentNode, path, options, print) {
  switch (node.type) {
    case "root": {
      const { children } = node;
      const parts = [];
      path.each((childPath, index) => {
        const document = children[index];
        const nextDocument = children[index + 1];
        if (index !== 0) {
          parts.push(hardline);
        }
        parts.push(print());
        if (shouldPrintDocumentEndMarker(document, nextDocument)) {
          parts.push(hardline, "...");
          if (hasTrailingComment(document)) {
            parts.push(" ", print("trailingComment"));
          }
        } else if (nextDocument && !hasTrailingComment(nextDocument.head)) {
          parts.push(hardline, "---");
        }
      }, "children");

      const lastDescendantNode = getLastDescendantNode(node);
      if (
        !isNode(lastDescendantNode, ["blockLiteral", "blockFolded"]) ||
        lastDescendantNode.chomping !== "keep"
      ) {
        parts.push(hardline);
      }
      return parts;
    }
    case "document": {
      const nextDocument = parentNode.children[path.getName() + 1];
      const parts = [];
      if (
        shouldPrintDocumentHeadEndMarker(
          node,
          nextDocument,
          parentNode,
          options
        ) === "head"
      ) {
        if (node.head.children.length > 0 || node.head.endComments.length > 0) {
          parts.push(print("head"));
        }

        if (hasTrailingComment(node.head)) {
          parts.push(["---", " ", print(["head", "trailingComment"])]);
        } else {
          parts.push("---");
        }
      }

      if (shouldPrintDocumentBody(node)) {
        parts.push(print("body"));
      }

      return join(hardline, parts);
    }
    case "documentHead":
      return join(hardline, [
        ...path.map(print, "children"),
        ...path.map(print, "endComments"),
      ]);
    case "documentBody": {
      const { children, endComments } = node;
      /** @type {Doc} */
      let separator = "";
      if (children.length > 0 && endComments.length > 0) {
        const lastDescendantNode = getLastDescendantNode(node);
        // there's already a newline printed at the end of blockValue (chomping=keep, lastDescendant=true)
        if (isNode(lastDescendantNode, ["blockFolded", "blockLiteral"])) {
          // an extra newline for better readability
          if (lastDescendantNode.chomping !== "keep") {
            separator = [hardline, hardline];
          }
        } else {
          separator = hardline;
        }
      }

      return [
        join(hardline, path.map(print, "children")),
        separator,
        join(hardline, path.map(print, "endComments")),
      ];
    }
    case "directive":
      return ["%", join(" ", [node.name, ...node.parameters])];
    case "comment":
      return ["#", node.value];
    case "alias":
      return ["*", node.value];
    case "tag":
      return options.originalText.slice(
        node.position.start.offset,
        node.position.end.offset
      );
    case "anchor":
      return ["&", node.value];
    case "plain":
      return printFlowScalarContent(
        node.type,
        options.originalText.slice(
          node.position.start.offset,
          node.position.end.offset
        ),
        options
      );
    case "quoteDouble":
    case "quoteSingle": {
      const singleQuote = "'";
      const doubleQuote = '"';

      const raw = options.originalText.slice(
        node.position.start.offset + 1,
        node.position.end.offset - 1
      );

      if (
        (node.type === "quoteSingle" && raw.includes("\\")) ||
        (node.type === "quoteDouble" && /\\[^"]/.test(raw))
      ) {
        // only quoteDouble can use escape chars
        // and quoteSingle do not need to escape backslashes
        const originalQuote =
          node.type === "quoteDouble" ? doubleQuote : singleQuote;
        return [
          originalQuote,
          printFlowScalarContent(node.type, raw, options),
          originalQuote,
        ];
      }

      if (raw.includes(doubleQuote)) {
        return [
          singleQuote,
          printFlowScalarContent(
            node.type,
            node.type === "quoteDouble"
              ? raw
                  // double quote needs to be escaped by backslash in quoteDouble
                  .replace(/\\"/g, doubleQuote)
                  .replace(/'/g, singleQuote.repeat(2))
              : raw,
            options
          ),
          singleQuote,
        ];
      }

      if (raw.includes(singleQuote)) {
        return [
          doubleQuote,
          printFlowScalarContent(
            node.type,
            node.type === "quoteSingle"
              ? // single quote needs to be escaped by 2 single quotes in quoteSingle
                raw.replace(/''/g, singleQuote)
              : raw,
            options
          ),
          doubleQuote,
        ];
      }

      const quote = options.singleQuote ? singleQuote : doubleQuote;
      return [quote, printFlowScalarContent(node.type, raw, options), quote];
    }
    case "blockFolded":
    case "blockLiteral": {
      return printBlock(path, print, options);
    }
    case "mapping":
    case "sequence":
      return join(hardline, path.map(print, "children"));
    case "sequenceItem":
      return ["- ", alignWithSpaces(2, node.content ? print("content") : "")];
    case "mappingKey":
    case "mappingValue":
      return !node.content ? "" : print("content");
    case "mappingItem":
    case "flowMappingItem": {
      return printMappingItem(node, parentNode, path, print, options);
    }
    case "flowMapping":
      return printFlowMapping(path, print, options);
    case "flowSequence":
      return printFlowSequence(path, print, options);
    case "flowSequenceItem":
      return print("content");
    // istanbul ignore next
    default:
      throw new Error(`Unexpected node type ${node.type}`);
  }
}