Repository: jhy/jsoup (java)

Issue:
Pretty printing consecutive text nodes should collapse consecutive whitespace
Since Jsoup 1.20.1, pretty-printing documents with consecutive text nodes does not fully collapse their whitespace:

```kotlin
val doc = Jsoup.parseBodyFragment("Before <span>Node</span> After")
doc.select("span").remove()
doc.toString()
```

```html
<html>
 <head></head>
 <body>Before  After</body>
</html>
```

(Note the double whitespace between "Before" and "After".)

Re-parsing the document and pretty-printing the result does then fully collapse the whitespace:

```kotlin
val doc = Jsoup.parseBodyFragment("Before <span>Node</span> After")
doc.select("span").remove()
Jsoup.parse(doc.toString()).toString()
```

```html
<html>
 <head></head>
 <body>Before After</body>
</html>
```

In my opinion, the pretty printer should produce the same result for consecutive or merged text nodes. Otherwise, the printer's output is not stable. (I.e. re-parsing and re-printing should not change the result.)

Pull request:
Collapse whitespace between adjacent TextNodes (#2347)
Fixes #2347

I'm not sure if this can be fixed more elegantly than looping over sequential text nodes. I'd prefer handling multiple text nodes in a single call to `Entities.doEscape`, but that would involve larger code changes.

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index a532a1fa..eb4e333b 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,10 @@
 # jsoup Changelog
 
+## 1.21.2 (PENDING)
+
+### Improvements
+* When pretty-printing, if there are consecutive text nodes (via DOM manipulation), the non-significant whitespace between them will be collapsed. [#2349](https://github.com/jhy/jsoup/pull/2349). 
+
 ## 1.21.1 (2025-Jun-23)
 
 ### Changes
diff --git a/src/main/java/org/jsoup/nodes/Printer.java b/src/main/java/org/jsoup/nodes/Printer.java
index 41ebbce9..20b3266d 100644
--- a/src/main/java/org/jsoup/nodes/Printer.java
+++ b/src/main/java/org/jsoup/nodes/Printer.java
@@ -126,8 +126,13 @@ class Printer implements NodeVisitor {
                     options |= Entities.TrimLeading;
             }
 
-            if (next == null || !(next instanceof TextNode) && shouldIndent(next))
-                options |= Entities.TrimTrailing; // don't trim if there is a TextNode sequence
+            if (next == null || !(next instanceof TextNode) && shouldIndent(next)) {
+                options |= Entities.TrimTrailing;
+            } else { // trim trailing whitespace if the next non-empty TextNode has leading whitespace
+                next = nextNonBlank(next);
+                if (next instanceof TextNode && StringUtil.isWhitespace(next.nodeValue().codePointAt(0)))
+                    options |= Entities.TrimTrailing;
+            }
 
             return options;
         }
@@ -141,7 +146,7 @@ class Printer implements NodeVisitor {
             Node prevSib = previousNonblank(node);
             if (isBlockEl(prevSib)) return true;
 
-            Element parent = (Element) node.parentNode;
+            Element parent = node.parentNode;
             if (!isBlockEl(parent) || parent.tag().is(Tag.InlineContainer) || !hasNonTextNodes(parent))
                 return false;
 


Test patch:
diff --git a/src/test/java/org/jsoup/nodes/NodeTest.java b/src/test/java/org/jsoup/nodes/NodeTest.java
index 16ecb0ec..51b4f71c 100644
--- a/src/test/java/org/jsoup/nodes/NodeTest.java
+++ b/src/test/java/org/jsoup/nodes/NodeTest.java
@@ -290,7 +290,7 @@ public class NodeTest {
         Document doc = Jsoup.parse("<div>One <span></span> Two</div>");
         Element span = doc.select("span").first();
         Node node = span.unwrap();
-        assertEquals("<div>One  Two</div>", TextUtil.stripNewlines(doc.body().html()));
+        assertEquals("<div>One Two</div>", TextUtil.stripNewlines(doc.body().html()));
         assertNull(node);
     }
 
diff --git a/src/test/java/org/jsoup/nodes/PrinterTest.java b/src/test/java/org/jsoup/nodes/PrinterTest.java
index c34add60..34b5a696 100644
--- a/src/test/java/org/jsoup/nodes/PrinterTest.java
+++ b/src/test/java/org/jsoup/nodes/PrinterTest.java
@@ -64,6 +64,18 @@ public class PrinterTest {
         assertEquals("<div>\n <div></div>\n Hello there.\n</div>", div.outerHtml());
     }
 
+    @Test void sequentialTextNodesCollapseAdjacentWhitespace() {
+        // https://github.com/jhy/jsoup/pull/2349
+        // Tests that the pretty printer collapses whitespace between sequential text nodes into a single space.
+        // This must also work with intermediate empty and blank text nodes.
+        Document doc = Jsoup.parseBodyFragment("Before <span> </span> After");
+        doc.expectFirst("span")
+                .after(new TextNode("")).after(new TextNode("")).after(new TextNode(" ")).after(new TextNode(""))
+                .remove();
+        assertEquals(6, doc.body().textNodes().size()); // no collapse before printing
+        assertEquals("Before After", doc.body().html());
+    }
+
     @Test void dontCollapseTextAfterNonElements() {
         Document doc = Jsoup.parse("<div><div></div>Hello <!-- -_- --> there</div>");
         Element body = doc.body();