From eff15210b0507f3eb20ff71a201061e841b40c77 Mon Sep 17 00:00:00 2001 From: Jonathan Hedley Date: Thu, 19 Oct 2023 09:19:02 +1100 Subject: [PATCH] Clear child nodes' parent on parent.empty() Fixes #2013 --- CHANGES | 5 +++ src/main/java/org/jsoup/nodes/Element.java | 7 ++- .../java/org/jsoup/nodes/ElementTest.java | 43 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index ed8d120c2c..e9265bd4e6 100644 --- a/CHANGES +++ b/CHANGES @@ -64,6 +64,11 @@ Release 1.16.2 [PENDING] Further, a StackOverflowError may occur when running the query. + * Bugfix: appending a node back to its original Element after empty() would throw an Index out of bounds exception. + Also, now the child nodes that were removed have their parent node cleared, fully detaching them from the original + parent. + + * Change: removed previously deprecated methods Document#normalise, Element#forEach(org.jsoup.helper.Consumer<>), Node#forEach(org.jsoup.helper.Consumer<>), and the org.jsoup.helper.Consumer interface; the latter being a previously required compatibility shim prior to Android's de-sugaring support. diff --git a/src/main/java/org/jsoup/nodes/Element.java b/src/main/java/org/jsoup/nodes/Element.java index 2205bae1e2..956d583377 100644 --- a/src/main/java/org/jsoup/nodes/Element.java +++ b/src/main/java/org/jsoup/nodes/Element.java @@ -829,11 +829,16 @@ public Element after(Node node) { } /** - * Remove all the element's child nodes. Any attributes are left as-is. + * Remove all the element's child nodes. Any attributes are left as-is. Each child node has its parent set to + * {@code null}. * @return this element */ @Override public Element empty() { + // Detach each of the children -> parent links: + for (Node child : childNodes) { + child.parentNode = null; + } childNodes.clear(); return this; } diff --git a/src/test/java/org/jsoup/nodes/ElementTest.java b/src/test/java/org/jsoup/nodes/ElementTest.java index 7d21613657..bba0940fc1 100644 --- a/src/test/java/org/jsoup/nodes/ElementTest.java +++ b/src/test/java/org/jsoup/nodes/ElementTest.java @@ -2773,4 +2773,47 @@ void prettySerializationRoundTrips(Document.OutputSettings settings) { "", out); // todo - I would prefer the to wrap down there - but need to reimplement pretty printer to simplify and track indented state } + + @Test void emptyDetachesChildren() { + String html = "

One

Two

Three
"; + Document doc = Jsoup.parse(html); + Element div = doc.expectFirst("div"); + assertEquals(3, div.childNodeSize()); + + List childNodes = div.childNodes(); + + div.empty(); + assertEquals(0, div.childNodeSize()); + assertEquals(3, childNodes.size()); // copied before removing + for (Node childNode : childNodes) { + assertNull(childNode.parentNode); + } + + Element p = (Element) childNodes.get(0); + assertEquals(p, p.childNode(0).parentNode()); // TextNode "One" still has parent p, as detachment is only on div element + } + + @Test void emptyAndAddPreviousChild() { + String html = "

One

Two

Three

"; + Document doc = Jsoup.parse(html); + Element div = doc.expectFirst("div"); + Element p = div.expectFirst("p"); + div + .empty() + .appendChild(p); + + assertEquals("

One

", div.html()); + } + + @Test void emptyAndAddPreviousDescendant() { + String html = "

One

Two

Three

"; + Document doc = Jsoup.parse(html); + Element header = doc.expectFirst("header"); + Element p = header.expectFirst("p"); + header + .empty() + .appendChild(p); + + assertEquals("

One

", header.html()); + } }