Repository: jhy/jsoup
Pull request #1971 (2023-06-12 23:56:36)

Bump maven-surefire-plugin from 3.1.0 to 3.1.2
Bumps [maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.1.0 to 3.1.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/apache/maven-surefire/releases">maven-surefire-plugin's releases</a>.</em></p>
<blockquote>
<h2>3.1.2</h2>
<h2><a href="https://issues.apache.org/jira/secure/ReleaseNote.jspa?projectId=12317927&amp;version=12353294">Release Notes - Maven Surefire - Version 3.1.2</a></h2>
<!-- raw HTML omitted -->
<ul>
<li>update commons compress to 1.23.0 (<a href="https://redirect.github.com/apache/maven-surefire/pull/655">#655</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li>Drop unused commons-lang 2.6 from management (<a href="https://redirect.github.com/apache/maven-surefire/pull/661">#661</a>) <a href="https://github.com/elilja"><code>@​elilja</code></a></li>
<li>Remove old junittoolbox dependency no longer used (<a href="https://redirect.github.com/apache/maven-surefire/pull/658">#658</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li>update test libraries (<a href="https://redirect.github.com/apache/maven-surefire/pull/657">#657</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li>update commons-io to 2.12.0 (<a href="https://redirect.github.com/apache/maven-surefire/pull/653">#653</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li>remove descriptions of Maven 2.x and very old versions (<a href="https://redirect.github.com/apache/maven-surefire/pull/647">#647</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li>Remove more details about Maven 2 and old versions (<a href="https://redirect.github.com/apache/maven-surefire/pull/654">#654</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li>Simplify method (<a href="https://redirect.github.com/apache/maven-surefire/pull/651">#651</a>) <a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
<li>Update Sufefire (<a href="https://redirect.github.com/apache/maven-surefire/pull/646">#646</a>) <a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
<li>Backport (<a href="https://redirect.github.com/apache/maven-surefire/pull/635">#635</a>) <a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/SUREFIRE-2166">[SUREFIRE-2166]</a> - Use ChoiceFormat to selective render percentage and elapsed time in SurefireReportRenderer (<a href="https://redirect.github.com/apache/maven-surefire/pull/639">#639</a>) <a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
<li>Simplify serialization/deserialization of elapsed time (SUREFIRE-2164 + SUREFIRE-2167) (<a href="https://redirect.github.com/apache/maven-surefire/pull/645">#645</a>) <a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/SUREFIRE-2169">[SUREFIRE-2169]</a> - Potential NPE in WrappedReportEntry when #getElapsed(… (<a href="https://redirect.github.com/apache/maven-surefire/pull/643">#643</a>) <a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
<li>fix SAX bug (<a href="https://redirect.github.com/apache/maven-surefire/pull/642">#642</a>) <a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MNG-6829">[MNG-6829]</a> - Replace StringUtils#isEmpty(String) &amp; #isNotEmpty(String) (<a href="https://redirect.github.com/apache/maven-surefire/pull/641">#641</a>) <a href="https://github.com/timtebeek"><code>@​timtebeek</code></a></li>
<li>Fixes (<a href="https://redirect.github.com/apache/maven-surefire/pull/636">#636</a>) <a href="https://github.com/michael-o"><code>@​michael-o</code></a></li>
</ul>
<h2>📦 Dependency updates</h2>
<ul>
<li>Bump animal-sniffer-maven-plugin from 1.21 to 1.23 (<a href="https://redirect.github.com/apache/maven-surefire/pull/624">#624</a>) <a href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
</ul>
<h2>👻 Maintenance</h2>
<ul>
<li><a href="https://issues.apache.org/jira/browse/SUREFIRE-2173">[SUREFIRE-2173]</a> - Pass build and tests with Java 20 (<a href="https://redirect.github.com/apache/maven-surefire/pull/648">#648</a>) <a href="https://github.com/slachiewicz"><code>@​slachiewicz</code></a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/apache/maven-surefire/commit/5e097b58edfe8317ef043e0d2e553cb475305b67"><code>5e097b5</code></a> [maven-release-plugin] prepare release surefire-3.1.2</li>
<li><a href="https://github.com/apache/maven-surefire/commit/255bb51cc0cdc627d1570a073d8c38e52a6ca55d"><code>255bb51</code></a> Update commons compress to 1.23.0</li>
<li><a href="https://github.com/apache/maven-surefire/commit/a77dfb24f8872b44635204d8bab844c78a022cf6"><code>a77dfb2</code></a> Drop unused commons-lang 2.6 from management (<a href="https://redirect.github.com/apache/maven-surefire/issues/661">#661</a>)</li>
<li><a href="https://github.com/apache/maven-surefire/commit/95e8e95ee02ee0e12af70850440bd3d4c053d5db"><code>95e8e95</code></a> [SUREFIRE-2157] Upgrade junit-jupiter to 5.9.3/junit-platform to 1.9.2</li>
<li><a href="https://github.com/apache/maven-surefire/commit/9092b5ec59ef638a6d9d84ca940cca597594c560"><code>9092b5e</code></a> [SUREFIRE-2157] Upgrade surefire IT to 3.1.0</li>
<li><a href="https://github.com/apache/maven-surefire/commit/1af92f5ffe91d111278ee9b88dddcf6721885c6c"><code>1af92f5</code></a> Remove old junittoolbox dependency no longer used (<a href="https://redirect.github.com/apache/maven-surefire/issues/658">#658</a>)</li>
<li><a href="https://github.com/apache/maven-surefire/commit/8878ed5e5cda4e0d365bf19e1d5d15fece8a5d17"><code>8878ed5</code></a> update test libraries (<a href="https://redirect.github.com/apache/maven-surefire/issues/657">#657</a>)</li>
<li><a href="https://github.com/apache/maven-surefire/commit/a6613a815018c832cf0bd5fd1a7f37731a727e79"><code>a6613a8</code></a> Remove redundant space</li>
<li><a href="https://github.com/apache/maven-surefire/commit/e0e89e44ab78445b25cc99bf5f7f2b2b1430756b"><code>e0e89e4</code></a> update commons-io to 2.12.0</li>
<li><a href="https://github.com/apache/maven-surefire/commit/d8ac134c3ac8019043ce4c7f523e1eaf5b26055b"><code>d8ac134</code></a> [MNGSITE-393] remove descriptions of Maven 2.x and very old versions</li>
<li>Additional commits viewable in <a href="https://github.com/apache/maven-surefire/compare/surefire-3.1.0...surefire-3.1.2">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-surefire-plugin&package-manager=maven&previous-version=3.1.0&new-version=3.1.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR 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>

> **Note**
> Automatic rebases have been disabled on this pull request as it has been open for over 30 days.



Patch:
diff --git a/.gitignore b/.gitignore
index c696abb0..b22249a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ target/
 .settings/
 *Thrash*
 bin/
+.vscode/
diff --git a/CHANGES b/CHANGES
index 42d63a34..989650e8 100644
--- a/CHANGES
+++ b/CHANGES
@@ -24,9 +24,32 @@ Release 1.16.2 [PENDING]
 
   * Improvement: tweaked the performance of parsing :pseudo selectors.
 
+  * Improvement: when using the `:empty` pseudo-selector, blank textnodes are now considered empty. Previously,
+    an element containing any whitespace was not considered empty.
+    <https://github.com/jhy/jsoup/issues/1976>
+
   * Bugfix: `form` elements and empty elements (such as `img`) did not have their attributes de-duplicated.
     <https://github.com/jhy/jsoup/pull/1950>
 
+  * Bugfix: if Document.OutputSettings was cloned from a clone, an NPE would be thrown when used.
+    <https://github.com/jhy/jsoup/pull/1964>
+
+  * Bugfix: in Jsoup.connect(url), URL paths containing a %2B were incorrectly recoded to a '+', or a '+' was recoded
+    to a ' '. Fixed by reverting to the previous behavior of not encoding supplied paths, other than normalizing to
+    ASCII.
+    <https://github.com/jhy/jsoup/issues/1952>
+
+  * Bugfix: in Jsoup.connect(url), the ConstrainableInputStream would clear Thread interrupts when reading the body.
+    This precluded callers from spawning a thread, running a number of requests for a length of time, then joining that
+    thread after interrupting it.
+    <https://github.com/jhy/jsoup/issues/1991>
+
+  * Bugfix: when tracking HTML source positions, the closing tags for H1...H6 elements were not tracked correctly.
+    <https://github.com/jhy/jsoup/issues/1987>
+
+  * Bugfix: in Jsoup.connect(), a DELETE method request did not support a request body.
+    <https://github.com/jhy/jsoup/issues/1972>
+
   * 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/pom.xml b/pom.xml
index 48ec01bb..ac85c8c2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -109,7 +109,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-source-plugin</artifactId>
-        <version>3.2.1</version>
+        <version>3.3.0</version>
         <configuration>
           <excludes>
             <exclude>org/jsoup/examples/**</exclude>
@@ -147,7 +147,7 @@
       <plugin>
         <groupId>org.apache.felix</groupId>
         <artifactId>maven-bundle-plugin</artifactId>
-        <version>5.1.8</version>
+        <version>5.1.9</version>
         <executions>
           <execution>
             <id>bundle-manifest</id>
@@ -172,12 +172,12 @@
       </plugin>
       <plugin>
         <artifactId>maven-release-plugin</artifactId>
-        <version>3.0.0</version>
+        <version>3.0.1</version>
       </plugin>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-plugin</artifactId>
-        <version>3.1.0</version>
+        <version>3.1.2</version>
         <configuration>
           <!-- smaller stack to find stack overflows -->
           <argLine>-Xss256k</argLine>
@@ -185,7 +185,7 @@
       </plugin>
       <plugin>
         <artifactId>maven-failsafe-plugin</artifactId>
-        <version>3.1.0</version>
+        <version>3.1.2</version>
         <executions>
           <execution>
             <goals>
@@ -307,7 +307,7 @@
         <plugins>
           <plugin>
             <artifactId>maven-failsafe-plugin</artifactId>
-            <version>3.1.0</version>
+            <version>3.1.2</version>
             <executions>
               <execution>
                 <goals>
diff --git a/src/main/java/org/jsoup/Connection.java b/src/main/java/org/jsoup/Connection.java
index ef64faa4..c6afd7f4 100644
--- a/src/main/java/org/jsoup/Connection.java
+++ b/src/main/java/org/jsoup/Connection.java
@@ -43,7 +43,7 @@ public interface Connection {
      * GET and POST http methods.
      */
     enum Method {
-        GET(false), POST(true), PUT(true), DELETE(false), PATCH(true), HEAD(false), OPTIONS(false), TRACE(false);
+        GET(false), POST(true), PUT(true), DELETE(true), PATCH(true), HEAD(false), OPTIONS(false), TRACE(false);
 
         private final boolean hasBody;
 
diff --git a/src/main/java/org/jsoup/helper/UrlBuilder.java b/src/main/java/org/jsoup/helper/UrlBuilder.java
index 89f46a13..4deda367 100644
--- a/src/main/java/org/jsoup/helper/UrlBuilder.java
+++ b/src/main/java/org/jsoup/helper/UrlBuilder.java
@@ -38,24 +38,20 @@ final class UrlBuilder {
                 u.getUserInfo(),
                 IDN.toASCII(decodePart(u.getHost())), // puny-code
                 u.getPort(),
-                decodePart(u.getPath()),
-                null, null // query and fragment appended later so as not to encode
+                null, null, null // path, query and fragment appended later so as not to encode
             );
 
-            String normUrl = uri.toASCIIString();
-            if (q != null || u.getRef() != null) {
-                StringBuilder sb = StringUtil.borrowBuilder().append(normUrl);
-                if (q != null) {
-                    sb.append('?');
-                    appendToAscii(StringUtil.releaseBuilder(q), true, sb);
-                }
-                if (u.getRef() != null) {
-                    sb.append('#');
-                    appendToAscii(u.getRef(), false, sb);
-                }
-                normUrl = StringUtil.releaseBuilder(sb);
+            StringBuilder normUrl = StringUtil.borrowBuilder().append(uri.toASCIIString());
+            appendToAscii(u.getPath(), false, normUrl);
+            if (q != null) {
+                normUrl.append('?');
+                appendToAscii(StringUtil.releaseBuilder(q), true, normUrl);
             }
-            u =  new URL(normUrl);
+            if (u.getRef() != null) {
+                normUrl.append('#');
+                appendToAscii(u.getRef(), false, normUrl);
+            }
+            u = new URL(StringUtil.releaseBuilder(normUrl));
             return u;
         } catch (MalformedURLException | URISyntaxException | UnsupportedEncodingException e) {
             // we assert here so that any incomplete normalization issues can be caught in devel. but in practise,
diff --git a/src/main/java/org/jsoup/internal/ConstrainableInputStream.java b/src/main/java/org/jsoup/internal/ConstrainableInputStream.java
index 54928f4e..aa2c2751 100644
--- a/src/main/java/org/jsoup/internal/ConstrainableInputStream.java
+++ b/src/main/java/org/jsoup/internal/ConstrainableInputStream.java
@@ -49,8 +49,8 @@ public final class ConstrainableInputStream extends BufferedInputStream {
     public int read(byte[] b, int off, int len) throws IOException {
         if (interrupted || capped && remaining <= 0)
             return -1;
-        if (Thread.interrupted()) {
-            // interrupted latches, because parse() may call twice (and we still want the thread interupt to clear)
+        if (Thread.currentThread().isInterrupted()) {
+            // interrupted latches, because parse() may call twice
             interrupted = true;
             return -1;
         }
diff --git a/src/main/java/org/jsoup/nodes/Document.java b/src/main/java/org/jsoup/nodes/Document.java
index 70c571f8..9930dc5e 100644
--- a/src/main/java/org/jsoup/nodes/Document.java
+++ b/src/main/java/org/jsoup/nodes/Document.java
@@ -385,9 +385,9 @@ public class Document extends Element {
         public enum Syntax {html, xml}
 
         private Entities.EscapeMode escapeMode = Entities.EscapeMode.base;
-        private Charset charset = DataUtil.UTF_8;
+        private Charset charset;
+        Entities.CoreCharset coreCharset; // fast encoders for ascii and utf8
         private final ThreadLocal<CharsetEncoder> encoderThreadLocal = new ThreadLocal<>(); // initialized by start of OuterHtmlVisitor
-        @Nullable Entities.CoreCharset coreCharset; // fast encoders for ascii and utf8
 
         private boolean prettyPrint = true;
         private boolean outline = false;
@@ -395,7 +395,9 @@ public class Document extends Element {
         private int maxPaddingWidth = 30;
         private Syntax syntax = Syntax.html;
 
-        public OutputSettings() {}
+        public OutputSettings() {
+            charset(DataUtil.UTF_8);
+        }
         
         /**
          * Get the document's current HTML escape mode: <code>base</code>, which provides a limited set of named HTML
@@ -439,6 +441,7 @@ public class Document extends Element {
          */
         public OutputSettings charset(Charset charset) {
             this.charset = charset;
+            coreCharset = Entities.CoreCharset.byName(charset.name());
             return this;
         }
 
@@ -456,7 +459,6 @@ public class Document extends Element {
             // created at start of OuterHtmlVisitor so each pass has own encoder, so OutputSettings can be shared among threads
             CharsetEncoder encoder = charset.newEncoder();
             encoderThreadLocal.set(encoder);
-            coreCharset = Entities.CoreCharset.byName(encoder.charset().name());
             return encoder;
         }
 
@@ -570,7 +572,7 @@ public class Document extends Element {
             } catch (CloneNotSupportedException e) {
                 throw new RuntimeException(e);
             }
-            clone.charset(charset.name()); // new charset and charset encoder
+            clone.charset(charset.name()); // new charset, coreCharset, and charset encoder
             clone.escapeMode = Entities.EscapeMode.valueOf(escapeMode.name());
             // indentAmount, maxPaddingWidth, and prettyPrint are primitives so object.clone() will handle
             return clone;
diff --git a/src/main/java/org/jsoup/parser/HtmlTreeBuilder.java b/src/main/java/org/jsoup/parser/HtmlTreeBuilder.java
index be0498c5..2e49e605 100644
--- a/src/main/java/org/jsoup/parser/HtmlTreeBuilder.java
+++ b/src/main/java/org/jsoup/parser/HtmlTreeBuilder.java
@@ -424,20 +424,12 @@ public class HtmlTreeBuilder extends TreeBuilder {
     // elnames is sorted, comes from Constants
     void popStackToClose(String... elNames) {
         for (int pos = stack.size() -1; pos >= 0; pos--) {
-            Element next = stack.get(pos);
+            Element el = stack.get(pos);
             stack.remove(pos);
-            if (inSorted(next.normalName(), elNames))
-                break;
-        }
-    }
-
-    void popStackToBefore(String elName) {
-        for (int pos = stack.size() -1; pos >= 0; pos--) {
-            Element next = stack.get(pos);
-            if (next.normalName().equals(elName)) {
+            if (inSorted(el.normalName(), elNames)) {
+                if (currentToken instanceof Token.EndTag)
+                    onNodeClosed(el, currentToken);
                 break;
-            } else {
-                stack.remove(pos);
             }
         }
     }
diff --git a/src/main/java/org/jsoup/select/Evaluator.java b/src/main/java/org/jsoup/select/Evaluator.java
index cb5d644b..d8929fc3 100644
--- a/src/main/java/org/jsoup/select/Evaluator.java
+++ b/src/main/java/org/jsoup/select/Evaluator.java
@@ -710,7 +710,10 @@ public abstract class Evaluator {
 		public boolean matches(Element root, Element element) {
         	List<Node> family = element.childNodes();
             for (Node n : family) {
-                if (!(n instanceof Comment || n instanceof XmlDeclaration || n instanceof DocumentType)) return false;
+                if (n instanceof TextNode)
+                    return ((TextNode)n).isBlank();
+                if (!(n instanceof Comment || n instanceof XmlDeclaration || n instanceof DocumentType))
+                    return false;
             }
         	return true;
 		}


Test patch:
diff --git a/src/test/java/org/jsoup/helper/HttpConnectionTest.java b/src/test/java/org/jsoup/helper/HttpConnectionTest.java
index 840d8a82..d7765880 100644
--- a/src/test/java/org/jsoup/helper/HttpConnectionTest.java
+++ b/src/test/java/org/jsoup/helper/HttpConnectionTest.java
@@ -255,25 +255,47 @@ public class HttpConnectionTest {
     }
 
     @Test public void encodeUrl() throws MalformedURLException {
-        URL url1 = new URL("https://test.com/foo bar/[One]?q=white space#frag");
+        URL url1 = new URL("https://test.com/foo%20bar/%5BOne%5D?q=white+space#frag");
         URL url2 = new UrlBuilder(url1).build();
         assertEquals("https://test.com/foo%20bar/%5BOne%5D?q=white+space#frag", url2.toExternalForm());
     }
 
     @Test void encodedUrlDoesntDoubleEncode() throws MalformedURLException {
-        URL url1 = new URL("https://test.com/foo bar/[One]?q=white space#frag ment");
+        URL url1 = new URL("https://test.com/foo%20bar/%5BOne%5D?q=white+space#frag%20ment");
         URL url2 = new UrlBuilder(url1).build();
         URL url3 = new UrlBuilder(url2).build();
         assertEquals("https://test.com/foo%20bar/%5BOne%5D?q=white+space#frag%20ment", url2.toExternalForm());
         assertEquals("https://test.com/foo%20bar/%5BOne%5D?q=white+space#frag%20ment", url3.toExternalForm());
     }
 
+    @Test void urlPathIsPreservedDoesntDoubleEncode() throws MalformedURLException {
+        URL url1 = new URL("https://test.com/[foo] bar+/%5BOne%5D?q=white space#frag ment");
+        URL url2 = new UrlBuilder(url1).build();
+        URL url3 = new UrlBuilder(url2).build();
+        assertEquals("https://test.com/[foo]%20bar+/%5BOne%5D?q=white+space#frag%20ment", url2.toExternalForm());
+        assertEquals("https://test.com/[foo]%20bar+/%5BOne%5D?q=white+space#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+string", url.toExternalForm());
     }
 
+    @Test void encodedUrlPathIsPreserved() {
+        // https://github.com/jhy/jsoup/issues/1952
+        Connection connect = Jsoup.connect("https://example.com/%2B32");
+        URL url = connect.request().url();
+        assertEquals("https://example.com/%2B32", url.toExternalForm());
+    }
+
+    @Test void urlPathPlusIsPreserved() {
+        // https://github.com/jhy/jsoup/issues/1952
+        Connection connect = Jsoup.connect("https://example.com/123+456");
+        URL url = connect.request().url();
+        assertEquals("https://example.com/123+456", url.toExternalForm());
+    }
+
     @Test public void noUrlThrowsValidationError() throws IOException {
         HttpConnection con = new HttpConnection();
         boolean threw = false;
diff --git a/src/test/java/org/jsoup/integration/ConnectIT.java b/src/test/java/org/jsoup/integration/ConnectIT.java
index 84315b4a..c3789357 100644
--- a/src/test/java/org/jsoup/integration/ConnectIT.java
+++ b/src/test/java/org/jsoup/integration/ConnectIT.java
@@ -14,6 +14,7 @@ import java.io.IOException;
 import java.net.SocketTimeoutException;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import static org.junit.jupiter.api.Assertions.*;
 
@@ -70,7 +71,29 @@ public class ConnectIT {
         assertTrue(runner.isInterrupted());
         runner.join();
 
-        assertEquals(0, body[0].length()); // doesn't ready a failed doc
+        assertEquals(0, body[0].length()); // doesn't read a failed doc
+    }
+
+    @Test public void canInterruptThenJoinASpawnedThread() throws InterruptedException {
+        // https://github.com/jhy/jsoup/issues/1991
+        AtomicBoolean ioException = new AtomicBoolean();
+        Thread runner = new Thread(() -> {
+            try {
+                while (!Thread.currentThread().isInterrupted()) {
+                    Document doc  = Jsoup.connect(SlowRider.Url)
+                        .timeout(4000)
+                        .get();
+                }
+            } catch (IOException e) {
+                ioException.set(true); // don't expect to catch, because the outer sleep will complete before this timeout
+            }
+        });
+
+        runner.start();
+        Thread.sleep(2 * 1000);
+        runner.interrupt();
+        runner.join();
+        assertFalse(ioException.get());
     }
 
     @Test
diff --git a/src/test/java/org/jsoup/integration/ConnectTest.java b/src/test/java/org/jsoup/integration/ConnectTest.java
index 07f9d2eb..f02840a0 100644
--- a/src/test/java/org/jsoup/integration/ConnectTest.java
+++ b/src/test/java/org/jsoup/integration/ConnectTest.java
@@ -3,6 +3,7 @@ package org.jsoup.integration;
 import org.jsoup.Connection;
 import org.jsoup.HttpStatusException;
 import org.jsoup.Jsoup;
+import org.jsoup.Connection.Method;
 import org.jsoup.helper.DataUtil;
 import org.jsoup.helper.W3CDom;
 import org.jsoup.integration.servlets.*;
@@ -254,6 +255,31 @@ public class ConnectTest {
         assertEquals("auth=token", ihVal("Cookie", doc));
     }
 
+    @Test
+    public void doesDeleteWithBody() throws IOException {
+        // https://github.com/jhy/jsoup/issues/1972
+        String body = "some body";
+        Connection.Response res = Jsoup.connect(echoUrl)
+            .requestBody(body)
+            .method(Method.DELETE)
+            .execute();
+
+        Document doc = res.parse();
+        assertEquals("DELETE", ihVal("Method", doc));
+        assertEquals(body, ihVal("Post Data", doc));
+    }
+
+    @Test
+    public void doesDeleteWithoutBody() throws IOException {
+        Connection.Response res = Jsoup.connect(echoUrl)
+            .method(Method.DELETE)
+            .execute();
+
+        Document doc = res.parse();
+        assertEquals("DELETE", ihVal("Method", doc));
+        assertEquals(null, ihVal("Post Data", doc));
+    }
+
     /**
      * Tests upload of content to a remote service.
      */
diff --git a/src/test/java/org/jsoup/integration/servlets/BaseServlet.java b/src/test/java/org/jsoup/integration/servlets/BaseServlet.java
index b1bf5cca..46d8e477 100644
--- a/src/test/java/org/jsoup/integration/servlets/BaseServlet.java
+++ b/src/test/java/org/jsoup/integration/servlets/BaseServlet.java
@@ -24,4 +24,9 @@ public abstract class BaseServlet extends HttpServlet {
     protected void doPut(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
         super.doPut(req, res);
     }
+
+    @Override
+    protected void doDelete(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
+        super.doPut(req, res);
+    }
 }
diff --git a/src/test/java/org/jsoup/integration/servlets/EchoServlet.java b/src/test/java/org/jsoup/integration/servlets/EchoServlet.java
index f5bf2ce3..c3a9c44a 100644
--- a/src/test/java/org/jsoup/integration/servlets/EchoServlet.java
+++ b/src/test/java/org/jsoup/integration/servlets/EchoServlet.java
@@ -40,6 +40,11 @@ public class EchoServlet extends BaseServlet {
         doIt(req, res);
     }
 
+    @Override
+    protected void doDelete(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
+        doIt(req, res);
+    }
+
     private void doIt(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException {
         int intCode = DefaultCode;
         String code = req.getHeader(CodeParam);
diff --git a/src/test/java/org/jsoup/nodes/EntitiesTest.java b/src/test/java/org/jsoup/nodes/EntitiesTest.java
index 886dfca5..7243c2f5 100644
--- a/src/test/java/org/jsoup/nodes/EntitiesTest.java
+++ b/src/test/java/org/jsoup/nodes/EntitiesTest.java
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test;
 
 import static org.jsoup.nodes.Document.OutputSettings;
 import static org.jsoup.nodes.Entities.EscapeMode.*;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 public class EntitiesTest {
@@ -163,4 +164,15 @@ public class EntitiesTest {
         Document xml = Jsoup.parse(input, "", Parser.xmlParser());
         assertEquals(input, xml.html());
     }
+    
+    @Test public void escapeByClonedOutputSettings() {
+        OutputSettings outputSettings = new OutputSettings();
+        String text = "Hello &<> Å å π 新 there ¾ © »";
+        OutputSettings clone1 = outputSettings.clone();
+        OutputSettings clone2 = outputSettings.clone();
+
+        String escaped1 = assertDoesNotThrow(() -> Entities.escape(text, clone1));
+        String escaped2 = assertDoesNotThrow(() -> Entities.escape(text, clone2));
+        assertEquals(escaped1, escaped2);
+    }
 }
diff --git a/src/test/java/org/jsoup/nodes/PositionTest.java b/src/test/java/org/jsoup/nodes/PositionTest.java
index 069d5c6a..1db73c9e 100644
--- a/src/test/java/org/jsoup/nodes/PositionTest.java
+++ b/src/test/java/org/jsoup/nodes/PositionTest.java
@@ -203,4 +203,20 @@ class PositionTest {
         }
     }
 
+    @Test void tracksClosingHeadingTags() {
+        // https://github.com/jhy/jsoup/issues/1987
+        String html = "<h1>One</h1><h2>Two</h2><h10>Ten</h10>";
+        Document doc = Jsoup.parse(html, TrackingParser);
+
+        Elements els = doc.body().children();
+        for (Element el : els) {
+            assertTrue(el.sourceRange().isTracked());
+            assertTrue(el.endSourceRange().isTracked());
+        }
+
+        Element h2 = doc.expectFirst("h2");
+        assertEquals("1,13:12-1,17:16", h2.sourceRange().toString());
+        assertEquals("1,20:19-1,25:24", h2.endSourceRange().toString());
+    }
+
 }
\ No newline at end of file
diff --git a/src/test/java/org/jsoup/select/SelectorTest.java b/src/test/java/org/jsoup/select/SelectorTest.java
index 5aa74a23..3196dc25 100644
--- a/src/test/java/org/jsoup/select/SelectorTest.java
+++ b/src/test/java/org/jsoup/select/SelectorTest.java
@@ -1164,4 +1164,21 @@ public class SelectorTest {
 
         assertEquals(1, map.size()); // root of doc 2
     }
+
+    @Test public void blankTextNodesAreConsideredEmpty() {
+        // https://github.com/jhy/jsoup/issues/1976
+        String html = "<li id=1>\n </li><li id=2></li><li id=3> </li><li id=4>One</li><li id=5><span></li>";
+        Document doc = Jsoup.parse(html);
+        Elements empty = doc.select("li:empty");
+        Elements notEmpty = doc.select("li:not(:empty)");
+
+        assertEquals(3, empty.size());
+        assertEquals(2, notEmpty.size());
+
+        assertEquals("1", empty.get(0).id());
+        assertEquals("2", empty.get(1).id());
+        assertEquals("3", empty.get(2).id());
+        assertEquals("4", notEmpty.get(0).id());
+        assertEquals("5", notEmpty.get(1).id());
+    }
 }