Repository: jhy/jsoup
Pull request #1899 (2023-02-20 23:56:33)

Bump maven-failsafe-plugin from 3.0.0-M8 to 3.0.0-M9
Bumps [maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.0.0-M8 to 3.0.0-M9.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/apache/maven-surefire/releases">maven-failsafe-plugin's releases</a>.</em></p>
<blockquote>
<h2>3.0.0-M9</h2>
<!-- raw HTML omitted -->
<h2>🚀 New features and improvements</h2>
<ul>
<li><a href="https://issues.apache.org/jira/browse/SUREFIRE-2146">[SUREFIRE-2146]</a> - Don't draw border around reporting tables (<a href="https://github-redirect.dependabot.com/apache/maven-surefire/pull/596">#596</a>) <a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
</ul>
<h2>🐛 Bug Fixes</h2>
<ul>
<li><a href="https://issues.apache.org/jira/browse/SUREFIRE-2145">[SUREFIRE-2145]</a> - Don't use Sink#figure()/Sink#figureCaption() since it… (<a href="https://github-redirect.dependabot.com/apache/maven-surefire/pull/595">#595</a>) <a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/SUREFIRE-2065">[SUREFIRE-2065]</a> - Fix parameterized JUnit4 test reporting (<a href="https://github-redirect.dependabot.com/apache/maven-surefire/pull/608">#608</a>) <a href="https://github.com/andpab"><code>@​andpab</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/SUREFIRE-2095">[SUREFIRE-2095]</a> - Fork crash doesn't fail build with -Dmaven.test.failure.ignore=true when run with failsafe (<a href="https://github-redirect.dependabot.com/apache/maven-surefire/pull/545">#545</a>) <a href="https://github.com/br0nstein"><code>@​br0nstein</code></a></li>
</ul>
<h2>📦 Dependency updates</h2>
<ul>
<li>Bump log4j-core from 2.13.1 to 2.17.1 in /surefire-its/src/test/resources/surefire-1659-stream-corruption (<a href="https://github-redirect.dependabot.com/apache/maven-surefire/pull/569">#569</a>) <a href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li>Bump junit from 4.12 to 4.13.1 in /surefire-its/src/test/resources/surefire-2065-junit4 (<a href="https://github-redirect.dependabot.com/apache/maven-surefire/pull/611">#611</a>) <a href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li>Bump commons-email from 1.2 to 1.5 in /surefire-its/src/test/resources/classpath-filtering (<a href="https://github-redirect.dependabot.com/apache/maven-surefire/pull/551">#551</a>) <a href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/apache/maven-surefire/commit/6463194dc397b282c2d8963ed8af96f2d12a7eb8"><code>6463194</code></a> [maven-release-plugin] prepare release surefire-3.0.0-M9</li>
<li><a href="https://github.com/apache/maven-surefire/commit/bdec059668cacd2a5744f69866cf0e41568ee616"><code>bdec059</code></a> [SUREFIRE-2145] Don't use Sink#figure()/Sink#figureCaption() since it distort...</li>
<li><a href="https://github.com/apache/maven-surefire/commit/c2c41804a1e80bea7a39b0c8fc2b4edfe731744c"><code>c2c4180</code></a> Bump log4j-core</li>
<li><a href="https://github.com/apache/maven-surefire/commit/c6ddebc08c398d40822fc3514f34cb7590e984cb"><code>c6ddebc</code></a> Bump junit in /surefire-its/src/test/resources/surefire-2065-junit4 (<a href="https://github-redirect.dependabot.com/apache/maven-surefire/issues/611">#611</a>)</li>
<li><a href="https://github.com/apache/maven-surefire/commit/ee5e81e2d768b9b7132bd4d844947d1991a483c9"><code>ee5e81e</code></a> [SUREFIRE-2146] Don't draw border around reporting tables (<a href="https://github-redirect.dependabot.com/apache/maven-surefire/issues/596">#596</a>)</li>
<li><a href="https://github.com/apache/maven-surefire/commit/11d05413b3104de6b856e7923ee7d327f3cba662"><code>11d0541</code></a> [SUREFIRE-2065] Fix parameterized JUnit4 test reporting (<a href="https://github-redirect.dependabot.com/apache/maven-surefire/issues/608">#608</a>)</li>
<li><a href="https://github.com/apache/maven-surefire/commit/b8887b43c59ffe9c446e265c290694fcb2583247"><code>b8887b4</code></a> Bump commons-email</li>
<li><a href="https://github.com/apache/maven-surefire/commit/59e096d9874e4cd719bef01e1dc45f9b5f7e9456"><code>59e096d</code></a> [SUREFIRE-2095] Fork crash doesn't fail build with -Dmaven.test.failure.ignor...</li>
<li><a href="https://github.com/apache/maven-surefire/commit/2c8fe402c11773c1bc75166327c3b5c4bf520d9e"><code>2c8fe40</code></a> [maven-release-plugin] prepare for next development iteration</li>
<li>See full diff in <a href="https://github.com/apache/maven-surefire/compare/surefire-3.0.0-M8...surefire-3.0.0-M9">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.apache.maven.plugins:maven-failsafe-plugin&package-manager=maven&previous-version=3.0.0-M8&new-version=3.0.0-M9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/CHANGES b/CHANGES
index 5382a458..5fd3b451 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,9 +1,34 @@
 jsoup changelog
 
 Release 1.16.1 [PENDING]
+  * Improvement: Calling Node.remove() on a node with no parent is now a no-op, vs a validation error.
+    <https://github.com/jhy/jsoup/issues/1898>
+
   * Bugfix: Corrected support for ruby elements (<ruby>, <rp>, <rt>, and <rtc>) to current spec.
     <https://github.com/jhy/jsoup/issues/1294>
 
+  * Bugfix: When using Node.before(node) or Node.after(node), if the incoming node was a sibling of the context node,
+    the incoming node may be inserted into the wrong relative location.
+    <https://github.com/jhy/jsoup/issues/1898>
+
+  * Bugfix: In Jsoup.connect(url), if the input URL had components that were already % escaped, they would be escaped
+    again, causing errors when fetched.
+    <https://github.com/jhy/jsoup/issues/1902>
+
+  * Bugfix: If the Document.OutputSettings class was initialized, and then Entities.escape(String) called, an NPE may be
+    thrown due to a class loading circular dependency.
+    <https://github.com/jhy/jsoup/issues/1910>
+
+  * Bugfix: when pretty-printing, the first inline Element or Comment in a block would not be wrap-indented if it were
+    preceded by a blank text node.
+    <https://github.com/jhy/jsoup/issues/1906>
+
+  * Bugfix: when pretty-printing a <pre> containing block tags, those tags were incorrectly indented.
+    <https://github.com/jhy/jsoup/issues/1891>
+
+  * Bugfix: <br> tags should be wrap-indented when in block tags (and not when in inline tags).
+    <https://github.com/jhy/jsoup/issues/1911>
+
 Release 1.15.4 [18-Feb-2023]
   * Improvement: added the ability to escape CSS selectors (tags, IDs, classes) to match elements that don't follow
     regular CSS syntax. For example, to match by classname <p class="one.two">, use document.select("p.one\\.two");
diff --git a/pom.xml b/pom.xml
index 94a4409b..4f5fb178 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,6 +33,7 @@
 
   <properties>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <jetty.version>9.4.51.v20230217</jetty.version>
   </properties>
 
   <build>
@@ -40,7 +41,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-compiler-plugin</artifactId>
-        <version>3.10.1</version>
+        <version>3.11.0</version>
         <configuration>
           <source>1.8</source>
           <target>1.8</target>
@@ -180,7 +181,7 @@
       </plugin>
       <plugin>
         <artifactId>maven-failsafe-plugin</artifactId>
-        <version>3.0.0-M8</version>
+        <version>3.0.0-M9</version>
         <executions>
           <execution>
             <goals>
@@ -304,7 +305,7 @@
         <plugins>
           <plugin>
             <artifactId>maven-failsafe-plugin</artifactId>
-            <version>3.0.0-M8</version>
+            <version>3.0.0-M9</version>
             <executions>
               <execution>
                 <goals>
@@ -341,7 +342,7 @@
       <!-- jetty for webserver integration tests. 9.x is last with Java7 support -->
       <groupId>org.eclipse.jetty</groupId>
       <artifactId>jetty-server</artifactId>
-      <version>9.4.50.v20221201</version>
+      <version>${jetty.version}</version>
       <scope>test</scope>
     </dependency>
 
@@ -349,7 +350,7 @@
       <!-- jetty for webserver integration tests -->
       <groupId>org.eclipse.jetty</groupId>
       <artifactId>jetty-servlet</artifactId>
-      <version>9.4.50.v20221201</version>
+      <version>${jetty.version}</version>
       <scope>test</scope>
     </dependency>
 
diff --git a/src/main/java/org/jsoup/helper/HttpConnection.java b/src/main/java/org/jsoup/helper/HttpConnection.java
index d183a521..88595c3f 100644
--- a/src/main/java/org/jsoup/helper/HttpConnection.java
+++ b/src/main/java/org/jsoup/helper/HttpConnection.java
@@ -20,6 +20,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
 import java.net.CookieManager;
 import java.net.CookieStore;
 import java.net.HttpURLConnection;
@@ -30,6 +31,7 @@ import java.net.Proxy;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.net.URLDecoder;
 import java.net.URLEncoder;
 import java.nio.Buffer;
 import java.nio.ByteBuffer;
@@ -127,14 +129,21 @@ public class HttpConnection implements Connection {
 	    u = punyUrl(u);
         try {
             // run the URL through URI, so components are encoded
-            URI uri = new URI(u.getProtocol(), u.getUserInfo(), u.getHost(), u.getPort(), u.getPath(), u.getQuery(), u.getRef());
+            URI uri = new URI(
+                u.getProtocol(), decodePart(u.getUserInfo()), u.getHost(), u.getPort(),
+                decodePart(u.getPath()), decodePart(u.getQuery()), decodePart(u.getRef()));
             return uri.toURL();
-        } catch (URISyntaxException | MalformedURLException e) {
+        } catch (URISyntaxException | MalformedURLException | UnsupportedEncodingException e) {
             // give up and return the original input
             return u;
         }
     }
 
+    @Nullable private static String decodePart(@Nullable String encoded) throws UnsupportedEncodingException {
+        if (encoded == null) return null;
+        return URLDecoder.decode(encoded, UTF_8.name());
+    }
+
     /**
      Convert an International URL to a Punycode URL.
      @param url input URL that may include an international hostname
diff --git a/src/main/java/org/jsoup/nodes/Comment.java b/src/main/java/org/jsoup/nodes/Comment.java
index 8ac8f702..f7fc9f3c 100644
--- a/src/main/java/org/jsoup/nodes/Comment.java
+++ b/src/main/java/org/jsoup/nodes/Comment.java
@@ -38,7 +38,7 @@ public class Comment extends LeafNode {
 
     @Override
 	void outerHtmlHead(Appendable accum, int depth, Document.OutputSettings out) throws IOException {
-        if (out.prettyPrint() && ((siblingIndex() == 0 && parentNode instanceof Element && ((Element) parentNode).tag().formatAsBlock()) || (out.outline() )))
+        if (out.prettyPrint() && ((isEffectivelyFirst() && parentNode instanceof Element && ((Element) parentNode).tag().formatAsBlock()) || (out.outline() )))
             indent(accum, depth, out);
         accum
                 .append("<!--")
diff --git a/src/main/java/org/jsoup/nodes/Element.java b/src/main/java/org/jsoup/nodes/Element.java
index 507fce96..5142fa2b 100644
--- a/src/main/java/org/jsoup/nodes/Element.java
+++ b/src/main/java/org/jsoup/nodes/Element.java
@@ -1670,7 +1670,7 @@ public class Element extends Node {
     }
 
     boolean shouldIndent(final Document.OutputSettings out) {
-        return out.prettyPrint() && isFormatAsBlock(out) && !isInlineable(out);
+        return out.prettyPrint() && isFormatAsBlock(out) && !isInlineable(out) && !preserveWhitespace(parentNode);
     }
 
     @Override
@@ -1701,7 +1701,8 @@ public class Element extends Node {
     void outerHtmlTail(Appendable accum, int depth, Document.OutputSettings out) throws IOException {
         if (!(childNodes.isEmpty() && tag.isSelfClosing())) {
             if (out.prettyPrint() && (!childNodes.isEmpty() && (
-                    tag.formatAsBlock() || (out.outline() && (childNodes.size()>1 || (childNodes.size()==1 && (childNodes.get(0) instanceof Element))))
+                (tag.formatAsBlock() && !preserveWhitespace(parentNode)) ||
+                    (out.outline() && (childNodes.size()>1 || (childNodes.size()==1 && (childNodes.get(0) instanceof Element))))
             )))
                 indent(accum, depth, out);
             accum.append("</").append(tagName()).append('>');
@@ -1846,9 +1847,11 @@ public class Element extends Node {
     }
 
     private boolean isInlineable(Document.OutputSettings out) {
-        return tag().isInline()
-            && (parent() == null || parent().isBlock())
-            && previousSibling() != null
-            && !out.outline();
+        if (!tag.isInline())
+            return false;
+        return (parent() == null || parent().isBlock())
+            && !isEffectivelyFirst()
+            && !out.outline()
+            && !isNode("br");
     }
 }
diff --git a/src/main/java/org/jsoup/nodes/Entities.java b/src/main/java/org/jsoup/nodes/Entities.java
index d330005d..be447bce 100644
--- a/src/main/java/org/jsoup/nodes/Entities.java
+++ b/src/main/java/org/jsoup/nodes/Entities.java
@@ -7,6 +7,7 @@ import org.jsoup.nodes.Document.OutputSettings;
 import org.jsoup.parser.CharacterReader;
 import org.jsoup.parser.Parser;
 
+import javax.annotation.Nullable;
 import java.io.IOException;
 import java.nio.charset.CharsetEncoder;
 import java.util.Arrays;
@@ -26,7 +27,6 @@ public class Entities {
     static final int codepointRadix = 36;
     private static final char[] codeDelims = {',', ';'};
     private static final HashMap<String, String> multipoints = new HashMap<>(); // name -> multiple character references
-    private static final OutputSettings DefaultOutput = new OutputSettings();
 
     public enum EscapeMode {
         /**
@@ -157,8 +157,11 @@ public class Entities {
      * @return the escaped string
      */
     public static String escape(String string) {
+        if (DefaultOutput == null)
+            DefaultOutput = new OutputSettings();
         return escape(string, DefaultOutput);
     }
+    private static @Nullable OutputSettings DefaultOutput; // lazy-init, to break circular dependency with OutputSettings
 
     // this method does a lot, but other breakups cause rescanning and stringbuilder generations
     static void escape(Appendable accum, String string, OutputSettings out,
diff --git a/src/main/java/org/jsoup/nodes/Node.java b/src/main/java/org/jsoup/nodes/Node.java
index 60f99420..851bd8a5 100644
--- a/src/main/java/org/jsoup/nodes/Node.java
+++ b/src/main/java/org/jsoup/nodes/Node.java
@@ -315,11 +315,12 @@ public abstract class Node implements Cloneable {
     }
 
     /**
-     * Remove (delete) this node from the DOM tree. If this node has children, they are also removed.
+     * Remove (delete) this node from the DOM tree. If this node has children, they are also removed. If this node is
+     * an orphan, nothing happens.
      */
     public void remove() {
-        Validate.notNull(parentNode);
-        parentNode.removeChild(this);
+        if (parentNode != null)
+            parentNode.removeChild(this);
     }
 
     /**
@@ -343,6 +344,9 @@ public abstract class Node implements Cloneable {
         Validate.notNull(node);
         Validate.notNull(parentNode);
 
+        // if the incoming node is a sibling of this, remove it first so siblingIndex is correct on add
+        if (node.parentNode == parentNode) node.remove();
+
         parentNode.addChildren(siblingIndex, node);
         return this;
     }
@@ -368,6 +372,9 @@ public abstract class Node implements Cloneable {
         Validate.notNull(node);
         Validate.notNull(parentNode);
 
+        // if the incoming node is a sibling of this, remove it first so siblingIndex is correct on add
+        if (node.parentNode == parentNode) node.remove();
+
         parentNode.addChildren(siblingIndex + 1, node);
         return this;
     }
@@ -755,6 +762,16 @@ public abstract class Node implements Cloneable {
         return normalName().equals(normalName);
     }
 
+    /** Test if this node is the first child, or first following blank text. */
+    final boolean isEffectivelyFirst() {
+        if (siblingIndex == 0) return true;
+        if (siblingIndex == 1) {
+            final Node prev = previousSibling();
+            return prev instanceof TextNode && (((TextNode) prev).isBlank());
+        }
+        return false;
+    }
+
     /**
      * Gets this node's outer HTML.
      * @return outer HTML.
diff --git a/src/main/java/org/jsoup/parser/Tag.java b/src/main/java/org/jsoup/parser/Tag.java
index d573033a..366bc63e 100644
--- a/src/main/java/org/jsoup/parser/Tag.java
+++ b/src/main/java/org/jsoup/parser/Tag.java
@@ -242,7 +242,7 @@ public class Tag implements Cloneable {
     };
     private static final String[] inlineTags = {
             "object", "base", "font", "tt", "i", "b", "u", "big", "small", "em", "strong", "dfn", "code", "samp", "kbd",
-            "var", "cite", "abbr", "time", "acronym", "mark", "ruby", "rt", "rp", "a", "img", "br", "wbr", "map", "q",
+            "var", "cite", "abbr", "time", "acronym", "mark", "ruby", "rt", "rp", "rtc", "a", "img", "br", "wbr", "map", "q",
             "sub", "sup", "bdo", "iframe", "embed", "span", "input", "select", "textarea", "label", "button", "optgroup",
             "option", "legend", "datalist", "keygen", "output", "progress", "meter", "area", "param", "source", "track",
             "summary", "command", "device", "area", "basefont", "bgsound", "menuitem", "param", "source", "track",


Test patch:
diff --git a/src/test/java/org/jsoup/helper/HttpConnectionTest.java b/src/test/java/org/jsoup/helper/HttpConnectionTest.java
index f7618fb9..b655f8ff 100644
--- a/src/test/java/org/jsoup/helper/HttpConnectionTest.java
+++ b/src/test/java/org/jsoup/helper/HttpConnectionTest.java
@@ -260,6 +260,20 @@ public class HttpConnectionTest {
         assertEquals("https://test.com/foo%20bar/%5BOne%5D?q=white%20space#frag", url2.toExternalForm());
     }
 
+    @Test void encodedUrlDoesntDoubleEncode() throws MalformedURLException {
+        URL url1 = new URL("https://test.com/foo bar/[One]?q=white space#frag ment");
+        URL url2 = HttpConnection.encodeUrl(url1);
+        URL url3 = HttpConnection.encodeUrl(url2);
+        assertEquals("https://test.com/foo%20bar/%5BOne%5D?q=white%20space#frag%20ment", url2.toExternalForm());
+        assertEquals("https://test.com/foo%20bar/%5BOne%5D?q=white%20space#frag%20ment", url3.toExternalForm());
+    }
+
+    @Test void connectToEncodedUrl() {
+        Connection connect = Jsoup.connect("https://example.com/a%20b%20c?query+string");
+        URL url = connect.request().url();
+        assertEquals("https://example.com/a%20b%20c?query%20string", url.toExternalForm());
+    }
+
     @Test public void noUrlThrowsValidationError() throws IOException {
         HttpConnection con = new HttpConnection();
         boolean threw = false;
diff --git a/src/test/java/org/jsoup/nodes/CommentTest.java b/src/test/java/org/jsoup/nodes/CommentTest.java
index fe1c1361..c622f184 100644
--- a/src/test/java/org/jsoup/nodes/CommentTest.java
+++ b/src/test/java/org/jsoup/nodes/CommentTest.java
@@ -41,6 +41,20 @@ public class CommentTest {
         assertEquals("<!-- a simple comment -->", c1.outerHtml());
     }
 
+    @Test void stableIndentInBlock() {
+        String html = "<div><!-- comment --> Text</div><p><!-- comment --> Text</p>";
+        Document doc = Jsoup.parse(html);
+        String out = doc.body().html();
+        assertEquals("<div>\n" +
+            " <!-- comment --> Text\n" +
+            "</div>\n" +
+            "<p><!-- comment --> Text</p>", out);
+
+        Document doc2 = Jsoup.parse(out);
+        String out2 = doc2.body().html();
+        assertEquals(out, out2);
+    }
+
     @Test
     public void testClone() {
         Comment c1 = comment.clone();
diff --git a/src/test/java/org/jsoup/nodes/ElementTest.java b/src/test/java/org/jsoup/nodes/ElementTest.java
index 0887a4e7..0bb433ce 100644
--- a/src/test/java/org/jsoup/nodes/ElementTest.java
+++ b/src/test/java/org/jsoup/nodes/ElementTest.java
@@ -157,6 +157,14 @@ public class ElementTest {
         assertEquals("<pre><code><span><b>code\n\ncode</b></span></code></pre>", doc.body().html());
     }
 
+    @Test void doesNotWrapBlocksInPre() {
+        // https://github.com/jhy/jsoup/issues/1891
+        String h = "<pre><span><foo><div>TEST\n TEST</div></foo></span></pre>";
+        Document doc = Jsoup.parse(h);
+        assertEquals("TEST\n TEST", doc.wholeText());
+        assertEquals(h, doc.body().html());
+    }
+
     @Test
     public void testBrHasSpace() {
         Document doc = Jsoup.parse("<p>Hello<br>there</p>");
@@ -2306,6 +2314,17 @@ public class ElementTest {
         assertEquals("<div>\n <br>\n</div>", doc.body().html()); // not div\n br\n \n/div
     }
 
+    @Test void prettyprintBrWhenNotFirstChild() {
+        // https://github.com/jhy/jsoup/issues/1911
+        String h = "<div><p><br>Foo</p><br></div>";
+        Document doc = Jsoup.parse(h);
+        assertEquals("<div>\n" +
+            " <p><br>\n  Foo</p>\n" +
+            " <br>\n" +
+            "</div>", doc.body().html());
+        // br gets wrapped if in div, but not in p (block vs inline), but always wraps after
+    }
+
     @Test void preformatFlowsToChildTextNodes() {
         // https://github.com/jhy/jsoup/issues/1776
         String html = "<div><pre>One\n<span>\nTwo</span>\n <span>  \nThree</span>\n <span>Four <span>Five</span>\n  Six\n</pre>";
@@ -2343,6 +2362,23 @@ public class ElementTest {
         assertEquals(expectOwn, div.child(0).wholeOwnText());
     }
 
+    @Test void inlineInBlockShouldIndent() {
+        // was inconsistent between <div>\n<span> and <div><span> - former would print inline, latter would wrap(!)
+        String html = "<div>One <span>Hello</span><span>!</span></div><div>\n<span>There</span></div><div> <span>Now</span></div>";
+        Document doc = Jsoup.parse(html);
+        assertEquals(
+            "<div>\n" +
+                " One <span>Hello</span><span>!</span>\n" +
+                "</div>\n" +
+                "<div>\n" +
+                " <span>There</span>\n" +
+                "</div>\n" +
+                "<div>\n" +
+                " <span>Now</span>\n" +
+                "</div>",
+            doc.body().html());
+    }
+
     @Test void testExpectFirst() {
         Document doc = Jsoup.parse("<p>One</p><p>Two <span>Three</span> <span>Four</span>");
 
@@ -2445,7 +2481,9 @@ public class ElementTest {
     @Test void divAInlineable() {
         String html = "<body><div> <a>Text</a>";
         Document doc = Jsoup.parse(html);
-        assertEquals("<div><a>Text</a>\n</div>", doc.body().html());
+        assertEquals("<div>\n" +
+            " <a>Text</a>\n" +
+            "</div>", doc.body().html());
     }
 
     @Test void noDanglingSpaceAfterCustomElement() {
@@ -2657,4 +2695,10 @@ public class ElementTest {
         doc.body().outerHtml(builder);
         assertEquals("<body>\n <div>\n  One\n </div>\n</body>", builder.toString());
     }
-}
\ No newline at end of file
+
+    @Test void rubyInline() {
+        String html = "<ruby>T<rp>(</rp><rtc>!</rtc><rt>)</rt></ruby>";
+        Document doc = Jsoup.parse(html);
+        assertEquals(html, doc.body().html());
+    }
+}
diff --git a/src/test/java/org/jsoup/nodes/NodeTest.java b/src/test/java/org/jsoup/nodes/NodeTest.java
index 5cd22c55..e0622be5 100644
--- a/src/test/java/org/jsoup/nodes/NodeTest.java
+++ b/src/test/java/org/jsoup/nodes/NodeTest.java
@@ -3,6 +3,7 @@ package org.jsoup.nodes;
 import org.jsoup.Jsoup;
 import org.jsoup.TextUtil;
 import org.jsoup.parser.Tag;
+import org.jsoup.select.Elements;
 import org.jsoup.select.NodeVisitor;
 import org.junit.jupiter.api.Test;
 
@@ -156,6 +157,14 @@ public class NodeTest {
         assertEquals("<span>two</span> three", TextUtil.stripNewlines(p.html()));
     }
 
+    @Test void removeOnOrphanIsNoop() {
+        // https://github.com/jhy/jsoup/issues/1898
+        Element node = new Element("div");
+        assertNull(node.parentNode());
+        node.remove();
+        assertNull(node.parentNode());
+    }
+
     @Test public void testReplace() {
         Document doc = Jsoup.parse("<p>One <span>two</span> three</p>");
         Element p = doc.select("p").first();
@@ -200,6 +209,25 @@ public class NodeTest {
         assertEquals("<p>One <em>four</em><i>five</i><b>two</b> three</p>", doc.body().html());
     }
 
+    @Test void beforeShuffle() {
+        // https://github.com/jhy/jsoup/issues/1898
+        Document doc = Jsoup.parse("<div><p>One<p>Two<p>Three</div>");
+        Element div = doc.select("div").get(0);
+        Elements ps = doc.select("p");
+        Element p1 = ps.get(0);
+        Element p2 = ps.get(1);
+        Element p3 = ps.get(2);
+
+        p2.before(p1);
+        p3.before(p2);
+        // ^ should be no-ops, they are already before
+        assertEquals("One Two Three", div.text());
+
+        p2.before(p1);
+        p1.before(p3);
+        assertEquals("Three One Two", div.text());
+    }
+
     @Test public void after() {
         Document doc = Jsoup.parse("<p>One <b>two</b> three</p>");
         Element newNode = new Element(Tag.valueOf("em"), "");
@@ -212,6 +240,25 @@ public class NodeTest {
         assertEquals("<p>One <b>two</b><i>five</i><em>four</em> three</p>", doc.body().html());
     }
 
+    @Test void afterShuffle() {
+        // https://github.com/jhy/jsoup/issues/1898
+        Document doc = Jsoup.parse("<div><p>One<p>Two<p>Three</div>");
+        Element div = doc.select("div").get(0);
+        Elements ps = doc.select("p");
+        Element p1 = ps.get(0);
+        Element p2 = ps.get(1);
+        Element p3 = ps.get(2);
+
+        p1.after(p2);
+        p2.after(p3);
+        // ^ should be no-ops, they are already before
+        assertEquals("One Two Three", div.text());
+
+        p3.after(p1);
+        p1.after(p2);
+        assertEquals("Three One Two", div.text());
+    }
+
     @Test public void unwrap() {
         Document doc = Jsoup.parse("<div>One <span>Two <b>Three</b></span> Four</div>");
         Element span = doc.select("span").first();
diff --git a/src/test/java/org/jsoup/parser/HtmlParserTest.java b/src/test/java/org/jsoup/parser/HtmlParserTest.java
index 1f83518e..26b546d5 100644
--- a/src/test/java/org/jsoup/parser/HtmlParserTest.java
+++ b/src/test/java/org/jsoup/parser/HtmlParserTest.java
@@ -1247,7 +1247,7 @@ public class HtmlParserTest {
         File in = ParseTest.getFile("/htmltests/comments.html");
         Document doc = Jsoup.parse(in, "UTF-8");
 
-        assertEquals("<!--?xml version=\"1.0\" encoding=\"utf-8\"?--><!-- so --> <!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"><!-- what --> <html xml:lang=\"en\" lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\"><!-- now --> <head><!-- then --> <meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\"> <title>A Certain Kind of Test</title> </head> <body> <h1>Hello</h1>h1&gt; (There is a UTF8 hidden BOM at the top of this file.) </body> </html>",
+        assertEquals("<!--?xml version=\"1.0\" encoding=\"utf-8\"?--><!-- so --> <!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"><!-- what --> <html xml:lang=\"en\" lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\"> <!-- now --> <head> <!-- then --> <meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\"> <title>A Certain Kind of Test</title> </head> <body> <h1>Hello</h1>h1&gt; (There is a UTF8 hidden BOM at the top of this file.) </body> </html>",
             StringUtil.normaliseWhitespace(doc.html()));
 
         assertEquals("A Certain Kind of Test", doc.head().select("title").text());
diff --git a/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java b/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java
index f05ee268..c2dbfe31 100644
--- a/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java
+++ b/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java
@@ -75,7 +75,8 @@ public class HtmlTreeBuilderStateTest {
         String s = Jsoup.parse(html).toString();
         assertEquals("<html>\n" +
             " <head></head>\n" +
-            " <body><a href=\"#1\"> </a>\n" +
+            " <body>\n" +
+            "  <a href=\"#1\"> </a>\n" +
             "  <div>\n" +
             "   <a href=\"#1\"> </a><a href=\"#2\">child</a>\n" +
             "  </div>\n" +
@@ -99,7 +100,8 @@ public class HtmlTreeBuilderStateTest {
         String s = Jsoup.parse(html).toString();
         assertEquals("<html>\n" +
             " <head></head>\n" +
-            " <body><a href=\"#1\"> </a>\n" +
+            " <body>\n" +
+            "  <a href=\"#1\"> </a>\n" +
             "  <div>\n" +
             "   <a href=\"#1\"> </a>\n" +
             "   <div>\n" +
diff --git a/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java b/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java
index 227dd096..d359a528 100644
--- a/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java
+++ b/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java
@@ -94,7 +94,7 @@ public class XmlTreeBuilderTest {
     public void testDoesNotForceSelfClosingKnownTags() {
         // html will force "<br>one</br>" to logically "<br />One<br />". XML should be stay "<br>one</br> -- don't recognise tag.
         Document htmlDoc = Jsoup.parse("<br>one</br>");
-        assertEquals("<br>\none<br>", htmlDoc.body().html());
+        assertEquals("<br>\none\n<br>", htmlDoc.body().html());
 
         Document xmlDoc = Jsoup.parse("<br>one</br>", "", Parser.xmlParser());
         assertEquals("<br>one</br>", xmlDoc.html());
diff --git a/src/test/java/org/jsoup/select/ElementsTest.java b/src/test/java/org/jsoup/select/ElementsTest.java
index eab17c2c..d1895752 100644
--- a/src/test/java/org/jsoup/select/ElementsTest.java
+++ b/src/test/java/org/jsoup/select/ElementsTest.java
@@ -193,7 +193,9 @@ public class ElementsTest {
         String h = "<div><font>One</font> <font><a href=\"/\">Two</a></font></div";
         Document doc = Jsoup.parse(h);
         doc.select("font").unwrap();
-        assertEquals("<div>One <a href=\"/\">Two</a></div>", TextUtil.stripNewlines(doc.body().html()));
+        assertEquals("<div>\n" +
+            " One <a href=\"/\">Two</a>\n" +
+            "</div>", doc.body().html());
     }
 
     @Test public void unwrapP() {