001package org.jsoup.select; 002 003import org.jsoup.helper.Validate; 004import org.jsoup.internal.StringUtil; 005import org.jsoup.nodes.Element; 006import org.jsoup.nodes.Node; 007import org.jspecify.annotations.Nullable; 008 009import java.util.ArrayList; 010import java.util.Arrays; 011import java.util.Collection; 012import java.util.Iterator; 013import java.util.List; 014import java.util.function.Predicate; 015import java.util.function.UnaryOperator; 016 017/** 018 A list of {@link Node} objects, with methods that act on every node in the list. 019 <p>Methods that {@link #set(int, T) set}, {@link #remove(int) remove}, or 020 {@link #replaceAll(UnaryOperator) replace} nodes in the list will also act on the underlying 021 {@link org.jsoup.nodes.Document DOM}.</p> 022 023 <p>If there are other bulk methods (perhaps from Elements) that would be useful here, please <a 024 href="https://jsoup.org/discussion">provide feedback</a>.</p> 025 026 @see Element#selectNodes(String) 027 @see Element#selectNodes(String, Class) 028 @since 1.21.1 */ 029public class Nodes<T extends Node> extends ArrayList<T> { 030 public Nodes() { 031 } 032 033 public Nodes(int initialCapacity) { 034 super(initialCapacity); 035 } 036 037 public Nodes(Collection<T> nodes) { 038 super(nodes); 039 } 040 041 public Nodes(List<T> nodes) { 042 super(nodes); 043 } 044 045 @SafeVarargs 046 public Nodes(T... nodes) { 047 super(Arrays.asList(nodes)); 048 } 049 050 /** 051 * Creates a deep copy of these nodes. 052 * @return a deep copy 053 */ 054 @Override 055 @SuppressWarnings("unchecked") 056 public Nodes<T> clone() { 057 Nodes<T> clone = new Nodes<>(size()); 058 for (T node : this) 059 clone.add((T) node.clone()); 060 return clone; 061 } 062 063 /** 064 Convenience method to get the Nodes as a plain ArrayList. This allows modification to the list of nodes 065 without modifying the source Document. I.e. whereas calling {@code nodes.remove(0)} will remove the nodes from 066 both the Nodes and the DOM, {@code nodes.asList().remove(0)} will remove the node from the list only. 067 <p>Each Node is still the same DOM connected Node.</p> 068 069 @return a new ArrayList containing the nodes in this list 070 @see #Nodes(List) 071 */ 072 public ArrayList<T> asList() { 073 return new ArrayList<>(this); 074 } 075 076 /** 077 Remove each matched node from the DOM. 078 <p>The nodes will still be retained in this list, in case further processing of them is desired.</p> 079 <p> 080 E.g. HTML: {@code <div><p>Hello</p> <p>there</p> <img></div>}<br> 081 <code>doc.select("p").remove();</code><br> 082 HTML = {@code <div> <img></div>} 083 <p> 084 Note that this method should not be used to clean user-submitted HTML; rather, use {@link org.jsoup.safety.Cleaner} 085 to clean HTML. 086 087 @return this, for chaining 088 @see Element#empty() 089 @see Elements#empty() 090 @see #clear() 091 */ 092 public Nodes<T> remove() { 093 for (T node : this) { 094 node.remove(); 095 } 096 return this; 097 } 098 099 /** 100 Get the combined outer HTML of all matched nodes. 101 102 @return string of all node's outer HTML. 103 @see Elements#text() 104 @see Elements#html() 105 */ 106 public String outerHtml() { 107 return stream() 108 .map(Node::outerHtml) 109 .collect(StringUtil.joining("\n")); 110 } 111 112 /** 113 Get the combined outer HTML of all matched nodes. Alias of {@link #outerHtml()}. 114 115 @return string of all the node's outer HTML. 116 @see Elements#text() 117 @see #outerHtml() 118 */ 119 @Override 120 public String toString() { 121 return outerHtml(); 122 } 123 124 /** 125 Insert the supplied HTML before each matched node's outer HTML. 126 127 @param html HTML to insert before each node 128 @return this, for chaining 129 @see Element#before(String) 130 */ 131 public Nodes<T> before(String html) { 132 for (T node : this) { 133 node.before(html); 134 } 135 return this; 136 } 137 138 /** 139 Insert the supplied HTML after each matched nodes's outer HTML. 140 141 @param html HTML to insert after each node 142 @return this, for chaining 143 @see Element#after(String) 144 */ 145 public Nodes<T> after(String html) { 146 for (T node : this) { 147 node.after(html); 148 } 149 return this; 150 } 151 152 /** 153 Wrap the supplied HTML around each matched node. For example, with HTML 154 {@code <p><b>This</b> is <b>Jsoup</b></p>}, 155 <code>doc.select("b").wrap("<i></i>");</code> 156 becomes {@code <p><i><b>This</b></i> is <i><b>jsoup</b></i></p>} 157 @param html HTML to wrap around each node, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep. 158 @return this (for chaining) 159 @see Element#wrap 160 */ 161 public Nodes<T> wrap(String html) { 162 Validate.notEmpty(html); 163 for (T node : this) { 164 node.wrap(html); 165 } 166 return this; 167 } 168 169 // list-like methods 170 /** 171 Get the first matched element. 172 @return The first matched element, or <code>null</code> if contents is empty. 173 */ 174 public @Nullable T first() { 175 return isEmpty() ? null : get(0); 176 } 177 178 /** 179 Get the last matched element. 180 @return The last matched element, or <code>null</code> if contents is empty. 181 */ 182 public @Nullable T last() { 183 return isEmpty() ? null : get(size() - 1); 184 } 185 186 // ArrayList<T> methods that update the DOM: 187 188 /** 189 Replace the node at the specified index in this list, and in the DOM. 190 191 @param index index of the node to replace 192 @param node node to be stored at the specified position 193 @return the old Node at this index 194 */ 195 @Override 196 public T set(int index, T node) { 197 Validate.notNull(node); 198 T old = super.set(index, node); 199 old.replaceWith(node); 200 return old; 201 } 202 203 /** 204 Remove the node at the specified index in this list, and from the DOM. 205 206 @param index the index of the node to be removed 207 @return the old node at this index 208 @see #deselect(int) 209 */ 210 @Override 211 public T remove(int index) { 212 T old = super.remove(index); 213 old.remove(); 214 return old; 215 } 216 217 /** 218 Remove the specified node from this list, and from the DOM. 219 220 @param o node to be removed from this list, if present 221 @return if this list contained the Node 222 @see #deselect(Object) 223 */ 224 @Override 225 public boolean remove(Object o) { 226 int index = super.indexOf(o); 227 if (index == -1) { 228 return false; 229 } else { 230 remove(index); 231 return true; 232 } 233 } 234 235 /** 236 Remove the node at the specified index in this list, but not from the DOM. 237 238 @param index the index of the node to be removed 239 @return the old node at this index 240 @see #remove(int) 241 */ 242 public T deselect(int index) { 243 return super.remove(index); 244 } 245 246 /** 247 Remove the specified node from this list, but not from the DOM. 248 249 @param o node to be removed from this list, if present 250 @return if this list contained the Node 251 @see #remove(Object) 252 */ 253 public boolean deselect(Object o) { 254 return super.remove(o); 255 } 256 257 /** 258 Removes all the nodes from this list, and each of them from the DOM. 259 260 @see #deselectAll() 261 */ 262 @Override 263 public void clear() { 264 remove(); 265 super.clear(); 266 } 267 268 /** 269 Like {@link #clear()}, removes all the nodes from this list, but not from the DOM. 270 271 @see #clear() 272 */ 273 public void deselectAll() { 274 super.clear(); 275 } 276 277 /** 278 Removes from this list, and from the DOM, each of the nodes that are contained in the specified collection and are 279 in this list. 280 281 @param c collection containing nodes to be removed from this list 282 @return {@code true} if nodes were removed from this list 283 */ 284 @Override 285 public boolean removeAll(Collection<?> c) { 286 boolean anyRemoved = false; 287 for (Object o : c) { 288 anyRemoved |= this.remove(o); 289 } 290 return anyRemoved; 291 } 292 293 /** 294 Retain in this list, and in the DOM, only the nodes that are in the specified collection and are in this list. In 295 other words, remove nodes from this list and the DOM any item that is in this list but not in the specified 296 collection. 297 298 @param toRemove collection containing nodes to be retained in this list 299 @return {@code true} if nodes were removed from this list 300 @since 1.17.1 301 */ 302 @Override 303 public boolean retainAll(Collection<?> toRemove) { 304 boolean anyRemoved = false; 305 for (Iterator<T> it = this.iterator(); it.hasNext(); ) { 306 T el = it.next(); 307 if (!toRemove.contains(el)) { 308 it.remove(); 309 anyRemoved = true; 310 } 311 } 312 return anyRemoved; 313 } 314 315 /** 316 Remove from the list, and from the DOM, all nodes in this list that mach the given predicate. 317 318 @param filter a predicate which returns {@code true} for nodes to be removed 319 @return {@code true} if nodes were removed from this list 320 */ 321 @Override 322 public boolean removeIf(Predicate<? super T> filter) { 323 boolean anyRemoved = false; 324 for (Iterator<T> it = this.iterator(); it.hasNext(); ) { 325 T node = it.next(); 326 if (filter.test(node)) { 327 it.remove(); 328 anyRemoved = true; 329 } 330 } 331 return anyRemoved; 332 } 333 334 /** 335 Replace each node in this list with the result of the operator, and update the DOM. 336 337 @param operator the operator to apply to each node 338 */ 339 @Override 340 public void replaceAll(UnaryOperator<T> operator) { 341 for (int i = 0; i < this.size(); i++) { 342 this.set(i, operator.apply(this.get(i))); 343 } 344 } 345}