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 */ 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}