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("<i></i>");</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 * @see <a href="{@docRoot}/org/jsoup/select/Selector.html#query-compatibility">Query compatibility changes</a> 496 */ 497 public Elements select(String query) { 498 return Selector.select(query, this); 499 } 500 501 /** 502 Find the first Element that matches the {@link Selector} CSS query within this element list. 503 <p>This is effectively the same as calling {@code elements.select(query).first()}, but is more efficient as query 504 execution stops on the first hit.</p> 505 506 @param cssQuery a {@link Selector} query 507 @return the first matching element, or <b>{@code null}</b> if there is no match. 508 @see #expectFirst(String) 509 @since 1.19.1 510 */ 511 public @Nullable Element selectFirst(String cssQuery) { 512 return Selector.selectFirst(cssQuery, this); 513 } 514 515 /** 516 Just like {@link #selectFirst(String)}, but if there is no match, throws an {@link IllegalArgumentException}. 517 518 @param cssQuery a {@link Selector} query 519 @return the first matching element 520 @throws IllegalArgumentException if no match is found 521 @since 1.19.1 522 */ 523 public Element expectFirst(String cssQuery) { 524 return Validate.expectNotNull( 525 Selector.selectFirst(cssQuery, this), 526 "No elements matched the query '%s' in the elements.", cssQuery 527 ); 528 } 529 530 /** 531 * Remove elements from this list that match the {@link Selector} query. 532 * <p> 533 * E.g. HTML: {@code <div class=logo>One</div> <div>Two</div>}<br> 534 * <code>Elements divs = doc.select("div").not(".logo");</code><br> 535 * Result: {@code divs: [<div>Two</div>]} 536 * <p> 537 * @param query the selector query whose results should be removed from these elements 538 * @return a new elements list that contains only the filtered results 539 * @see <a href="{@docRoot}/org/jsoup/select/Selector.html#query-compatibility">Query compatibility changes</a> 540 */ 541 public Elements not(String query) { 542 Evaluator evaluator = Selector.evaluatorOf(query); 543 if (evaluator.web) { 544 Elements filtered = new Elements(); 545 for (Element element : this) { 546 if (!element.is(evaluator)) filtered.add(element); 547 } 548 return filtered; 549 } 550 Elements out = Selector.selectAcrossRoots(evaluator, this); 551 return Selector.filterOut(this, out); 552 } 553 554 /** 555 * Get the <i>nth</i> matched element as an Elements object. 556 * <p> 557 * See also {@link #get(int)} to retrieve an Element. 558 * @param index the (zero-based) index of the element in the list to retain 559 * @return Elements containing only the specified element, or, if that element did not exist, an empty list. 560 */ 561 public Elements eq(int index) { 562 return size() > index ? new Elements(get(index)) : new Elements(); 563 } 564 565 /** 566 * Test if any of the matched elements match the supplied query. 567 * @param query A selector 568 * @return true if at least one element in the list matches the query. 569 * @see <a href="{@docRoot}/org/jsoup/select/Selector.html#query-compatibility">Query compatibility changes</a> 570 */ 571 public boolean is(String query) { 572 Evaluator eval = Selector.evaluatorOf(query); 573 for (Element e : this) { 574 if (e.is(eval)) 575 return true; 576 } 577 return false; 578 } 579 580 /** 581 * Get the immediate next element sibling of each element in this list. 582 * @return next element siblings. 583 */ 584 public Elements next() { 585 return siblings(null, true, false); 586 } 587 588 /** 589 * Get the immediate next element sibling of each element in this list, filtered by the query. 590 * @param query CSS query to match siblings against 591 * @return next element siblings. 592 */ 593 public Elements next(String query) { 594 return siblings(query, true, false); 595 } 596 597 /** 598 * Get each of the following element siblings of each element in this list. 599 * @return all following element siblings. 600 */ 601 public Elements nextAll() { 602 return siblings(null, true, true); 603 } 604 605 /** 606 * Get each of the following element siblings of each element in this list, that match the query. 607 * @param query CSS query to match siblings against 608 * @return all following element siblings. 609 */ 610 public Elements nextAll(String query) { 611 return siblings(query, true, true); 612 } 613 614 /** 615 * Get the immediate previous element sibling of each element in this list. 616 * @return previous element siblings. 617 */ 618 public Elements prev() { 619 return siblings(null, false, false); 620 } 621 622 /** 623 * Get the immediate previous element sibling of each element in this list, filtered by the query. 624 * @param query CSS query to match siblings against 625 * @return previous element siblings. 626 */ 627 public Elements prev(String query) { 628 return siblings(query, false, false); 629 } 630 631 /** 632 * Get each of the previous element siblings of each element in this list. 633 * @return all previous element siblings. 634 */ 635 public Elements prevAll() { 636 return siblings(null, false, true); 637 } 638 639 /** 640 * Get each of the previous element siblings of each element in this list, that match the query. 641 * @param query CSS query to match siblings against 642 * @return all previous element siblings. 643 */ 644 public Elements prevAll(String query) { 645 return siblings(query, false, true); 646 } 647 648 private Elements siblings(@Nullable String query, boolean next, boolean all) { 649 Elements els = new Elements(); 650 Evaluator eval = query != null? Selector.evaluatorOf(query) : null; 651 for (Element e : this) { 652 do { 653 Element sib = next ? e.nextElementSibling() : e.previousElementSibling(); 654 if (sib == null) break; 655 if (eval == null || sib.is(eval)) els.add(sib); 656 e = sib; 657 } while (all); 658 } 659 return els; 660 } 661 662 /** 663 * Get all of the parents and ancestor elements of the matched elements. 664 * @return all of the parents and ancestor elements of the matched elements 665 */ 666 public Elements parents() { 667 HashSet<Element> combo = new LinkedHashSet<>(); 668 for (Element e: this) { 669 combo.addAll(e.parents()); 670 } 671 return new Elements(combo); 672 } 673 674 // list-like methods 675 /** 676 Get the first matched element. 677 @return The first matched element, or <code>null</code> if contents is empty. 678 */ 679 @Override 680 public @Nullable Element first() { 681 return super.first(); 682 } 683 684 /** 685 Get the last matched element. 686 @return The last matched element, or <code>null</code> if contents is empty. 687 */ 688 @Override 689 public @Nullable Element last() { 690 return super.last(); 691 } 692 693 /** 694 * Perform a depth-first traversal on each of the selected elements. 695 * @param nodeVisitor the visitor callbacks to perform on each node 696 * @return this, for chaining 697 */ 698 public Elements traverse(NodeVisitor nodeVisitor) { 699 NodeTraversor.traverse(nodeVisitor, this); 700 return this; 701 } 702 703 /** 704 * Perform a depth-first filtering on each of the selected elements. 705 * @param nodeFilter the filter callbacks to perform on each node 706 * @return this, for chaining 707 */ 708 public Elements filter(NodeFilter nodeFilter) { 709 NodeTraversor.filter(nodeFilter, this); 710 return this; 711 } 712 713 /** 714 * Get the {@link FormElement} forms from the selected elements, if any. 715 * @return a list of {@link FormElement}s pulled from the matched elements. The list will be empty if the elements contain 716 * no forms. 717 */ 718 public List<FormElement> forms() { 719 ArrayList<FormElement> forms = new ArrayList<>(); 720 for (Element el: this) 721 if (el instanceof FormElement) 722 forms.add((FormElement) el); 723 return forms; 724 } 725 726 /** 727 * Get {@link Comment} nodes that are direct child nodes of the selected elements. 728 * @return Comment nodes, or an empty list if none. 729 */ 730 public List<Comment> comments() { 731 return childNodesOfType(Comment.class); 732 } 733 734 /** 735 * Get {@link TextNode} nodes that are direct child nodes of the selected elements. 736 * @return TextNode nodes, or an empty list if none. 737 */ 738 public List<TextNode> textNodes() { 739 return childNodesOfType(TextNode.class); 740 } 741 742 /** 743 * Get {@link DataNode} nodes that are direct child nodes of the selected elements. DataNode nodes contain the 744 * content of tags such as {@code script}, {@code style} etc and are distinct from {@link TextNode}s. 745 * @return Comment nodes, or an empty list if none. 746 */ 747 public List<DataNode> dataNodes() { 748 return childNodesOfType(DataNode.class); 749 } 750 751 private <T extends Node> List<T> childNodesOfType(Class<T> tClass) { 752 ArrayList<T> nodes = new ArrayList<>(); 753 for (Element el: this) { 754 for (int i = 0; i < el.childNodeSize(); i++) { 755 Node node = el.childNode(i); 756 if (tClass.isInstance(node)) 757 nodes.add(tClass.cast(node)); 758 } 759 } 760 return nodes; 761 } 762 763 // list methods that update the DOM: 764 765 /** 766 Replace the Element at the specified index in this list, and in the DOM. 767 768 @param index index of the element to replace 769 @param element element to be stored at the specified position 770 @return the old Element at this index 771 @since 1.17.1 772 */ 773 @Override 774 public Element set(int index, Element element) { 775 return super.set(index, element); 776 } 777 778 /** 779 Remove the Element at the specified index in this ist, and from the DOM. 780 781 @param index the index of the element to be removed 782 @return the old element at this index 783 @see #deselect(int) 784 @since 1.17.1 785 */ 786 @Override 787 public Element remove(int index) { 788 return super.remove(index); 789 } 790 791 792 /** 793 Remove the Element at the specified index in this list, but not from the DOM. 794 795 @param index the index of the element to be removed 796 @return the old element at this index 797 @see #remove(int) 798 @since 1.19.2 799 */ 800 @Override 801 public Element deselect(int index) { 802 return super.deselect(index); 803 } 804}