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}