LeafNode.java

package org.jsoup.nodes;

import org.jsoup.helper.Validate;
import org.jsoup.internal.QuietAppendable;
import org.jspecify.annotations.Nullable;

import java.util.List;

/**
 A node that does not hold any children. E.g.: {@link TextNode}, {@link DataNode}, {@link Comment}.
 */
public abstract class LeafNode extends Node {
    Object value; // either a string, tracked string, or attributes object

    public LeafNode() {
        value = "";
    }

    protected LeafNode(String coreValue) {
        Validate.notNull(coreValue);
        value = coreValue;
    }

    @Override protected final boolean hasAttributes() {
        return value instanceof Attributes;
    }

    @Override
    public final Attributes attributes() {
        ensureAttributes();
        return (Attributes) value;
    }

    private void ensureAttributes() {
        if (!hasAttributes()) {
            String coreValue = coreValue();
            Attributes attributes = new Attributes();
            Range.Spans rangeSpans = spans();
            value = attributes;
            attributes.put(nodeName(), coreValue);
            if (rangeSpans != null)
                attributes.putSpans(rangeSpans);
        }
    }

    String coreValue() {
        if (value instanceof Attributes)   return ((Attributes) value).get(nodeName());
        if (value instanceof TrackedValue) return ((TrackedValue) value).coreValue;
        return (String) value;
    }

    @Override @Nullable
    public Element parent() {
        return parentNode;
    }

    @Override
    public String nodeValue() {
        return coreValue();
    }

    void coreValue(String value) {
        if (this.value instanceof Attributes)
            ((Attributes) this.value).put(nodeName(), value);
        else if (this.value instanceof TrackedValue)
            ((TrackedValue) this.value).coreValue = value;
        else
            this.value = value;
    }

    @Override
    public String attr(String key) {
        if (!hasAttributes())
            return nodeName().equals(key) ? coreValue() : EmptyString;
        return super.attr(key);
    }

    @Override
    public Node attr(String key, String value) {
        if (!hasAttributes() && key.equals(nodeName())) {
            coreValue(value);
        } else {
            ensureAttributes();
            super.attr(key, value);
        }
        return this;
    }

    @Override
    public boolean hasAttr(String key) {
        ensureAttributes();
        return super.hasAttr(key);
    }

    @Override
    public Node removeAttr(String key) {
        ensureAttributes();
        return super.removeAttr(key);
    }

    @Override
    public String absUrl(String key) {
        ensureAttributes();
        return super.absUrl(key);
    }

    @Override
    public String baseUri() {
        return parentNode != null ? parentNode.baseUri() : "";
    }

    @Override
    protected void doSetBaseUri(String baseUri) {
        // noop
    }

    @Override
    public int childNodeSize() {
        return 0;
    }

    @Override
    public Node empty() {
        return this;
    }

    @Override
    protected List<Node> ensureChildNodes() {
        return EmptyNodes;
    }

    @Override
    void outerHtmlTail(QuietAppendable accum, Document.OutputSettings out) {}

    @Override
    protected LeafNode doClone(Node parent) {
        LeafNode clone = (LeafNode) super.doClone(parent);

        // Object value could be plain string, tracked string, or attributes - need to clone.
        if (hasAttributes())
            clone.value = ((Attributes) value).clone();
        else if (value instanceof TrackedValue)
            clone.value = ((TrackedValue) value).copy();

        return clone;
    }

    @Override Range.@Nullable Spans spans() {
        if (value instanceof TrackedValue)
            return ((TrackedValue) value).spans;
        return super.spans();
    }

    @Override Range.Spans ensureSpans() {
        // Leaf nodes normally hold just their core string. When source ranges are tracked, keep the string plus spans
        // in a small wrapper so leaf nodes do not expand to Attributes just for parser metadata. If attributes are
        // later requested, ensureAttributes() moves these same spans into the Attributes object.
        if (value instanceof TrackedValue)
            return ((TrackedValue) value).spans;
        if (value instanceof Attributes)
            return ((Attributes) value).ensureSpans();

        TrackedValue trackedValue = new TrackedValue((String) value);
        value = trackedValue;
        return trackedValue.spans;
    }

    /**
     Holds a compact leaf value plus ranges without expanding to Attributes.
     */
    private static final class TrackedValue {
        String coreValue;
        final Range.Spans spans;

        /**
         Creates a tracked leaf value around the core text.
         */
        TrackedValue(String coreValue) {
            this(coreValue, new Range.Spans());
        }

        /**
         Creates a tracked leaf value around copied range spans.
         */
        TrackedValue(String coreValue, Range.Spans spans) {
            this.coreValue = coreValue;
            this.spans = spans;
        }

        /**
         Returns a copy so cloned leaf nodes can mutate range spans independently.
         */
        TrackedValue copy() {
            return new TrackedValue(coreValue, spans.copy());
        }
    }
}