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("&lt;i&gt;&lt;/i&gt;");</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}