diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3f482eea04..6c5a744694 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -20,11 +20,11 @@ jobs: distribution: 'temurin' cache: 'maven' - name: CodeQL Initialization - uses: github/codeql-action/init@v4.37.4 + uses: github/codeql-action/init@v4.37.6 with: languages: java queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v4.37.4 + uses: github/codeql-action/autobuild@v4.37.6 - name: CodeQL Analysis - uses: github/codeql-action/analyze@v4.37.4 + uses: github/codeql-action/analyze@v4.37.6 diff --git a/CHANGES.md b/CHANGES.md index 117558e131..30caabdf83 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ * Improved `W3CDom` conversion performance for documents with many nested namespace declarations. The W3C converter now uses the same optimized namespace tracking as the XML parser. [#2559](https://github.com/jhy/jsoup/pull/2559) * Improved `W3CDom` XML conversion to retain processing instructions, comments outside the root element, and CDATA sections, which were previously dropped or converted to text. [#2572](https://github.com/jhy/jsoup/issues/2572) * DOM mutation methods, including child insertion and replacement, now reject operations that would create a cycle, such as making a node its own child or moving an ancestor beneath a descendant. [#2552](https://github.com/jhy/jsoup/issues/2552) +* Added `Elements#before(Node)`, `after(Node)`, `prepend(Node)`, and `append(Node)` to match the existing HTML string methods. [#953](https://github.com/jhy/jsoup/issues/953) * XML serialization now repairs element and attribute names that start with an invalid character, rather than outputting `` elements or dropping attributes. For example, an attribute named `1a` is written as `_1a`. Additional leading underscores keep repaired attribute names unique if they conflict with another attribute. [#2573](https://github.com/jhy/jsoup/issues/2573) ### Changes diff --git a/pom.xml b/pom.xml index 5113f5037b..812268399a 100644 --- a/pom.xml +++ b/pom.xml @@ -625,7 +625,7 @@ io.netty netty-bom - 4.2.16.Final + 4.2.17.Final pom import diff --git a/src/main/java/org/jsoup/select/Elements.java b/src/main/java/org/jsoup/select/Elements.java index e84e2a0e7b..aaca336fd1 100644 --- a/src/main/java/org/jsoup/select/Elements.java +++ b/src/main/java/org/jsoup/select/Elements.java @@ -14,10 +14,9 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; -import java.util.function.Predicate; +import java.util.function.BiConsumer; import java.util.function.UnaryOperator; /** @@ -314,6 +313,17 @@ public Elements prepend(String html) { } return this; } + + /** + Add the supplied node to the start of each matched element's inner HTML. The node is cloned for each target. + + @param node the node to add inside each element, before the existing HTML + @return this, for chaining + @see Element#prependChild(Node) + */ + public Elements prepend(Node node) { + return insert(node, Element::prependChild); + } /** * Add the supplied HTML to the end of each matched element's inner HTML. @@ -328,6 +338,17 @@ public Elements append(String html) { return this; } + /** + Add the supplied node to the end of each matched element's inner HTML. The node is cloned for each target. + + @param node the node to add inside each element, after the existing HTML + @return this, for chaining + @see Element#appendChild(Node) + */ + public Elements append(Node node) { + return insert(node, Element::appendChild); + } + /** Insert the supplied HTML before each matched element's outer HTML. @@ -341,6 +362,17 @@ public Elements before(String html) { return this; } + /** + Insert the supplied node before each matched element's outer HTML. The node is cloned for each target. + + @param node the node to insert before each element + @return this, for chaining + @see Element#before(Node) + */ + public Elements before(Node node) { + return insert(node, Element::before); + } + /** Insert the supplied HTML after each matched element's outer HTML. @@ -354,6 +386,31 @@ public Elements after(String html) { return this; } + /** + Insert the supplied node after each matched element's outer HTML. The node is cloned for each target. + + @param node the node to insert after each element + @return this, for chaining + @see Element#after(Node) + */ + public Elements after(Node node) { + return insert(node, Element::after); + } + + /** + Applies a node insertion to each matched element, cloning the node for each target. + + @param node the node to insert + @param inserter the insertion operation + @return this, for chaining + */ + private Elements insert(Node node, BiConsumer inserter) { + Validate.notNull(node); + for (Element element : this) + inserter.accept(element, node.clone()); + return this; + } + /** Wrap the supplied HTML around each matched elements. For example, with HTML {@code

This is Jsoup

}, diff --git a/src/test/java/org/jsoup/select/ElementsTest.java b/src/test/java/org/jsoup/select/ElementsTest.java index b1b9b3dde4..2a39215d6f 100644 --- a/src/test/java/org/jsoup/select/ElementsTest.java +++ b/src/test/java/org/jsoup/select/ElementsTest.java @@ -169,12 +169,40 @@ public class ElementsTest { assertEquals("

This foois foojsoup.

", TextUtil.stripNewlines(doc.body().html())); } + @Test public void beforeNode() { + Document doc = Jsoup.parse("

This is jsoup.

"); + Element span = new Element("span").text("foo"); + doc.select("a").before(span); + assertEquals("

This foois foojsoup.

", TextUtil.stripNewlines(doc.body().html())); + assertNull(span.parent()); // cloned per target; original is left alone + assertNotSame(span, doc.selectFirst("span")); + } + @Test public void after() { Document doc = Jsoup.parse("

This is jsoup.

"); doc.select("a").after("foo"); assertEquals("

This isfoo jsoupfoo.

", TextUtil.stripNewlines(doc.body().html())); } + @Test public void afterNode() { + Document doc = Jsoup.parse("

This is jsoup.

"); + Element span = new Element("span").text("foo"); + doc.select("a").after(span); + assertEquals("

This isfoo jsoupfoo.

", TextUtil.stripNewlines(doc.body().html())); + assertNull(span.parent()); + } + + @Test public void prependAppendNode() { + Document doc = Jsoup.parse("

One

Two

Three

"); + Elements ps = doc.select("p"); + Element bold = new Element("b").text("Bold"); + Element ital = new Element("i").text("Ital"); + ps.prepend(bold).append(ital); + assertEquals("

BoldTwoItal

", TextUtil.stripNewlines(ps.get(1).outerHtml())); + assertNull(bold.parent()); + assertNull(ital.parent()); + } + @Test public void wrap() { String h = "

This is jsoup

"; Document doc = Jsoup.parse(h);