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}