001package org.jsoup.nodes;
002
003import org.jsoup.helper.Validate;
004import org.jsoup.internal.QuietAppendable;
005import org.jspecify.annotations.Nullable;
006
007import java.util.List;
008
009/**
010 A node that does not hold any children. E.g.: {@link TextNode}, {@link DataNode}, {@link Comment}.
011 */
012public abstract class LeafNode extends Node {
013    Object value; // either a string, tracked string, or attributes object
014
015    public LeafNode() {
016        value = "";
017    }
018
019    protected LeafNode(String coreValue) {
020        Validate.notNull(coreValue);
021        value = coreValue;
022    }
023
024    @Override protected final boolean hasAttributes() {
025        return value instanceof Attributes;
026    }
027
028    @Override
029    public final Attributes attributes() {
030        ensureAttributes();
031        return (Attributes) value;
032    }
033
034    /** Expands this leaf's compact value into attribute storage when needed. */
035    final void ensureAttributes() {
036        if (!hasAttributes()) {
037            String coreValue = coreValue();
038            Range.Spans rangeSpans = spans();
039            Attributes attributes = newAttributes(coreValue);
040            value = attributes;
041            if (rangeSpans != null)
042                attributes.putSpans(rangeSpans);
043        }
044    }
045
046    /** Expands this leaf's compact value into attribute storage. */
047    Attributes newAttributes(String coreValue) {
048        Attributes attributes = new Attributes();
049        attributes.put(nodeName(), coreValue);
050        return attributes;
051    }
052
053    String coreValue() {
054        if (value instanceof Attributes)   return ((Attributes) value).get(nodeName());
055        if (value instanceof TrackedValue) return ((TrackedValue) value).coreValue;
056        return (String) value;
057    }
058
059    @Override @Nullable
060    public Element parent() {
061        return parentNode;
062    }
063
064    @Override
065    public String nodeValue() {
066        return coreValue();
067    }
068
069    void coreValue(String value) {
070        if (this.value instanceof Attributes)
071            ((Attributes) this.value).put(nodeName(), value);
072        else if (this.value instanceof TrackedValue)
073            ((TrackedValue) this.value).coreValue = value;
074        else
075            this.value = value;
076    }
077
078    @Override
079    public String attr(String key) {
080        if (!hasAttributes())
081            return nodeName().equals(key) ? coreValue() : EmptyString;
082        return super.attr(key);
083    }
084
085    @Override
086    public Node attr(String key, String value) {
087        if (!hasAttributes() && key.equals(nodeName())) {
088            coreValue(value);
089        } else {
090            ensureAttributes();
091            super.attr(key, value);
092        }
093        return this;
094    }
095
096    @Override
097    public boolean hasAttr(String key) {
098        ensureAttributes();
099        return super.hasAttr(key);
100    }
101
102    @Override
103    public Node removeAttr(String key) {
104        ensureAttributes();
105        return super.removeAttr(key);
106    }
107
108    @Override
109    public String absUrl(String key) {
110        ensureAttributes();
111        return super.absUrl(key);
112    }
113
114    @Override
115    public String baseUri() {
116        return parentNode != null ? parentNode.baseUri() : "";
117    }
118
119    @Override
120    protected void doSetBaseUri(String baseUri) {
121        // noop
122    }
123
124    @Override
125    public int childNodeSize() {
126        return 0;
127    }
128
129    @Override
130    public Node empty() {
131        return this;
132    }
133
134    @Override
135    protected List<Node> ensureChildNodes() {
136        return EmptyNodes;
137    }
138
139    @Override
140    void outerHtmlTail(QuietAppendable accum, Document.OutputSettings out) {}
141
142    @Override
143    protected LeafNode doClone(Node parent) {
144        LeafNode clone = (LeafNode) super.doClone(parent);
145
146        // Object value could be plain string, tracked string, or attributes - need to clone.
147        if (hasAttributes())
148            clone.value = ((Attributes) value).clone();
149        else if (value instanceof TrackedValue)
150            clone.value = ((TrackedValue) value).copy();
151
152        return clone;
153    }
154
155    @Override Range.@Nullable Spans spans() {
156        if (value instanceof TrackedValue)
157            return ((TrackedValue) value).spans;
158        return super.spans();
159    }
160
161    @Override Range.Spans ensureSpans() {
162        // Leaf nodes normally hold just their core string. When source ranges are tracked, keep the string plus spans
163        // in a small wrapper so leaf nodes do not expand to Attributes just for parser metadata. If attributes are
164        // later requested, ensureAttributes() moves these same spans into the Attributes object.
165        if (value instanceof TrackedValue)
166            return ((TrackedValue) value).spans;
167        if (value instanceof Attributes)
168            return ((Attributes) value).ensureSpans();
169
170        TrackedValue trackedValue = new TrackedValue((String) value);
171        value = trackedValue;
172        return trackedValue.spans;
173    }
174
175    /**
176     Holds a compact leaf value plus ranges without expanding to Attributes.
177     */
178    private static final class TrackedValue {
179        String coreValue;
180        final Range.Spans spans;
181
182        /**
183         Creates a tracked leaf value around the core text.
184         */
185        TrackedValue(String coreValue) {
186            this(coreValue, new Range.Spans());
187        }
188
189        /**
190         Creates a tracked leaf value around copied range spans.
191         */
192        TrackedValue(String coreValue, Range.Spans spans) {
193            this.coreValue = coreValue;
194            this.spans = spans;
195        }
196
197        /**
198         Returns a copy so cloned leaf nodes can mutate range spans independently.
199         */
200        TrackedValue copy() {
201            return new TrackedValue(coreValue, spans.copy());
202        }
203    }
204}