001package org.jsoup.select;
002
003import org.jsoup.helper.Validate;
004import org.jsoup.internal.StringUtil;
005import org.jsoup.nodes.Comment;
006import org.jsoup.nodes.DataNode;
007import org.jsoup.nodes.Element;
008import org.jsoup.nodes.FormElement;
009import org.jsoup.nodes.Node;
010import org.jsoup.nodes.TextNode;
011import org.jspecify.annotations.Nullable;
012
013import java.util.ArrayList;
014import java.util.Arrays;
015import java.util.Collection;
016import java.util.HashSet;
017import java.util.LinkedHashSet;
018import java.util.List;
019import java.util.function.BiConsumer;
020import java.util.function.UnaryOperator;
021
022/**
023 A list of {@link Element}s, with methods that act on every element in the list.
024 <p>To get an {@code Elements} object, use the {@link Element#select(String)} method.</p>
025 <p>Methods that {@link #set(int, Element) set}, {@link #remove(int) remove}, or {@link #replaceAll(UnaryOperator)
026 replace} Elements in the list will also act on the underlying {@link org.jsoup.nodes.Document DOM}.</p>
027
028 @author Jonathan Hedley, jonathan@hedley.net */
029public class Elements extends Nodes<Element> {
030    public Elements() {
031    }
032
033    public Elements(int initialCapacity) {
034        super(initialCapacity);
035    }
036
037    public Elements(Collection<Element> elements) {
038        super(elements);
039    }
040
041    public Elements(List<Element> elements) {
042        super(elements);
043    }
044
045    public Elements(Element... elements) {
046        super(Arrays.asList(elements));
047    }
048
049    /**
050     * Creates a deep copy of these elements.
051     * @return a deep copy
052     */
053    @Override
054    public Elements clone() {
055        Elements clone = new Elements(size());
056        for (Element e : this)
057            clone.add(e.clone());
058        return clone;
059    }
060
061    /**
062     Convenience method to get the Elements as a plain ArrayList. This allows modification to the list of elements
063     without modifying the source Document. I.e. whereas calling {@code elements.remove(0)} will remove the element from
064     both the Elements and the DOM, {@code elements.asList().remove(0)} will remove the element from the list only.
065     <p>Each Element is still the same DOM connected Element.</p>
066
067     @return a new ArrayList containing the elements in this list
068     @since 1.19.2
069     @see #Elements(List)
070     */
071    @Override
072    public ArrayList<Element> asList() {
073        return new ArrayList<>(this);
074    }
075
076    // attribute methods
077    /**
078     Get an attribute value from the first matched element that has the attribute.
079     @param attributeKey The attribute key.
080     @return The attribute value from the first matched element that has the attribute. If no elements were matched (isEmpty() == true),
081     or if the no elements have the attribute, returns empty string.
082     @see #hasAttr(String)
083     */
084    public String attr(String attributeKey) {
085        for (Element element : this) {
086            if (element.hasAttr(attributeKey))
087                return element.attr(attributeKey);
088        }
089        return "";
090    }
091
092    /**
093     Checks if any of the matched elements have this attribute defined.
094     @param attributeKey attribute key
095     @return true if any of the elements have the attribute; false if none do.
096     */
097    public boolean hasAttr(String attributeKey) {
098        for (Element element : this) {
099            if (element.hasAttr(attributeKey))
100                return true;
101        }
102        return false;
103    }
104
105    /**
106     * Get the attribute value for each of the matched elements. If an element does not have this attribute, no value is
107     * included in the result set for that element.
108     * @param attributeKey the attribute name to return values for. You can add the {@code abs:} prefix to the key to
109     * get absolute URLs from relative URLs, e.g.: {@code doc.select("a").eachAttr("abs:href")} .
110     * @return a list of each element's attribute value for the attribute
111     */
112    public List<String> eachAttr(String attributeKey) {
113        List<String> attrs = new ArrayList<>(size());
114        for (Element element : this) {
115            if (element.hasAttr(attributeKey))
116                attrs.add(element.attr(attributeKey));
117        }
118        return attrs;
119    }
120
121    /**
122     * Set an attribute on all matched elements.
123     * @param attributeKey attribute key
124     * @param attributeValue attribute value
125     * @return this
126     */
127    public Elements attr(String attributeKey, String attributeValue) {
128        for (Element element : this) {
129            element.attr(attributeKey, attributeValue);
130        }
131        return this;
132    }
133
134    /**
135     * Remove an attribute from every matched element.
136     * @param attributeKey The attribute to remove.
137     * @return this (for chaining)
138     */
139    public Elements removeAttr(String attributeKey) {
140        for (Element element : this) {
141            element.removeAttr(attributeKey);
142        }
143        return this;
144    }
145
146    /**
147     Add the class name to every matched element's {@code class} attribute.
148     @param className class name to add
149     @return this
150     */
151    public Elements addClass(String className) {
152        for (Element element : this) {
153            element.addClass(className);
154        }
155        return this;
156    }
157
158    /**
159     Remove the class name from every matched element's {@code class} attribute, if present.
160     @param className class name to remove
161     @return this
162     */
163    public Elements removeClass(String className) {
164        for (Element element : this) {
165            element.removeClass(className);
166        }
167        return this;
168    }
169
170    /**
171     Toggle the class name on every matched element's {@code class} attribute.
172     @param className class name to add if missing, or remove if present, from every element.
173     @return this
174     */
175    public Elements toggleClass(String className) {
176        for (Element element : this) {
177            element.toggleClass(className);
178        }
179        return this;
180    }
181
182    /**
183     Determine if any of the matched elements have this class name set in their {@code class} attribute.
184     @param className class name to check for
185     @return true if any do, false if none do
186     */
187    public boolean hasClass(String className) {
188        for (Element element : this) {
189            if (element.hasClass(className))
190                return true;
191        }
192        return false;
193    }
194    
195    /**
196     * Get the form element's value of the first matched element.
197     * @return The form element's value, or empty if not set.
198     * @see Element#val()
199     */
200    public String val() {
201        if (size() > 0)
202            //noinspection ConstantConditions
203            return first().val(); // first() != null as size() > 0
204        else
205            return "";
206    }
207    
208    /**
209     * Set the form element's value in each of the matched elements.
210     * @param value The value to set into each matched element
211     * @return this (for chaining)
212     */
213    public Elements val(String value) {
214        for (Element element : this)
215            element.val(value);
216        return this;
217    }
218    
219    /**
220     * Get the combined text of all the matched elements.
221     * <p>
222     * Note that it is possible to get repeats if the matched elements contain both parent elements and their own
223     * children, as the Element.text() method returns the combined text of a parent and all its children.
224     * @return string of all text: unescaped and no HTML.
225     * @see Element#text()
226     * @see #eachText()
227     */
228    public String text() {
229        return stream()
230            .map(Element::text)
231            .collect(StringUtil.joining(" "));
232    }
233
234    /**
235     Test if any matched Element has any text content, that is not just whitespace.
236     @return true if any element has non-blank text content.
237     @see Element#hasText()
238     */
239    public boolean hasText() {
240        for (Element element: this) {
241            if (element.hasText())
242                return true;
243        }
244        return false;
245    }
246
247    /**
248     * Get the text content of each of the matched elements. If an element has no text, then it is not included in the
249     * result.
250     * @return A list of each matched element's text content.
251     * @see Element#text()
252     * @see Element#hasText()
253     * @see #text()
254     */
255    public List<String> eachText() {
256        ArrayList<String> texts = new ArrayList<>(size());
257        for (Element el: this) {
258            if (el.hasText())
259                texts.add(el.text());
260        }
261        return texts;
262    }
263    
264    /**
265     * Get the combined inner HTML of all matched elements.
266     * @return string of all element's inner HTML.
267     * @see #text()
268     * @see #outerHtml()
269     */
270    public String html() {
271        return stream()
272            .map(Element::html)
273            .collect(StringUtil.joining("\n"));
274    }
275
276    /**
277     * Update (rename) the tag name of each matched element. For example, to change each {@code <i>} to a {@code <em>}, do
278     * {@code doc.select("i").tagName("em");}
279     *
280     * @param tagName the new tag name
281     * @return this, for chaining
282     * @see Element#tagName(String)
283     */
284    public Elements tagName(String tagName) {
285        for (Element element : this) {
286            element.tagName(tagName);
287        }
288        return this;
289    }
290    
291    /**
292     * Set the inner HTML of each matched element.
293     * @param html HTML to parse and set into each matched element.
294     * @return this, for chaining
295     * @see Element#html(String)
296     */
297    public Elements html(String html) {
298        for (Element element : this) {
299            element.html(html);
300        }
301        return this;
302    }
303    
304    /**
305     * Add the supplied HTML to the start of each matched element's inner HTML.
306     * @param html HTML to add inside each element, before the existing HTML
307     * @return this, for chaining
308     * @see Element#prepend(String)
309     */
310    public Elements prepend(String html) {
311        for (Element element : this) {
312            element.prepend(html);
313        }
314        return this;
315    }
316
317    /**
318     Add the supplied node to the start of each matched element's inner HTML. The node is cloned for each target.
319
320     @param node the node to add inside each element, before the existing HTML
321     @return this, for chaining
322     @see Element#prependChild(Node)
323     */
324    public Elements prepend(Node node) {
325        return insert(node, Element::prependChild);
326    }
327    
328    /**
329     * Add the supplied HTML to the end of each matched element's inner HTML.
330     * @param html HTML to add inside each element, after the existing HTML
331     * @return this, for chaining
332     * @see Element#append(String)
333     */
334    public Elements append(String html) {
335        for (Element element : this) {
336            element.append(html);
337        }
338        return this;
339    }
340
341    /**
342     Add the supplied node to the end of each matched element's inner HTML. The node is cloned for each target.
343
344     @param node the node to add inside each element, after the existing HTML
345     @return this, for chaining
346     @see Element#appendChild(Node)
347     */
348    public Elements append(Node node) {
349        return insert(node, Element::appendChild);
350    }
351
352    /**
353     Insert the supplied HTML before each matched element's outer HTML.
354
355     @param html HTML to insert before each element
356     @return this, for chaining
357     @see Element#before(String)
358     */
359    @Override
360    public Elements before(String html) {
361        super.before(html);
362        return this;
363    }
364
365    /**
366     Insert the supplied node before each matched element's outer HTML. The node is cloned for each target.
367
368     @param node the node to insert before each element
369     @return this, for chaining
370     @see Element#before(Node)
371     */
372    public Elements before(Node node) {
373        return insert(node, Element::before);
374    }
375
376    /**
377     Insert the supplied HTML after each matched element's outer HTML.
378
379     @param html HTML to insert after each element
380     @return this, for chaining
381     @see Element#after(String)
382     */
383    @Override
384    public Elements after(String html) {
385        super.after(html);
386        return this;
387    }
388
389    /**
390     Insert the supplied node after each matched element's outer HTML. The node is cloned for each target.
391
392     @param node the node to insert after each element
393     @return this, for chaining
394     @see Element#after(Node)
395     */
396    public Elements after(Node node) {
397        return insert(node, Element::after);
398    }
399
400    /**
401     Applies a node insertion to each matched element, cloning the node for each target.
402
403     @param node     the node to insert
404     @param inserter the insertion operation
405     @return this, for chaining
406     */
407    private Elements insert(Node node, BiConsumer<Element, Node> inserter) {
408        Validate.notNull(node);
409        for (Element element : this)
410            inserter.accept(element, node.clone());
411        return this;
412    }
413
414    /**
415     Wrap the supplied HTML around each matched elements. For example, with HTML
416     {@code <p><b>This</b> is <b>Jsoup</b></p>},
417     <code>doc.select("b").wrap("&lt;i&gt;&lt;/i&gt;");</code>
418     becomes {@code <p><i><b>This</b></i> is <i><b>jsoup</b></i></p>}
419
420     @param html HTML to wrap around each element, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep.
421     @return this (for chaining)
422     @see Element#wrap
423     */
424    @Override
425    public Elements wrap(String html) {
426        super.wrap(html);
427        return this;
428    }
429
430    /**
431     * Removes the matched elements from the DOM, and moves their children up into their parents. This has the effect of
432     * dropping the elements but keeping their children.
433     * <p>
434     * This is useful for e.g removing unwanted formatting elements but keeping their contents.
435     * </p>
436     * 
437     * E.g. with HTML: <p>{@code <div><font>One</font> <font><a href="/">Two</a></font></div>}</p>
438     * <p>{@code doc.select("font").unwrap();}</p>
439     * <p>HTML = {@code <div>One <a href="/">Two</a></div>}</p>
440     *
441     * @return this (for chaining)
442     * @see Node#unwrap
443     */
444    public Elements unwrap() {
445        for (Element element : this) {
446            element.unwrap();
447        }
448        return this;
449    }
450
451    /**
452     * Empty (remove all child nodes from) each matched element. This is similar to setting the inner HTML of each
453     * element to nothing.
454     * <p>
455     * E.g. HTML: {@code <div><p>Hello <b>there</b></p> <p>now</p></div>}<br>
456     * <code>doc.select("p").empty();</code><br>
457     * HTML = {@code <div><p></p> <p></p></div>}
458     * @return this, for chaining
459     * @see Element#empty()
460     * @see #remove()
461     */
462    public Elements empty() {
463        for (Element element : this) {
464            element.empty();
465        }
466        return this;
467    }
468
469    /**
470     * Remove each matched element from the DOM. This is similar to setting the outer HTML of each element to nothing.
471     * <p>The elements will still be retained in this list, in case further processing of them is desired.</p>
472     * <p>
473     * E.g. HTML: {@code <div><p>Hello</p> <p>there</p> <img /></div>}<br>
474     * <code>doc.select("p").remove();</code><br>
475     * HTML = {@code <div> <img /></div>}
476     * <p>
477     * Note that this method should not be used to clean user-submitted HTML; rather, use {@link org.jsoup.safety.Cleaner} to clean HTML.
478     * @return this, for chaining
479     * @see Element#empty()
480     * @see #empty()
481     * @see #clear()
482     */
483    @Override
484    public Elements remove() {
485        super.remove();
486        return this;
487    }
488    
489    // filters
490    
491    /**
492     * Find matching elements within this element list.
493     * @param query A {@link Selector} query
494     * @return the filtered list of elements, or an empty list if none match.
495     */
496    public Elements select(String query) {
497        return Selector.select(query, this);
498    }
499
500    /**
501     Find the first Element that matches the {@link Selector} CSS query within this element list.
502     <p>This is effectively the same as calling {@code elements.select(query).first()}, but is more efficient as query
503     execution stops on the first hit.</p>
504
505     @param cssQuery a {@link Selector} query
506     @return the first matching element, or <b>{@code null}</b> if there is no match.
507     @see #expectFirst(String)
508     @since 1.19.1
509     */
510    public @Nullable Element selectFirst(String cssQuery) {
511        return Selector.selectFirst(cssQuery, this);
512    }
513
514    /**
515     Just like {@link #selectFirst(String)}, but if there is no match, throws an {@link IllegalArgumentException}.
516
517     @param cssQuery a {@link Selector} query
518     @return the first matching element
519     @throws IllegalArgumentException if no match is found
520     @since 1.19.1
521     */
522    public Element expectFirst(String cssQuery) {
523        return Validate.expectNotNull(
524            Selector.selectFirst(cssQuery, this),
525            "No elements matched the query '%s' in the elements.", cssQuery
526        );
527    }
528
529    /**
530     * Remove elements from this list that match the {@link Selector} query.
531     * <p>
532     * E.g. HTML: {@code <div class=logo>One</div> <div>Two</div>}<br>
533     * <code>Elements divs = doc.select("div").not(".logo");</code><br>
534     * Result: {@code divs: [<div>Two</div>]}
535     * <p>
536     * @param query the selector query whose results should be removed from these elements
537     * @return a new elements list that contains only the filtered results
538     */
539    public Elements not(String query) {
540        Elements out = Selector.select(query, this);
541        return Selector.filterOut(this, out);
542    }
543    
544    /**
545     * Get the <i>nth</i> matched element as an Elements object.
546     * <p>
547     * See also {@link #get(int)} to retrieve an Element.
548     * @param index the (zero-based) index of the element in the list to retain
549     * @return Elements containing only the specified element, or, if that element did not exist, an empty list.
550     */
551    public Elements eq(int index) {
552        return size() > index ? new Elements(get(index)) : new Elements();
553    }
554    
555    /**
556     * Test if any of the matched elements match the supplied query.
557     * @param query A selector
558     * @return true if at least one element in the list matches the query.
559     */
560    public boolean is(String query) {
561        Evaluator eval = Selector.evaluatorOf(query);
562        for (Element e : this) {
563            if (e.is(eval))
564                return true;
565        }
566        return false;
567    }
568
569    /**
570     * Get the immediate next element sibling of each element in this list.
571     * @return next element siblings.
572     */
573    public Elements next() {
574        return siblings(null, true, false);
575    }
576
577    /**
578     * Get the immediate next element sibling of each element in this list, filtered by the query.
579     * @param query CSS query to match siblings against
580     * @return next element siblings.
581     */
582    public Elements next(String query) {
583        return siblings(query, true, false);
584    }
585
586    /**
587     * Get each of the following element siblings of each element in this list.
588     * @return all following element siblings.
589     */
590    public Elements nextAll() {
591        return siblings(null, true, true);
592    }
593
594    /**
595     * Get each of the following element siblings of each element in this list, that match the query.
596     * @param query CSS query to match siblings against
597     * @return all following element siblings.
598     */
599    public Elements nextAll(String query) {
600        return siblings(query, true, true);
601    }
602
603    /**
604     * Get the immediate previous element sibling of each element in this list.
605     * @return previous element siblings.
606     */
607    public Elements prev() {
608        return siblings(null, false, false);
609    }
610
611    /**
612     * Get the immediate previous element sibling of each element in this list, filtered by the query.
613     * @param query CSS query to match siblings against
614     * @return previous element siblings.
615     */
616    public Elements prev(String query) {
617        return siblings(query, false, false);
618    }
619
620    /**
621     * Get each of the previous element siblings of each element in this list.
622     * @return all previous element siblings.
623     */
624    public Elements prevAll() {
625        return siblings(null, false, true);
626    }
627
628    /**
629     * Get each of the previous element siblings of each element in this list, that match the query.
630     * @param query CSS query to match siblings against
631     * @return all previous element siblings.
632     */
633    public Elements prevAll(String query) {
634        return siblings(query, false, true);
635    }
636
637    private Elements siblings(@Nullable String query, boolean next, boolean all) {
638        Elements els = new Elements();
639        Evaluator eval = query != null? Selector.evaluatorOf(query) : null;
640        for (Element e : this) {
641            do {
642                Element sib = next ? e.nextElementSibling() : e.previousElementSibling();
643                if (sib == null) break;
644                if (eval == null || sib.is(eval)) els.add(sib);
645                e = sib;
646            } while (all);
647        }
648        return els;
649    }
650
651    /**
652     * Get all of the parents and ancestor elements of the matched elements.
653     * @return all of the parents and ancestor elements of the matched elements
654     */
655    public Elements parents() {
656        HashSet<Element> combo = new LinkedHashSet<>();
657        for (Element e: this) {
658            combo.addAll(e.parents());
659        }
660        return new Elements(combo);
661    }
662
663    // list-like methods
664    /**
665     Get the first matched element.
666     @return The first matched element, or <code>null</code> if contents is empty.
667     */
668    @Override
669    public @Nullable Element first() {
670        return super.first();
671    }
672
673    /**
674     Get the last matched element.
675     @return The last matched element, or <code>null</code> if contents is empty.
676     */
677    @Override
678    public @Nullable Element last() {
679        return super.last();
680    }
681
682    /**
683     * Perform a depth-first traversal on each of the selected elements.
684     * @param nodeVisitor the visitor callbacks to perform on each node
685     * @return this, for chaining
686     */
687    public Elements traverse(NodeVisitor nodeVisitor) {
688        NodeTraversor.traverse(nodeVisitor, this);
689        return this;
690    }
691
692    /**
693     * Perform a depth-first filtering on each of the selected elements.
694     * @param nodeFilter the filter callbacks to perform on each node
695     * @return this, for chaining
696     */
697    public Elements filter(NodeFilter nodeFilter) {
698        NodeTraversor.filter(nodeFilter, this);
699        return this;
700    }
701
702    /**
703     * Get the {@link FormElement} forms from the selected elements, if any.
704     * @return a list of {@link FormElement}s pulled from the matched elements. The list will be empty if the elements contain
705     * no forms.
706     */
707    public List<FormElement> forms() {
708        ArrayList<FormElement> forms = new ArrayList<>();
709        for (Element el: this)
710            if (el instanceof FormElement)
711                forms.add((FormElement) el);
712        return forms;
713    }
714
715    /**
716     * Get {@link Comment} nodes that are direct child nodes of the selected elements.
717     * @return Comment nodes, or an empty list if none.
718     */
719    public List<Comment> comments() {
720        return childNodesOfType(Comment.class);
721    }
722
723    /**
724     * Get {@link TextNode} nodes that are direct child nodes of the selected elements.
725     * @return TextNode nodes, or an empty list if none.
726     */
727    public List<TextNode> textNodes() {
728        return childNodesOfType(TextNode.class);
729    }
730
731    /**
732     * Get {@link DataNode} nodes that are direct child nodes of the selected elements. DataNode nodes contain the
733     * content of tags such as {@code script}, {@code style} etc and are distinct from {@link TextNode}s.
734     * @return Comment nodes, or an empty list if none.
735     */
736    public List<DataNode> dataNodes() {
737        return childNodesOfType(DataNode.class);
738    }
739
740    private <T extends Node> List<T> childNodesOfType(Class<T> tClass) {
741        ArrayList<T> nodes = new ArrayList<>();
742        for (Element el: this) {
743            for (int i = 0; i < el.childNodeSize(); i++) {
744                Node node = el.childNode(i);
745                if (tClass.isInstance(node))
746                    nodes.add(tClass.cast(node));
747            }
748        }
749        return nodes;
750    }
751
752    // list methods that update the DOM:
753
754    /**
755     Replace the Element at the specified index in this list, and in the DOM.
756
757     @param index index of the element to replace
758     @param element element to be stored at the specified position
759     @return the old Element at this index
760     @since 1.17.1
761     */
762    @Override
763    public Element set(int index, Element element) {
764        return super.set(index, element);
765    }
766
767    /**
768     Remove the Element at the specified index in this ist, and from the DOM.
769
770     @param index the index of the element to be removed
771     @return the old element at this index
772     @see #deselect(int)
773     @since 1.17.1
774     */
775    @Override
776    public Element remove(int index) {
777        return super.remove(index);
778    }
779
780
781    /**
782     Remove the Element at the specified index in this list, but not from the DOM.
783
784     @param index the index of the element to be removed
785     @return the old element at this index
786     @see #remove(int)
787     @since 1.19.2
788     */
789    @Override
790    public Element deselect(int index) {
791        return super.deselect(index);
792    }
793}