Printer.java

package org.jsoup.nodes;

import org.jsoup.internal.QuietAppendable;
import org.jsoup.internal.StringUtil;
import org.jsoup.nodes.Document.OutputSettings;
import org.jsoup.parser.Tag;
import org.jsoup.select.NodeVisitor;
import org.jspecify.annotations.Nullable;

/** Base Printer */
class Printer implements NodeVisitor {
    final Node root;
    final QuietAppendable accum;
    final OutputSettings settings;

    Printer(Node root, QuietAppendable accum, OutputSettings settings) {
        this.root = root;
        this.accum = accum;
        this.settings = settings;
    }

    void addHead(Element el, int depth) {
        el.outerHtmlHead(accum, settings);
    }

    void addTail(Element el, int depth) {
        el.outerHtmlTail(accum, settings);
    }

    void addText(TextNode textNode, int textOptions, int depth) {
        int options = Entities.ForText | textOptions;
        Entities.escape(accum, textNode.coreValue(), settings, options);
    }

    void addNode(LeafNode node, int depth) {
        node.outerHtmlHead(accum, settings);
    }

    void indent(int depth) {
        accum.append('\n').append(StringUtil.padding(depth * settings.indentAmount(), settings.maxPaddingWidth()));
    }

    @Override
    public void head(Node node, int depth) {
        if (node.getClass() == TextNode.class)  addText((TextNode) node, 0, depth); // Excludes CData; falls to addNode
        else if (node instanceof Element)       addHead((Element) node, depth);
        else                                    addNode((LeafNode) node, depth);
    }

    @Override
    public void tail(Node node, int depth) {
        if (node instanceof Element) { // otherwise a LeafNode
            addTail((Element) node, depth);
        }
    }

    /** Pretty Printer */
    static class Pretty extends Printer {
        boolean preserveWhitespace = false;

        Pretty(Node root, QuietAppendable accum, OutputSettings settings) {
            super(root, accum, settings);

            // check if there is a pre on stack
            for (Node node = root; node != null; node = node.parentNode()) {
                if (tagIs(Tag.PreserveWhitespace, node)) {
                    preserveWhitespace = true;
                    break;
                }
            }
        }

        @Override
        void addHead(Element el, int depth) {
            if (shouldIndent(el))
                indent(depth);
            super.addHead(el, depth);
            if (tagIs(Tag.PreserveWhitespace, el)) preserveWhitespace = true;
        }

        @Override
        void addTail(Element el, int depth) {
            if (shouldIndent(nextNonBlank(el.firstChild()))) {
                indent(depth);
            }
            super.addTail(el, depth);

            // clear the preserveWhitespace if this element is not, and there are none on the stack above
            if (preserveWhitespace && el.tag.is(Tag.PreserveWhitespace)) {
                for (Element parent = el.parent(); parent != null; parent = parent.parent()) {
                    if (parent.tag().preserveWhitespace()) return; // keep
                }
                preserveWhitespace = false;
            }
        }

        @Override
        void addNode(LeafNode node, int depth) {
            if (shouldIndent(node))
                indent(depth);
            super.addNode(node, depth);
        }

        @Override
        void addText(TextNode node, int textOptions, int depth) {
            if (!preserveWhitespace) {
                textOptions |= Entities.Normalise;
                textOptions = textTrim(node, textOptions);

                if (!node.isBlank() && isBlockEl(node.parentNode) && shouldIndent(node))
                    indent(depth);
            }

            super.addText(node, textOptions, depth);
        }

        int textTrim(TextNode node, int options) {
            if (!isBlockEl(node.parentNode)) return options; // don't trim inline, whitespace significant
            Node prev = node.previousSibling();
            Node next = node.nextSibling();

            // if previous is not an inline element
            if (!(prev instanceof Element && !isBlockEl(prev))) {
                // if there is no previous sib; or not a text node and should be indented
                if (prev == null || !(prev instanceof TextNode) && shouldIndent(prev))
                    options |= Entities.TrimLeading;
            }

            if (next == null || !(next instanceof TextNode) && shouldIndent(next)) {
                options |= Entities.TrimTrailing;
            } else { // trim trailing whitespace if the next non-empty TextNode has leading whitespace
                next = nextNonBlank(next);
                if (next instanceof TextNode && StringUtil.isWhitespace(next.nodeValue().codePointAt(0)))
                    options |= Entities.TrimTrailing;
            }

            return options;
        }

        boolean shouldIndent(@Nullable Node node) {
            if (node == null || node == root || preserveWhitespace || isBlankText(node))
                return false;
            if (isBlockEl(node))
                return true;

            Node prevSib = previousNonblank(node);
            if (isBlockEl(prevSib)) return true;

            Element parent = node.parentNode;
            if (!isBlockEl(parent) || parent.tag().is(Tag.InlineContainer) || !hasNonTextNodes(parent))
                return false;

            return prevSib == null ||
                (!(prevSib instanceof TextNode) &&
                    (isBlockEl(prevSib) || !(prevSib instanceof Element)));
        }

        boolean isBlockEl(@Nullable Node node) {
            if (node == null) return false;
            if (node instanceof Element) {
                Element el = (Element) node;
                return el.isBlock() ||
                    (!el.tag.isKnownTag() && (el.parentNode instanceof Document || hasChildBlocks(el)));
            }

            return false;
        }

        /**
         Returns true if any of the Element's child nodes should indent. Checks the last 5 nodes only (to minimize
         scans).
         */
        static boolean hasChildBlocks(Element el) {
            Element child = el.firstElementChild();
            for (int i = 0; i < maxScan && child != null; i++) {
                if (child.isBlock() || !child.tag.isKnownTag()) return true;
                child = child.nextElementSibling();
            }
            return false;
        }
        static private final int maxScan = 5;

        static boolean hasNonTextNodes(Element el) {
            Node child = el.firstChild();
            for (int i = 0; i < maxScan && child != null; i++) {
                if (!(child instanceof TextNode)) return true;
                child = child.nextSibling();
            }
            return false;
        }

        static @Nullable Node previousNonblank(Node node) {
            Node prev = node.previousSibling();
            while (isBlankText(prev)) prev = prev.previousSibling();
            return prev;
        }

        static @Nullable Node nextNonBlank(@Nullable Node node) {
            while (isBlankText(node)) node = node.nextSibling();
            return node;
        }

        static boolean isBlankText(@Nullable Node node) {
            return node instanceof TextNode && ((TextNode) node).isBlank();
        }

        static boolean tagIs(int option, @Nullable Node node) {
            return node instanceof Element && ((Element) node).tag.is(option);
        }
    }

    /** Outline Printer */
    static class Outline extends Pretty {
        Outline(Node root, QuietAppendable accum, OutputSettings settings) {
            super(root, accum, settings);
        }

        @Override
        boolean isBlockEl(@Nullable Node node) {
            return node != null;
        }

        @Override
        boolean shouldIndent(@Nullable Node node) {
            if (node == null || node == root || preserveWhitespace || isBlankText(node))
                return false;
            if (node instanceof TextNode) {
                return node.previousSibling() != null || node.nextSibling() != null;
            }
            return true;
        }
    }

    static Printer printerFor(Node root, QuietAppendable accum) {
        OutputSettings settings = NodeUtils.outputSettings(root);
        if (settings.outline())     return new Printer.Outline(root, accum, settings);
        if (settings.prettyPrint()) return new Printer.Pretty(root, accum, settings);
        return new Printer(root, accum, settings);
    }
}