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    private void ensureAttributes() {
035        if (!hasAttributes()) {
036            String coreValue = coreValue();
037            Attributes attributes = new Attributes();
038            Range.Spans rangeSpans = spans();
039            value = attributes;
040            attributes.put(nodeName(), coreValue);
041            if (rangeSpans != null)
042                attributes.putSpans(rangeSpans);
043        }
044    }
045
046    String coreValue() {
047        if (value instanceof Attributes)   return ((Attributes) value).get(nodeName());
048        if (value instanceof TrackedValue) return ((TrackedValue) value).coreValue;
049        return (String) value;
050    }
051
052    @Override @Nullable
053    public Element parent() {
054        return parentNode;
055    }
056
057    @Override
058    public String nodeValue() {
059        return coreValue();
060    }
061
062    void coreValue(String value) {
063        if (this.value instanceof Attributes)
064            ((Attributes) this.value).put(nodeName(), value);
065        else if (this.value instanceof TrackedValue)
066            ((TrackedValue) this.value).coreValue = value;
067        else
068            this.value = value;
069    }
070
071    @Override
072    public String attr(String key) {
073        if (!hasAttributes())
074            return nodeName().equals(key) ? coreValue() : EmptyString;
075        return super.attr(key);
076    }
077
078    @Override
079    public Node attr(String key, String value) {
080        if (!hasAttributes() && key.equals(nodeName())) {
081            coreValue(value);
082        } else {
083            ensureAttributes();
084            super.attr(key, value);
085        }
086        return this;
087    }
088
089    @Override
090    public boolean hasAttr(String key) {
091        ensureAttributes();
092        return super.hasAttr(key);
093    }
094
095    @Override
096    public Node removeAttr(String key) {
097        ensureAttributes();
098        return super.removeAttr(key);
099    }
100
101    @Override
102    public String absUrl(String key) {
103        ensureAttributes();
104        return super.absUrl(key);
105    }
106
107    @Override
108    public String baseUri() {
109        return parentNode != null ? parentNode.baseUri() : "";
110    }
111
112    @Override
113    protected void doSetBaseUri(String baseUri) {
114        // noop
115    }
116
117    @Override
118    public int childNodeSize() {
119        return 0;
120    }
121
122    @Override
123    public Node empty() {
124        return this;
125    }
126
127    @Override
128    protected List<Node> ensureChildNodes() {
129        return EmptyNodes;
130    }
131
132    @Override
133    void outerHtmlTail(QuietAppendable accum, Document.OutputSettings out) {}
134
135    @Override
136    protected LeafNode doClone(Node parent) {
137        LeafNode clone = (LeafNode) super.doClone(parent);
138
139        // Object value could be plain string, tracked string, or attributes - need to clone.
140        if (hasAttributes())
141            clone.value = ((Attributes) value).clone();
142        else if (value instanceof TrackedValue)
143            clone.value = ((TrackedValue) value).copy();
144
145        return clone;
146    }
147
148    @Override Range.@Nullable Spans spans() {
149        if (value instanceof TrackedValue)
150            return ((TrackedValue) value).spans;
151        return super.spans();
152    }
153
154    @Override Range.Spans ensureSpans() {
155        // Leaf nodes normally hold just their core string. When source ranges are tracked, keep the string plus spans
156        // in a small wrapper so leaf nodes do not expand to Attributes just for parser metadata. If attributes are
157        // later requested, ensureAttributes() moves these same spans into the Attributes object.
158        if (value instanceof TrackedValue)
159            return ((TrackedValue) value).spans;
160        if (value instanceof Attributes)
161            return ((Attributes) value).ensureSpans();
162
163        TrackedValue trackedValue = new TrackedValue((String) value);
164        value = trackedValue;
165        return trackedValue.spans;
166    }
167
168    /**
169     Holds a compact leaf value plus ranges without expanding to Attributes.
170     */
171    private static final class TrackedValue {
172        String coreValue;
173        final Range.Spans spans;
174
175        /**
176         Creates a tracked leaf value around the core text.
177         */
178        TrackedValue(String coreValue) {
179            this(coreValue, new Range.Spans());
180        }
181
182        /**
183         Creates a tracked leaf value around copied range spans.
184         */
185        TrackedValue(String coreValue, Range.Spans spans) {
186            this.coreValue = coreValue;
187            this.spans = spans;
188        }
189
190        /**
191         Returns a copy so cloned leaf nodes can mutate range spans independently.
192         */
193        TrackedValue copy() {
194            return new TrackedValue(coreValue, spans.copy());
195        }
196    }
197}