Repository: jhy/jsoup
Pull request #1403 (2020-06-15 15:58:35)

Fix regression on Elements.forms()
the `Elements.forms()` method used to return FormElement-s that are directly in the Elements collection. This behaviour was broken since a657ae0240b875a703a1e4909d56ad59997d362d. See https://github.com/jhy/jsoup/issues/1384

I wrote the fix accordingly to what's written in the javadocs of the methods involved:
* `comments`, `textNodes` and `dataNodes` says they return direct child nodes. I specialized the private `nodesOfType` method to handle child node lookup only (and renamed it `childNodesOfType`)
* `forms()` doc says it returns the forms elements present in the selected elements, not child nodes. I removed the use of `nodesOfType` and put back the previous code.


Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..6b618a49
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,40 @@
+name: Build
+on: [push, pull_request]
+
+jobs:
+  test:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, windows-latest, macOS-latest]
+        # choosing to run a reduced set of LTS, current, and next, to balance coverage and execution time
+        java: [8, 11, 15]
+        # Temporarily removed 16-ea, as it started failing in the japicmp plugin phase.
+        # Working (with japacmp): https://github.com/jhy/jsoup/runs/1556442000?check_suite_focus=true
+        # Failing: https://github.com/jhy/jsoup/runs/1556669892?check_suite_focus=true
+        # Issue raised: https://github.com/siom79/japicmp/issues/275
+      fail-fast: false
+    name: Test JDK ${{ matrix.java }}, ${{ matrix.os }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Set up JDK ${{ matrix.java }}
+        uses: actions/setup-java@v1
+        with:
+          java-version: ${{ matrix.java }}
+
+      - name: Cache local Maven repository
+        uses: actions/cache@v2
+        with:
+          path: ~/.m2/repository
+          key: ${{ runner.os }}-maven-java-${{ matrix.java }}-${{ hashFiles('**/pom.xml') }}
+          restore-keys: |
+            ${{ runner.os }}-maven-
+
+      - name: Maven Compile
+        run: mvn -X compile -B --file pom.xml
+
+      - name: Maven Verify
+        run: mvn -X verify -B --file pom.xml
+...
diff --git a/.gitignore b/.gitignore
index 1200b560..c696abb0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,4 @@ target/
 .project
 .settings/
 *Thrash*
-
+bin/
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 988bd1b8..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-# From https://github.com/DanySK/Gravis-CI
-
-language: bash
-dist: xenial
-os:
-  - linux
-
-env:
-  global:
-    - GRAVIS="https://raw.githubusercontent.com/DanySK/Gravis-CI/master/"
-
-  matrix:
-    # List any JDK you want to build your software with.
-    # You can see the list of supported environments by installing Jabba and using ls-remote:
-    # https://github.com/shyiko/jabba#usage
-    - JDK="adopt@1.8."
-    - JDK="amazon-corretto@1.8."
-    - JDK="openjdk@1.9."
-    - JDK="openjdk@1.11."
-    - JDK="openjdk@1.12."
-    - JDK="openjdk@1.13."
-
-cache:
-  directories:
-    # Avoid re-downloading the JDK every time
-    - $HOME/.jabba/
-    - $HOME/.m2/
-
-before_install:
-  # Sets up the Java environment
-  - curl "${GRAVIS}.install-jdk-travis.sh" --output .install-jdk-travis.sh
-  # Get maven 3.1.1
-  - "wget http://apache.mirror.iphh.net/maven/maven-3/3.1.1/binaries/apache-maven-3.1.1-bin.tar.gz && tar xfz apache-maven-3.1.1-bin.tar.gz && sudo mv apache-maven-3.1.1 /usr/local/maven-3.1.1 && sudo rm -f /usr/local/maven && sudo ln -s /usr/local/maven-3.1.1 /usr/local/maven"
-
-before_script:
-  - bash .install-jdk-travis.sh
-
-script:
-  - /usr/local/maven/bin/mvn verify -B
diff --git a/CHANGES b/CHANGES
index 44bc0194..225c32dd 100644
--- a/CHANGES
+++ b/CHANGES
@@ -5,8 +5,65 @@ jsoup changelog
 
   * Change: updated the minimum Android API level from 8 to 10.
 
+  * Improvement: renamed the Whitelist class to Safelist, with the goal of more inclusive language. A shim is provided
+    for backwards compatibility (source and binary). This shim is marked as deprecated and will be removed in the
+    jsoup 1.15.1 release.
+    <https://github.com/jhy/jsoup/pull/1464>
+
   * Improvement: added support for loading and parsing gzipped HTML files in Jsoup.parse(File in, charset, baseUri).
 
+  * Improvement: reduced thread contention in HttpConnection and Document.
+    <https://github.com/jhy/jsoup/pull/1455>
+
+  * Improvement: better parsing performance when under high thread concurrency
+    <https://github.com/jhy/jsoup/pull/1402>
+
+  * Improvement: added Element#id(String) ID attribute setter.
+
+  * Improvement: in Document, #body() and #head() accessors will now automatically create those elements, if they were
+    missing (e.g. if the Document was not parsed from HTML).
+
+  * Improvement: when cleaning a document, the output settings of the original document are cloned into the cleaned
+    document.
+    <https://github.com/jhy/jsoup/issues/1417>
+
+  * Improvement: when parsing XML, disable pretty-printing by default.
+    <https://github.com/jhy/jsoup/issues/1168>
+
+  * Improvement: much better performance in Node#clone() for large and deeply nested documents. Complexity was O(n^2) or
+    worse, now O(n).
+
+  * Build Improvement: moved to GitHub Workflows for build verification.
+
+  * Build Improvement: updated Jetty (used for integration tests; not bundled) to 9.4.35.v2020112.
+
+  * Build Improvement: added nullability annotations and initial settings.
+    <https://github.com/jhy/jsoup/pull/1467>
+
+  * Bugfix: when parsing HTML, could throw NPEs on some tags (isindex or table>input).
+    <https://github.com/jhy/jsoup/issues/1404>
+
+  * Bugfix: in HttpConnection.Request, headers beginning with "sec-" (e.g. Sec-Fetch-Mode) were silently discarded by
+    the underlying Java HttpURLConnection. These are now settable correctly.
+    <https://github.com/jhy/jsoup/issues/1461>
+
+  * Bugfix: when adding child Nodes to a Node, could incorrectly reparent all nodes if the first parent had the same
+    length of children as the incoming node list.
+
+  * Bugfix: when wrapping an orphaned element, would throw an NPE.
+
+  * Bugfix: when wrapping an element with HTML that included multiple sibling elements, those siblings were incorrectly
+    added as children of the wrapper instead of siblings.
+
+  * Bugfix: when setting the content of a script or style tag via the Element#html(String) method, the content is
+    treated as a DataNode, not a TextNode. This means that characters like '<' will no longer be incorrectly escaped.
+    As a related ergonomic improvement, the same behavior applies for Element#text(String) (i.e. the content will be
+    treated as a DataNode, despite calling the text() method.
+    <https://github.com/jhy/jsoup/issues/1419>
+
+  * Bugfix: when wrapping HTML around an existing element with Element#wrap(String), will no take the content as
+    provided and ignore normal HTML tree-building rules. This allows for e.g. a div tag to be placed inside of p tags.
+
 *** Release 1.13.1 [2020-Feb-29]
   * Improvement: added Element#closest(selector), which walks up the tree to find the nearest element matching the
     selector.
@@ -195,7 +252,7 @@ jsoup changelog
     Attribute#getValue() method.
     <https://github.com/jhy/jsoup/issues/1065>
 
-  * Bugix: orphan Attribute objects (i.e. created outside of a parse or an Element) would throw an NPE on
+  * Bugfix: orphan Attribute objects (i.e. created outside of a parse or an Element) would throw an NPE on
     Attribute#setValue(val)
     <https://github.com/jhy/jsoup/issues/1107>
 
@@ -343,7 +400,7 @@ jsoup changelog
 
   * Updated Jsoup.connect().timeout() to implement a total connect + combined read timeout. Previously it specified
     connect and buffer read times only, so to implement a combined total timeout, you had to have another thread send
-    an interupt.
+    an interrupt.
 
   * Improved performance of Node.addChildren (was quadratic)
     <https://github.com/jhy/jsoup/pull/930>
@@ -357,7 +414,7 @@ jsoup changelog
   * Improved Node traversal, including less object creation, and partial and filtering traversor support.
     <https://github.com/jhy/jsoup/pull/849>
 
-  * Bugfix: if a document was was redecoded after character set detection, the HTML parser was not reset correctly,
+  * Bugfix: if a document was was re-decoded after character set detection, the HTML parser was not reset correctly,
     which could lead to an incorrect DOM.
     <https://github.com/jhy/jsoup/issues/877>
 
@@ -445,12 +502,12 @@ jsoup changelog
  * Added the new selector :containsData(), to find elements that hold data, like script and style tags.
 
  * Changed Jsoup.isValid(bodyHtml) to validate that the input contains only body HTML that is safe according to the
-   whitelist, and does not include HTML errors. And in the Jsoup.Cleaner.isValid(Document) method, make sure the doc
+   safelist, and does not include HTML errors. And in the Jsoup.Cleaner.isValid(Document) method, make sure the doc
    only includes body HTML.
    <https://github.com/jhy/jsoup/issues/245>
    <https://github.com/jhy/jsoup/issues/632>
 
- * In Whitelists, validate that a removed protocol exists before removing said protocol.
+ * In Safelists, validate that a removed protocol exists before removing said protocol.
 
  * Allow the Jsoup.Connect thread to be interrupted when reading the input stream; helps when reading from a long stream
    of data that doesn't read timeout.
@@ -498,7 +555,7 @@ jsoup changelog
    is not defined by the server, or is defined incorrectly.
    <https://github.com/jhy/jsoup/issues/743>
 
- * Improved performance of class selectors by reducing memory allocation and garbase collection.
+ * Improved performance of class selectors by reducing memory allocation and garbage collection.
    <https://github.com/jhy/jsoup/pull/753>
 
  * Improved performance of HTML output by reducing the creation of temporary attribute list iterators.
@@ -645,10 +702,10 @@ jsoup changelog
    or your JDK doesn't support SNI.
    <https://github.com/jhy/jsoup/pull/343>
 
- * Added ability to further tweak the canned Cleaner Whitelists by removing existing settings.
+ * Added ability to further tweak the canned Cleaner Safelists by removing existing settings.
    <https://github.com/jhy/jsoup/pull/449>
 
- * Added option in Cleaner Whitelist to allow linking to in-page anchors (#)
+ * Added option in Cleaner Safelist to allow linking to in-page anchors (#)
    <https://github.com/jhy/jsoup/pull/441>
 
  * Use a lowercase doctype tag for HTML5 documents.
@@ -689,7 +746,7 @@ jsoup changelog
  * Fixed performance issue when parsing HTML with elements with many children that need re-parenting.
    <https://github.com/jhy/jsoup/pull/506>
 
- * Fixed an issue where a server returning an unsupport character set response would cause a runtime
+ * Fixed an issue where a server returning an unsupported character set response would cause a runtime
    UnsupportedCharsetException, instead of falling back to the default UTF-8 charset.
    <https://github.com/jhy/jsoup/pull/509>
 
@@ -721,7 +778,7 @@ jsoup changelog
  * If pretty-print is disabled, don't trim outer whitespace in Element.html()
    <https://github.com/jhy/jsoup/issues/368>
 
- * In the HTML Cleaner, allow span tags in the basic whitelist, and span and div tags in the relaxed whitelist.
+ * In the HTML Cleaner, allow span tags in the basic safelist, and span and div tags in the relaxed safelist.
 
   * Added Element.cssSelector(), which returns a unique CSS selector/path for an element.
     <https://github.com/jhy/jsoup/pull/459>
@@ -756,7 +813,7 @@ jsoup changelog
  * Added support for 'application/*+xml' mimetypes.
    <https://github.com/jhy/jsoup/pull/444>
 
- * Fixed support for allowing script tags in cleaner whitelists.
+ * Fixed support for allowing script tags in cleaner Safelists.
    <https://github.com/jhy/jsoup/issues/299>
    <https://github.com/jhy/jsoup/issues/388>
 
@@ -820,7 +877,7 @@ jsoup changelog
  * Introduced Parser.parseXmlFragment(), to allow easy parsing of XML fragments.
    <https://github.com/jhy/jsoup/issues/279>
 
- * Allow Whitelist test methods to be extended
+ * Allow Safelist test methods to be extended
    <https://github.com/jhy/jsoup/issues/85>
 
  * Added Document.OutputSettings.outline mode, to aid HTML debugging by printing out in outline mode, similar to
@@ -830,7 +887,7 @@ jsoup changelog
  * When parsing, allow all tags to self-close. Tags that aren't expected to self-close will get an end tag.
    <https://github.com/jhy/jsoup/issues/258>
 
- * Fixed an issue when parsing <textarea>/RCData tags containing unescaped closing tags that would drop the traling >.
+ * Fixed an issue when parsing <textarea>/RCData tags containing unescaped closing tags that would drop the trailing >.
 
  * Corrected the javadoc for Element#child() to note that it throws IndexOutOfBounds.
    <https://github.com/jhy/jsoup/issues/277>
@@ -912,13 +969,13 @@ jsoup changelog
  * Fixed NPE when HTML fragment parsing a <style> tag
    <https://github.com/jhy/jsoup/issues/189>
 
- * Fixed issue with :all pseudo-tag in HTML sanitizer when cleaning tags previously defined in whitelist
+ * Fixed issue with :all pseudo-tag in HTML sanitizer when cleaning tags previously defined in safelist
    <https://github.com/jhy/jsoup/issues/156>
 
  * Fixed NPE in Parser.parseFragment() when context parameter is null.
    <https://github.com/jhy/jsoup/issues/195>
 
- * In HTML whitelists, when defining allowed attributes for a tag, automatically add the tag to the allowed list.
+ * In HTML Safelists, when defining allowed attributes for a tag, automatically add the tag to the allowed list.
 
 *** Release 1.6.2 [2012-Mar-27]
  * Added a simplified XML parsing mode, which can usefully parse valid and invalid XML, but does not enforce any HTML
@@ -937,7 +994,7 @@ jsoup changelog
  * Updated jsoup.connect so that when requests made as POSTs are redirected, the redirect is followed as a GET.
    <https://github.com/jhy/jsoup/issues/120>
 
- * Updated the Cleaner and whitelists to optionally preserve related links in elements, instead of converting them
+ * Updated the Cleaner and Safelists to optionally preserve related links in elements, instead of converting them
    to absolute links.
 
  * Updated the Cleaner to support custom allowed protocols such as "cid:" and "data:".
@@ -1242,7 +1299,7 @@ jsoup changelog
     prepend(String), append(String); bulk methods for corresponding
     methods in Element.
 
- * New feature: Jsoup.isValid(html, whitelist) method for user input
+ * New feature: Jsoup.isValid(html, safelist) method for user input
     form validation.
  
  * Improved Elements.attr(String) to find first matching element
@@ -1258,7 +1315,7 @@ jsoup changelog
     also collection class methods on Elements.
  * New feature: supports Element#wrap(html) and Elements#wrap(html).
  * New selector syntax: supports E + F adjacent sibling selector
- * New selector systax: supports E ~ F preceding sibling selector
+ * New selector syntax: supports E ~ F preceding sibling selector
  * New: supports Element#elementSiblingIndex()
 
  * Improved document normalisation.
diff --git a/README.md b/README.md
index 7dada4fb..76d88556 100644
--- a/README.md
+++ b/README.md
@@ -7,14 +7,14 @@
 * scrape and [parse](https://jsoup.org/cookbook/input/parse-document-from-string) HTML from a URL, file, or string
 * find and [extract data](https://jsoup.org/cookbook/extracting-data/selector-syntax), using DOM traversal or CSS selectors
 * manipulate the [HTML elements](https://jsoup.org/cookbook/modifying-data/set-html), attributes, and text
-* [clean](https://jsoup.org/cookbook/cleaning-html/whitelist-sanitizer) user-submitted content against a safe white-list, to prevent XSS attacks
+* [clean](https://jsoup.org/cookbook/cleaning-html/safelist-sanitizer) user-submitted content against a safe-list, to prevent XSS attacks
 * output tidy HTML
 
 jsoup is designed to deal with all varieties of HTML found in the wild; from pristine and validating, to invalid tag-soup; jsoup will create a sensible parse tree.
 
 See [**jsoup.org**](https://jsoup.org/) for downloads and the full [API documentation](https://jsoup.org/apidocs/).
 
-[![Build Status](https://travis-ci.org/jhy/jsoup.svg?branch=master)](https://travis-ci.org/jhy/jsoup)
+[![Build Status](https://github.com/jhy/jsoup/workflows/Build/badge.svg)](https://github.com/jhy/jsoup/actions?query=workflow%3ABuild)
 
 ## Example
 Fetch the [Wikipedia](https://en.wikipedia.org/wiki/Main_Page) homepage, parse it to a [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction), and select the headlines from the *In the News* section into a list of [Elements](https://jsoup.org/apidocs/index.html?org/jsoup/select/Elements.html):
@@ -31,7 +31,7 @@ for (Element headline : newsHeadlines) {
 [Online sample](https://try.jsoup.org/~LGB7rk_atM2roavV0d-czMt3J_g), [full source](https://github.com/jhy/jsoup/blob/master/src/main/java/org/jsoup/examples/Wikipedia.java).
 
 ## Open source
-jsoup is an open source project distributed under the liberal [MIT license](https://jsoup.org/license). The source code is available at [GitHub](https://github.com/jhy/jsoup/tree/master/src/main/java/org/jsoup).
+jsoup is an open source project distributed under the liberal [MIT license](https://jsoup.org/license). The source code is available at [GitHub](https://github.com/jhy/jsoup).
 
 ## Getting started
 1. [Download](https://jsoup.org/download) the latest jsoup jar (or add it to your Maven/Gradle build)
diff --git a/pom.xml b/pom.xml
index 3b6fdb59..105b546f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,6 +31,10 @@
     <url>https://jhy.io/</url>
   </organization>
 
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
   <build>
     <plugins>
       <plugin>
@@ -50,7 +54,7 @@
         </configuration>
       </plugin>
       <plugin>
-        <!-- this plugin allows us to ensure Java 8 API compatibility -->
+        <!-- Ensure Java 8 and Android 10 API compatibility -->
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>animal-sniffer-maven-plugin</artifactId>
         <version>1.16</version>
@@ -145,6 +149,7 @@
         <configuration>
           <instructions>
             <Bundle-DocURL>https://jsoup.org/</Bundle-DocURL>
+            <Export-Package>org.jsoup.*</Export-Package>
           </instructions>
         </configuration>
       </plugin>
@@ -178,6 +183,37 @@
           <threadCount>8</threadCount>
         </configuration>
       </plugin>
+      <plugin>
+        <!-- API version compat check - https://siom79.github.io/japicmp/ -->
+        <groupId>com.github.siom79.japicmp</groupId>
+        <artifactId>japicmp-maven-plugin</artifactId>
+        <version>0.14.4</version>
+        <configuration>
+          <!-- hard code previous version; can't detect when running stateless on build server -->
+          <oldVersion>
+            <dependency>
+              <groupId>org.jsoup</groupId>
+              <artifactId>jsoup</artifactId>
+              <version>1.13.1</version>
+              <type>jar</type>
+            </dependency>
+          </oldVersion>
+          <parameter>
+            <!-- jsoup policy is ok to remove deprecated methods on minor but not builds. will need to temp remove on bump to 1.15.1 and manually validate -->
+            <onlyModified>true</onlyModified>
+            <breakBuildOnBinaryIncompatibleModifications>true</breakBuildOnBinaryIncompatibleModifications>
+            <breakBuildOnSourceIncompatibleModifications>true</breakBuildOnSourceIncompatibleModifications>
+          </parameter>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>verify</phase>
+            <goals>
+              <goal>cmp</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
     </plugins>
     <resources>
       <resource>
@@ -276,19 +312,26 @@
       <!-- jetty for webserver integration tests. 9.2 is last with Java7 support -->
       <groupId>org.eclipse.jetty</groupId>
       <artifactId>jetty-server</artifactId>
-      <version>9.4.26.v20200117</version>
+      <version>9.4.35.v20201120</version>
       <scope>test</scope>
     </dependency>
 
-
     <dependency>
       <!-- jetty for webserver integration tests -->
       <groupId>org.eclipse.jetty</groupId>
       <artifactId>jetty-servlet</artifactId>
-      <version>9.4.26.v20200117</version>
+      <version>9.4.35.v20201120</version>
       <scope>test</scope>
     </dependency>
 
+    <dependency>
+      <!-- javax.annotations.nonnull, with Apache 2 (not GPL) license. Not distributed. -->
+      <groupId>com.google.code.findbugs</groupId>
+      <artifactId>jsr305</artifactId>
+      <version>3.0.2</version>
+      <scope>compile</scope>
+    </dependency>
+
   </dependencies>
 
   <dependencyManagement>
@@ -296,10 +339,6 @@
     </dependencies>
   </dependencyManagement>
 
-  <properties>
-    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-  </properties>
-
   <developers>
     <developer>
       <id>jhy</id>
diff --git a/src/main/java/org/jsoup/Connection.java b/src/main/java/org/jsoup/Connection.java
index 12d4a615..d7c2df89 100644
--- a/src/main/java/org/jsoup/Connection.java
+++ b/src/main/java/org/jsoup/Connection.java
@@ -3,6 +3,7 @@ package org.jsoup;
 import org.jsoup.nodes.Document;
 import org.jsoup.parser.Parser;
 
+import javax.annotation.Nullable;
 import javax.net.ssl.SSLSocketFactory;
 import java.io.BufferedInputStream;
 import java.io.IOException;
@@ -217,7 +218,7 @@ public interface Connection {
      * @param key the data key
      * @return null if not set
      */
-    KeyVal data(String key);
+    @Nullable KeyVal data(String key);
 
     /**
      * Set a POST (or PUT) request body. Useful when a server expects a plain request body, not a set for URL
@@ -345,9 +346,9 @@ public interface Connection {
 
         /**
          * Get the URL
-         * @return URL
+         * @return URL, or {@code null} if it has not yet been set.
          */
-        URL url();
+        @Nullable URL url();
 
         /**
          * Set the URL
@@ -358,9 +359,9 @@ public interface Connection {
 
         /**
          * Get the request method
-         * @return method
+         * @return method, or {@code null} if it has not yet been set.
          */
-        Method method();
+        @Nullable Method method();
 
         /**
          * Set the request method
@@ -380,7 +381,7 @@ public interface Connection {
          * @see #hasHeader(String)
          * @see #cookie(String)
          */
-        String header(String name);
+        @Nullable String header(String name);
 
         /**
          * Get the values of a header.
@@ -494,7 +495,7 @@ public interface Connection {
          * Get the proxy used for this request.
          * @return the proxy; <code>null</code> if not enabled.
          */
-        Proxy proxy();
+        @Nullable Proxy proxy();
 
         /**
          * Update the proxy for this request.
@@ -786,6 +787,6 @@ public interface Connection {
          * Get the current Content Type, or {@code null} if not set.
          * @return the current Content Type.
          */
-        String contentType();
+        @Nullable String contentType();
     }
 }
diff --git a/src/main/java/org/jsoup/HttpStatusException.java b/src/main/java/org/jsoup/HttpStatusException.java
index 75cd6cdd..2ad83b7d 100644
--- a/src/main/java/org/jsoup/HttpStatusException.java
+++ b/src/main/java/org/jsoup/HttpStatusException.java
@@ -6,8 +6,8 @@ import java.io.IOException;
  * Signals that a HTTP request resulted in a not OK HTTP response.
  */
 public class HttpStatusException extends IOException {
-    private int statusCode;
-    private String url;
+    private final int statusCode;
+    private final String url;
 
     public HttpStatusException(String message, int statusCode, String url) {
         super(message);
diff --git a/src/main/java/org/jsoup/Jsoup.java b/src/main/java/org/jsoup/Jsoup.java
index 84a5e34e..ff74f7a3 100644
--- a/src/main/java/org/jsoup/Jsoup.java
+++ b/src/main/java/org/jsoup/Jsoup.java
@@ -1,12 +1,14 @@
 package org.jsoup;
 
+import org.jsoup.helper.DataUtil;
+import org.jsoup.helper.HttpConnection;
 import org.jsoup.nodes.Document;
 import org.jsoup.parser.Parser;
 import org.jsoup.safety.Cleaner;
+import org.jsoup.safety.Safelist;
 import org.jsoup.safety.Whitelist;
-import org.jsoup.helper.DataUtil;
-import org.jsoup.helper.HttpConnection;
 
+import javax.annotation.Nullable;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
@@ -16,6 +18,7 @@ import java.net.URL;
  The core public access point to the jsoup functionality.
 
  @author Jonathan Hedley */
+
 public class Jsoup {
     private Jsoup() {}
 
@@ -84,7 +87,7 @@ public class Jsoup {
 
      @throws IOException if the file could not be found, or read, or if the charsetName is invalid.
      */
-    public static Document parse(File in, String charsetName, String baseUri) throws IOException {
+    public static Document parse(File in, @Nullable String charsetName, String baseUri) throws IOException {
         return DataUtil.load(in, charsetName, baseUri);
     }
 
@@ -99,7 +102,7 @@ public class Jsoup {
      @throws IOException if the file could not be found, or read, or if the charsetName is invalid.
      @see #parse(File, String, String)
      */
-    public static Document parse(File in, String charsetName) throws IOException {
+    public static Document parse(File in, @Nullable String charsetName) throws IOException {
         return DataUtil.load(in, charsetName, in.getAbsolutePath());
     }
 
@@ -114,7 +117,7 @@ public class Jsoup {
 
      @throws IOException if the file could not be found, or read, or if the charsetName is invalid.
      */
-    public static Document parse(InputStream in, String charsetName, String baseUri) throws IOException {
+    public static Document parse(InputStream in, @Nullable String charsetName, String baseUri) throws IOException {
         return DataUtil.load(in, charsetName, baseUri);
     }
 
@@ -131,7 +134,7 @@ public class Jsoup {
 
      @throws IOException if the file could not be found, or read, or if the charsetName is invalid.
      */
-    public static Document parse(InputStream in, String charsetName, String baseUri, Parser parser) throws IOException {
+    public static Document parse(InputStream in, @Nullable String charsetName, String baseUri, Parser parser) throws IOException {
         return DataUtil.load(in, charsetName, baseUri, parser);
     }
 
@@ -184,70 +187,106 @@ public class Jsoup {
     }
 
     /**
-     Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a white-list of permitted
+     Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through an allow-list of safe
      tags and attributes.
 
      @param bodyHtml  input untrusted HTML (body fragment)
      @param baseUri   URL to resolve relative URLs against
-     @param whitelist white-list of permitted HTML elements
+     @param safelist  list of permitted HTML elements
      @return safe HTML (body fragment)
 
      @see Cleaner#clean(Document)
      */
-    public static String clean(String bodyHtml, String baseUri, Whitelist whitelist) {
+    public static String clean(String bodyHtml, String baseUri, Safelist safelist) {
         Document dirty = parseBodyFragment(bodyHtml, baseUri);
-        Cleaner cleaner = new Cleaner(whitelist);
+        Cleaner cleaner = new Cleaner(safelist);
         Document clean = cleaner.clean(dirty);
         return clean.body().html();
     }
 
     /**
-     Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a white-list of permitted
+     Use {@link #clean(String, String, Safelist)} instead.
+     @deprecated as of 1.14.1.
+     */
+    @Deprecated
+    public static String clean(String bodyHtml, String baseUri, Whitelist safelist) {
+        return clean(bodyHtml, baseUri, (Safelist) safelist);
+    }
+
+    /**
+     Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a safe-list of permitted
      tags and attributes.
 
      @param bodyHtml  input untrusted HTML (body fragment)
-     @param whitelist white-list of permitted HTML elements
+     @param safelist list of permitted HTML elements
      @return safe HTML (body fragment)
 
      @see Cleaner#clean(Document)
      */
-    public static String clean(String bodyHtml, Whitelist whitelist) {
-        return clean(bodyHtml, "", whitelist);
+    public static String clean(String bodyHtml, Safelist safelist) {
+        return clean(bodyHtml, "", safelist);
     }
 
     /**
-     * Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a white-list of
+     Use {@link #clean(String, Safelist)} instead.
+     @deprecated as of 1.14.1.
+     */
+    @Deprecated
+    public static String clean(String bodyHtml, Whitelist safelist) {
+        return clean(bodyHtml, (Safelist) safelist);
+    }
+
+    /**
+     * Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a safe-list of
      * permitted tags and attributes.
      * <p>The HTML is treated as a body fragment; it's expected the cleaned HTML will be used within the body of an
      * existing document. If you want to clean full documents, use {@link Cleaner#clean(Document)} instead, and add
-     * structural tags (<code>html, head, body</code> etc) to the whitelist.
+     * structural tags (<code>html, head, body</code> etc) to the safelist.
      *
      * @param bodyHtml input untrusted HTML (body fragment)
      * @param baseUri URL to resolve relative URLs against
-     * @param whitelist white-list of permitted HTML elements
+     * @param safelist list of permitted HTML elements
      * @param outputSettings document output settings; use to control pretty-printing and entity escape modes
      * @return safe HTML (body fragment)
      * @see Cleaner#clean(Document)
      */
-    public static String clean(String bodyHtml, String baseUri, Whitelist whitelist, Document.OutputSettings outputSettings) {
+    public static String clean(String bodyHtml, String baseUri, Safelist safelist, Document.OutputSettings outputSettings) {
         Document dirty = parseBodyFragment(bodyHtml, baseUri);
-        Cleaner cleaner = new Cleaner(whitelist);
+        Cleaner cleaner = new Cleaner(safelist);
         Document clean = cleaner.clean(dirty);
         clean.outputSettings(outputSettings);
         return clean.body().html();
     }
 
     /**
-     Test if the input body HTML has only tags and attributes allowed by the Whitelist. Useful for form validation.
+     Use {@link #clean(String, String, Safelist, Document.OutputSettings)} instead.
+     @deprecated as of 1.14.1.
+     */
+    @Deprecated
+    public static String clean(String bodyHtml, String baseUri, Whitelist safelist, Document.OutputSettings outputSettings) {
+        return clean(bodyHtml, baseUri, (Safelist) safelist, outputSettings);
+    }
+
+    /**
+     Test if the input body HTML has only tags and attributes allowed by the Safelist. Useful for form validation.
      <p>The input HTML should still be run through the cleaner to set up enforced attributes, and to tidy the output.
      <p>Assumes the HTML is a body fragment (i.e. will be used in an existing HTML document body.)
      @param bodyHtml HTML to test
-     @param whitelist whitelist to test against
+     @param safelist safelist to test against
      @return true if no tags or attributes were removed; false otherwise
-     @see #clean(String, org.jsoup.safety.Whitelist) 
+     @see #clean(String, Safelist)
+     */
+    public static boolean isValid(String bodyHtml, Safelist safelist) {
+        return new Cleaner(safelist).isValidBodyHtml(bodyHtml);
+    }
+
+    /**
+     Use {@link #isValid(String, Safelist)} instead.
+     @deprecated as of 1.14.1.
      */
-    public static boolean isValid(String bodyHtml, Whitelist whitelist) {
-        return new Cleaner(whitelist).isValidBodyHtml(bodyHtml);
+    @Deprecated
+    public static boolean isValid(String bodyHtml, Whitelist safelist) {
+        return isValid(bodyHtml, (Safelist) safelist);
     }
     
 }
diff --git a/src/main/java/org/jsoup/UnsupportedMimeTypeException.java b/src/main/java/org/jsoup/UnsupportedMimeTypeException.java
index 124bc971..40fbc870 100644
--- a/src/main/java/org/jsoup/UnsupportedMimeTypeException.java
+++ b/src/main/java/org/jsoup/UnsupportedMimeTypeException.java
@@ -6,8 +6,8 @@ import java.io.IOException;
  * Signals that a HTTP response returned a mime type that is not supported.
  */
 public class UnsupportedMimeTypeException extends IOException {
-    private String mimeType;
-    private String url;
+    private final String mimeType;
+    private final String url;
 
     public UnsupportedMimeTypeException(String message, String mimeType, String url) {
         super(message);
diff --git a/src/main/java/org/jsoup/helper/DataUtil.java b/src/main/java/org/jsoup/helper/DataUtil.java
index c4d94bbe..7806bd0e 100644
--- a/src/main/java/org/jsoup/helper/DataUtil.java
+++ b/src/main/java/org/jsoup/helper/DataUtil.java
@@ -12,6 +12,7 @@ import org.jsoup.nodes.XmlDeclaration;
 import org.jsoup.parser.Parser;
 import org.jsoup.select.Elements;
 
+import javax.annotation.Nullable;
 import java.io.BufferedReader;
 import java.io.CharArrayReader;
 import java.io.File;
@@ -37,7 +38,8 @@ import java.util.zip.GZIPInputStream;
  */
 public final class DataUtil {
     private static final Pattern charsetPattern = Pattern.compile("(?i)\\bcharset=\\s*(?:[\"'])?([^\\s,;\"']*)");
-    static final String defaultCharset = "UTF-8"; // used if not found in header or meta charset
+    public static final Charset UTF_8 = Charset.forName("UTF-8"); // Don't use StandardCharsets, as those only appear in Android API 19, and we target 10.
+    static final String defaultCharsetName = UTF_8.name(); // used if not found in header or meta charset
     private static final int firstReadBufferSize = 1024 * 5;
     static final int bufferSize = 1024 * 32;
     private static final char[] mimeBoundaryChars =
@@ -108,12 +110,12 @@ public final class DataUtil {
         }
     }
 
-    static Document parseInputStream(InputStream input, String charsetName, String baseUri, Parser parser) throws IOException  {
+    static Document parseInputStream(@Nullable InputStream input, @Nullable String charsetName, String baseUri, Parser parser) throws IOException  {
         if (input == null) // empty body
             return new Document(baseUri);
         input = ConstrainableInputStream.wrap(input, bufferSize, 0);
 
-        Document doc = null;
+        @Nullable Document doc = null;
 
         // read the start of the stream and look for a BOM or meta charset
         input.mark(bufferSize);
@@ -128,9 +130,9 @@ public final class DataUtil {
 
         if (charsetName == null) { // determine from meta. safe first parse as UTF-8
             try {
-                CharBuffer defaultDecoded = Charset.forName(defaultCharset).decode(firstBytes);
+                CharBuffer defaultDecoded = UTF_8.decode(firstBytes);
                 if (defaultDecoded.hasArray())
-                    doc = parser.parseInput(new CharArrayReader(defaultDecoded.array()), baseUri);
+                    doc = parser.parseInput(new CharArrayReader(defaultDecoded.array(), defaultDecoded.arrayOffset(), defaultDecoded.limit()), baseUri);
                 else
                     doc = parser.parseInput(defaultDecoded.toString(), baseUri);
             } catch (UncheckedIOException e) {
@@ -166,7 +168,7 @@ public final class DataUtil {
                 }
             }
             foundCharset = validateCharset(foundCharset);
-            if (foundCharset != null && !foundCharset.equalsIgnoreCase(defaultCharset)) { // need to re-decode. (case insensitive check here to match how validate works)
+            if (foundCharset != null && !foundCharset.equalsIgnoreCase(defaultCharsetName)) { // need to re-decode. (case insensitive check here to match how validate works)
                 foundCharset = foundCharset.trim().replaceAll("[\"']", "");
                 charsetName = foundCharset;
                 doc = null;
@@ -178,7 +180,7 @@ public final class DataUtil {
         }
         if (doc == null) {
             if (charsetName == null)
-                charsetName = defaultCharset;
+                charsetName = defaultCharsetName;
             BufferedReader reader = new BufferedReader(new InputStreamReader(input, charsetName), bufferSize);
             if (bomCharset != null && bomCharset.offset) { // creating the buffered reader ignores the input pos, so must skip here
                 long skipped = reader.skip(1);
@@ -190,11 +192,11 @@ public final class DataUtil {
                 // io exception when parsing (not seen before because reading the stream as we go)
                 throw e.ioException();
             }
-            Charset charset = Charset.forName(charsetName);
+            Charset charset = charsetName.equals(defaultCharsetName) ? UTF_8 : Charset.forName(charsetName);
             doc.outputSettings().charset(charset);
             if (!charset.canEncode()) {
                 // some charsets can read but not encode; switch to an encodable charset and update the meta el
-                doc.charset(Charset.forName(defaultCharset));
+                doc.charset(UTF_8);
             }
         }
         input.close();
@@ -225,7 +227,7 @@ public final class DataUtil {
      * @param contentType e.g. "text/html; charset=EUC-JP"
      * @return "EUC-JP", or null if not found. Charset is trimmed and uppercased.
      */
-    static String getCharsetFromContentType(String contentType) {
+    static @Nullable String getCharsetFromContentType(@Nullable String contentType) {
         if (contentType == null) return null;
         Matcher m = charsetPattern.matcher(contentType);
         if (m.find()) {
@@ -236,7 +238,7 @@ public final class DataUtil {
         return null;
     }
 
-    private static String validateCharset(String cs) {
+    private @Nullable static String validateCharset(@Nullable String cs) {
         if (cs == null || cs.length() == 0) return null;
         cs = cs.trim().replaceAll("[\"']", "");
         try {
@@ -261,7 +263,7 @@ public final class DataUtil {
         return StringUtil.releaseBuilder(mime);
     }
 
-    private static BomCharset detectCharsetFromBom(final ByteBuffer byteData) {
+    private static @Nullable BomCharset detectCharsetFromBom(final ByteBuffer byteData) {
         final Buffer buffer = byteData; // .mark and rewind used to return Buffer, now ByteBuffer, so cast for backward compat
         buffer.mark();
         byte[] bom = new byte[4];
diff --git a/src/main/java/org/jsoup/helper/HttpConnection.java b/src/main/java/org/jsoup/helper/HttpConnection.java
index 73cb7d4b..6297feb2 100644
--- a/src/main/java/org/jsoup/helper/HttpConnection.java
+++ b/src/main/java/org/jsoup/helper/HttpConnection.java
@@ -10,6 +10,7 @@ import org.jsoup.nodes.Document;
 import org.jsoup.parser.Parser;
 import org.jsoup.parser.TokenQueue;
 
+import javax.annotation.Nullable;
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLSocketFactory;
 import java.io.BufferedInputStream;
@@ -19,7 +20,6 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
-import java.io.UnsupportedEncodingException;
 import java.net.HttpURLConnection;
 import java.net.InetSocketAddress;
 import java.net.MalformedURLException;
@@ -64,6 +64,8 @@ public class HttpConnection implements Connection {
     public static final String FORM_URL_ENCODED = "application/x-www-form-urlencoded";
     private static final int HTTP_TEMP_REDIR = 307; // http/1.1 temporary redirect, not in Java's set.
     private static final String DefaultUploadType = "application/octet-stream";
+    private static final Charset UTF_8 = Charset.forName("UTF-8"); // Don't use StandardCharsets, not in Android API 10.
+    private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
 
     public static Connection connect(String url) {
         Connection con = new HttpConnection();
@@ -110,8 +112,6 @@ public class HttpConnection implements Connection {
     }
 
     private static String encodeMimeName(String val) {
-        if (val == null)
-            return null;
         return val.replace("\"", "%22");
     }
 
@@ -323,8 +323,8 @@ public class HttpConnection implements Connection {
 
     @SuppressWarnings({"unchecked"})
     private static abstract class Base<T extends Connection.Base> implements Connection.Base<T> {
-        URL url;
-        Method method;
+        @Nullable URL url;
+        @Nullable Method method;
         Map<String, List<String>> headers;
         Map<String, String> cookies;
 
@@ -367,6 +367,7 @@ public class HttpConnection implements Connection {
         @Override
         public T addHeader(String name, String value) {
             Validate.notEmpty(name);
+            //noinspection ConstantConditions
             value = value == null ? "" : value;
 
             List<String> values = headers(name);
@@ -386,15 +387,10 @@ public class HttpConnection implements Connection {
         }
 
         private static String fixHeaderEncoding(String val) {
-            try {
-                byte[] bytes = val.getBytes("ISO-8859-1");
-                if (!looksLikeUtf8(bytes))
-                    return val;
-                return new String(bytes, "UTF-8");
-            } catch (UnsupportedEncodingException e) {
-                // shouldn't happen as these both always exist
+            byte[] bytes = val.getBytes(ISO_8859_1);
+            if (!looksLikeUtf8(bytes))
                 return val;
-            }
+            return new String(bytes, UTF_8);
         }
 
         private static boolean looksLikeUtf8(byte[] input) {
@@ -498,7 +494,7 @@ public class HttpConnection implements Connection {
             return Collections.emptyList();
         }
 
-        private Map.Entry<String, List<String>> scanHeaders(String name) {
+        private @Nullable Map.Entry<String, List<String>> scanHeaders(String name) {
             String lc = lowerCase(name);
             for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
                 if (lowerCase(entry.getKey()).equals(lc))
@@ -536,18 +532,22 @@ public class HttpConnection implements Connection {
     }
 
     public static class Request extends HttpConnection.Base<Connection.Request> implements Connection.Request {
-        private Proxy proxy; // nullable
+        static {
+            System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
+            // make sure that we can send Sec-Fetch-Site headers etc.
+        }
+        private @Nullable Proxy proxy;
         private int timeoutMilliseconds;
         private int maxBodySizeBytes;
         private boolean followRedirects;
-        private Collection<Connection.KeyVal> data;
-        private String body = null;
+        private final Collection<Connection.KeyVal> data;
+        private @Nullable String body = null;
         private boolean ignoreHttpErrors = false;
         private boolean ignoreContentType = false;
         private Parser parser;
         private boolean parserDefined = false; // called parser(...) vs initialized in ctor
-        private String postDataCharset = DataUtil.defaultCharset;
-        private SSLSocketFactory sslSocketFactory;
+        private String postDataCharset = DataUtil.defaultCharsetName;
+        private @Nullable SSLSocketFactory sslSocketFactory;
 
         Request() {
             timeoutMilliseconds = 30000; // 30 seconds
@@ -857,11 +857,8 @@ public class HttpConnection implements Connection {
         public String body() {
             prepareByteData();
             // charset gets set from header on execute, and from meta-equiv on parse. parse may not have happened yet
-            String body;
-            if (charset == null)
-                body = Charset.forName(DataUtil.defaultCharset).decode(byteData).toString();
-            else
-                body = Charset.forName(charset).decode(byteData).toString();
+            String body = (charset == null ? DataUtil.UTF_8 : Charset.forName(charset))
+                .decode(byteData).toString();
             ((Buffer)byteData).rewind(); // cast to avoid covariant return type change in jdk9
             return body;
         }
@@ -1004,7 +1001,7 @@ public class HttpConnection implements Connection {
             }
         }
 
-        private static String setOutputContentType(final Connection.Request req) {
+        private @Nullable static String setOutputContentType(final Connection.Request req) {
             String bound = null;
             if (req.hasHeader(CONTENT_TYPE)) {
                 // no-op; don't add content type as already set (e.g. for requestBody())
@@ -1027,7 +1024,7 @@ public class HttpConnection implements Connection {
             return bound;
         }
 
-        private static void writePost(final Connection.Request req, final OutputStream outputStream, final String bound) throws IOException {
+        private static void writePost(final Connection.Request req, final OutputStream outputStream, @Nullable final String bound) throws IOException {
             final Collection<Connection.KeyVal> data = req.data();
             final BufferedWriter w = new BufferedWriter(new OutputStreamWriter(outputStream, req.postDataCharset()));
 
@@ -1044,7 +1041,8 @@ public class HttpConnection implements Connection {
                         w.write("; filename=\"");
                         w.write(encodeMimeName(keyVal.value()));
                         w.write("\"\r\nContent-Type: ");
-                        w.write(keyVal.contentType() != null ? keyVal.contentType() : DefaultUploadType);
+                        String contentType = keyVal.contentType();
+                        w.write(contentType != null ? contentType : DefaultUploadType);
                         w.write("\r\n\r\n");
                         w.flush(); // flush
                         DataUtil.crossStreams(keyVal.inputStream(), outputStream);
@@ -1116,9 +1114,9 @@ public class HttpConnection implements Connection {
                 else
                     first = false;
                 url
-                    .append(URLEncoder.encode(keyVal.key(), DataUtil.defaultCharset))
+                    .append(URLEncoder.encode(keyVal.key(), DataUtil.defaultCharsetName))
                     .append('=')
-                    .append(URLEncoder.encode(keyVal.value(), DataUtil.defaultCharset));
+                    .append(URLEncoder.encode(keyVal.value(), DataUtil.defaultCharsetName));
             }
             req.url(new URL(StringUtil.releaseBuilder(url)));
             req.data().clear(); // moved into url as get params
diff --git a/src/main/java/org/jsoup/helper/package-info.java b/src/main/java/org/jsoup/helper/package-info.java
new file mode 100644
index 00000000..2074b7aa
--- /dev/null
+++ b/src/main/java/org/jsoup/helper/package-info.java
@@ -0,0 +1,7 @@
+/**
+ Package containing classes supporting the core jsoup code.
+ */
+@NonnullByDefault
+package org.jsoup.helper;
+
+import org.jsoup.internal.NonnullByDefault;
diff --git a/src/main/java/org/jsoup/internal/FieldsAreNonnullByDefault.java b/src/main/java/org/jsoup/internal/FieldsAreNonnullByDefault.java
new file mode 100644
index 00000000..f501ea39
--- /dev/null
+++ b/src/main/java/org/jsoup/internal/FieldsAreNonnullByDefault.java
@@ -0,0 +1,20 @@
+package org.jsoup.internal;
+
+import javax.annotation.Nonnull;
+import javax.annotation.meta.TypeQualifierDefault;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Documented
+@Nonnull
+@TypeQualifierDefault(ElementType.FIELD)
+@Retention(value = RetentionPolicy.RUNTIME)
+
+/**
+ Indicates that fields types are not nullable, unless otherwise specified by @Nullable.
+ @see javax.annotation.ParametersAreNonnullByDefault
+ */
+public @interface FieldsAreNonnullByDefault {
+}
diff --git a/src/main/java/org/jsoup/internal/NonnullByDefault.java b/src/main/java/org/jsoup/internal/NonnullByDefault.java
new file mode 100644
index 00000000..e3497045
--- /dev/null
+++ b/src/main/java/org/jsoup/internal/NonnullByDefault.java
@@ -0,0 +1,20 @@
+package org.jsoup.internal;
+
+import javax.annotation.Nonnull;
+import javax.annotation.meta.TypeQualifierDefault;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Documented
+@Nonnull
+@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
+@Retention(value = RetentionPolicy.RUNTIME)
+
+/**
+ Indicates that all components (methods, returns, fields) are not nullable, unless otherwise specified by @Nullable.
+ @see javax.annotation.ParametersAreNonnullByDefault
+ */
+public @interface NonnullByDefault {
+}
diff --git a/src/main/java/org/jsoup/internal/ReturnsAreNonnullByDefault.java b/src/main/java/org/jsoup/internal/ReturnsAreNonnullByDefault.java
new file mode 100644
index 00000000..3f004a68
--- /dev/null
+++ b/src/main/java/org/jsoup/internal/ReturnsAreNonnullByDefault.java
@@ -0,0 +1,20 @@
+package org.jsoup.internal;
+
+import javax.annotation.Nonnull;
+import javax.annotation.meta.TypeQualifierDefault;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Documented
+@Nonnull
+@TypeQualifierDefault(ElementType.METHOD)
+@Retention(value = RetentionPolicy.RUNTIME)
+
+/**
+ Indicates return types are not nullable, unless otherwise specified by @Nullable.
+ @see javax.annotation.ParametersAreNonnullByDefault
+ */
+public @interface ReturnsAreNonnullByDefault {
+}
diff --git a/src/main/java/org/jsoup/internal/StringUtil.java b/src/main/java/org/jsoup/internal/StringUtil.java
index bc1d5887..a3ad0d31 100644
--- a/src/main/java/org/jsoup/internal/StringUtil.java
+++ b/src/main/java/org/jsoup/internal/StringUtil.java
@@ -17,6 +17,7 @@ public final class StringUtil {
     static final String[] padding = {"", " ", "  ", "   ", "    ", "     ", "      ", "       ", "        ",
         "         ", "          ", "           ", "            ", "             ", "              ", "               ",
         "                ", "                 ", "                  ", "                   ", "                    "};
+    private static final int maxPaddingWidth = 30; // so very deeply nested nodes don't get insane padding amounts
 
     /**
      * Join a collection of strings by a separator
@@ -61,7 +62,7 @@ public final class StringUtil {
     }
 
     /**
-     * Returns space padding
+     * Returns space padding (up to a max of 30).
      * @param width amount of padding desired
      * @return string of spaces * width
      */
@@ -71,6 +72,7 @@ public final class StringUtil {
 
         if (width < padding.length)
             return padding[width];
+        width = Math.min(width, maxPaddingWidth);
         char[] out = new char[width];
         for (int i = 0; i < width; i++)
             out[i] = ' ';
@@ -229,7 +231,12 @@ public final class StringUtil {
         }
     }
 
-    private static final Stack<StringBuilder> builders = new Stack<>();
+    private static final ThreadLocal<Stack<StringBuilder>> threadLocalBuilders = new ThreadLocal<Stack<StringBuilder>>() {
+        @Override
+        protected Stack<StringBuilder> initialValue() {
+            return new Stack<>();
+        }
+    };
 
     /**
      * Maintains cached StringBuilders in a flyweight pattern, to minimize new StringBuilder GCs. The StringBuilder is
@@ -239,11 +246,10 @@ public final class StringUtil {
      * @return an empty StringBuilder
      */
     public static StringBuilder borrowBuilder() {
-        synchronized (builders) {
-            return builders.empty() ?
-                new StringBuilder(MaxCachedBuilderSize) :
-                builders.pop();
-        }
+        Stack<StringBuilder> builders = threadLocalBuilders.get();
+        return builders.empty() ?
+            new StringBuilder(MaxCachedBuilderSize) :
+            builders.pop();
     }
 
     /**
@@ -261,12 +267,11 @@ public final class StringUtil {
         else
             sb.delete(0, sb.length()); // make sure it's emptied on release
 
-        synchronized (builders) {
-            builders.push(sb);
+        Stack<StringBuilder> builders = threadLocalBuilders.get();
+        builders.push(sb);
 
-            while (builders.size() > MaxIdleBuilders) {
-                builders.pop();
-            }
+        while (builders.size() > MaxIdleBuilders) {
+            builders.pop();
         }
         return string;
     }
diff --git a/src/main/java/org/jsoup/nodes/Attribute.java b/src/main/java/org/jsoup/nodes/Attribute.java
index ae74971f..3bbc24a8 100644
--- a/src/main/java/org/jsoup/nodes/Attribute.java
+++ b/src/main/java/org/jsoup/nodes/Attribute.java
@@ -4,6 +4,7 @@ import org.jsoup.SerializationException;
 import org.jsoup.internal.StringUtil;
 import org.jsoup.helper.Validate;
 
+import javax.annotation.Nullable;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Map;
@@ -20,8 +21,8 @@ public class Attribute implements Map.Entry<String, String>, Cloneable  {
     };
 
     private String key;
-    private String val;
-    Attributes parent; // used to update the holding Attributes when the key / value is changed via this interface
+    @Nullable private String val;
+    @Nullable Attributes parent; // used to update the holding Attributes when the key / value is changed via this interface
 
     /**
      * Create a new attribute from unencoded (raw) key and value.
@@ -29,17 +30,17 @@ public class Attribute implements Map.Entry<String, String>, Cloneable  {
      * @param value attribute value (may be null)
      * @see #createFromEncoded
      */
-    public Attribute(String key, String value) {
+    public Attribute(String key, @Nullable String value) {
         this(key, value, null);
     }
 
     /**
      * Create a new attribute from unencoded (raw) key and value.
      * @param key attribute key; case is preserved.
-     * @param val attribute value
+     * @param val attribute value (may be null)
      * @param parent the containing Attributes (this Attribute is not automatically added to said Attributes)
      * @see #createFromEncoded*/
-    public Attribute(String key, String val, Attributes parent) {
+    public Attribute(String key, @Nullable String val, @Nullable Attributes parent) {
         Validate.notNull(key);
         key = key.trim();
         Validate.notEmpty(key); // trimming could potentially make empty, so validate here
@@ -119,7 +120,7 @@ public class Attribute implements Map.Entry<String, String>, Cloneable  {
         return StringUtil.releaseBuilder(sb);
     }
 
-    protected static void html(String key, String val, Appendable accum, Document.OutputSettings out) throws IOException {
+    protected static void html(String key, @Nullable String val, Appendable accum, Document.OutputSettings out) throws IOException {
         accum.append(key);
         if (!shouldCollapseAttribute(key, val, out)) {
             accum.append("=\"");
@@ -170,7 +171,7 @@ public class Attribute implements Map.Entry<String, String>, Cloneable  {
         return shouldCollapseAttribute(key, val, out);
     }
 
-    protected static boolean shouldCollapseAttribute(final String key, final String val, final Document.OutputSettings out) {
+    protected static boolean shouldCollapseAttribute(final String key, @Nullable final String val, final Document.OutputSettings out) {
         return (
             out.syntax() == Document.OutputSettings.Syntax.html &&
                 (val == null || ("".equals(val) || val.equalsIgnoreCase(key)) && Attribute.isBooleanAttribute(key)));
diff --git a/src/main/java/org/jsoup/nodes/Attributes.java b/src/main/java/org/jsoup/nodes/Attributes.java
index 0f22f36c..1d6e6904 100644
--- a/src/main/java/org/jsoup/nodes/Attributes.java
+++ b/src/main/java/org/jsoup/nodes/Attributes.java
@@ -5,6 +5,7 @@ import org.jsoup.helper.Validate;
 import org.jsoup.internal.StringUtil;
 import org.jsoup.parser.ParseSettings;
 
+import javax.annotation.Nullable;
 import java.io.IOException;
 import java.util.AbstractMap;
 import java.util.AbstractSet;
@@ -82,7 +83,7 @@ public class Attributes implements Iterable<Attribute>, Cloneable {
     }
 
     // we track boolean attributes as null in values - they're just keys. so returns empty for consumers
-    static String checkNotNull(String val) {
+    static String checkNotNull(@Nullable String val) {
         return val == null ? EmptyString : val;
     }
 
@@ -111,7 +112,7 @@ public class Attributes implements Iterable<Attribute>, Cloneable {
      * Adds a new attribute. Will produce duplicates if the key already exists.
      * @see Attributes#put(String, String)
      */
-    public Attributes add(String key, String value) {
+    public Attributes add(String key, @Nullable String value) {
         checkCapacity(size + 1);
         keys[size] = key;
         vals[size] = value;
@@ -135,7 +136,7 @@ public class Attributes implements Iterable<Attribute>, Cloneable {
         return this;
     }
 
-    void putIgnoreCase(String key, String value) {
+    void putIgnoreCase(String key, @Nullable String value) {
         int i = indexOfKeyIgnoreCase(key);
         if (i != NotFound) {
             vals[i] = value;
@@ -173,6 +174,7 @@ public class Attributes implements Iterable<Attribute>, Cloneable {
     }
 
     // removes and shifts up
+    @SuppressWarnings("AssignmentToNull")
     private void remove(int index) {
         Validate.isFalse(index >= size);
         int shifted = size - index - 1;
diff --git a/src/main/java/org/jsoup/nodes/Comment.java b/src/main/java/org/jsoup/nodes/Comment.java
index d80c5006..c966d77d 100644
--- a/src/main/java/org/jsoup/nodes/Comment.java
+++ b/src/main/java/org/jsoup/nodes/Comment.java
@@ -3,6 +3,7 @@ package org.jsoup.nodes;
 import org.jsoup.Jsoup;
 import org.jsoup.parser.Parser;
 
+import javax.annotation.Nullable;
 import java.io.IOException;
 
 /**
@@ -69,7 +70,7 @@ public class Comment extends LeafNode {
      * Attempt to cast this comment to an XML Declaration note.
      * @return an XML declaration if it could be parsed as one, null otherwise.
      */
-    public XmlDeclaration asXmlDeclaration() {
+    public @Nullable XmlDeclaration asXmlDeclaration() {
         String data = getData();
         Document doc = Jsoup.parse("<" + data.substring(1, data.length() -1) + ">", baseUri(), Parser.xmlParser());
         XmlDeclaration decl = null;
diff --git a/src/main/java/org/jsoup/nodes/Document.java b/src/main/java/org/jsoup/nodes/Document.java
index 92ae0b72..a8ddad86 100644
--- a/src/main/java/org/jsoup/nodes/Document.java
+++ b/src/main/java/org/jsoup/nodes/Document.java
@@ -1,12 +1,15 @@
 package org.jsoup.nodes;
 
-import org.jsoup.internal.StringUtil;
+import org.jsoup.helper.DataUtil;
 import org.jsoup.helper.Validate;
+import org.jsoup.internal.StringUtil;
 import org.jsoup.parser.ParseSettings;
 import org.jsoup.parser.Parser;
 import org.jsoup.parser.Tag;
 import org.jsoup.select.Elements;
+import org.jsoup.select.Evaluator;
 
+import javax.annotation.Nullable;
 import java.nio.charset.Charset;
 import java.nio.charset.CharsetEncoder;
 import java.util.ArrayList;
@@ -32,6 +35,7 @@ public class Document extends Element {
     public Document(String baseUri) {
         super(Tag.valueOf("#root", ParseSettings.htmlDefault), baseUri);
         this.location = baseUri;
+        this.parser = Parser.htmlParser(); // default, but overridable
     }
 
     /**
@@ -64,7 +68,7 @@ public class Document extends Element {
      * Returns this Document's doctype.
      * @return document type, or null if not set
      */
-    public DocumentType documentType() {
+    public @Nullable DocumentType documentType() {
         for (Node node : childNodes) {
             if (node instanceof DocumentType)
                 return (DocumentType) node;
@@ -74,21 +78,51 @@ public class Document extends Element {
         return null;
         // todo - add a set document type?
     }
-    
+
+    /**
+     Find the root HTML element, or create it if it doesn't exist.
+     @return the root HTML element.
+     */
+    private Element htmlEl() {
+        for (Element el: childElementsList()) {
+            if (el.normalName().equals("html"))
+                return el;
+        }
+        return appendElement("html");
+    }
+
     /**
-     Accessor to the document's {@code head} element.
-     @return {@code head}
+     Get this document's {@code head} element.
+     <p>
+     As a side-effect, if this Document does not already have a HTML structure, it will be created. If you do not want
+     that, use {@code #selectFirst("head")} instead.
+
+     @return {@code head} element.
      */
     public Element head() {
-        return findFirstElementByTagName("head", this);
+        Element html = htmlEl();
+        for (Element el: html.childElementsList()) {
+            if (el.normalName().equals("head"))
+                return el;
+        }
+        return html.prependElement("head");
     }
 
     /**
-     Accessor to the document's {@code body} element.
-     @return {@code body}
+     Get this document's {@code body} element.
+     <p>
+     As a side-effect, if this Document does not already have a HTML structure, it will be created. If you do not want
+     that, use {@code #selectFirst("body")} instead.
+
+     @return {@code body} element.
      */
     public Element body() {
-        return findFirstElementByTagName("body", this);
+        Element html = htmlEl();
+        for (Element el: html.childElementsList()) {
+            if (el.normalName().equals("body"))
+                return el;
+        }
+        return html.appendElement("body");
     }
 
     /**
@@ -97,9 +131,10 @@ public class Document extends Element {
      */
     public String title() {
         // title is a preserve whitespace tag (for document output), but normalised here
-        Element titleEl = getElementsByTag("title").first();
+        Element titleEl = head().selectFirst(titleEval);
         return titleEl != null ? StringUtil.normaliseWhitespace(titleEl.text()).trim() : "";
     }
+    private static final Evaluator titleEval = new Evaluator.Tag("title");
 
     /**
      Set the document's {@code title} element. Updates the existing element, or adds {@code title} to {@code head} if
@@ -108,12 +143,10 @@ public class Document extends Element {
      */
     public void title(String title) {
         Validate.notNull(title);
-        Element titleEl = getElementsByTag("title").first();
-        if (titleEl == null) { // add to head
-            head().appendElement("title").text(title);
-        } else {
-            titleEl.text(title);
-        }
+        Element titleEl = head().selectFirst(titleEval);
+        if (titleEl == null) // add to head
+            titleEl = head().appendElement("title");
+        titleEl.text(title);
     }
 
     /**
@@ -131,17 +164,13 @@ public class Document extends Element {
      @return this document after normalisation
      */
     public Document normalise() {
-        Element htmlEl = findFirstElementByTagName("html", this);
-        if (htmlEl == null)
-            htmlEl = appendElement("html");
-        if (head() == null)
-            htmlEl.prependElement("head");
-        if (body() == null)
-            htmlEl.appendElement("body");
+        Element htmlEl = htmlEl(); // these all create if not found
+        Element head = head();
+        body();
 
         // pull text nodes out of root, html, and head els, and push into body. non-text nodes are already taken care
         // of. do in inverse order to maintain text order.
-        normaliseTextNodes(head());
+        normaliseTextNodes(head);
         normaliseTextNodes(htmlEl);
         normaliseTextNodes(this);
 
@@ -188,24 +217,24 @@ public class Document extends Element {
                 master.appendChild(dupe);
         }
         // ensure parented by <html>
-        if (!master.parent().equals(htmlEl)) {
+        if (master.parent() != null && !master.parent().equals(htmlEl)) {
             htmlEl.appendChild(master); // includes remove()            
         }
     }
 
-    // fast method to get first by tag name, used for html, head, body finders
-    private Element findFirstElementByTagName(String tag, Node node) {
-        if (node.nodeName().equals(tag))
-            return (Element) node;
+    // fast method to get first by tag name, used for html, head, body finders - no recursive descent
+    private @Nullable Element findFirstElementByTagName(String tag) {
+        Element root = child(0); // the HTML element - Document sits above
+        if (root.nodeName().equals(tag))
+            return this;
         else {
-            int size = node.childNodeSize();
+            int size = root.childNodeSize();
             for (int i = 0; i < size; i++) {
-                Element found = findFirstElementByTagName(tag, node.childNode(i));
-                if (found != null)
-                    return found;
+                if (root.childNode(i).nodeName().equals(tag))
+                    return (Element) root.childNode(i);
             }
         }
-        return null;
+        return null; // todo - make this blow up in most cases - how to handle frameset (no body)?)
     }
 
     @Override
@@ -331,46 +360,31 @@ public class Document extends Element {
             OutputSettings.Syntax syntax = outputSettings().syntax();
 
             if (syntax == OutputSettings.Syntax.html) {
-                Element metaCharset = select("meta[charset]").first();
-
+                Element metaCharset = selectFirst("meta[charset]");
                 if (metaCharset != null) {
                     metaCharset.attr("charset", charset().displayName());
                 } else {
-                    Element head = head();
-
-                    if (head != null) {
-                        head.appendElement("meta").attr("charset", charset().displayName());
-                    }
+                    head().appendElement("meta").attr("charset", charset().displayName());
                 }
-
-                // Remove obsolete elements
-                select("meta[name=charset]").remove();
+                select("meta[name=charset]").remove(); // Remove obsolete elements
             } else if (syntax == OutputSettings.Syntax.xml) {
                 Node node = childNodes().get(0);
-
                 if (node instanceof XmlDeclaration) {
                     XmlDeclaration decl = (XmlDeclaration) node;
-
                     if (decl.name().equals("xml")) {
                         decl.attr("encoding", charset().displayName());
-
-                        final String version = decl.attr("version");
-
-                        if (version != null) {
+                        if (decl.hasAttr("version"))
                             decl.attr("version", "1.0");
-                        }
                     } else {
                         decl = new XmlDeclaration("xml", false);
                         decl.attr("version", "1.0");
                         decl.attr("encoding", charset().displayName());
-
                         prependChild(decl);
                     }
                 } else {
                     XmlDeclaration decl = new XmlDeclaration("xml", false);
                     decl.attr("version", "1.0");
                     decl.attr("encoding", charset().displayName());
-
                     prependChild(decl);
                 }
             }
@@ -388,18 +402,16 @@ public class Document extends Element {
         public enum Syntax {html, xml}
 
         private Entities.EscapeMode escapeMode = Entities.EscapeMode.base;
-        private Charset charset;
-        private ThreadLocal<CharsetEncoder> encoderThreadLocal = new ThreadLocal<>(); // initialized by start of OuterHtmlVisitor
-        Entities.CoreCharset coreCharset; // fast encoders for ascii and utf8
+        private Charset charset = DataUtil.UTF_8;
+        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;
         private int indentAmount = 1;
         private Syntax syntax = Syntax.html;
 
-        public OutputSettings() {
-            charset(Charset.forName("UTF8"));
-        }
+        public OutputSettings() {}
         
         /**
          * Get the document's current HTML escape mode: <code>base</code>, which provides a limited set of named HTML
diff --git a/src/main/java/org/jsoup/nodes/Element.java b/src/main/java/org/jsoup/nodes/Element.java
index 583f7d1a..376a281c 100644
--- a/src/main/java/org/jsoup/nodes/Element.java
+++ b/src/main/java/org/jsoup/nodes/Element.java
@@ -2,6 +2,7 @@ package org.jsoup.nodes;
 
 import org.jsoup.helper.ChangeNotifyingArrayList;
 import org.jsoup.helper.Validate;
+import org.jsoup.internal.NonnullByDefault;
 import org.jsoup.internal.StringUtil;
 import org.jsoup.parser.ParseSettings;
 import org.jsoup.parser.Tag;
@@ -14,6 +15,7 @@ import org.jsoup.select.NodeVisitor;
 import org.jsoup.select.QueryParser;
 import org.jsoup.select.Selector;
 
+import javax.annotation.Nullable;
 import java.io.IOException;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
@@ -37,14 +39,15 @@ import static org.jsoup.internal.Normalizer.normalize;
  * 
  * @author Jonathan Hedley, jonathan@hedley.net
  */
+@NonnullByDefault
 public class Element extends Node {
     private static final List<Node> EMPTY_NODES = Collections.emptyList();
     private static final Pattern classSplit = Pattern.compile("\\s+");
     private static final String baseUriKey = Attributes.internalKey("baseUri");
     private Tag tag;
-    private WeakReference<List<Element>> shadowChildrenRef; // points to child elements shadowed from node children
+    private @Nullable WeakReference<List<Element>> shadowChildrenRef; // points to child elements shadowed from node children
     List<Node> childNodes;
-    private Attributes attributes;
+    private @Nullable Attributes attributes; // field is nullable but all methods for attributes are non null
 
     /**
      * Create a new, standalone element.
@@ -63,7 +66,7 @@ public class Element extends Node {
      * @see #appendChild(Node)
      * @see #appendElement(String)
      */
-    public Element(Tag tag, String baseUri, Attributes attributes) {
+    public Element(Tag tag, @Nullable String baseUri, @Nullable Attributes attributes) {
         Validate.notNull(tag);
         childNodes = EMPTY_NODES;
         this.attributes = attributes;
@@ -97,7 +100,7 @@ public class Element extends Node {
 
     @Override
     public Attributes attributes() {
-        if (!hasAttributes())
+        if (attributes == null) // not using hasAttributes, as doesn't clear warning
             attributes = new Attributes();
         return attributes;
     }
@@ -110,7 +113,7 @@ public class Element extends Node {
     private static String searchUpForAttribute(final Element start, final String key) {
         Element el = start;
         while (el != null) {
-            if (el.hasAttributes() && el.attributes.hasKey(key))
+            if (el.attributes != null && el.attributes.hasKey(key))
                 return el.attributes.get(key);
             el = el.parent();
         }
@@ -176,7 +179,7 @@ public class Element extends Node {
     
     /**
      * Test if this element is a block-level element. (E.g. {@code <div> == true} or an inline element
-     * {@code <p> == false}).
+     * {@code <span> == false}).
      * 
      * @return true if block, false if not (and thus inline)
      */
@@ -190,7 +193,18 @@ public class Element extends Node {
      * @return The id attribute, if present, or an empty string if not.
      */
     public String id() {
-        return hasAttributes() ? attributes.getIgnoreCase("id") :"";
+        return attributes != null ? attributes.getIgnoreCase("id") :"";
+    }
+
+    /**
+     Set the {@code id} attribute of this element.
+     @param id the ID value to use
+     @return this Element, for chaining
+     */
+    public Element id(String id) {
+        Validate.notNull(id);
+        attr("id", id);
+        return this;
     }
 
     /**
@@ -306,7 +320,7 @@ public class Element extends Node {
      * TODO - think about pulling this out as a helper as there are other shadow lists (like in Attributes) kept around.
      * @return a list of child elements
      */
-    private List<Element> childElementsList() {
+    List<Element> childElementsList() {
         List<Element> children;
         if (shadowChildrenRef == null || (children = shadowChildrenRef.get()) == null) {
             final int size = childNodes.size();
@@ -416,7 +430,7 @@ public class Element extends Node {
      * @param cssQuery cssQuery a {@link Selector} CSS-like query
      * @return the first matching element, or <b>{@code null}</b> if there is no match.
      */
-    public Element selectFirst(String cssQuery) {
+    public @Nullable Element selectFirst(String cssQuery) {
         return Selector.selectFirst(cssQuery, this);
     }
 
@@ -428,7 +442,7 @@ public class Element extends Node {
      * @return the first matching element (walking down the tree, starting from this element), or {@code null} if none
      *     matchn.
      */
-    public Element selectFirst(Evaluator evaluator) {
+    public @Nullable Element selectFirst(Evaluator evaluator) {
         return Collector.findFirst(evaluator, this);
     }
 
@@ -459,7 +473,7 @@ public class Element extends Node {
      * @return the closest ancestor element (possibly itself) that matches the provided evaluator. {@code null} if not
      * found.
      */
-    public Element closest(String cssQuery) {
+    public @Nullable Element closest(String cssQuery) {
         return closest(QueryParser.parse(cssQuery));
     }
 
@@ -470,7 +484,7 @@ public class Element extends Node {
      * @return the closest ancestor element (possibly itself) that matches the provided evaluator. {@code null} if not
      * found.
      */
-    public Element closest(Evaluator evaluator) {
+    public @Nullable Element closest(Evaluator evaluator) {
         Validate.notNull(evaluator);
         Element el = this;
         final Element root = root();
@@ -768,7 +782,7 @@ public class Element extends Node {
      * @return the next element, or null if there is no next element
      * @see #previousElementSibling()
      */
-    public Element nextElementSibling() {
+    public @Nullable Element nextElementSibling() {
         if (parentNode == null) return null;
         List<Element> siblings = parent().childElementsList();
         int index = indexInList(this, siblings);
@@ -792,7 +806,7 @@ public class Element extends Node {
      * @return the previous element, or null if there is no previous element
      * @see #nextElementSibling()
      */
-    public Element previousElementSibling() {
+    public @Nullable Element previousElementSibling() {
         if (parentNode == null) return null;
         List<Element> siblings = parent().childElementsList();
         int index = indexInList(this, siblings);
@@ -820,13 +834,15 @@ public class Element extends Node {
     }
 
     /**
-     * Gets the first element sibling of this element.
+     * Gets the first Element sibling of this element. That may be this element.
      * @return the first sibling that is an element (aka the parent's first element child) 
      */
     public Element firstElementSibling() {
-        // todo: should firstSibling() exclude this?
-        List<Element> siblings = parent().childElementsList();
-        return siblings.size() > 1 ? siblings.get(0) : null;
+        if (parent() != null) {
+            List<Element> siblings = parent().childElementsList();
+            return siblings.size() > 1 ? siblings.get(0) : this;
+        } else
+            return this; // orphan is its own first sibling
     }
     
     /**
@@ -840,12 +856,15 @@ public class Element extends Node {
     }
 
     /**
-     * Gets the last element sibling of this element
+     * Gets the last element sibling of this element. That may be this element.
      * @return the last sibling that is an element (aka the parent's last element child) 
      */
     public Element lastElementSibling() {
-        List<Element> siblings = parent().childElementsList();
-        return siblings.size() > 1 ? siblings.get(siblings.size() - 1) : null;
+        if (parent() != null) {
+            List<Element> siblings = parent().childElementsList();
+            return siblings.size() > 1 ? siblings.get(siblings.size() - 1) : this;
+        } else
+            return this;
     }
 
     private static <E extends Element> int indexInList(Element search, List<E> elements) {
@@ -880,7 +899,7 @@ public class Element extends Node {
      * @param id The ID to search for.
      * @return The first matching element by ID, starting with this element, or null if none found.
      */
-    public Element getElementById(String id) {
+    public @Nullable Element getElementById(String id) {
         Validate.notEmpty(id);
         
         Elements elements = Collector.collect(new Evaluator.Id(id), this);
@@ -1126,14 +1145,20 @@ public class Element extends Node {
     }
 
     /**
-     * Gets the combined text of this element and all its children. Whitespace is normalized and trimmed.
-     * <p>
-     * For example, given HTML {@code <p>Hello  <b>there</b> now! </p>}, {@code p.text()} returns {@code "Hello there now!"}
-     *
-     * @return unencoded, normalized text, or empty string if none.
-     * @see #wholeText() if you don't want the text to be normalized.
-     * @see #ownText()
-     * @see #textNodes()
+     Gets the <b>normalized, combined text</b> of this element and all its children. Whitespace is normalized and
+     trimmed.
+     <p>For example, given HTML {@code <p>Hello  <b>there</b> now! </p>}, {@code p.text()} returns {@code "Hello there
+    now!"}
+     <p>If you do not want normalized text, use {@link #wholeText()}. If you want just the text of this node (and not
+     children), use {@link #ownText()}
+     <p>Note that this method returns the textual content that would be presented to a reader. The contents of data
+     nodes (such as {@code <script>} tags are not considered text. Use {@link #data()} or {@link #html()} to retrieve
+     that content.
+
+     @return unencoded, normalized text, or empty string if none.
+     @see #wholeText()
+     @see #ownText()
+     @see #textNodes()
      */
     public String text() {
         final StringBuilder accum = StringUtil.borrowBuilder();
@@ -1190,7 +1215,7 @@ public class Element extends Node {
     }
 
     /**
-     * Gets the text owned by this element only; does not get the combined text of all children.
+     * Gets the (normalized) text owned by this element only; does not get the combined text of all children.
      * <p>
      * For example, given HTML {@code <p>Hello <b>there</b> now!</p>}, {@code p.ownText()} returns {@code "Hello now!"},
      * whereas {@code p.text()} returns {@code "Hello there now!"}.
@@ -1231,7 +1256,7 @@ public class Element extends Node {
             accum.append(" ");
     }
 
-    static boolean preserveWhitespace(Node node) {
+    static boolean preserveWhitespace(@Nullable Node node) {
         // looks only at this element and five levels up, to prevent recursion & needless stack searches
         if (node instanceof Element) {
             Element el = (Element) node;
@@ -1247,16 +1272,22 @@ public class Element extends Node {
     }
 
     /**
-     * Set the text of this element. Any existing contents (text or elements) will be cleared
+     * Set the text of this element. Any existing contents (text or elements) will be cleared.
+     * <p>As a special case, for {@code <script>} and {@code <style> tags, the input text will be treated as data,
+     * not visible text.</p>
      * @param text unencoded text
      * @return this element
      */
     public Element text(String text) {
         Validate.notNull(text);
-
         empty();
-        TextNode textNode = new TextNode(text);
-        appendChild(textNode);
+        // special case for script/style in HTML: should be data node
+        Document owner = ownerDocument();
+        // an alternate impl would be to run through the parser
+        if (owner != null && owner.parser().isContentForTagData(normalName()))
+            appendChild(new DataNode(text));
+        else
+            appendChild(new TextNode(text));
 
         return this;
     }
@@ -1281,8 +1312,8 @@ public class Element extends Node {
     }
 
     /**
-     * Get the combined data of this element. Data is e.g. the inside of a {@code script} tag. Note that data is NOT the
-     * text of the element. Use {@link #text()} to get the text that would be visible to a user, and {@link #data()}
+     * Get the combined data of this element. Data is e.g. the inside of a {@code <script>} tag. Note that data is NOT the
+     * text of the element. Use {@link #text()} to get the text that would be visible to a user, and {@code data()}
      * for the contents of scripts, comments, CSS styles, etc.
      *
      * @return the data, or empty string if none
@@ -1358,7 +1389,7 @@ public class Element extends Node {
      */
     // performance sensitive
     public boolean hasClass(String className) {
-        if (!hasAttributes())
+        if (attributes == null)
             return false;
 
         final String classAttr = attributes.getIgnoreCase("class");
@@ -1555,12 +1586,11 @@ public class Element extends Node {
     }
 
     @Override
-    protected Element doClone(Node parent) {
+    protected Element doClone(@Nullable Node parent) {
         Element clone = (Element) super.doClone(parent);
         clone.attributes = attributes != null ? attributes.clone() : null;
         clone.childNodes = new NodeList(clone, childNodes.size());
         clone.childNodes.addAll(childNodes); // the children then get iterated and cloned in Node.clone
-        clone.setBaseUri(baseUri());
 
         return clone;
     }
@@ -1616,7 +1646,7 @@ public class Element extends Node {
     private boolean isInlineable(Document.OutputSettings out) {
         return tag().isInline()
             && !tag().isEmpty()
-            && parent().isBlock()
+            && (parent() == null || parent().isBlock())
             && previousSibling() != null
             && !out.outline();
     }
diff --git a/src/main/java/org/jsoup/nodes/Entities.java b/src/main/java/org/jsoup/nodes/Entities.java
index 1e534591..7c0c15b6 100644
--- a/src/main/java/org/jsoup/nodes/Entities.java
+++ b/src/main/java/org/jsoup/nodes/Entities.java
@@ -45,7 +45,7 @@ public class Entities {
         private int[] codeVals; // limitation is the few references with multiple characters; those go into multipoints.
 
         // table of codepoints to named entities.
-        private int[] codeKeys; // we don' support multicodepoints to single named value currently
+        private int[] codeKeys; // we don't support multicodepoints to single named value currently
         private String[] nameVals;
 
         EscapeMode(String file, int size) {
diff --git a/src/main/java/org/jsoup/nodes/FormElement.java b/src/main/java/org/jsoup/nodes/FormElement.java
index 7d631fbb..261fc1e0 100644
--- a/src/main/java/org/jsoup/nodes/FormElement.java
+++ b/src/main/java/org/jsoup/nodes/FormElement.java
@@ -62,7 +62,7 @@ public class FormElement extends Element {
     public Connection submit() {
         String action = hasAttr("action") ? absUrl("action") : baseUri();
         Validate.notEmpty(action, "Could not determine a form action URL for submit. Ensure you set a base URI when parsing.");
-        Connection.Method method = attr("method").toUpperCase().equals("POST") ?
+        Connection.Method method = attr("method").equalsIgnoreCase("POST") ?
                 Connection.Method.POST : Connection.Method.GET;
 
         return Jsoup.connect(action)
@@ -96,7 +96,7 @@ public class FormElement extends Element {
                     set = true;
                 }
                 if (!set) {
-                    Element option = el.select("option").first();
+                    Element option = el.selectFirst("option");
                     if (option != null)
                         data.add(HttpConnection.KeyVal.create(name, option.val()));
                 }
diff --git a/src/main/java/org/jsoup/nodes/Node.java b/src/main/java/org/jsoup/nodes/Node.java
index 58296313..30303296 100644
--- a/src/main/java/org/jsoup/nodes/Node.java
+++ b/src/main/java/org/jsoup/nodes/Node.java
@@ -7,6 +7,7 @@ import org.jsoup.select.NodeFilter;
 import org.jsoup.select.NodeTraversor;
 import org.jsoup.select.NodeVisitor;
 
+import javax.annotation.Nullable;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -21,7 +22,7 @@ import java.util.List;
  @author Jonathan Hedley, jonathan@hedley.net */
 public abstract class Node implements Cloneable {
     static final String EmptyString = "";
-    Node parentNode;
+    @Nullable Node parentNode; // Nodes don't always have parents
     int siblingIndex;
 
     /**
@@ -41,6 +42,11 @@ public abstract class Node implements Cloneable {
      */
     protected abstract boolean hasAttributes();
 
+    /**
+     Checks if this node has a parent. Nodes won't have parents if e.g. they are newly created and not added as a child
+     to an exsiting node, or if they are a {@link #shallowClone()}. In such cases, {@link #parent()} will return {@code null}.
+     @return if this node has a parent.
+     */
     public boolean hasParent() {
         return parentNode != null;
     }
@@ -240,8 +246,9 @@ public abstract class Node implements Cloneable {
     /**
      Gets this node's parent node.
      @return parent node; or null if no parent.
+     @see #hasParent()
      */
-    public Node parent() {
+    public @Nullable Node parent() {
         return parentNode;
     }
 
@@ -249,7 +256,7 @@ public abstract class Node implements Cloneable {
      Gets this node's parent node. Not overridable by extending classes, so useful if you really just need the Node type.
      @return parent node; or null if no parent.
      */
-    public final Node parentNode() {
+    public @Nullable final Node parentNode() {
         return parentNode;
     }
 
@@ -268,7 +275,7 @@ public abstract class Node implements Cloneable {
      * Gets the Document associated with this Node.
      * @return the Document associated with this Node, or null if there is no such Document.
      */
-    public Document ownerDocument() {
+    public @Nullable Document ownerDocument() {
         Node root = root();
         return (root instanceof Document) ? (Document) root : null;
     }
@@ -342,30 +349,42 @@ public abstract class Node implements Cloneable {
 
     /**
      Wrap the supplied HTML around this node.
-     @param html HTML to wrap around this element, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep.
+
+     @param html HTML to wrap around this node, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep. If
+     the input HTML does not parse to a result starting with an Element, this will be a no-op.
      @return this node, for chaining.
      */
     public Node wrap(String html) {
         Validate.notEmpty(html);
 
-        Element context = parent() instanceof Element ? (Element) parent() : null;
+        // Parse context - parent (because wrapping), this, or null
+        Element context =
+            parentNode != null && parentNode instanceof Element ? (Element) parentNode :
+                this instanceof Element ? (Element) this :
+                    null;
         List<Node> wrapChildren = NodeUtils.parser(this).parseFragmentInput(html, context, baseUri());
         Node wrapNode = wrapChildren.get(0);
         if (!(wrapNode instanceof Element)) // nothing to wrap with; noop
-            return null;
+            return this;
 
         Element wrap = (Element) wrapNode;
         Element deepest = getDeepChild(wrap);
-        parentNode.replaceChild(this, wrap);
-        deepest.addChildren(this);
+        if (parentNode != null)
+            parentNode.replaceChild(this, wrap);
+        deepest.addChildren(this); // side effect of tricking wrapChildren to lose first
 
         // remainder (unbalanced wrap, like <div></div><p></p> -- The <p> is remainder
         if (wrapChildren.size() > 0) {
             //noinspection ForLoopReplaceableByForEach (beacause it allocates an Iterator which is wasteful here)
             for (int i = 0; i < wrapChildren.size(); i++) {
                 Node remainder = wrapChildren.get(i);
-                remainder.parentNode.removeChild(remainder);
-                wrap.appendChild(remainder);
+                // if no parent, this could be the wrap node, so skip
+                if (wrap == remainder)
+                    continue;
+
+                if (remainder.parentNode != null)
+                    remainder.parentNode.removeChild(remainder);
+                wrap.after(remainder);
             }
         }
         return this;
@@ -382,11 +401,11 @@ public abstract class Node implements Cloneable {
      * <p>{@code <div>One Two <b>Three</b></div>}</p>
      * and the {@code "Two "} {@link TextNode} being returned.
      *
-     * @return the first child of this node, after the node has been unwrapped. Null if the node had no children.
+     * @return the first child of this node, after the node has been unwrapped. @{code Null} if the node had no children.
      * @see #remove()
      * @see #wrap(String)
      */
-    public Node unwrap() {
+    public @Nullable Node unwrap() {
         Validate.notNull(parentNode);
         final List<Node> childNodes = ensureChildNodes();
         Node firstChild = childNodes.size() > 0 ? childNodes.get(0) : null;
@@ -477,14 +496,16 @@ public abstract class Node implements Cloneable {
                     break;
                 }
             }
-            firstParent.empty();
-            nodes.addAll(index, Arrays.asList(children));
-            i = children.length;
-            while (i-- > 0) {
-                children[i].parentNode = this;
+            if (sameList) { // moving, so OK to empty firstParent and short-circuit
+                firstParent.empty();
+                nodes.addAll(index, Arrays.asList(children));
+                i = children.length;
+                while (i-- > 0) {
+                    children[i].parentNode = this;
+                }
+                reindexChildren(index);
+                return;
             }
-            reindexChildren(index);
-            return;
         }
 
         Validate.noNullElements(children);
@@ -526,9 +547,9 @@ public abstract class Node implements Cloneable {
 
     /**
      Get this node's next sibling.
-     @return next sibling, or null if this is the last sibling
+     @return next sibling, or @{code null} if this is the last sibling
      */
-    public Node nextSibling() {
+    public @Nullable Node nextSibling() {
         if (parentNode == null)
             return null; // root
 
@@ -542,9 +563,9 @@ public abstract class Node implements Cloneable {
 
     /**
      Get this node's previous sibling.
-     @return the previous sibling, or null if this is the first sibling
+     @return the previous sibling, or @{code null} if this is the first sibling
      */
-    public Node previousSibling() {
+    public @Nullable Node previousSibling() {
         if (parentNode == null)
             return null; // root
 
@@ -641,9 +662,10 @@ public abstract class Node implements Cloneable {
 
     /**
      * Check if this node is the same instance of another (object identity test).
+     * <p>For an node value equality check, see {@link #hasSameValue(Object)}</p>
      * @param o other object to compare to
      * @return true if the content of this node is the same as the other
-     * @see Node#hasSameValue(Object) to compare nodes by their value
+     * @see Node#hasSameValue(Object)
      */
     @Override
     public boolean equals(Object o) {
@@ -657,7 +679,7 @@ public abstract class Node implements Cloneable {
      * @param o other object to compare to
      * @return true if the content of this node is the same as the other
      */
-    public boolean hasSameValue(Object o) {
+    public boolean hasSameValue(@Nullable Object o) {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
 
@@ -710,7 +732,7 @@ public abstract class Node implements Cloneable {
      * Return a clone of the node using the given parent (which can be null).
      * Not a deep copy of children.
      */
-    protected Node doClone(Node parent) {
+    protected Node doClone(@Nullable Node parent) {
         Node clone;
 
         try {
@@ -726,8 +748,8 @@ public abstract class Node implements Cloneable {
     }
 
     private static class OuterHtmlVisitor implements NodeVisitor {
-        private Appendable accum;
-        private Document.OutputSettings out;
+        private final Appendable accum;
+        private final Document.OutputSettings out;
 
         OuterHtmlVisitor(Appendable accum, Document.OutputSettings out) {
             this.accum = accum;
diff --git a/src/main/java/org/jsoup/nodes/package-info.java b/src/main/java/org/jsoup/nodes/package-info.java
index 24b12803..7e66d2ca 100644
--- a/src/main/java/org/jsoup/nodes/package-info.java
+++ b/src/main/java/org/jsoup/nodes/package-info.java
@@ -1,4 +1,7 @@
 /**
  HTML document structure nodes.
  */
-package org.jsoup.nodes;
\ No newline at end of file
+@NonnullByDefault
+package org.jsoup.nodes;
+
+import org.jsoup.internal.NonnullByDefault;
diff --git a/src/main/java/org/jsoup/package-info.java b/src/main/java/org/jsoup/package-info.java
index 5c569659..835aafba 100644
--- a/src/main/java/org/jsoup/package-info.java
+++ b/src/main/java/org/jsoup/package-info.java
@@ -1,4 +1,7 @@
 /**
  Contains the main {@link org.jsoup.Jsoup} class, which provides convenient static access to the jsoup functionality.
  */
+@NonnullByDefault
 package org.jsoup;
+
+import org.jsoup.internal.NonnullByDefault;
diff --git a/src/main/java/org/jsoup/parser/CharacterReader.java b/src/main/java/org/jsoup/parser/CharacterReader.java
index d4f0d2b8..94710fd0 100644
--- a/src/main/java/org/jsoup/parser/CharacterReader.java
+++ b/src/main/java/org/jsoup/parser/CharacterReader.java
@@ -33,7 +33,7 @@ public final class CharacterReader {
         Validate.notNull(input);
         Validate.isTrue(input.markSupported());
         reader = input;
-        charBuf = new char[sz > maxBufferLen ? maxBufferLen : sz];
+        charBuf = new char[Math.min(sz, maxBufferLen)];
         bufferUp();
     }
 
@@ -93,7 +93,7 @@ public final class CharacterReader {
                 bufPos = offset;
                 if (bufMark != -1)
                     bufMark = 0;
-                bufSplitPoint = bufLength > readAheadLimit ? readAheadLimit : bufLength;
+                bufSplitPoint = Math.min(bufLength, readAheadLimit);
             }
         } catch (IOException e) {
             throw new UncheckedIOException(e);
@@ -327,7 +327,7 @@ public final class CharacterReader {
                 case '\'':
                     if (single) break OUTER;
                 case '"':
-                    if (!single) break OUTER;;
+                    if (!single) break OUTER;
                 default:
                     pos++;
             }
diff --git a/src/main/java/org/jsoup/parser/HtmlTreeBuilder.java b/src/main/java/org/jsoup/parser/HtmlTreeBuilder.java
index b879de3e..ddb9bd01 100644
--- a/src/main/java/org/jsoup/parser/HtmlTreeBuilder.java
+++ b/src/main/java/org/jsoup/parser/HtmlTreeBuilder.java
@@ -12,6 +12,8 @@ import org.jsoup.nodes.Node;
 import org.jsoup.nodes.TextNode;
 import org.jsoup.select.Elements;
 
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
 import java.io.Reader;
 import java.io.StringReader;
 import java.util.ArrayList;
@@ -23,7 +25,7 @@ import static org.jsoup.internal.StringUtil.inSorted;
  * HTML Tree Builder; creates a DOM from Tokens.
  */
 public class HtmlTreeBuilder extends TreeBuilder {
-    // tag searches. must be sorted, used in inSorted. MUST update HtmlTreeBuilderTest if more arrays are added.
+    // tag searches. must be sorted, used in inSorted. HtmlTreeBuilderTest validates they're sorted.
     static final String[] TagsSearchInScope = new String[]{"applet", "caption", "html", "marquee", "object", "table", "td", "th"};
     static final String[] TagSearchList = new String[]{"ol", "ul"};
     static final String[] TagSearchButton = new String[]{"button"};
@@ -60,7 +62,7 @@ public class HtmlTreeBuilder extends TreeBuilder {
         return ParseSettings.htmlDefault;
     }
 
-    @Override
+    @Override @ParametersAreNonnullByDefault
     protected void initialiseParse(Reader input, String baseUri, Parser parser) {
         super.initialiseParse(input, baseUri, parser);
 
@@ -79,7 +81,7 @@ public class HtmlTreeBuilder extends TreeBuilder {
         fragmentParsing = false;
     }
 
-    List<Node> parseFragment(String inputFragment, Element context, String baseUri, Parser parser) {
+    List<Node> parseFragment(String inputFragment, @Nullable Element context, String baseUri, Parser parser) {
         // context may be null
         state = HtmlTreeBuilderState.Initial;
         initialiseParse(new StringReader(inputFragment), baseUri, parser);
@@ -106,7 +108,7 @@ public class HtmlTreeBuilder extends TreeBuilder {
             else
                 tokeniser.transition(TokeniserState.Data); // default
 
-            root = new Element(Tag.valueOf("html", settings), baseUri);
+            root = new Element(Tag.valueOf(contextTag, settings), baseUri);
             doc.appendChild(root);
             stack.add(root);
             resetInsertionMode();
@@ -124,8 +126,14 @@ public class HtmlTreeBuilder extends TreeBuilder {
         }
 
         runParser();
-        if (context != null)
+        if (context != null) {
+            // depending on context and the input html, content may have been added outside of the root el
+            // e.g. context=p, input=div, the div will have been pushed out.
+            List<Node> nodes = root.siblingNodes();
+            if (!nodes.isEmpty())
+                root.insertChildren(-1, nodes);
             return root.childNodes();
+        }
         else
             return doc.childNodes();
     }
@@ -196,7 +204,7 @@ public class HtmlTreeBuilder extends TreeBuilder {
 
     Element insert(final Token.StartTag startTag) {
         // cleanup duplicate attributes:
-        if (startTag.attributes != null && !startTag.attributes.isEmpty()) {
+        if (startTag.hasAttributes() && !startTag.attributes.isEmpty()) {
             int dupes = startTag.attributes.deduplicate(settings);
             if (dupes > 0) {
                 error("Duplicate attribute");
@@ -269,7 +277,7 @@ public class HtmlTreeBuilder extends TreeBuilder {
 
         if (characterToken.isCData())
             node = new CDataNode(data);
-        else if (tagName.equals("script") || tagName.equals("style"))
+        else if (isContentForTagData(tagName))
             node = new DataNode(data);
         else
             node = new TextNode(data);
@@ -741,4 +749,13 @@ public class HtmlTreeBuilder extends TreeBuilder {
                 ", currentElement=" + currentElement() +
                 '}';
     }
+
+    private static final String[] dataTags = {"script", "style"};
+    protected boolean isContentForTagData(String normalName) {
+        for (String tag : dataTags) {
+            if (tag.equals(normalName))
+                return true;
+        }
+        return false;
+    }
 }
diff --git a/src/main/java/org/jsoup/parser/HtmlTreeBuilderState.java b/src/main/java/org/jsoup/parser/HtmlTreeBuilderState.java
index 21c8f64c..0d660d19 100644
--- a/src/main/java/org/jsoup/parser/HtmlTreeBuilderState.java
+++ b/src/main/java/org/jsoup/parser/HtmlTreeBuilderState.java
@@ -339,9 +339,11 @@ enum HtmlTreeBuilderState {
                     tb.error(this);
                     // merge attributes onto real html
                     Element html = tb.getStack().get(0);
-                    for (Attribute attribute : startTag.getAttributes()) {
-                        if (!html.hasAttr(attribute.getKey()))
-                            html.attributes().put(attribute);
+                    if (startTag.hasAttributes()) {
+                        for (Attribute attribute : startTag.attributes) {
+                            if (!html.hasAttr(attribute.getKey()))
+                                html.attributes().put(attribute);
+                        }
                     }
                     break;
                 case "body":
@@ -353,9 +355,11 @@ enum HtmlTreeBuilderState {
                     } else {
                         tb.framesetOk(false);
                         Element body = stack.get(1);
-                        for (Attribute attribute : startTag.getAttributes()) {
-                            if (!body.hasAttr(attribute.getKey()))
-                                body.attributes().put(attribute);
+                        if (startTag.hasAttributes()) {
+                            for (Attribute attribute : startTag.attributes) {
+                                if (!body.hasAttr(attribute.getKey()))
+                                    body.attributes().put(attribute);
+                            }
                         }
                     }
                     break;
@@ -451,14 +455,14 @@ enum HtmlTreeBuilderState {
                         return false;
 
                     tb.processStartTag("form");
-                    if (startTag.attributes.hasKey("action")) {
+                    if (startTag.hasAttribute("action")) {
                         Element form = tb.getFormElement();
                         form.attr("action", startTag.attributes.get("action"));
                     }
                     tb.processStartTag("hr");
                     tb.processStartTag("label");
                     // hope you like english.
-                    String prompt = startTag.attributes.hasKey("prompt") ?
+                    String prompt = startTag.hasAttribute("prompt") ?
                         startTag.attributes.get("prompt") :
                         "This is a searchable index. Enter search keywords: ";
 
@@ -466,9 +470,11 @@ enum HtmlTreeBuilderState {
 
                     // input
                     Attributes inputAttribs = new Attributes();
-                    for (Attribute attr : startTag.attributes) {
-                        if (!inSorted(attr.getKey(), Constants.InBodyStartInputAttribs))
-                            inputAttribs.put(attr);
+                    if (startTag.hasAttributes()) {
+                        for (Attribute attr : startTag.attributes) {
+                            if (!inSorted(attr.getKey(), Constants.InBodyStartInputAttribs))
+                                inputAttribs.put(attr);
+                        }
                     }
                     inputAttribs.put("name", "isindex");
                     tb.processStartTag("input", inputAttribs);
@@ -931,7 +937,7 @@ enum HtmlTreeBuilderState {
                 } else if (inSorted(name, InTableToHead)) {
                     return tb.process(t, InHead);
                 } else if (name.equals("input")) {
-                    if (!startTag.attributes.get("type").equalsIgnoreCase("hidden")) {
+                    if (!(startTag.hasAttributes() && startTag.attributes.get("type").equalsIgnoreCase("hidden"))) {
                         return anythingElse(t, tb);
                     } else {
                         tb.insertEmpty(startTag);
diff --git a/src/main/java/org/jsoup/parser/Parser.java b/src/main/java/org/jsoup/parser/Parser.java
index ae64918d..2c755384 100644
--- a/src/main/java/org/jsoup/parser/Parser.java
+++ b/src/main/java/org/jsoup/parser/Parser.java
@@ -93,6 +93,14 @@ public class Parser {
         return settings;
     }
 
+    /**
+     (An internal method, visible for Element. For HTML parse, signals that script and style text should be treated as
+     Data Nodes).
+     */
+    public boolean isContentForTagData(String normalName) {
+        return getTreeBuilder().isContentForTagData(normalName);
+    }
+
     // static parse functions below
     /**
      * Parse HTML into a Document.
diff --git a/src/main/java/org/jsoup/parser/Token.java b/src/main/java/org/jsoup/parser/Token.java
index f9061377..4cdde952 100644
--- a/src/main/java/org/jsoup/parser/Token.java
+++ b/src/main/java/org/jsoup/parser/Token.java
@@ -123,6 +123,14 @@ abstract class Token {
             pendingAttributeValueS = null;
         }
 
+        final boolean hasAttributes() {
+            return attributes != null;
+        }
+
+        final boolean hasAttribute(String key) {
+            return attributes != null && attributes.hasKey(key);
+        }
+
         final void finaliseTag() {
             // finalises for emit
             if (pendingAttributeName != null) {
@@ -141,6 +149,10 @@ abstract class Token {
             return normalName;
         }
 
+        final String toStringName() {
+            return tagName != null ? tagName : "[unset]";
+        }
+
         final Tag name(String name) {
             tagName = name;
             normalName = lowerCase(name);
@@ -151,12 +163,6 @@ abstract class Token {
             return selfClosing;
         }
 
-        final Attributes getAttributes() {
-            if (attributes == null)
-                attributes = new Attributes();
-            return attributes;
-        }
-
         // these appenders are rarely hit in not null state-- caused by null chars.
         final void appendTagName(String append) {
             tagName = tagName == null ? append : tagName.concat(append);
@@ -237,10 +243,10 @@ abstract class Token {
 
         @Override
         public String toString() {
-            if (attributes != null && attributes.size() > 0)
-                return "<" + name() + " " + attributes.toString() + ">";
+            if (hasAttributes() && attributes.size() > 0)
+                return "<" + toStringName() + " " + attributes.toString() + ">";
             else
-                return "<" + name() + ">";
+                return "<" + toStringName() + ">";
         }
     }
 
@@ -252,7 +258,7 @@ abstract class Token {
 
         @Override
         public String toString() {
-            return "</" + (tagName != null ? tagName : "(unset)") + ">";
+            return "</" + toStringName() + ">";
         }
     }
 
diff --git a/src/main/java/org/jsoup/parser/TokenQueue.java b/src/main/java/org/jsoup/parser/TokenQueue.java
index 8b4cbb2e..f2690517 100644
--- a/src/main/java/org/jsoup/parser/TokenQueue.java
+++ b/src/main/java/org/jsoup/parser/TokenQueue.java
@@ -268,7 +268,7 @@ public class TokenQueue {
         do {
             if (isEmpty()) break;
             char c = consume();
-            if (last == 0 || last != ESC) {
+            if (last != ESC) {
                 if (c == '\'' && c != open && !inDoubleQuote)
                     inSingleQuote = !inSingleQuote;
                 else if (c == '"' && c != open && !inSingleQuote)
@@ -306,7 +306,7 @@ public class TokenQueue {
         char last = 0;
         for (char c : in.toCharArray()) {
             if (c == ESC) {
-                if (last != 0 && last == ESC)
+                if (last == ESC)
                     out.append(c);
             }
             else 
@@ -396,7 +396,7 @@ public class TokenQueue {
      @return remained of queue.
      */
     public String remainder() {
-        final String remainder = queue.substring(pos, queue.length());
+        final String remainder = queue.substring(pos);
         pos = queue.length();
         return remainder;
     }
diff --git a/src/main/java/org/jsoup/parser/Tokeniser.java b/src/main/java/org/jsoup/parser/Tokeniser.java
index 6d2e8445..0f08cae2 100644
--- a/src/main/java/org/jsoup/parser/Tokeniser.java
+++ b/src/main/java/org/jsoup/parser/Tokeniser.java
@@ -85,7 +85,7 @@ final class Tokeniser {
             lastStartTag = startTag.tagName;
         } else if (token.type == Token.TokenType.EndTag) {
             Token.EndTag endTag = (Token.EndTag) token;
-            if (endTag.attributes != null)
+            if (endTag.hasAttributes())
                 error("Attributes incorrectly present on end tag");
         }
     }
@@ -183,7 +183,6 @@ final class Tokeniser {
             if (charval == -1 || (charval >= 0xD800 && charval <= 0xDFFF) || charval > 0x10FFFF) {
                 characterReferenceError("character outside of valid range");
                 codeRef[0] = replacementChar;
-                return codeRef;
             } else {
                 // fix illegal unicode characters to match browser behavior
                 if (charval >= win1252ExtensionsStart && charval < win1252ExtensionsStart + win1252Extensions.length) {
@@ -194,8 +193,8 @@ final class Tokeniser {
                 // todo: implement number replacement table
                 // todo: check for extra illegal unicode points as parse errors
                 codeRef[0] = charval;
-                return codeRef;
             }
+            return codeRef;
         } else { // named
             // get as many letters as possible, and look for matching entities.
             String nameRef = reader.consumeLetterThenDigitSequence();
diff --git a/src/main/java/org/jsoup/parser/TreeBuilder.java b/src/main/java/org/jsoup/parser/TreeBuilder.java
index 5292705c..baa45037 100644
--- a/src/main/java/org/jsoup/parser/TreeBuilder.java
+++ b/src/main/java/org/jsoup/parser/TreeBuilder.java
@@ -6,6 +6,7 @@ import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Element;
 import org.jsoup.nodes.Node;
 
+import javax.annotation.ParametersAreNonnullByDefault;
 import java.io.Reader;
 import java.util.ArrayList;
 import java.util.List;
@@ -27,9 +28,11 @@ abstract class TreeBuilder {
     private Token.EndTag end  = new Token.EndTag();
     abstract ParseSettings defaultSettings();
 
+    @ParametersAreNonnullByDefault
     protected void initialiseParse(Reader input, String baseUri, Parser parser) {
         Validate.notNull(input, "String input must not be null");
         Validate.notNull(baseUri, "BaseURI must not be null");
+        Validate.notNull(parser);
 
         doc = new Document(baseUri);
         doc.parser(parser);
@@ -42,6 +45,7 @@ abstract class TreeBuilder {
         this.baseUri = baseUri;
     }
 
+    @ParametersAreNonnullByDefault
     Document parse(Reader input, String baseUri, Parser parser) {
         initialiseParse(input, baseUri, parser);
         runParser();
@@ -104,7 +108,6 @@ abstract class TreeBuilder {
         return size > 0 ? stack.get(size-1) : null;
     }
 
-
     /**
      * If the parser is tracking errors, and an error at the current position.
      * @param msg error message
@@ -114,4 +117,12 @@ abstract class TreeBuilder {
         if (errors.canAddError())
             errors.add(new ParseError(reader.pos(), msg));
     }
+
+    /**
+     (An internal method, visible for Element. For HTML parse, signals that script and style text should be treated as
+     Data Nodes).
+     */
+    protected boolean isContentForTagData(String normalName) {
+        return false;
+    }
 }
diff --git a/src/main/java/org/jsoup/parser/XmlTreeBuilder.java b/src/main/java/org/jsoup/parser/XmlTreeBuilder.java
index 36172511..b85cc3f5 100644
--- a/src/main/java/org/jsoup/parser/XmlTreeBuilder.java
+++ b/src/main/java/org/jsoup/parser/XmlTreeBuilder.java
@@ -10,6 +10,7 @@ import org.jsoup.nodes.Node;
 import org.jsoup.nodes.TextNode;
 import org.jsoup.nodes.XmlDeclaration;
 
+import javax.annotation.ParametersAreNonnullByDefault;
 import java.io.Reader;
 import java.io.StringReader;
 import java.util.List;
@@ -26,11 +27,13 @@ public class XmlTreeBuilder extends TreeBuilder {
         return ParseSettings.preserveCase;
     }
 
-    @Override
+    @Override @ParametersAreNonnullByDefault
     protected void initialiseParse(Reader input, String baseUri, Parser parser) {
         super.initialiseParse(input, baseUri, parser);
         stack.add(doc); // place the document onto the stack. differs from HtmlTreeBuilder (not on stack)
-        doc.outputSettings().syntax(Document.OutputSettings.Syntax.xml);
+        doc.outputSettings()
+            .syntax(Document.OutputSettings.Syntax.xml)
+            .prettyPrint(false); // as XML, we don't understand what whitespace is significant or not
     }
 
     Document parse(Reader input, String baseUri) {
@@ -75,7 +78,7 @@ public class XmlTreeBuilder extends TreeBuilder {
     Element insert(Token.StartTag startTag) {
         Tag tag = Tag.valueOf(startTag.name(), settings);
         // todo: wonder if for xml parsing, should treat all tags as unknown? because it's not html.
-        if (startTag.attributes != null)
+        if (startTag.hasAttributes())
             startTag.attributes.deduplicate(settings);
 
         Element el = new Element(tag, null, settings.normalizeAttributes(startTag.attributes));
diff --git a/src/main/java/org/jsoup/safety/Cleaner.java b/src/main/java/org/jsoup/safety/Cleaner.java
index 22ecb0b3..bbe44ce4 100644
--- a/src/main/java/org/jsoup/safety/Cleaner.java
+++ b/src/main/java/org/jsoup/safety/Cleaner.java
@@ -18,35 +18,46 @@ import java.util.List;
 
 
 /**
- The whitelist based HTML cleaner. Use to ensure that end-user provided HTML contains only the elements and attributes
+ The safelist based HTML cleaner. Use to ensure that end-user provided HTML contains only the elements and attributes
  that you are expecting; no junk, and no cross-site scripting attacks!
  <p>
- The HTML cleaner parses the input as HTML and then runs it through a white-list, so the output HTML can only contain
- HTML that is allowed by the whitelist.
+ The HTML cleaner parses the input as HTML and then runs it through a safe-list, so the output HTML can only contain
+ HTML that is allowed by the safelist.
  </p>
  <p>
  It is assumed that the input HTML is a body fragment; the clean methods only pull from the source's body, and the
- canned white-lists only allow body contained tags.
+ canned safe-lists only allow body contained tags.
  </p>
  <p>
  Rather than interacting directly with a Cleaner object, generally see the {@code clean} methods in {@link org.jsoup.Jsoup}.
  </p>
  */
 public class Cleaner {
-    private Whitelist whitelist;
+    private final Safelist safelist;
 
     /**
-     Create a new cleaner, that sanitizes documents using the supplied whitelist.
-     @param whitelist white-list to clean with
+     Create a new cleaner, that sanitizes documents using the supplied safelist.
+     @param safelist safe-list to clean with
      */
+    public Cleaner(Safelist safelist) {
+        Validate.notNull(safelist);
+        this.safelist = safelist;
+    }
+
+    /**
+     Use {@link #Cleaner(Safelist)} instead.
+     @deprecated as of 1.14.1.
+     */
+    @Deprecated
     public Cleaner(Whitelist whitelist) {
         Validate.notNull(whitelist);
-        this.whitelist = whitelist;
+        this.safelist = whitelist;
     }
 
     /**
-     Creates a new, clean document, from the original dirty document, containing only elements allowed by the whitelist.
-     The original document is not modified. Only elements from the dirt document's <code>body</code> are used.
+     Creates a new, clean document, from the original dirty document, containing only elements allowed by the safelist.
+     The original document is not modified. Only elements from the dirty document's <code>body</code> are used. The
+     OutputSettings of the original document are cloned into the clean document.
      @param dirtyDocument Untrusted base document to clean.
      @return cleaned document.
      */
@@ -54,15 +65,15 @@ public class Cleaner {
         Validate.notNull(dirtyDocument);
 
         Document clean = Document.createShell(dirtyDocument.baseUri());
-        if (dirtyDocument.body() != null) // frameset documents won't have a body. the clean doc will have empty body.
-            copySafeNodes(dirtyDocument.body(), clean.body());
+        copySafeNodes(dirtyDocument.body(), clean.body());
+        clean.outputSettings(dirtyDocument.outputSettings().clone());
 
         return clean;
     }
 
     /**
-     Determines if the input document <b>body</b>is valid, against the whitelist. It is considered valid if all the tags and attributes
-     in the input HTML are allowed by the whitelist, and that there is no content in the <code>head</code>.
+     Determines if the input document <b>body</b>is valid, against the safelist. It is considered valid if all the tags and attributes
+     in the input HTML are allowed by the safelist, and that there is no content in the <code>head</code>.
      <p>
      This method can be used as a validator for user input. An invalid document will still be cleaned successfully
      using the {@link #clean(Document)} document. If using as a validator, it is recommended to still clean the document
@@ -107,7 +118,7 @@ public class Cleaner {
             if (source instanceof Element) {
                 Element sourceEl = (Element) source;
 
-                if (whitelist.isSafeTag(sourceEl.normalName())) { // safe, clone and copy safe attrs
+                if (safelist.isSafeTag(sourceEl.normalName())) { // safe, clone and copy safe attrs
                     ElementMeta meta = createSafeElement(sourceEl);
                     Element destChild = meta.el;
                     destination.appendChild(destChild);
@@ -121,7 +132,7 @@ public class Cleaner {
                 TextNode sourceText = (TextNode) source;
                 TextNode destText = new TextNode(sourceText.getWholeText());
                 destination.appendChild(destText);
-            } else if (source instanceof DataNode && whitelist.isSafeTag(source.parent().nodeName())) {
+            } else if (source instanceof DataNode && safelist.isSafeTag(source.parent().nodeName())) {
               DataNode sourceData = (DataNode) source;
               DataNode destData = new DataNode(sourceData.getWholeData());
               destination.appendChild(destData);
@@ -131,7 +142,7 @@ public class Cleaner {
         }
 
         public void tail(Node source, int depth) {
-            if (source instanceof Element && whitelist.isSafeTag(source.nodeName())) {
+            if (source instanceof Element && safelist.isSafeTag(source.nodeName())) {
                 destination = destination.parent(); // would have descended, so pop destination stack
             }
         }
@@ -151,12 +162,12 @@ public class Cleaner {
 
         Attributes sourceAttrs = sourceEl.attributes();
         for (Attribute sourceAttr : sourceAttrs) {
-            if (whitelist.isSafeAttribute(sourceTag, sourceEl, sourceAttr))
+            if (safelist.isSafeAttribute(sourceTag, sourceEl, sourceAttr))
                 destAttrs.put(sourceAttr);
             else
                 numDiscarded++;
         }
-        Attributes enforcedAttrs = whitelist.getEnforcedAttributes(sourceTag);
+        Attributes enforcedAttrs = safelist.getEnforcedAttributes(sourceTag);
         destAttrs.addAll(enforcedAttrs);
 
         return new ElementMeta(dest, numDiscarded);
diff --git a/src/main/java/org/jsoup/safety/Safelist.java b/src/main/java/org/jsoup/safety/Safelist.java
new file mode 100644
index 00000000..91ad06b3
--- /dev/null
+++ b/src/main/java/org/jsoup/safety/Safelist.java
@@ -0,0 +1,655 @@
+package org.jsoup.safety;
+
+/*
+    Thank you to Ryan Grove (wonko.com) for the Ruby HTML cleaner http://github.com/rgrove/sanitize/, which inspired
+    this safe-list configuration, and the initial defaults.
+ */
+
+import org.jsoup.helper.Validate;
+import org.jsoup.nodes.Attribute;
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.jsoup.internal.Normalizer.lowerCase;
+
+
+/**
+ Safe-lists define what HTML (elements and attributes) to allow through the cleaner. Everything else is removed.
+ <p>
+ Start with one of the defaults:
+ </p>
+ <ul>
+ <li>{@link #none}
+ <li>{@link #simpleText}
+ <li>{@link #basic}
+ <li>{@link #basicWithImages}
+ <li>{@link #relaxed}
+ </ul>
+ <p>
+ If you need to allow more through (please be careful!), tweak a base safelist with:
+ </p>
+ <ul>
+ <li>{@link #addTags}
+ <li>{@link #addAttributes}
+ <li>{@link #addEnforcedAttribute}
+ <li>{@link #addProtocols}
+ </ul>
+ <p>
+ You can remove any setting from an existing safelist with:
+ </p>
+ <ul>
+ <li>{@link #removeTags}
+ <li>{@link #removeAttributes}
+ <li>{@link #removeEnforcedAttribute}
+ <li>{@link #removeProtocols}
+ </ul>
+ 
+ <p>
+ The cleaner and these safelist assume that you want to clean a <code>body</code> fragment of HTML (to add user
+ supplied HTML into a templated page), and not to clean a full HTML document. If the latter is the case, either wrap the
+ document HTML around the cleaned body HTML, or create a safelist that allows <code>html</code> and <code>head</code>
+ elements as appropriate.
+ </p>
+ <p>
+ If you are going to extend a safelist, please be very careful. Make sure you understand what attributes may lead to
+ XSS attack vectors. URL attributes are particularly vulnerable and require careful validation. See 
+ http://ha.ckers.org/xss.html for some XSS attack examples.
+ </p>
+
+ @author Jonathan Hedley
+ */
+public class Safelist {
+    private Set<TagName> tagNames; // tags allowed, lower case. e.g. [p, br, span]
+    private Map<TagName, Set<AttributeKey>> attributes; // tag -> attribute[]. allowed attributes [href] for a tag.
+    private Map<TagName, Map<AttributeKey, AttributeValue>> enforcedAttributes; // always set these attribute values
+    private Map<TagName, Map<AttributeKey, Set<Protocol>>> protocols; // allowed URL protocols for attributes
+    private boolean preserveRelativeLinks; // option to preserve relative links
+
+    /**
+     This safelist allows only text nodes: all HTML will be stripped.
+
+     @return safelist
+     */
+    public static Safelist none() {
+        return new Safelist();
+    }
+
+    /**
+     This safelist allows only simple text formatting: <code>b, em, i, strong, u</code>. All other HTML (tags and
+     attributes) will be removed.
+
+     @return safelist
+     */
+    public static Safelist simpleText() {
+        return new Safelist()
+                .addTags("b", "em", "i", "strong", "u")
+                ;
+    }
+
+    /**
+     <p>
+     This safelist allows a fuller range of text nodes: <code>a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li,
+     ol, p, pre, q, small, span, strike, strong, sub, sup, u, ul</code>, and appropriate attributes.
+     </p>
+     <p>
+     Links (<code>a</code> elements) can point to <code>http, https, ftp, mailto</code>, and have an enforced
+     <code>rel=nofollow</code> attribute.
+     </p>
+     <p>
+     Does not allow images.
+     </p>
+
+     @return safelist
+     */
+    public static Safelist basic() {
+        return new Safelist()
+                .addTags(
+                        "a", "b", "blockquote", "br", "cite", "code", "dd", "dl", "dt", "em",
+                        "i", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong", "sub",
+                        "sup", "u", "ul")
+
+                .addAttributes("a", "href")
+                .addAttributes("blockquote", "cite")
+                .addAttributes("q", "cite")
+
+                .addProtocols("a", "href", "ftp", "http", "https", "mailto")
+                .addProtocols("blockquote", "cite", "http", "https")
+                .addProtocols("cite", "cite", "http", "https")
+
+                .addEnforcedAttribute("a", "rel", "nofollow")
+                ;
+
+    }
+
+    /**
+     This safelist allows the same text tags as {@link #basic}, and also allows <code>img</code> tags, with appropriate
+     attributes, with <code>src</code> pointing to <code>http</code> or <code>https</code>.
+
+     @return safelist
+     */
+    public static Safelist basicWithImages() {
+        return basic()
+                .addTags("img")
+                .addAttributes("img", "align", "alt", "height", "src", "title", "width")
+                .addProtocols("img", "src", "http", "https")
+                ;
+    }
+
+    /**
+     This safelist allows a full range of text and structural body HTML: <code>a, b, blockquote, br, caption, cite,
+     code, col, colgroup, dd, div, dl, dt, em, h1, h2, h3, h4, h5, h6, i, img, li, ol, p, pre, q, small, span, strike, strong, sub,
+     sup, table, tbody, td, tfoot, th, thead, tr, u, ul</code>
+     <p>
+     Links do not have an enforced <code>rel=nofollow</code> attribute, but you can add that if desired.
+     </p>
+
+     @return safelist
+     */
+    public static Safelist relaxed() {
+        return new Safelist()
+                .addTags(
+                        "a", "b", "blockquote", "br", "caption", "cite", "code", "col",
+                        "colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6",
+                        "i", "img", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong",
+                        "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u",
+                        "ul")
+
+                .addAttributes("a", "href", "title")
+                .addAttributes("blockquote", "cite")
+                .addAttributes("col", "span", "width")
+                .addAttributes("colgroup", "span", "width")
+                .addAttributes("img", "align", "alt", "height", "src", "title", "width")
+                .addAttributes("ol", "start", "type")
+                .addAttributes("q", "cite")
+                .addAttributes("table", "summary", "width")
+                .addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width")
+                .addAttributes(
+                        "th", "abbr", "axis", "colspan", "rowspan", "scope",
+                        "width")
+                .addAttributes("ul", "type")
+
+                .addProtocols("a", "href", "ftp", "http", "https", "mailto")
+                .addProtocols("blockquote", "cite", "http", "https")
+                .addProtocols("cite", "cite", "http", "https")
+                .addProtocols("img", "src", "http", "https")
+                .addProtocols("q", "cite", "http", "https")
+                ;
+    }
+
+    /**
+     Create a new, empty safelist. Generally it will be better to start with a default prepared safelist instead.
+
+     @see #basic()
+     @see #basicWithImages()
+     @see #simpleText()
+     @see #relaxed()
+     */
+    public Safelist() {
+        tagNames = new HashSet<>();
+        attributes = new HashMap<>();
+        enforcedAttributes = new HashMap<>();
+        protocols = new HashMap<>();
+        preserveRelativeLinks = false;
+    }
+
+    /**
+     Deep copy an existing Safelist to a new Safelist.
+     @param copy the Safelist to copy
+     */
+    public Safelist(Safelist copy) {
+        this();
+        tagNames.addAll(copy.tagNames);
+        attributes.putAll(copy.attributes);
+        enforcedAttributes.putAll(copy.enforcedAttributes);
+        protocols.putAll(copy.protocols);
+        preserveRelativeLinks = copy.preserveRelativeLinks;
+    }
+
+    /**
+     Add a list of allowed elements to a safelist. (If a tag is not allowed, it will be removed from the HTML.)
+
+     @param tags tag names to allow
+     @return this (for chaining)
+     */
+    public Safelist addTags(String... tags) {
+        Validate.notNull(tags);
+
+        for (String tagName : tags) {
+            Validate.notEmpty(tagName);
+            tagNames.add(TagName.valueOf(tagName));
+        }
+        return this;
+    }
+
+    /**
+     Remove a list of allowed elements from a safelist. (If a tag is not allowed, it will be removed from the HTML.)
+
+     @param tags tag names to disallow
+     @return this (for chaining)
+     */
+    public Safelist removeTags(String... tags) {
+        Validate.notNull(tags);
+
+        for(String tag: tags) {
+            Validate.notEmpty(tag);
+            TagName tagName = TagName.valueOf(tag);
+
+            if(tagNames.remove(tagName)) { // Only look in sub-maps if tag was allowed
+                attributes.remove(tagName);
+                enforcedAttributes.remove(tagName);
+                protocols.remove(tagName);
+            }
+        }
+        return this;
+    }
+
+    /**
+     Add a list of allowed attributes to a tag. (If an attribute is not allowed on an element, it will be removed.)
+     <p>
+     E.g.: <code>addAttributes("a", "href", "class")</code> allows <code>href</code> and <code>class</code> attributes
+     on <code>a</code> tags.
+     </p>
+     <p>
+     To make an attribute valid for <b>all tags</b>, use the pseudo tag <code>:all</code>, e.g.
+     <code>addAttributes(":all", "class")</code>.
+     </p>
+
+     @param tag  The tag the attributes are for. The tag will be added to the allowed tag list if necessary.
+     @param attributes List of valid attributes for the tag
+     @return this (for chaining)
+     */
+    public Safelist addAttributes(String tag, String... attributes) {
+        Validate.notEmpty(tag);
+        Validate.notNull(attributes);
+        Validate.isTrue(attributes.length > 0, "No attribute names supplied.");
+
+        TagName tagName = TagName.valueOf(tag);
+        tagNames.add(tagName);
+        Set<AttributeKey> attributeSet = new HashSet<>();
+        for (String key : attributes) {
+            Validate.notEmpty(key);
+            attributeSet.add(AttributeKey.valueOf(key));
+        }
+        if (this.attributes.containsKey(tagName)) {
+            Set<AttributeKey> currentSet = this.attributes.get(tagName);
+            currentSet.addAll(attributeSet);
+        } else {
+            this.attributes.put(tagName, attributeSet);
+        }
+        return this;
+    }
+
+    /**
+     Remove a list of allowed attributes from a tag. (If an attribute is not allowed on an element, it will be removed.)
+     <p>
+     E.g.: <code>removeAttributes("a", "href", "class")</code> disallows <code>href</code> and <code>class</code>
+     attributes on <code>a</code> tags.
+     </p>
+     <p>
+     To make an attribute invalid for <b>all tags</b>, use the pseudo tag <code>:all</code>, e.g.
+     <code>removeAttributes(":all", "class")</code>.
+     </p>
+
+     @param tag  The tag the attributes are for.
+     @param attributes List of invalid attributes for the tag
+     @return this (for chaining)
+     */
+    public Safelist removeAttributes(String tag, String... attributes) {
+        Validate.notEmpty(tag);
+        Validate.notNull(attributes);
+        Validate.isTrue(attributes.length > 0, "No attribute names supplied.");
+
+        TagName tagName = TagName.valueOf(tag);
+        Set<AttributeKey> attributeSet = new HashSet<>();
+        for (String key : attributes) {
+            Validate.notEmpty(key);
+            attributeSet.add(AttributeKey.valueOf(key));
+        }
+        if(tagNames.contains(tagName) && this.attributes.containsKey(tagName)) { // Only look in sub-maps if tag was allowed
+            Set<AttributeKey> currentSet = this.attributes.get(tagName);
+            currentSet.removeAll(attributeSet);
+
+            if(currentSet.isEmpty()) // Remove tag from attribute map if no attributes are allowed for tag
+                this.attributes.remove(tagName);
+        }
+        if(tag.equals(":all")) // Attribute needs to be removed from all individually set tags
+            for(TagName name: this.attributes.keySet()) {
+                Set<AttributeKey> currentSet = this.attributes.get(name);
+                currentSet.removeAll(attributeSet);
+
+                if(currentSet.isEmpty()) // Remove tag from attribute map if no attributes are allowed for tag
+                    this.attributes.remove(name);
+            }
+        return this;
+    }
+
+    /**
+     Add an enforced attribute to a tag. An enforced attribute will always be added to the element. If the element
+     already has the attribute set, it will be overridden with this value.
+     <p>
+     E.g.: <code>addEnforcedAttribute("a", "rel", "nofollow")</code> will make all <code>a</code> tags output as
+     <code>&lt;a href="..." rel="nofollow"&gt;</code>
+     </p>
+
+     @param tag   The tag the enforced attribute is for. The tag will be added to the allowed tag list if necessary.
+     @param attribute   The attribute name
+     @param value The enforced attribute value
+     @return this (for chaining)
+     */
+    public Safelist addEnforcedAttribute(String tag, String attribute, String value) {
+        Validate.notEmpty(tag);
+        Validate.notEmpty(attribute);
+        Validate.notEmpty(value);
+
+        TagName tagName = TagName.valueOf(tag);
+        tagNames.add(tagName);
+        AttributeKey attrKey = AttributeKey.valueOf(attribute);
+        AttributeValue attrVal = AttributeValue.valueOf(value);
+
+        if (enforcedAttributes.containsKey(tagName)) {
+            enforcedAttributes.get(tagName).put(attrKey, attrVal);
+        } else {
+            Map<AttributeKey, AttributeValue> attrMap = new HashMap<>();
+            attrMap.put(attrKey, attrVal);
+            enforcedAttributes.put(tagName, attrMap);
+        }
+        return this;
+    }
+
+    /**
+     Remove a previously configured enforced attribute from a tag.
+
+     @param tag   The tag the enforced attribute is for.
+     @param attribute   The attribute name
+     @return this (for chaining)
+     */
+    public Safelist removeEnforcedAttribute(String tag, String attribute) {
+        Validate.notEmpty(tag);
+        Validate.notEmpty(attribute);
+
+        TagName tagName = TagName.valueOf(tag);
+        if(tagNames.contains(tagName) && enforcedAttributes.containsKey(tagName)) {
+            AttributeKey attrKey = AttributeKey.valueOf(attribute);
+            Map<AttributeKey, AttributeValue> attrMap = enforcedAttributes.get(tagName);
+            attrMap.remove(attrKey);
+
+            if(attrMap.isEmpty()) // Remove tag from enforced attribute map if no enforced attributes are present
+                enforcedAttributes.remove(tagName);
+        }
+        return this;
+    }
+
+    /**
+     * Configure this Safelist to preserve relative links in an element's URL attribute, or convert them to absolute
+     * links. By default, this is <b>false</b>: URLs will be  made absolute (e.g. start with an allowed protocol, like
+     * e.g. {@code http://}.
+     * <p>
+     * Note that when handling relative links, the input document must have an appropriate {@code base URI} set when
+     * parsing, so that the link's protocol can be confirmed. Regardless of the setting of the {@code preserve relative
+     * links} option, the link must be resolvable against the base URI to an allowed protocol; otherwise the attribute
+     * will be removed.
+     * </p>
+     *
+     * @param preserve {@code true} to allow relative links, {@code false} (default) to deny
+     * @return this Safelist, for chaining.
+     * @see #addProtocols
+     */
+    public Safelist preserveRelativeLinks(boolean preserve) {
+        preserveRelativeLinks = preserve;
+        return this;
+    }
+
+    /**
+     Add allowed URL protocols for an element's URL attribute. This restricts the possible values of the attribute to
+     URLs with the defined protocol.
+     <p>
+     E.g.: <code>addProtocols("a", "href", "ftp", "http", "https")</code>
+     </p>
+     <p>
+     To allow a link to an in-page URL anchor (i.e. <code>&lt;a href="#anchor"&gt;</code>, add a <code>#</code>:<br>
+     E.g.: <code>addProtocols("a", "href", "#")</code>
+     </p>
+
+     @param tag       Tag the URL protocol is for
+     @param attribute       Attribute name
+     @param protocols List of valid protocols
+     @return this, for chaining
+     */
+    public Safelist addProtocols(String tag, String attribute, String... protocols) {
+        Validate.notEmpty(tag);
+        Validate.notEmpty(attribute);
+        Validate.notNull(protocols);
+
+        TagName tagName = TagName.valueOf(tag);
+        AttributeKey attrKey = AttributeKey.valueOf(attribute);
+        Map<AttributeKey, Set<Protocol>> attrMap;
+        Set<Protocol> protSet;
+
+        if (this.protocols.containsKey(tagName)) {
+            attrMap = this.protocols.get(tagName);
+        } else {
+            attrMap = new HashMap<>();
+            this.protocols.put(tagName, attrMap);
+        }
+        if (attrMap.containsKey(attrKey)) {
+            protSet = attrMap.get(attrKey);
+        } else {
+            protSet = new HashSet<>();
+            attrMap.put(attrKey, protSet);
+        }
+        for (String protocol : protocols) {
+            Validate.notEmpty(protocol);
+            Protocol prot = Protocol.valueOf(protocol);
+            protSet.add(prot);
+        }
+        return this;
+    }
+
+    /**
+     Remove allowed URL protocols for an element's URL attribute. If you remove all protocols for an attribute, that
+     attribute will allow any protocol.
+     <p>
+     E.g.: <code>removeProtocols("a", "href", "ftp")</code>
+     </p>
+
+     @param tag Tag the URL protocol is for
+     @param attribute Attribute name
+     @param removeProtocols List of invalid protocols
+     @return this, for chaining
+     */
+    public Safelist removeProtocols(String tag, String attribute, String... removeProtocols) {
+        Validate.notEmpty(tag);
+        Validate.notEmpty(attribute);
+        Validate.notNull(removeProtocols);
+
+        TagName tagName = TagName.valueOf(tag);
+        AttributeKey attr = AttributeKey.valueOf(attribute);
+
+        // make sure that what we're removing actually exists; otherwise can open the tag to any data and that can
+        // be surprising
+        Validate.isTrue(protocols.containsKey(tagName), "Cannot remove a protocol that is not set.");
+        Map<AttributeKey, Set<Protocol>> tagProtocols = protocols.get(tagName);
+        Validate.isTrue(tagProtocols.containsKey(attr), "Cannot remove a protocol that is not set.");
+
+        Set<Protocol> attrProtocols = tagProtocols.get(attr);
+        for (String protocol : removeProtocols) {
+            Validate.notEmpty(protocol);
+            attrProtocols.remove(Protocol.valueOf(protocol));
+        }
+
+        if (attrProtocols.isEmpty()) { // Remove protocol set if empty
+            tagProtocols.remove(attr);
+            if (tagProtocols.isEmpty()) // Remove entry for tag if empty
+                protocols.remove(tagName);
+        }
+        return this;
+    }
+
+    /**
+     * Test if the supplied tag is allowed by this safelist
+     * @param tag test tag
+     * @return true if allowed
+     */
+    protected boolean isSafeTag(String tag) {
+        return tagNames.contains(TagName.valueOf(tag));
+    }
+
+    /**
+     * Test if the supplied attribute is allowed by this safelist for this tag
+     * @param tagName tag to consider allowing the attribute in
+     * @param el element under test, to confirm protocol
+     * @param attr attribute under test
+     * @return true if allowed
+     */
+    protected boolean isSafeAttribute(String tagName, Element el, Attribute attr) {
+        TagName tag = TagName.valueOf(tagName);
+        AttributeKey key = AttributeKey.valueOf(attr.getKey());
+
+        Set<AttributeKey> okSet = attributes.get(tag);
+        if (okSet != null && okSet.contains(key)) {
+            if (protocols.containsKey(tag)) {
+                Map<AttributeKey, Set<Protocol>> attrProts = protocols.get(tag);
+                // ok if not defined protocol; otherwise test
+                return !attrProts.containsKey(key) || testValidProtocol(el, attr, attrProts.get(key));
+            } else { // attribute found, no protocols defined, so OK
+                return true;
+            }
+        }
+        // might be an enforced attribute?
+        Map<AttributeKey, AttributeValue> enforcedSet = enforcedAttributes.get(tag);
+        if (enforcedSet != null) {
+            Attributes expect = getEnforcedAttributes(tagName);
+            String attrKey = attr.getKey();
+            if (expect.hasKeyIgnoreCase(attrKey)) {
+                return expect.getIgnoreCase(attrKey).equals(attr.getValue());
+            }
+        }
+        // no attributes defined for tag, try :all tag
+        return !tagName.equals(":all") && isSafeAttribute(":all", el, attr);
+    }
+
+    private boolean testValidProtocol(Element el, Attribute attr, Set<Protocol> protocols) {
+        // try to resolve relative urls to abs, and optionally update the attribute so output html has abs.
+        // rels without a baseuri get removed
+        String value = el.absUrl(attr.getKey());
+        if (value.length() == 0)
+            value = attr.getValue(); // if it could not be made abs, run as-is to allow custom unknown protocols
+        if (!preserveRelativeLinks)
+            attr.setValue(value);
+        
+        for (Protocol protocol : protocols) {
+            String prot = protocol.toString();
+
+            if (prot.equals("#")) { // allows anchor links
+                if (isValidAnchor(value)) {
+                    return true;
+                } else {
+                    continue;
+                }
+            }
+
+            prot += ":";
+
+            if (lowerCase(value).startsWith(prot)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isValidAnchor(String value) {
+        return value.startsWith("#") && !value.matches(".*\\s.*");
+    }
+
+    Attributes getEnforcedAttributes(String tagName) {
+        Attributes attrs = new Attributes();
+        TagName tag = TagName.valueOf(tagName);
+        if (enforcedAttributes.containsKey(tag)) {
+            Map<AttributeKey, AttributeValue> keyVals = enforcedAttributes.get(tag);
+            for (Map.Entry<AttributeKey, AttributeValue> entry : keyVals.entrySet()) {
+                attrs.put(entry.getKey().toString(), entry.getValue().toString());
+            }
+        }
+        return attrs;
+    }
+    
+    // named types for config. All just hold strings, but here for my sanity.
+
+    static class TagName extends TypedValue {
+        TagName(String value) {
+            super(value);
+        }
+
+        static TagName valueOf(String value) {
+            return new TagName(value);
+        }
+    }
+
+    static class AttributeKey extends TypedValue {
+        AttributeKey(String value) {
+            super(value);
+        }
+
+        static AttributeKey valueOf(String value) {
+            return new AttributeKey(value);
+        }
+    }
+
+    static class AttributeValue extends TypedValue {
+        AttributeValue(String value) {
+            super(value);
+        }
+
+        static AttributeValue valueOf(String value) {
+            return new AttributeValue(value);
+        }
+    }
+
+    static class Protocol extends TypedValue {
+        Protocol(String value) {
+            super(value);
+        }
+
+        static Protocol valueOf(String value) {
+            return new Protocol(value);
+        }
+    }
+
+    abstract static class TypedValue {
+        private String value;
+
+        TypedValue(String value) {
+            Validate.notNull(value);
+            this.value = value;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((value == null) ? 0 : value.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            TypedValue other = (TypedValue) obj;
+            if (value == null) {
+                return other.value == null;
+            } else return value.equals(other.value);
+        }
+
+        @Override
+        public String toString() {
+            return value;
+        }
+    }
+}
diff --git a/src/main/java/org/jsoup/safety/Whitelist.java b/src/main/java/org/jsoup/safety/Whitelist.java
index 229ab360..986cf4d0 100644
--- a/src/main/java/org/jsoup/safety/Whitelist.java
+++ b/src/main/java/org/jsoup/safety/Whitelist.java
@@ -1,643 +1,115 @@
 package org.jsoup.safety;
 
-/*
-    Thank you to Ryan Grove (wonko.com) for the Ruby HTML cleaner http://github.com/rgrove/sanitize/, which inspired
-    this whitelist configuration, and the initial defaults.
- */
-
-import org.jsoup.helper.Validate;
 import org.jsoup.nodes.Attribute;
 import org.jsoup.nodes.Attributes;
 import org.jsoup.nodes.Element;
 
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-import static org.jsoup.internal.Normalizer.lowerCase;
-
-
 /**
- Whitelists define what HTML (elements and attributes) to allow through the cleaner. Everything else is removed.
- <p>
- Start with one of the defaults:
- </p>
- <ul>
- <li>{@link #none}
- <li>{@link #simpleText}
- <li>{@link #basic}
- <li>{@link #basicWithImages}
- <li>{@link #relaxed}
- </ul>
+ @deprecated As of release <code>v1.14.1</code>, this class is deprecated in favour of {@link Safelist}. The name has
+ been changed with the intent of promoting more inclusive language. {@link Safelist} is a drop-in replacement, and no
+ further changes other than updating the name in your code are required to cleanly migrate. This class will be
+ removed in <code>v1.15.1</code>. Until that release, this class acts as a shim to maintain code compatibility
+ (source and binary).
  <p>
- If you need to allow more through (please be careful!), tweak a base whitelist with:
- </p>
- <ul>
- <li>{@link #addTags}
- <li>{@link #addAttributes}
- <li>{@link #addEnforcedAttribute}
- <li>{@link #addProtocols}
- </ul>
- <p>
- You can remove any setting from an existing whitelist with:
- </p>
- <ul>
- <li>{@link #removeTags}
- <li>{@link #removeAttributes}
- <li>{@link #removeEnforcedAttribute}
- <li>{@link #removeProtocols}
- </ul>
- 
- <p>
- The cleaner and these whitelists assume that you want to clean a <code>body</code> fragment of HTML (to add user
- supplied HTML into a templated page), and not to clean a full HTML document. If the latter is the case, either wrap the
- document HTML around the cleaned body HTML, or create a whitelist that allows <code>html</code> and <code>head</code>
- elements as appropriate.
- </p>
- <p>
- If you are going to extend a whitelist, please be very careful. Make sure you understand what attributes may lead to
- XSS attack vectors. URL attributes are particularly vulnerable and require careful validation. See 
- http://ha.ckers.org/xss.html for some XSS attack examples.
- </p>
-
- @author Jonathan Hedley
- */
-public class Whitelist {
-    private Set<TagName> tagNames; // tags allowed, lower case. e.g. [p, br, span]
-    private Map<TagName, Set<AttributeKey>> attributes; // tag -> attribute[]. allowed attributes [href] for a tag.
-    private Map<TagName, Map<AttributeKey, AttributeValue>> enforcedAttributes; // always set these attribute values
-    private Map<TagName, Map<AttributeKey, Set<Protocol>>> protocols; // allowed URL protocols for attributes
-    private boolean preserveRelativeLinks; // option to preserve relative links
-
-    /**
-     This whitelist allows only text nodes: all HTML will be stripped.
-
-     @return whitelist
-     */
-    public static Whitelist none() {
-        return new Whitelist();
+ For a clear rationale of the removal of this change, please see
+ <a href="https://tools.ietf.org/html/draft-knodel-terminology-04" title="draft-knodel-terminology-04">Terminology,
+ Power, and Inclusive Language in Internet-Drafts and RFCs</a> */
+@Deprecated
+public class Whitelist extends Safelist {
+    public Whitelist() {
+        super();
     }
 
-    /**
-     This whitelist allows only simple text formatting: <code>b, em, i, strong, u</code>. All other HTML (tags and
-     attributes) will be removed.
-
-     @return whitelist
-     */
-    public static Whitelist simpleText() {
-        return new Whitelist()
-                .addTags("b", "em", "i", "strong", "u")
-                ;
+    public Whitelist(Safelist copy) {
+        super(copy);
     }
 
-    /**
-     <p>
-     This whitelist allows a fuller range of text nodes: <code>a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li,
-     ol, p, pre, q, small, span, strike, strong, sub, sup, u, ul</code>, and appropriate attributes.
-     </p>
-     <p>
-     Links (<code>a</code> elements) can point to <code>http, https, ftp, mailto</code>, and have an enforced
-     <code>rel=nofollow</code> attribute.
-     </p>
-     <p>
-     Does not allow images.
-     </p>
-
-     @return whitelist
-     */
-    public static Whitelist basic() {
-        return new Whitelist()
-                .addTags(
-                        "a", "b", "blockquote", "br", "cite", "code", "dd", "dl", "dt", "em",
-                        "i", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong", "sub",
-                        "sup", "u", "ul")
-
-                .addAttributes("a", "href")
-                .addAttributes("blockquote", "cite")
-                .addAttributes("q", "cite")
-
-                .addProtocols("a", "href", "ftp", "http", "https", "mailto")
-                .addProtocols("blockquote", "cite", "http", "https")
-                .addProtocols("cite", "cite", "http", "https")
-
-                .addEnforcedAttribute("a", "rel", "nofollow")
-                ;
-
+    static public Whitelist basic() {
+        return new Whitelist(Safelist.basic());
     }
 
-    /**
-     This whitelist allows the same text tags as {@link #basic}, and also allows <code>img</code> tags, with appropriate
-     attributes, with <code>src</code> pointing to <code>http</code> or <code>https</code>.
-
-     @return whitelist
-     */
-    public static Whitelist basicWithImages() {
-        return basic()
-                .addTags("img")
-                .addAttributes("img", "align", "alt", "height", "src", "title", "width")
-                .addProtocols("img", "src", "http", "https")
-                ;
+    static public Whitelist basicWithImages() {
+        return new Whitelist(Safelist.basicWithImages());
     }
 
-    /**
-     This whitelist allows a full range of text and structural body HTML: <code>a, b, blockquote, br, caption, cite,
-     code, col, colgroup, dd, div, dl, dt, em, h1, h2, h3, h4, h5, h6, i, img, li, ol, p, pre, q, small, span, strike, strong, sub,
-     sup, table, tbody, td, tfoot, th, thead, tr, u, ul</code>
-     <p>
-     Links do not have an enforced <code>rel=nofollow</code> attribute, but you can add that if desired.
-     </p>
-
-     @return whitelist
-     */
-    public static Whitelist relaxed() {
-        return new Whitelist()
-                .addTags(
-                        "a", "b", "blockquote", "br", "caption", "cite", "code", "col",
-                        "colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6",
-                        "i", "img", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong",
-                        "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u",
-                        "ul")
-
-                .addAttributes("a", "href", "title")
-                .addAttributes("blockquote", "cite")
-                .addAttributes("col", "span", "width")
-                .addAttributes("colgroup", "span", "width")
-                .addAttributes("img", "align", "alt", "height", "src", "title", "width")
-                .addAttributes("ol", "start", "type")
-                .addAttributes("q", "cite")
-                .addAttributes("table", "summary", "width")
-                .addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width")
-                .addAttributes(
-                        "th", "abbr", "axis", "colspan", "rowspan", "scope",
-                        "width")
-                .addAttributes("ul", "type")
-
-                .addProtocols("a", "href", "ftp", "http", "https", "mailto")
-                .addProtocols("blockquote", "cite", "http", "https")
-                .addProtocols("cite", "cite", "http", "https")
-                .addProtocols("img", "src", "http", "https")
-                .addProtocols("q", "cite", "http", "https")
-                ;
+    static public Whitelist none() {
+        return new Whitelist(Safelist.none());
     }
 
-    /**
-     Create a new, empty whitelist. Generally it will be better to start with a default prepared whitelist instead.
-
-     @see #basic()
-     @see #basicWithImages()
-     @see #simpleText()
-     @see #relaxed()
-     */
-    public Whitelist() {
-        tagNames = new HashSet<>();
-        attributes = new HashMap<>();
-        enforcedAttributes = new HashMap<>();
-        protocols = new HashMap<>();
-        preserveRelativeLinks = false;
+    static public Whitelist relaxed() {
+        return new Whitelist(Safelist.relaxed());
     }
 
-    /**
-     Add a list of allowed elements to a whitelist. (If a tag is not allowed, it will be removed from the HTML.)
+    static public Whitelist simpleText() {
+        return new Whitelist(Safelist.simpleText());
+    }
 
-     @param tags tag names to allow
-     @return this (for chaining)
-     */
+    @Override
     public Whitelist addTags(String... tags) {
-        Validate.notNull(tags);
-
-        for (String tagName : tags) {
-            Validate.notEmpty(tagName);
-            tagNames.add(TagName.valueOf(tagName));
-        }
+        super.addTags(tags);
         return this;
     }
 
-    /**
-     Remove a list of allowed elements from a whitelist. (If a tag is not allowed, it will be removed from the HTML.)
-
-     @param tags tag names to disallow
-     @return this (for chaining)
-     */
+    @Override
     public Whitelist removeTags(String... tags) {
-        Validate.notNull(tags);
-
-        for(String tag: tags) {
-            Validate.notEmpty(tag);
-            TagName tagName = TagName.valueOf(tag);
-
-            if(tagNames.remove(tagName)) { // Only look in sub-maps if tag was allowed
-                attributes.remove(tagName);
-                enforcedAttributes.remove(tagName);
-                protocols.remove(tagName);
-            }
-        }
+        super.removeTags(tags);
         return this;
     }
 
-    /**
-     Add a list of allowed attributes to a tag. (If an attribute is not allowed on an element, it will be removed.)
-     <p>
-     E.g.: <code>addAttributes("a", "href", "class")</code> allows <code>href</code> and <code>class</code> attributes
-     on <code>a</code> tags.
-     </p>
-     <p>
-     To make an attribute valid for <b>all tags</b>, use the pseudo tag <code>:all</code>, e.g.
-     <code>addAttributes(":all", "class")</code>.
-     </p>
-
-     @param tag  The tag the attributes are for. The tag will be added to the allowed tag list if necessary.
-     @param attributes List of valid attributes for the tag
-     @return this (for chaining)
-     */
+    @Override
     public Whitelist addAttributes(String tag, String... attributes) {
-        Validate.notEmpty(tag);
-        Validate.notNull(attributes);
-        Validate.isTrue(attributes.length > 0, "No attribute names supplied.");
-
-        TagName tagName = TagName.valueOf(tag);
-        tagNames.add(tagName);
-        Set<AttributeKey> attributeSet = new HashSet<>();
-        for (String key : attributes) {
-            Validate.notEmpty(key);
-            attributeSet.add(AttributeKey.valueOf(key));
-        }
-        if (this.attributes.containsKey(tagName)) {
-            Set<AttributeKey> currentSet = this.attributes.get(tagName);
-            currentSet.addAll(attributeSet);
-        } else {
-            this.attributes.put(tagName, attributeSet);
-        }
+        super.addAttributes(tag, attributes);
         return this;
     }
 
-    /**
-     Remove a list of allowed attributes from a tag. (If an attribute is not allowed on an element, it will be removed.)
-     <p>
-     E.g.: <code>removeAttributes("a", "href", "class")</code> disallows <code>href</code> and <code>class</code>
-     attributes on <code>a</code> tags.
-     </p>
-     <p>
-     To make an attribute invalid for <b>all tags</b>, use the pseudo tag <code>:all</code>, e.g.
-     <code>removeAttributes(":all", "class")</code>.
-     </p>
-
-     @param tag  The tag the attributes are for.
-     @param attributes List of invalid attributes for the tag
-     @return this (for chaining)
-     */
+    @Override
     public Whitelist removeAttributes(String tag, String... attributes) {
-        Validate.notEmpty(tag);
-        Validate.notNull(attributes);
-        Validate.isTrue(attributes.length > 0, "No attribute names supplied.");
-
-        TagName tagName = TagName.valueOf(tag);
-        Set<AttributeKey> attributeSet = new HashSet<>();
-        for (String key : attributes) {
-            Validate.notEmpty(key);
-            attributeSet.add(AttributeKey.valueOf(key));
-        }
-        if(tagNames.contains(tagName) && this.attributes.containsKey(tagName)) { // Only look in sub-maps if tag was allowed
-            Set<AttributeKey> currentSet = this.attributes.get(tagName);
-            currentSet.removeAll(attributeSet);
-
-            if(currentSet.isEmpty()) // Remove tag from attribute map if no attributes are allowed for tag
-                this.attributes.remove(tagName);
-        }
-        if(tag.equals(":all")) // Attribute needs to be removed from all individually set tags
-            for(TagName name: this.attributes.keySet()) {
-                Set<AttributeKey> currentSet = this.attributes.get(name);
-                currentSet.removeAll(attributeSet);
-
-                if(currentSet.isEmpty()) // Remove tag from attribute map if no attributes are allowed for tag
-                    this.attributes.remove(name);
-            }
+        super.removeAttributes(tag, attributes);
         return this;
     }
 
-    /**
-     Add an enforced attribute to a tag. An enforced attribute will always be added to the element. If the element
-     already has the attribute set, it will be overridden with this value.
-     <p>
-     E.g.: <code>addEnforcedAttribute("a", "rel", "nofollow")</code> will make all <code>a</code> tags output as
-     <code>&lt;a href="..." rel="nofollow"&gt;</code>
-     </p>
-
-     @param tag   The tag the enforced attribute is for. The tag will be added to the allowed tag list if necessary.
-     @param attribute   The attribute name
-     @param value The enforced attribute value
-     @return this (for chaining)
-     */
+    @Override
     public Whitelist addEnforcedAttribute(String tag, String attribute, String value) {
-        Validate.notEmpty(tag);
-        Validate.notEmpty(attribute);
-        Validate.notEmpty(value);
-
-        TagName tagName = TagName.valueOf(tag);
-        tagNames.add(tagName);
-        AttributeKey attrKey = AttributeKey.valueOf(attribute);
-        AttributeValue attrVal = AttributeValue.valueOf(value);
-
-        if (enforcedAttributes.containsKey(tagName)) {
-            enforcedAttributes.get(tagName).put(attrKey, attrVal);
-        } else {
-            Map<AttributeKey, AttributeValue> attrMap = new HashMap<>();
-            attrMap.put(attrKey, attrVal);
-            enforcedAttributes.put(tagName, attrMap);
-        }
+        super.addEnforcedAttribute(tag, attribute, value);
         return this;
     }
 
-    /**
-     Remove a previously configured enforced attribute from a tag.
-
-     @param tag   The tag the enforced attribute is for.
-     @param attribute   The attribute name
-     @return this (for chaining)
-     */
+    @Override
     public Whitelist removeEnforcedAttribute(String tag, String attribute) {
-        Validate.notEmpty(tag);
-        Validate.notEmpty(attribute);
-
-        TagName tagName = TagName.valueOf(tag);
-        if(tagNames.contains(tagName) && enforcedAttributes.containsKey(tagName)) {
-            AttributeKey attrKey = AttributeKey.valueOf(attribute);
-            Map<AttributeKey, AttributeValue> attrMap = enforcedAttributes.get(tagName);
-            attrMap.remove(attrKey);
-
-            if(attrMap.isEmpty()) // Remove tag from enforced attribute map if no enforced attributes are present
-                enforcedAttributes.remove(tagName);
-        }
+        super.removeEnforcedAttribute(tag, attribute);
         return this;
     }
 
-    /**
-     * Configure this Whitelist to preserve relative links in an element's URL attribute, or convert them to absolute
-     * links. By default, this is <b>false</b>: URLs will be  made absolute (e.g. start with an allowed protocol, like
-     * e.g. {@code http://}.
-     * <p>
-     * Note that when handling relative links, the input document must have an appropriate {@code base URI} set when
-     * parsing, so that the link's protocol can be confirmed. Regardless of the setting of the {@code preserve relative
-     * links} option, the link must be resolvable against the base URI to an allowed protocol; otherwise the attribute
-     * will be removed.
-     * </p>
-     *
-     * @param preserve {@code true} to allow relative links, {@code false} (default) to deny
-     * @return this Whitelist, for chaining.
-     * @see #addProtocols
-     */
+    @Override
     public Whitelist preserveRelativeLinks(boolean preserve) {
-        preserveRelativeLinks = preserve;
+        super.preserveRelativeLinks(preserve);
         return this;
     }
 
-    /**
-     Add allowed URL protocols for an element's URL attribute. This restricts the possible values of the attribute to
-     URLs with the defined protocol.
-     <p>
-     E.g.: <code>addProtocols("a", "href", "ftp", "http", "https")</code>
-     </p>
-     <p>
-     To allow a link to an in-page URL anchor (i.e. <code>&lt;a href="#anchor"&gt;</code>, add a <code>#</code>:<br>
-     E.g.: <code>addProtocols("a", "href", "#")</code>
-     </p>
-
-     @param tag       Tag the URL protocol is for
-     @param attribute       Attribute name
-     @param protocols List of valid protocols
-     @return this, for chaining
-     */
+    @Override
     public Whitelist addProtocols(String tag, String attribute, String... protocols) {
-        Validate.notEmpty(tag);
-        Validate.notEmpty(attribute);
-        Validate.notNull(protocols);
-
-        TagName tagName = TagName.valueOf(tag);
-        AttributeKey attrKey = AttributeKey.valueOf(attribute);
-        Map<AttributeKey, Set<Protocol>> attrMap;
-        Set<Protocol> protSet;
-
-        if (this.protocols.containsKey(tagName)) {
-            attrMap = this.protocols.get(tagName);
-        } else {
-            attrMap = new HashMap<>();
-            this.protocols.put(tagName, attrMap);
-        }
-        if (attrMap.containsKey(attrKey)) {
-            protSet = attrMap.get(attrKey);
-        } else {
-            protSet = new HashSet<>();
-            attrMap.put(attrKey, protSet);
-        }
-        for (String protocol : protocols) {
-            Validate.notEmpty(protocol);
-            Protocol prot = Protocol.valueOf(protocol);
-            protSet.add(prot);
-        }
+        super.addProtocols(tag, attribute, protocols);
         return this;
     }
 
-    /**
-     Remove allowed URL protocols for an element's URL attribute. If you remove all protocols for an attribute, that
-     attribute will allow any protocol.
-     <p>
-     E.g.: <code>removeProtocols("a", "href", "ftp")</code>
-     </p>
-
-     @param tag Tag the URL protocol is for
-     @param attribute Attribute name
-     @param removeProtocols List of invalid protocols
-     @return this, for chaining
-     */
+    @Override
     public Whitelist removeProtocols(String tag, String attribute, String... removeProtocols) {
-        Validate.notEmpty(tag);
-        Validate.notEmpty(attribute);
-        Validate.notNull(removeProtocols);
-
-        TagName tagName = TagName.valueOf(tag);
-        AttributeKey attr = AttributeKey.valueOf(attribute);
-
-        // make sure that what we're removing actually exists; otherwise can open the tag to any data and that can
-        // be surprising
-        Validate.isTrue(protocols.containsKey(tagName), "Cannot remove a protocol that is not set.");
-        Map<AttributeKey, Set<Protocol>> tagProtocols = protocols.get(tagName);
-        Validate.isTrue(tagProtocols.containsKey(attr), "Cannot remove a protocol that is not set.");
-
-        Set<Protocol> attrProtocols = tagProtocols.get(attr);
-        for (String protocol : removeProtocols) {
-            Validate.notEmpty(protocol);
-            attrProtocols.remove(Protocol.valueOf(protocol));
-        }
-
-        if (attrProtocols.isEmpty()) { // Remove protocol set if empty
-            tagProtocols.remove(attr);
-            if (tagProtocols.isEmpty()) // Remove entry for tag if empty
-                protocols.remove(tagName);
-        }
+        super.removeProtocols(tag, attribute, removeProtocols);
         return this;
     }
 
-    /**
-     * Test if the supplied tag is allowed by this whitelist
-     * @param tag test tag
-     * @return true if allowed
-     */
+    @Override
     protected boolean isSafeTag(String tag) {
-        return tagNames.contains(TagName.valueOf(tag));
+        return super.isSafeTag(tag);
     }
 
-    /**
-     * Test if the supplied attribute is allowed by this whitelist for this tag
-     * @param tagName tag to consider allowing the attribute in
-     * @param el element under test, to confirm protocol
-     * @param attr attribute under test
-     * @return true if allowed
-     */
+    @Override
     protected boolean isSafeAttribute(String tagName, Element el, Attribute attr) {
-        TagName tag = TagName.valueOf(tagName);
-        AttributeKey key = AttributeKey.valueOf(attr.getKey());
-
-        Set<AttributeKey> okSet = attributes.get(tag);
-        if (okSet != null && okSet.contains(key)) {
-            if (protocols.containsKey(tag)) {
-                Map<AttributeKey, Set<Protocol>> attrProts = protocols.get(tag);
-                // ok if not defined protocol; otherwise test
-                return !attrProts.containsKey(key) || testValidProtocol(el, attr, attrProts.get(key));
-            } else { // attribute found, no protocols defined, so OK
-                return true;
-            }
-        }
-        // might be an enforced attribute?
-        Map<AttributeKey, AttributeValue> enforcedSet = enforcedAttributes.get(tag);
-        if (enforcedSet != null) {
-            Attributes expect = getEnforcedAttributes(tagName);
-            String attrKey = attr.getKey();
-            if (expect.hasKeyIgnoreCase(attrKey)) {
-                return expect.getIgnoreCase(attrKey).equals(attr.getValue());
-            }
-        }
-        // no attributes defined for tag, try :all tag
-        return !tagName.equals(":all") && isSafeAttribute(":all", el, attr);
-    }
-
-    private boolean testValidProtocol(Element el, Attribute attr, Set<Protocol> protocols) {
-        // try to resolve relative urls to abs, and optionally update the attribute so output html has abs.
-        // rels without a baseuri get removed
-        String value = el.absUrl(attr.getKey());
-        if (value.length() == 0)
-            value = attr.getValue(); // if it could not be made abs, run as-is to allow custom unknown protocols
-        if (!preserveRelativeLinks)
-            attr.setValue(value);
-        
-        for (Protocol protocol : protocols) {
-            String prot = protocol.toString();
-
-            if (prot.equals("#")) { // allows anchor links
-                if (isValidAnchor(value)) {
-                    return true;
-                } else {
-                    continue;
-                }
-            }
-
-            prot += ":";
-
-            if (lowerCase(value).startsWith(prot)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private boolean isValidAnchor(String value) {
-        return value.startsWith("#") && !value.matches(".*\\s.*");
+        return super.isSafeAttribute(tagName, el, attr);
     }
 
+    @Override
     Attributes getEnforcedAttributes(String tagName) {
-        Attributes attrs = new Attributes();
-        TagName tag = TagName.valueOf(tagName);
-        if (enforcedAttributes.containsKey(tag)) {
-            Map<AttributeKey, AttributeValue> keyVals = enforcedAttributes.get(tag);
-            for (Map.Entry<AttributeKey, AttributeValue> entry : keyVals.entrySet()) {
-                attrs.put(entry.getKey().toString(), entry.getValue().toString());
-            }
-        }
-        return attrs;
-    }
-    
-    // named types for config. All just hold strings, but here for my sanity.
-
-    static class TagName extends TypedValue {
-        TagName(String value) {
-            super(value);
-        }
-
-        static TagName valueOf(String value) {
-            return new TagName(value);
-        }
-    }
-
-    static class AttributeKey extends TypedValue {
-        AttributeKey(String value) {
-            super(value);
-        }
-
-        static AttributeKey valueOf(String value) {
-            return new AttributeKey(value);
-        }
-    }
-
-    static class AttributeValue extends TypedValue {
-        AttributeValue(String value) {
-            super(value);
-        }
-
-        static AttributeValue valueOf(String value) {
-            return new AttributeValue(value);
-        }
-    }
-
-    static class Protocol extends TypedValue {
-        Protocol(String value) {
-            super(value);
-        }
-
-        static Protocol valueOf(String value) {
-            return new Protocol(value);
-        }
-    }
-
-    abstract static class TypedValue {
-        private String value;
-
-        TypedValue(String value) {
-            Validate.notNull(value);
-            this.value = value;
-        }
-
-        @Override
-        public int hashCode() {
-            final int prime = 31;
-            int result = 1;
-            result = prime * result + ((value == null) ? 0 : value.hashCode());
-            return result;
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (this == obj) return true;
-            if (obj == null) return false;
-            if (getClass() != obj.getClass()) return false;
-            TypedValue other = (TypedValue) obj;
-            if (value == null) {
-                return other.value == null;
-            } else return value.equals(other.value);
-        }
-
-        @Override
-        public String toString() {
-            return value;
-        }
+        return super.getEnforcedAttributes(tagName);
     }
 }
-
diff --git a/src/main/java/org/jsoup/safety/package-info.java b/src/main/java/org/jsoup/safety/package-info.java
index ac890f06..26b4b701 100644
--- a/src/main/java/org/jsoup/safety/package-info.java
+++ b/src/main/java/org/jsoup/safety/package-info.java
@@ -1,4 +1,4 @@
 /**
- Contains the jsoup HTML cleaner, and whitelist definitions.
+ Contains the jsoup HTML cleaner, and safelist definitions.
  */
 package org.jsoup.safety;
diff --git a/src/main/java/org/jsoup/select/Collector.java b/src/main/java/org/jsoup/select/Collector.java
index de34eddf..fb9177aa 100644
--- a/src/main/java/org/jsoup/select/Collector.java
+++ b/src/main/java/org/jsoup/select/Collector.java
@@ -3,6 +3,8 @@ package org.jsoup.select;
 import org.jsoup.nodes.Element;
 import org.jsoup.nodes.Node;
 
+import javax.annotation.Nullable;
+
 import static org.jsoup.select.NodeFilter.FilterResult.CONTINUE;
 import static org.jsoup.select.NodeFilter.FilterResult.STOP;
 
@@ -13,8 +15,7 @@ import static org.jsoup.select.NodeFilter.FilterResult.STOP;
  */
 public class Collector {
 
-    private Collector() {
-    }
+    private Collector() {}
 
     /**
      Build a list of elements, by visiting root and every descendant of root, and testing it against the evaluator.
@@ -52,7 +53,14 @@ public class Collector {
         }
     }
 
-    public static Element findFirst(Evaluator eval, Element root) {
+    /**
+     Finds the first Element that matches the Evaluator that descends from the root, and stops the query once that first
+     match is found.
+     @param eval Evaluator to test elements against
+     @param root root of tree to descend
+     @return the first match; {@code null} if none
+     */
+    public static @Nullable Element findFirst(Evaluator eval, Element root) {
         FirstFinder finder = new FirstFinder(root, eval);
         NodeTraversor.filter(finder, root);
         return finder.match;
@@ -60,7 +68,7 @@ public class Collector {
 
     private static class FirstFinder implements NodeFilter {
         private final Element root;
-        private Element match = null;
+        private @Nullable Element match = null;
         private final Evaluator eval;
 
         FirstFinder(Element root, Evaluator eval) {
diff --git a/src/main/java/org/jsoup/select/CombiningEvaluator.java b/src/main/java/org/jsoup/select/CombiningEvaluator.java
index 7ced9e98..8974a38b 100644
--- a/src/main/java/org/jsoup/select/CombiningEvaluator.java
+++ b/src/main/java/org/jsoup/select/CombiningEvaluator.java
@@ -3,6 +3,7 @@ package org.jsoup.select;
 import org.jsoup.internal.StringUtil;
 import org.jsoup.nodes.Element;
 
+import javax.annotation.Nullable;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -25,7 +26,7 @@ abstract class CombiningEvaluator extends Evaluator {
         updateNumEvaluators();
     }
 
-    Evaluator rightMostEvaluator() {
+    @Nullable Evaluator rightMostEvaluator() {
         return num > 0 ? evaluators.get(num - 1) : null;
     }
     
diff --git a/src/main/java/org/jsoup/select/Elements.java b/src/main/java/org/jsoup/select/Elements.java
index 7689021f..88dc51f1 100644
--- a/src/main/java/org/jsoup/select/Elements.java
+++ b/src/main/java/org/jsoup/select/Elements.java
@@ -9,6 +9,7 @@ import org.jsoup.nodes.FormElement;
 import org.jsoup.nodes.Node;
 import org.jsoup.nodes.TextNode;
 
+import javax.annotation.Nullable;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -183,7 +184,8 @@ public class Elements extends ArrayList<Element> {
      */
     public String val() {
         if (size() > 0)
-            return first().val();
+            //noinspection ConstantConditions
+            return first().val(); // first() != null as size() > 0
         else
             return "";
     }
@@ -564,7 +566,7 @@ public class Elements extends ArrayList<Element> {
         return siblings(query, false, true);
     }
 
-    private Elements siblings(String query, boolean next, boolean all) {
+    private Elements siblings(@Nullable String query, boolean next, boolean all) {
         Elements els = new Elements();
         Evaluator eval = query != null? QueryParser.parse(query) : null;
         for (Element e : this) {
@@ -598,7 +600,7 @@ public class Elements extends ArrayList<Element> {
      Get the first matched element.
      @return The first matched element, or <code>null</code> if contents is empty.
      */
-    public Element first() {
+    public @Nullable Element first() {
         return isEmpty() ? null : get(0);
     }
 
@@ -606,7 +608,7 @@ public class Elements extends ArrayList<Element> {
      Get the last matched element.
      @return The last matched element, or <code>null</code> if contents is empty.
      */
-    public Element last() {
+    public @Nullable Element last() {
         return isEmpty() ? null : get(size() - 1);
     }
 
@@ -636,7 +638,11 @@ public class Elements extends ArrayList<Element> {
      * no forms.
      */
     public List<FormElement> forms() {
-        return nodesOfType(FormElement.class);
+        ArrayList<FormElement> forms = new ArrayList<>();
+        for (Element el: this)
+            if (el instanceof FormElement)
+                forms.add((FormElement) el);
+        return forms;
     }
 
     /**
@@ -644,7 +650,7 @@ public class Elements extends ArrayList<Element> {
      * @return Comment nodes, or an empty list if none.
      */
     public List<Comment> comments() {
-        return nodesOfType(Comment.class);
+        return childNodesOfType(Comment.class);
     }
 
     /**
@@ -652,7 +658,7 @@ public class Elements extends ArrayList<Element> {
      * @return TextNode nodes, or an empty list if none.
      */
     public List<TextNode> textNodes() {
-        return nodesOfType(TextNode.class);
+        return childNodesOfType(TextNode.class);
     }
 
     /**
@@ -661,20 +667,16 @@ public class Elements extends ArrayList<Element> {
      * @return Comment nodes, or an empty list if none.
      */
     public List<DataNode> dataNodes() {
-        return nodesOfType(DataNode.class);
+        return childNodesOfType(DataNode.class);
     }
 
-    private <T extends Node> List<T> nodesOfType(Class<T> tClass) {
+    private <T extends Node> List<T> childNodesOfType(Class<T> tClass) {
         ArrayList<T> nodes = new ArrayList<>();
         for (Element el: this) {
-            if (el.getClass().isInstance(tClass)) { // Handles FormElements
-                nodes.add(tClass.cast(el));
-            } else if (Node.class.isAssignableFrom(tClass)) { // check if child nodes match
-                for (int i = 0; i < el.childNodeSize(); i++) {
-                    Node node = el.childNode(i);
-                    if (tClass.isInstance(node))
-                        nodes.add(tClass.cast(node));
-                }
+            for (int i = 0; i < el.childNodeSize(); i++) {
+                Node node = el.childNode(i);
+                if (tClass.isInstance(node))
+                    nodes.add(tClass.cast(node));
             }
         }
         return nodes;
diff --git a/src/main/java/org/jsoup/select/Evaluator.java b/src/main/java/org/jsoup/select/Evaluator.java
index d7d78575..e086be37 100644
--- a/src/main/java/org/jsoup/select/Evaluator.java
+++ b/src/main/java/org/jsoup/select/Evaluator.java
@@ -39,7 +39,7 @@ public abstract class Evaluator {
      * Evaluator for tag name
      */
     public static final class Tag extends Evaluator {
-        private String tagName;
+        private final String tagName;
 
         public Tag(String tagName) {
             this.tagName = tagName;
@@ -61,7 +61,7 @@ public abstract class Evaluator {
      * Evaluator for tag name that ends with
      */
     public static final class TagEndsWith extends Evaluator {
-        private String tagName;
+        private final String tagName;
 
         public TagEndsWith(String tagName) {
             this.tagName = tagName;
@@ -82,7 +82,7 @@ public abstract class Evaluator {
      * Evaluator for element id
      */
     public static final class Id extends Evaluator {
-        private String id;
+        private final String id;
 
         public Id(String id) {
             this.id = id;
@@ -104,7 +104,7 @@ public abstract class Evaluator {
      * Evaluator for element class
      */
     public static final class Class extends Evaluator {
-        private String className;
+        private final String className;
 
         public Class(String className) {
             this.className = className;
@@ -126,7 +126,7 @@ public abstract class Evaluator {
      * Evaluator for attribute name matching
      */
     public static final class Attribute extends Evaluator {
-        private String key;
+        private final String key;
 
         public Attribute(String key) {
             this.key = key;
@@ -148,7 +148,7 @@ public abstract class Evaluator {
      * Evaluator for attribute name prefix matching
      */
     public static final class AttributeStarting extends Evaluator {
-        private String keyPrefix;
+        private final String keyPrefix;
 
         public AttributeStarting(String keyPrefix) {
             Validate.notEmpty(keyPrefix);
@@ -504,6 +504,8 @@ public abstract class Evaluator {
 
         @Override
         protected int calculatePosition(Element root, Element element) {
+    	    if (element.parent() == null)
+    	        return 0;
         	return element.parent().children().size() - element.elementSiblingIndex();
         }
 
@@ -524,6 +526,8 @@ public abstract class Evaluator {
 
 		protected int calculatePosition(Element root, Element element) {
 			int pos = 0;
+            if (element.parent() == null)
+                return 0;
         	Elements family = element.parent().children();
             for (Element el : family) {
                 if (el.tag().equals(element.tag())) pos++;
@@ -547,6 +551,8 @@ public abstract class Evaluator {
 		@Override
 		protected int calculatePosition(Element root, Element element) {
 			int pos = 0;
+            if (element.parent() == null)
+                return 0;
         	Elements family = element.parent().children();
         	for (int i = element.elementSiblingIndex(); i < family.size(); i++) {
         		if (family.get(i).tag().equals(element.tag())) pos++;
@@ -656,7 +662,7 @@ public abstract class Evaluator {
      * Evaluator for matching Element (and its descendants) text
      */
     public static final class ContainsText extends Evaluator {
-        private String searchText;
+        private final String searchText;
 
         public ContainsText(String searchText) {
             this.searchText = lowerCase(searchText);
@@ -677,7 +683,7 @@ public abstract class Evaluator {
      * Evaluator for matching Element (and its descendants) data
      */
     public static final class ContainsData extends Evaluator {
-        private String searchText;
+        private final String searchText;
 
         public ContainsData(String searchText) {
             this.searchText = lowerCase(searchText);
@@ -698,7 +704,7 @@ public abstract class Evaluator {
      * Evaluator for matching Element's own text
      */
     public static final class ContainsOwnText extends Evaluator {
-        private String searchText;
+        private final String searchText;
 
         public ContainsOwnText(String searchText) {
             this.searchText = lowerCase(searchText);
@@ -719,7 +725,7 @@ public abstract class Evaluator {
      * Evaluator for matching Element (and its descendants) text with regex
      */
     public static final class Matches extends Evaluator {
-        private Pattern pattern;
+        private final Pattern pattern;
 
         public Matches(Pattern pattern) {
             this.pattern = pattern;
@@ -741,7 +747,7 @@ public abstract class Evaluator {
      * Evaluator for matching Element's own text with regex
      */
     public static final class MatchesOwn extends Evaluator {
-        private Pattern pattern;
+        private final Pattern pattern;
 
         public MatchesOwn(Pattern pattern) {
             this.pattern = pattern;
diff --git a/src/main/java/org/jsoup/select/NodeTraversor.java b/src/main/java/org/jsoup/select/NodeTraversor.java
index 9f3a5672..99488277 100644
--- a/src/main/java/org/jsoup/select/NodeTraversor.java
+++ b/src/main/java/org/jsoup/select/NodeTraversor.java
@@ -27,7 +27,9 @@ public class NodeTraversor {
                 node = node.childNode(0);
                 depth++;
             } else {
-                while (node.nextSibling() == null && depth > 0) {
+                while (true) {
+                    assert node != null; // as depth > 0, will have parent
+                    if (!(node.nextSibling() == null && depth > 0)) break;
                     visitor.tail(node, depth);
                     node = node.parentNode();
                     depth--;
@@ -73,7 +75,9 @@ public class NodeTraversor {
                 continue;
             }
             // No siblings, move upwards:
-            while (node.nextSibling() == null && depth > 0) {
+            while (true) {
+                assert node != null; // depth > 0, so has parent
+                if (!(node.nextSibling() == null && depth > 0)) break;
                 // 'tail' current node:
                 if (result == FilterResult.CONTINUE || result == FilterResult.SKIP_CHILDREN) {
                     result = filter.tail(node, depth);
diff --git a/src/main/java/org/jsoup/select/QueryParser.java b/src/main/java/org/jsoup/select/QueryParser.java
index 001daf4e..57636b9b 100644
--- a/src/main/java/org/jsoup/select/QueryParser.java
+++ b/src/main/java/org/jsoup/select/QueryParser.java
@@ -18,9 +18,9 @@ public class QueryParser {
     private final static String[] combinators = {",", ">", "+", "~", " "};
     private static final String[] AttributeEvals = new String[]{"=", "!=", "^=", "$=", "*=", "~="};
 
-    private TokenQueue tq;
-    private String query;
-    private List<Evaluator> evals = new ArrayList<>();
+    private final TokenQueue tq;
+    private final String query;
+    private final List<Evaluator> evals = new ArrayList<>();
 
     /**
      * Create a new QueryParser.
@@ -95,6 +95,7 @@ public class QueryParser {
             // make sure OR (,) has precedence:
             if (rootEval instanceof CombiningEvaluator.Or && combinator != ',') {
                 currentEval = ((CombiningEvaluator.Or) currentEval).rightMostEvaluator();
+                assert currentEval != null; // rightMost signature can return null (if none set), but always will have one by this point
                 replaceRightMost = true;
             }
         }
@@ -116,12 +117,11 @@ public class QueryParser {
             CombiningEvaluator.Or or;
             if (currentEval instanceof CombiningEvaluator.Or) {
                 or = (CombiningEvaluator.Or) currentEval;
-                or.add(newEval);
             } else {
                 or = new CombiningEvaluator.Or();
                 or.add(currentEval);
-                or.add(newEval);
             }
+            or.add(newEval);
             currentEval = or;
         }
         else
diff --git a/src/main/java/org/jsoup/select/Selector.java b/src/main/java/org/jsoup/select/Selector.java
index 61d7f2d0..b564a067 100644
--- a/src/main/java/org/jsoup/select/Selector.java
+++ b/src/main/java/org/jsoup/select/Selector.java
@@ -3,6 +3,7 @@ package org.jsoup.select;
 import org.jsoup.helper.Validate;
 import org.jsoup.nodes.Element;
 
+import javax.annotation.Nullable;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.IdentityHashMap;
@@ -155,7 +156,7 @@ public class Selector {
      * @param root root element to descend into
      * @return the matching element, or <b>null</b> if none.
      */
-    public static Element selectFirst(String cssQuery, Element root) {
+    public static @Nullable Element selectFirst(String cssQuery, Element root) {
         Validate.notEmpty(cssQuery);
         return Collector.findFirst(QueryParser.parse(cssQuery), root);
     }
diff --git a/src/main/java/org/jsoup/select/StructuralEvaluator.java b/src/main/java/org/jsoup/select/StructuralEvaluator.java
index 4ac28f28..979e40c0 100644
--- a/src/main/java/org/jsoup/select/StructuralEvaluator.java
+++ b/src/main/java/org/jsoup/select/StructuralEvaluator.java
@@ -58,7 +58,7 @@ abstract class StructuralEvaluator extends Evaluator {
                 return false;
 
             Element parent = element.parent();
-            while (true) {
+            while (parent != null) {
                 if (evaluator.matches(root, parent))
                     return true;
                 if (parent == root)
diff --git a/src/main/java/org/jsoup/select/package-info.java b/src/main/java/org/jsoup/select/package-info.java
index e6871fc9..5bbdb855 100644
--- a/src/main/java/org/jsoup/select/package-info.java
+++ b/src/main/java/org/jsoup/select/package-info.java
@@ -2,4 +2,7 @@
  Packages to support the CSS-style element selector.
  {@link org.jsoup.select.Selector Selector defines the query syntax.}
  */
+@NonnullByDefault
 package org.jsoup.select;
+
+import org.jsoup.internal.NonnullByDefault;
diff --git a/src/main/javadoc/overview.html b/src/main/javadoc/overview.html
index bbeac18a..597cdcb4 100644
--- a/src/main/javadoc/overview.html
+++ b/src/main/javadoc/overview.html
@@ -1,31 +1,31 @@
 <!DOCTYPE html>
-<html>
+<html lang="en">
 <head>
-    <title>jsoup Javadoc overview</title>
+  <title>jsoup Javadoc overview</title>
 </head>
 <body>
 <h1>jsoup: Java HTML parser that makes sense of real-world HTML soup.</h1>
 
-<p><b>jsoup</b> is a Java library for working with real-world HTML. It provides a very convenient API
-for extracting and manipulating data, using the best of DOM, CSS, and jquery-like methods.</p>
+<p><b>jsoup</b> is a Java library for working with real-world HTML. It provides a very convenient API for fetching URLs
+  and extracting and manipulating data, using the best of HTML5 DOM methods and CSS selectors.</p>
 
-<p>jsoup implements the <a href="http://whatwg.org/html">WHATWG HTML</a> specification, and parses HTML to the same DOM
-as modern browsers do.</p>
+<p>jsoup implements the <a href="https://html.spec.whatwg.org/multipage/">WHATWG HTML</a> specification, and parses HTML to the same DOM
+  as modern browsers do.</p>
 
 <ul>
-<li>parse HTML from a URL, file, or string
-<li>find and extract data, using DOM traversal or CSS selectors
-<li>manipulate the HTML elements, attributes, and text
-<li>clean user-submitted content against a safe white-list, to prevent XSS
-<li>output tidy HTML
+  <li>parse HTML from a URL, file, or string
+  <li>find and extract data, using DOM traversal or CSS selectors
+  <li>manipulate the HTML elements, attributes, and text
+  <li>clean user-submitted content against a safelist, to prevent XSS
+  <li>output tidy HTML
 </ul>
 
 <p>jsoup is designed to deal with all varieties of HTML found in the wild; from pristine and validating,
-to invalid tag-soup; jsoup will create a sensible parse tree.</p>
+  to invalid tag-soup; jsoup will create a sensible parse tree.</p>
 
-<p>See <a href="https://jsoup.org/"><b>jsoup.org</b></a> for downloads, documentation, and examples...</p>
+<p>See <a href="https://jsoup.org/"><b>jsoup.org</b></a> for downloads, documentation, and examples.</p>
 
-@author <a href="http://jonathanhedley.com/">Jonathan Hedley</a>
+@author <a href="https://jonathanhedley.com/">Jonathan Hedley</a>
 
 </body>
 </html>


Test patch:
diff --git a/src/test/java/org/jsoup/helper/DataUtilTest.java b/src/test/java/org/jsoup/helper/DataUtilTest.java
index f585bb26..a775723f 100644
--- a/src/test/java/org/jsoup/helper/DataUtilTest.java
+++ b/src/test/java/org/jsoup/helper/DataUtilTest.java
@@ -161,6 +161,14 @@ public class DataUtilTest {
         assertEquals("OK", doc.head().select("title").text());
     }
 
+    @Test
+    public void noExtraNULLBytes() throws IOException {
+    	final byte[] b = "<html><head><meta charset=\"UTF-8\"></head><body><div><u>ü</u>ü</div></body></html>".getBytes("UTF-8");
+    	
+    	Document doc = Jsoup.parse(new ByteArrayInputStream(b), null, "");
+    	assertFalse( doc.outerHtml().contains("\u0000") );
+    }
+
     @Test
     public void supportsZippedUTF8BOM() throws IOException {
         File in = getFile("/bomtests/bom_utf8.html.gz");
diff --git a/src/test/java/org/jsoup/helper/ValidateTest.java b/src/test/java/org/jsoup/helper/ValidateTest.java
new file mode 100644
index 00000000..138e5324
--- /dev/null
+++ b/src/test/java/org/jsoup/helper/ValidateTest.java
@@ -0,0 +1,19 @@
+package org.jsoup.helper;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class ValidateTest {
+
+    @Test
+    public void testNotNull() {
+        Validate.notNull("foo");
+        boolean threw = false;
+        try {
+            Validate.notNull(null);
+        } catch (IllegalArgumentException e) {
+            threw = true;
+        }
+        Assertions.assertTrue(threw);
+    }
+}
diff --git a/src/test/java/org/jsoup/integration/ConnectTest.java b/src/test/java/org/jsoup/integration/ConnectTest.java
index 663ad448..91f67a9c 100644
--- a/src/test/java/org/jsoup/integration/ConnectTest.java
+++ b/src/test/java/org/jsoup/integration/ConnectTest.java
@@ -139,6 +139,35 @@ public class ConnectTest {
         assertEquals("度一下", ihVal("百", doc));
     }
 
+    @Test
+    public void canSendSecFetchHeaders() throws IOException {
+        // https://github.com/jhy/jsoup/issues/1461
+        Document doc = Jsoup.connect(echoUrl)
+            .header("Random-Header-name", "hello")
+            .header("Sec-Fetch-Site", "cross-site")
+            .header("Sec-Fetch-Mode", "cors")
+            .get();
+
+        assertEquals("hello", ihVal("Random-Header-name", doc));
+        assertEquals("cross-site", ihVal("Sec-Fetch-Site", doc));
+        assertEquals("cors", ihVal("Sec-Fetch-Mode", doc));
+    }
+
+    @Test
+    public void secFetchHeadersSurviveRedirect() throws IOException {
+        Document doc = Jsoup
+            .connect(RedirectServlet.Url)
+            .data(RedirectServlet.LocationParam, echoUrl)
+            .header("Random-Header-name", "hello")
+            .header("Sec-Fetch-Site", "cross-site")
+            .header("Sec-Fetch-Mode", "cors")
+            .get();
+
+        assertEquals("hello", ihVal("Random-Header-name", doc));
+        assertEquals("cross-site", ihVal("Sec-Fetch-Site", doc));
+        assertEquals("cors", ihVal("Sec-Fetch-Mode", doc));
+    }
+
     @Test
     public void sendsRequestBodyJsonWithData() throws IOException {
         final String body = "{key:value}";
@@ -441,9 +470,10 @@ public class ConnectTest {
         con.data(FileServlet.ContentTypeParam, "application/rss+xml");
         Document doc = con.get();
         Element title = doc.selectFirst("title");
+        assertNotNull(title);
         assertEquals("jsoup RSS news", title.text());
         assertEquals("channel", title.parent().nodeName());
-        assertEquals("jsoup RSS news", doc.title());
+        assertEquals("", doc.title()); // the document title is unset, this tag is channel>title, not html>head>title
         assertEquals(3, doc.select("link").size());
         assertEquals("application/rss+xml", con.response().contentType());
         assertEquals(Document.OutputSettings.Syntax.xml, doc.outputSettings().syntax());
diff --git a/src/test/java/org/jsoup/integration/SafelistExtensionTest.java b/src/test/java/org/jsoup/integration/SafelistExtensionTest.java
new file mode 100644
index 00000000..82f9316a
--- /dev/null
+++ b/src/test/java/org/jsoup/integration/SafelistExtensionTest.java
@@ -0,0 +1,49 @@
+package org.jsoup.integration;
+
+import org.jsoup.Jsoup;
+import org.jsoup.TextUtil;
+import org.jsoup.nodes.Attribute;
+import org.jsoup.nodes.Element;
+import org.jsoup.safety.Safelist;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ Check that we can extend Safelist methods
+ */
+public class SafelistExtensionTest {
+    @Test public void canCustomizeSafeTests() {
+        OpenSafelist openSafelist = new OpenSafelist(Safelist.relaxed());
+        Safelist safelist = Safelist.relaxed();
+
+        String html = "<p><opentag openattr>Hello</opentag></p>";
+
+        String openClean = Jsoup.clean(html, openSafelist);
+        String clean = Jsoup.clean(html, safelist);
+
+        assertEquals("<p><opentag openattr=\"\">Hello</opentag></p>", TextUtil.stripNewlines(openClean));
+        assertEquals("<p>Hello</p>", clean);
+    }
+
+    // passes tags and attributes starting with "open"
+    private static class OpenSafelist extends Safelist {
+        public OpenSafelist(Safelist safelist) {
+            super(safelist);
+        }
+
+        @Override
+        protected boolean isSafeAttribute(String tagName, Element el, Attribute attr) {
+            if (attr.getKey().startsWith("open"))
+                return true;
+            return super.isSafeAttribute(tagName, el, attr);
+        }
+
+        @Override
+        protected boolean isSafeTag(String tag) {
+            if (tag.startsWith("open"))
+                return true;
+            return super.isSafeTag(tag);
+        }
+    }
+}
diff --git a/src/test/java/org/jsoup/internal/StringUtilTest.java b/src/test/java/org/jsoup/internal/StringUtilTest.java
index c674b97e..f53cb844 100644
--- a/src/test/java/org/jsoup/internal/StringUtilTest.java
+++ b/src/test/java/org/jsoup/internal/StringUtilTest.java
@@ -24,7 +24,7 @@ public class StringUtilTest {
         assertEquals(" ", StringUtil.padding(1));
         assertEquals("  ", StringUtil.padding(2));
         assertEquals("               ", StringUtil.padding(15));
-        assertEquals("                                             ", StringUtil.padding(45));
+        assertEquals("                              ", StringUtil.padding(45)); // we tap out at 30
     }
 
     @Test public void paddingInACan() {
diff --git a/src/test/java/org/jsoup/nodes/DocumentTest.java b/src/test/java/org/jsoup/nodes/DocumentTest.java
index 47177ea4..aebaa9a1 100644
--- a/src/test/java/org/jsoup/nodes/DocumentTest.java
+++ b/src/test/java/org/jsoup/nodes/DocumentTest.java
@@ -5,6 +5,8 @@ import org.jsoup.TextUtil;
 import org.jsoup.integration.ParseTest;
 import org.jsoup.nodes.Document.OutputSettings;
 import org.jsoup.nodes.Document.OutputSettings.Syntax;
+import org.jsoup.parser.ParseSettings;
+import org.jsoup.parser.Parser;
 import org.jsoup.select.Elements;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -75,6 +77,34 @@ public class DocumentTest {
         assertEquals("<html><head><script>one</script><noscript>&lt;p&gt;two</noscript></head><body><p>three</p><p>four</p></body></html>", TextUtil.stripNewlines(doc.html()));
     }
 
+    @Test public void accessorsWillNormalizeStructure() {
+        Document doc = new Document("");
+        assertEquals("", doc.html());
+
+        Element body = doc.body();
+        assertEquals("body", body.tagName());
+        Element head = doc.head();
+        assertEquals("head", head.tagName());
+        assertEquals("<html><head></head><body></body></html>", TextUtil.stripNewlines(doc.html()));
+    }
+
+    @Test public void accessorsAreCaseInsensitive() {
+        Parser parser = Parser.htmlParser().settings(ParseSettings.preserveCase);
+        Document doc = parser.parseInput("<!DOCTYPE html><HTML><HEAD><TITLE>SHOUTY</TITLE></HEAD><BODY>HELLO</BODY></HTML>", "");
+
+        Element body = doc.body();
+        assertEquals("BODY", body.tagName());
+        assertEquals("body", body.normalName());
+        Element head = doc.head();
+        assertEquals("HEAD", head.tagName());
+        assertEquals("body", body.normalName());
+
+        Element root = doc.selectFirst("html");
+        assertEquals("HTML", root.tagName());
+        assertEquals("html", root.normalName());
+        assertEquals("SHOUTY", doc.title());
+    }
+
     @Test public void testClone() {
         Document doc = Jsoup.parse("<title>Hello</title> <p>One<p>Two");
         Document clone = doc.clone();
@@ -148,17 +178,21 @@ public class DocumentTest {
     	assertEquals(htmlContent, document.html(new StringWriter()).toString());
     }
 
-    // Ignored since this test can take awhile to run.
-    @Disabled
     @Test public void testOverflowClone() {
-        StringBuilder builder = new StringBuilder();
+        StringBuilder sb = new StringBuilder();
+        sb.append("<head><base href='https://jsoup.org/'>");
         for (int i = 0; i < 100000; i++) {
-            builder.insert(0, "<i>");
-            builder.append("</i>");
+            sb.append("<div>");
         }
+        sb.append("<p>Hello <a href='/example.html'>there</a>");
+
+        Document doc = Jsoup.parse(sb.toString());
 
-        Document doc = Jsoup.parse(builder.toString());
-        doc.clone();
+        String expectedLink = "https://jsoup.org/example.html";
+        assertEquals(expectedLink, doc.selectFirst("a").attr("abs:href"));
+        Document clone = doc.clone();
+        doc.hasSameValue(clone);
+        assertEquals(expectedLink, clone.selectFirst("a").attr("abs:href"));
     }
 
     @Test public void DocumentsWithSameContentAreEqual() {
@@ -455,4 +489,34 @@ public class DocumentTest {
         assertNotNull(documentType);
         assertEquals("html", documentType.name());
     }
+
+    @Test public void framesetSupportsBodyMethod() {
+        String html = "<html><head><title>Frame Test</title></head><frameset id=id><frame src=foo.html></frameset>";
+        Document doc = Jsoup.parse(html);
+        Element head = doc.head();
+        assertNotNull(head);
+        assertEquals("Frame Test", doc.title());
+
+        // Frameset docs per html5 spec have no body.
+        assertNull(doc.selectFirst("body"));
+
+        // but we want the .body() tag to not null; so we auto-normalize it
+        // doing it in body() vs parse keeps the html close to original for round-trip option
+        Element body = doc.body();
+        assertNotNull(body);
+        assertEquals("", body.html());
+
+        assertNotNull(doc.selectFirst("body"));
+
+        String expected = "<html>\n" +
+            " <head>\n" +
+            "  <title>Frame Test</title>\n" +
+            " </head>\n" +
+            " <frameset id=\"id\">\n" +
+            "  <frame src=\"foo.html\">\n" +
+            " </frameset>\n" +
+            " <body></body>\n" +
+            "</html>";
+        assertEquals(expected, doc.html());
+    }
 }
diff --git a/src/test/java/org/jsoup/nodes/ElementTest.java b/src/test/java/org/jsoup/nodes/ElementTest.java
index b22e66ec..8b3eb76b 100644
--- a/src/test/java/org/jsoup/nodes/ElementTest.java
+++ b/src/test/java/org/jsoup/nodes/ElementTest.java
@@ -2,6 +2,7 @@ package org.jsoup.nodes;
 
 import org.jsoup.Jsoup;
 import org.jsoup.TextUtil;
+import org.jsoup.parser.Parser;
 import org.jsoup.parser.Tag;
 import org.jsoup.select.*;
 import org.junit.jupiter.api.Test;
@@ -19,6 +20,19 @@ import static org.junit.jupiter.api.Assertions.*;
 public class ElementTest {
     private String reference = "<div id=div1><p>Hello</p><p>Another <b>element</b></p><div id=div2><img src=foo.png></div></div>";
 
+    @Test public void testId() {
+        Document doc = Jsoup.parse("<div id=Foo>");
+        Element el = doc.selectFirst("div");
+        assertEquals("Foo", el.id());
+    }
+
+    @Test public void testSetId() {
+        Document doc = Jsoup.parse("<div id=Boo>");
+        Element el = doc.selectFirst("div");
+        el.id("Foo");
+        assertEquals("Foo", el.id());
+    }
+
     @Test public void getElementsByTagName() {
         Document doc = Jsoup.parse(reference);
         List<Element> divs = doc.getElementsByTag("div");
@@ -143,6 +157,25 @@ public class ElementTest {
         assertEquals("element", p.lastElementSibling().text());
     }
 
+    @Test public void testFirstElementSiblingOnOrphan() {
+        Element p = new Element("p");
+        assertSame(p, p.firstElementSibling());
+        assertSame(p, p.lastElementSibling());
+    }
+
+    @Test public void testFirstAndLastSiblings() {
+        Document doc = Jsoup.parse("<div><p>One<p>Two<p>Three");
+        Element div = doc.selectFirst("div");
+        Element one = div.child(0);
+        Element two = div.child(1);
+        Element three = div.child(2);
+
+        assertSame(one, one.firstElementSibling());
+        assertSame(one, two.firstElementSibling());
+        assertSame(three, three.lastElementSibling());
+        assertSame(three, two.lastElementSibling());
+    }
+
     @Test public void testGetParents() {
         Document doc = Jsoup.parse("<div><p>Hello <span>there</span></div>");
         Element span = doc.select("span").first();
@@ -561,6 +594,35 @@ public class ElementTest {
         assertEquals(ret, p);
     }
 
+    @Test public void testWrapNoop() {
+        Document doc = Jsoup.parse("<div><p>Hello</p></div>");
+        Node p = doc.select("p").first();
+        Node wrapped = p.wrap("Some junk");
+        assertSame(p, wrapped);
+        assertEquals("<div><p>Hello</p></div>", TextUtil.stripNewlines(doc.body().html()));
+        // should be a NOOP
+    }
+
+    @Test public void testWrapOnOrphan() {
+        Element orphan = new Element("span").text("Hello!");
+        assertFalse(orphan.hasParent());
+        Element wrapped = orphan.wrap("<div></div> There!");
+        assertSame(orphan, wrapped);
+        assertTrue(orphan.hasParent()); // should now be in the DIV
+        assertNotNull(orphan.parent());
+        assertEquals("div", orphan.parent().tagName());
+        assertEquals("<div>\n <span>Hello!</span>\n</div>", orphan.parent().outerHtml());
+    }
+
+    @Test public void testWrapArtificialStructure() {
+        // div normally couldn't get into a p, but explicitly want to wrap
+        Document doc = Jsoup.parse("<p>Hello <i>there</i> now.");
+        Element i = doc.selectFirst("i");
+        i.wrap("<div id=id1></div> quite");
+        assertEquals("div", i.parent().tagName());
+        assertEquals("<p>Hello <div id=\"id1\"><i>there</i></div> quite now.</p>", TextUtil.stripNewlines(doc.body().html()));
+    }
+
     @Test public void before() {
         Document doc = Jsoup.parse("<div><p>Hello</p><p>There</p></div>");
         Element p1 = doc.select("p").first();
@@ -585,7 +647,24 @@ public class ElementTest {
         Document doc = Jsoup.parse("<div><p>Hello</p></div>");
         Element p = doc.select("p").first();
         p.wrap("<div class='head'></div><p>There!</p>");
-        assertEquals("<div><div class=\"head\"><p>Hello</p><p>There!</p></div></div>", TextUtil.stripNewlines(doc.body().html()));
+        assertEquals("<div><div class=\"head\"><p>Hello</p></div><p>There!</p></div>", TextUtil.stripNewlines(doc.body().html()));
+    }
+
+    @Test public void testWrapWithSimpleRemainder() {
+        Document doc = Jsoup.parse("<p>Hello");
+        Element p = doc.selectFirst("p");
+        Element body = p.parent();
+        assertNotNull(body);
+        assertEquals("body", body.tagName());
+
+        p.wrap("<div></div> There");
+        Element div = p.parent();
+        assertNotNull(div);
+        assertEquals("div", div.tagName());
+        assertSame(div, p.parent());
+        assertSame(body, div.parent());
+
+        assertEquals("<div><p>Hello</p></div> There", TextUtil.stripNewlines(doc.body().html()));
     }
 
     @Test public void testHasText() {
@@ -641,6 +720,11 @@ public class ElementTest {
         assertEquals("<img src=\"foo\">", img.toString());
     }
 
+    @Test public void orphanDivToString() {
+        Element orphan = new Element("div").id("foo").text("Hello");
+        assertEquals("<div id=\"foo\">\n Hello\n</div>", orphan.toString());
+    }
+
     @Test public void testClone() {
         Document doc = Jsoup.parse("<div><p>One<p><span>Two</div>");
 
@@ -1590,6 +1674,31 @@ public class ElementTest {
         assertEquals("<p>Four</p><p>Three</p><p>Four</p><div><p>One</p><p>Two</p></div><p>Three</p>", TextUtil.stripNewlines(doc.body().html()));
     }
 
+    @Test
+    public void testNotActuallyAReparent() {
+        // prep
+        String html = "<div>";
+        Document doc = Jsoup.parse(html);
+        Element div = doc.selectFirst("div");
+        Element new1 = new Element("p").text("One");
+        Element new2 = new Element("p").text("Two");
+        div.addChildren(new1, new2);
+
+        assertEquals("<div><p>One</p><p>Two</p></div>", TextUtil.stripNewlines(div.outerHtml()));
+
+        // and the issue setup:
+        Element new3 = new Element("p").text("Three");
+        Element wrap = new Element("nav");
+        wrap.addChildren(0, new1, new3);
+
+        assertEquals("<nav><p>One</p><p>Three</p></nav>", TextUtil.stripNewlines(wrap.outerHtml()));
+        div.addChildren(wrap);
+        // now should be that One moved into wrap, leaving Two in div.
+
+        assertEquals("<div><p>Two</p><nav><p>One</p><p>Three</p></nav></div>", TextUtil.stripNewlines(div.outerHtml()));
+        assertEquals("<div><p>Two</p><nav><p>One</p><p>Three</p></nav></div>", TextUtil.stripNewlines(div.outerHtml()));
+    }
+
     @Test
     public void testChildSizeWithMixedContent() {
         Document doc = Jsoup.parse("<table><tbody>\n<tr>\n<td>15:00</td>\n<td>sport</td>\n</tr>\n</tbody></table>");
@@ -1597,4 +1706,77 @@ public class ElementTest {
         assertEquals(2, row.childrenSize());
         assertEquals(5, row.childNodeSize());
     }
+
+    @Test public void isBlock() {
+        String html = "<div><p><span>Hello</span>";
+        Document doc = Jsoup.parse(html);
+        assertTrue(doc.selectFirst("div").isBlock());
+        assertTrue(doc.selectFirst("p").isBlock());
+        assertFalse(doc.selectFirst("span").isBlock());
+    }
+
+    @Test public void testScriptTextHtmlSetAsData() {
+        String src = "var foo = 5 < 2;\nvar bar = 1 && 2;";
+        String html = "<script>" + src + "</script>";
+        Document doc = Jsoup.parse(html);
+        Element el = doc.selectFirst("script");
+        assertNotNull(el);
+
+        validateScriptContents(src, el);
+
+        src = "var foo = 4 < 2;\nvar bar > 1 && 2;";
+        el.html(src);
+        validateScriptContents(src, el);
+
+        // special case for .text (in HTML; in XML will just be regular text)
+        el.text(src);
+        validateScriptContents(src, el);
+
+        // XML, no special treatment, get escaped correctly
+        Document xml = Parser.xmlParser().parseInput(html, "");
+        Element xEl = xml.selectFirst("script");
+        assertNotNull(xEl);
+        src = "var foo = 5 < 2;\nvar bar = 1 && 2;";
+        String escaped = "var foo = 5 &lt; 2;\nvar bar = 1 &amp;&amp; 2;";
+        validateXmlScriptContents(xEl);
+        xEl.text(src);
+        validateXmlScriptContents(xEl);
+        xEl.html(src);
+        validateXmlScriptContents(xEl);
+
+        assertEquals("<script>var foo = 4 < 2;\nvar bar > 1 && 2;</script>", el.outerHtml());
+        assertEquals("<script>" + escaped + "</script>", xEl.outerHtml()); // escaped in xml as no special treatment
+
+    }
+
+    private static void validateScriptContents(String src, Element el) {
+        assertEquals("", el.text()); // it's not text
+        assertEquals("", el.ownText());
+        assertEquals("", el.wholeText());
+        assertEquals(src, el.html());
+        assertEquals(src, el.data());
+    }
+
+    private static void validateXmlScriptContents(Element el) {
+        assertEquals("var foo = 5 < 2; var bar = 1 && 2;", el.text());
+        assertEquals("var foo = 5 < 2; var bar = 1 && 2;", el.ownText());
+        assertEquals("var foo = 5 < 2;\nvar bar = 1 && 2;", el.wholeText());
+        assertEquals("var foo = 5 &lt; 2;\nvar bar = 1 &amp;&amp; 2;", el.html());
+        assertEquals("", el.data());
+    }
+
+    @Test public void testShallowCloneToString() {
+        // https://github.com/jhy/jsoup/issues/1410
+        Document doc = Jsoup.parse("<p><i>Hello</i></p>");
+        Element p = doc.selectFirst("p");
+        Element i = doc.selectFirst("i");
+        String pH = p.shallowClone().toString();
+        String iH = i.shallowClone().toString();
+
+        assertEquals("<p></p>", pH); // shallow, so no I
+        assertEquals("<i></i>", iH);
+
+        assertEquals(p.outerHtml(), p.toString());
+        assertEquals(i.outerHtml(), i.toString());
+    }
 }
diff --git a/src/test/java/org/jsoup/parser/HtmlParserTest.java b/src/test/java/org/jsoup/parser/HtmlParserTest.java
index 000ff978..36d4a310 100644
--- a/src/test/java/org/jsoup/parser/HtmlParserTest.java
+++ b/src/test/java/org/jsoup/parser/HtmlParserTest.java
@@ -5,7 +5,7 @@ import org.jsoup.TextUtil;
 import org.jsoup.integration.ParseTest;
 import org.jsoup.internal.StringUtil;
 import org.jsoup.nodes.*;
-import org.jsoup.safety.Whitelist;
+import org.jsoup.safety.Safelist;
 import org.jsoup.select.Elements;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -434,6 +434,22 @@ public class HtmlParserTest {
         assertEquals("http://example.com/foo", doc.select("a").first().absUrl("href"));
     }
 
+    @Test public void parseBodyIsIndexNoAttributes() {
+        // https://github.com/jhy/jsoup/issues/1404
+        String expectedHtml = "<form>\n" +
+            " <hr><label>This is a searchable index. Enter search keywords: <input name=\"isindex\"></label>\n" +
+            " <hr>\n" +
+            "</form>";
+        Document doc = Jsoup.parse("<isindex>");
+        assertEquals(expectedHtml, doc.body().html());
+
+        doc = Jsoup.parseBodyFragment("<isindex>");
+        assertEquals(expectedHtml, doc.body().html());
+
+        doc = Jsoup.parseBodyFragment("<table><input></table>");
+        assertEquals("<input>\n<table></table>", doc.body().html());
+    }
+
     @Test public void handlesUnknownNamespaceTags() {
         // note that the first foo:bar should not really be allowed to be self closing, if parsed in html mode.
         String h = "<foo:bar id='1' /><abc:def id=2>Foo<p>Hello</p></abc:def><foo:bar>There</foo:bar>";
@@ -1142,8 +1158,8 @@ public class HtmlParserTest {
         parser.parseInput(html, "");
         assertEquals(0, parser.getErrors().size());
 
-        assertTrue(Jsoup.isValid(html, Whitelist.basic()));
-        String clean = Jsoup.clean(html, Whitelist.basic());
+        assertTrue(Jsoup.isValid(html, Safelist.basic()));
+        String clean = Jsoup.clean(html, Safelist.basic());
         assertEquals("<p>test<br>test<br></p>", clean);
     }
 
@@ -1154,8 +1170,8 @@ public class HtmlParserTest {
         assertEquals(1, parser.getErrors().size());
         assertEquals("18: Tag cannot be self closing; not a void tag", parser.getErrors().get(0).toString());
 
-        assertFalse(Jsoup.isValid(html, Whitelist.relaxed()));
-        String clean = Jsoup.clean(html, Whitelist.relaxed());
+        assertFalse(Jsoup.isValid(html, Safelist.relaxed()));
+        String clean = Jsoup.clean(html, Safelist.relaxed());
         assertEquals("<p>test</p> <div></div> <div> Two </div>", StringUtil.normaliseWhitespace(clean));
     }
 
@@ -1278,7 +1294,7 @@ public class HtmlParserTest {
     public void testH20() {
         // https://github.com/jhy/jsoup/issues/731
         String html = "H<sub>2</sub>O";
-        String clean = Jsoup.clean(html, Whitelist.basic());
+        String clean = Jsoup.clean(html, Safelist.basic());
         assertEquals("H<sub>2</sub>O", clean);
 
         Document doc = Jsoup.parse(html);
@@ -1289,7 +1305,7 @@ public class HtmlParserTest {
     public void testUNewlines() {
         // https://github.com/jhy/jsoup/issues/851
         String html = "t<u>es</u>t <b>on</b> <i>f</i><u>ir</u>e";
-        String clean = Jsoup.clean(html, Whitelist.basic());
+        String clean = Jsoup.clean(html, Safelist.basic());
         assertEquals("t<u>es</u>t <b>on</b> <i>f</i><u>ir</u>e", clean);
 
         Document doc = Jsoup.parse(html);
diff --git a/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java b/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java
index 97bd5287..3aea3bdf 100644
--- a/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java
+++ b/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java
@@ -13,12 +13,13 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 public class HtmlTreeBuilderStateTest {
-    static List<Object[]> findArrays() {
+    static List<Object[]> findConstantArrays(Class aClass) {
         ArrayList<Object[]> array = new ArrayList<>();
-        Field[] fields = Constants.class.getDeclaredFields();
+        Field[] fields = aClass.getDeclaredFields();
 
         for (Field field : fields) {
-            if (Modifier.isStatic(field.getModifiers()) && field.getType().isArray()) {
+            int modifiers = field.getModifiers();
+            if (Modifier.isStatic(modifiers) && !Modifier.isPrivate(modifiers) && field.getType().isArray()) {
                 try {
                     array.add((Object[]) field.get(null));
                 } catch (IllegalAccessException e) {
@@ -40,7 +41,7 @@ public class HtmlTreeBuilderStateTest {
 
     @Test
     public void ensureArraysAreSorted() {
-        List<Object[]> constants = findArrays();
+        List<Object[]> constants = findConstantArrays(Constants.class);
         ensureSorted(constants);
         assertEquals(38, constants.size());
     }
diff --git a/src/test/java/org/jsoup/parser/HtmlTreeBuilderTest.java b/src/test/java/org/jsoup/parser/HtmlTreeBuilderTest.java
index 04630dc9..133af5fb 100644
--- a/src/test/java/org/jsoup/parser/HtmlTreeBuilderTest.java
+++ b/src/test/java/org/jsoup/parser/HtmlTreeBuilderTest.java
@@ -3,27 +3,45 @@ package org.jsoup.parser;
 
 import org.junit.jupiter.api.Test;
 
+import javax.annotation.Nonnull;
+import javax.annotation.ParametersAreNonnullByDefault;
+import java.io.Reader;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
 import java.util.Arrays;
+import java.util.List;
 
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.*;
 
 public class HtmlTreeBuilderTest {
     @Test
     public void ensureSearchArraysAreSorted() {
-        String[][] arrays = {
-            HtmlTreeBuilder.TagsSearchInScope,
-            HtmlTreeBuilder.TagSearchList,
-            HtmlTreeBuilder.TagSearchButton,
-            HtmlTreeBuilder.TagSearchTableScope,
-            HtmlTreeBuilder.TagSearchSelectScope,
-            HtmlTreeBuilder.TagSearchEndTags,
-            HtmlTreeBuilder.TagSearchSpecial
-        };
-
-        for (String[] array : arrays) {
-            String[] copy = Arrays.copyOf(array, array.length);
-            Arrays.sort(array);
-            assertArrayEquals(array, copy);
+        List<Object[]> constants = HtmlTreeBuilderStateTest.findConstantArrays(HtmlTreeBuilder.class);
+        HtmlTreeBuilderStateTest.ensureSorted(constants);
+        assertEquals(7, constants.size());
+    }
+
+    @Test
+    public void nonnull() {
+        assertThrows(IllegalArgumentException.class, () -> {
+                HtmlTreeBuilder treeBuilder = new HtmlTreeBuilder();
+                treeBuilder.parse(null, null, null); // not sure how to test that these visual warnings actually appear! - test below checks for method annotation
+            }
+        ); // I'm not convinced that this lambda is easier to read than the old Junit 4 @Test(expected=IEA.class)...
+    }
+
+    @Test public void nonnullAssertions() throws NoSuchMethodException {
+        Method parseMethod = TreeBuilder.class.getDeclaredMethod("parse", Reader.class, String.class, Parser.class);
+        assertNotNull(parseMethod);
+        Annotation[] declaredAnnotations = parseMethod.getDeclaredAnnotations();
+        boolean seen = false;
+        for (Annotation annotation : declaredAnnotations) {
+            if (annotation.annotationType().isAssignableFrom(ParametersAreNonnullByDefault.class))
+                seen = true;
         }
+
+        // would need to rework this if/when that annotation moves from the method to the class / package.
+        assertTrue(seen);
+
     }
 }
diff --git a/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java b/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java
index 8aa7547a..b51755db 100644
--- a/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java
+++ b/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java
@@ -103,8 +103,7 @@ public class XmlTreeBuilderTest {
     @Test public void handlesXmlDeclarationAsDeclaration() {
         String html = "<?xml encoding='UTF-8' ?><body>One</body><!-- comment -->";
         Document doc = Jsoup.parse(html, "", Parser.xmlParser());
-        assertEquals("<?xml encoding=\"UTF-8\"?> <body> One </body><!-- comment -->",
-                StringUtil.normaliseWhitespace(doc.outerHtml()));
+        assertEquals("<?xml encoding=\"UTF-8\"?><body>One</body><!-- comment -->",doc.outerHtml());
         assertEquals("#declaration", doc.childNode(0).nodeName());
         assertEquals("#comment", doc.childNode(2).nodeName());
     }
@@ -188,6 +187,13 @@ public class XmlTreeBuilderTest {
         assertEquals("<One>One<Two ID=\"2\">Two</Two></One>", TextUtil.stripNewlines(doc.html()));
     }
 
+    @Test
+    public void disablesPrettyPrintingByDefault() {
+        String xml = "\n\n<div><one>One</one><one>\n Two</one>\n</div>\n ";
+        Document doc = Jsoup.parse(xml, "", Parser.xmlParser());
+        assertEquals(xml, doc.html());
+    }
+
     @Test
     public void canNormalizeCase() {
         String xml = "<TEST ID=1>Check</TEST>";
@@ -198,7 +204,7 @@ public class XmlTreeBuilderTest {
     @Test public void normalizesDiscordantTags() {
         Parser parser = Parser.xmlParser().settings(ParseSettings.htmlDefault);
         Document document = Jsoup.parse("<div>test</DIV><p></p>", "", parser);
-        assertEquals("<div>\n test\n</div>\n<p></p>", document.html());
+        assertEquals("<div>test</div><p></p>", document.html());
         // was failing -> toString() = "<div>\n test\n <p></p>\n</div>"
     }
 
@@ -211,7 +217,7 @@ public class XmlTreeBuilderTest {
         assertEquals(0, div.children().size());
         assertEquals(1, div.childNodeSize()); // no elements, one text node
 
-        assertEquals("<div id=\"1\"><![CDATA[\n<html>\n <foo><&amp;]]>\n</div>", div.outerHtml());
+        assertEquals("<div id=\"1\"><![CDATA[\n<html>\n <foo><&amp;]]></div>", div.outerHtml());
 
         CDataNode cdata = (CDataNode) div.textNodes().get(0);
         assertEquals("\n<html>\n <foo><&amp;", cdata.text());
diff --git a/src/test/java/org/jsoup/safety/CleanerTest.java b/src/test/java/org/jsoup/safety/CleanerTest.java
index 707313f8..33380549 100644
--- a/src/test/java/org/jsoup/safety/CleanerTest.java
+++ b/src/test/java/org/jsoup/safety/CleanerTest.java
@@ -18,21 +18,21 @@ import static org.junit.jupiter.api.Assertions.*;
 public class CleanerTest {
     @Test public void simpleBehaviourTest() {
         String h = "<div><p class=foo><a href='http://evil.com'>Hello <b id=bar>there</b>!</a></div>";
-        String cleanHtml = Jsoup.clean(h, Whitelist.simpleText());
+        String cleanHtml = Jsoup.clean(h, Safelist.simpleText());
 
         assertEquals("Hello <b>there</b>!", TextUtil.stripNewlines(cleanHtml));
     }
 
     @Test public void simpleBehaviourTest2() {
         String h = "Hello <b>there</b>!";
-        String cleanHtml = Jsoup.clean(h, Whitelist.simpleText());
+        String cleanHtml = Jsoup.clean(h, Safelist.simpleText());
 
         assertEquals("Hello <b>there</b>!", TextUtil.stripNewlines(cleanHtml));
     }
 
     @Test public void basicBehaviourTest() {
         String h = "<div><p><a href='javascript:sendAllMoney()'>Dodgy</a> <A HREF='HTTP://nice.com'>Nice</a></p><blockquote>Hello</blockquote>";
-        String cleanHtml = Jsoup.clean(h, Whitelist.basic());
+        String cleanHtml = Jsoup.clean(h, Safelist.basic());
 
         assertEquals("<p><a rel=\"nofollow\">Dodgy</a> <a href=\"http://nice.com\" rel=\"nofollow\">Nice</a></p><blockquote>Hello</blockquote>",
                 TextUtil.stripNewlines(cleanHtml));
@@ -40,33 +40,33 @@ public class CleanerTest {
 
     @Test public void basicWithImagesTest() {
         String h = "<div><p><img src='http://example.com/' alt=Image></p><p><img src='ftp://ftp.example.com'></p></div>";
-        String cleanHtml = Jsoup.clean(h, Whitelist.basicWithImages());
+        String cleanHtml = Jsoup.clean(h, Safelist.basicWithImages());
         assertEquals("<p><img src=\"http://example.com/\" alt=\"Image\"></p><p><img></p>", TextUtil.stripNewlines(cleanHtml));
     }
 
     @Test public void testRelaxed() {
         String h = "<h1>Head</h1><table><tr><td>One<td>Two</td></tr></table>";
-        String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
+        String cleanHtml = Jsoup.clean(h, Safelist.relaxed());
         assertEquals("<h1>Head</h1><table><tbody><tr><td>One</td><td>Two</td></tr></tbody></table>", TextUtil.stripNewlines(cleanHtml));
     }
 
     @Test public void testRemoveTags() {
         String h = "<div><p><A HREF='HTTP://nice.com'>Nice</a></p><blockquote>Hello</blockquote>";
-        String cleanHtml = Jsoup.clean(h, Whitelist.basic().removeTags("a"));
+        String cleanHtml = Jsoup.clean(h, Safelist.basic().removeTags("a"));
 
         assertEquals("<p>Nice</p><blockquote>Hello</blockquote>", TextUtil.stripNewlines(cleanHtml));
     }
 
     @Test public void testRemoveAttributes() {
         String h = "<div><p>Nice</p><blockquote cite='http://example.com/quotations'>Hello</blockquote>";
-        String cleanHtml = Jsoup.clean(h, Whitelist.basic().removeAttributes("blockquote", "cite"));
+        String cleanHtml = Jsoup.clean(h, Safelist.basic().removeAttributes("blockquote", "cite"));
 
         assertEquals("<p>Nice</p><blockquote>Hello</blockquote>", TextUtil.stripNewlines(cleanHtml));
     }
 
     @Test public void testRemoveEnforcedAttributes() {
         String h = "<div><p><A HREF='HTTP://nice.com'>Nice</a></p><blockquote>Hello</blockquote>";
-        String cleanHtml = Jsoup.clean(h, Whitelist.basic().removeEnforcedAttribute("a", "rel"));
+        String cleanHtml = Jsoup.clean(h, Safelist.basic().removeEnforcedAttribute("a", "rel"));
 
         assertEquals("<p><a href=\"http://nice.com\">Nice</a></p><blockquote>Hello</blockquote>",
                 TextUtil.stripNewlines(cleanHtml));
@@ -74,53 +74,53 @@ public class CleanerTest {
 
     @Test public void testRemoveProtocols() {
         String h = "<p>Contact me <a href='mailto:info@example.com'>here</a></p>";
-        String cleanHtml = Jsoup.clean(h, Whitelist.basic().removeProtocols("a", "href", "ftp", "mailto"));
+        String cleanHtml = Jsoup.clean(h, Safelist.basic().removeProtocols("a", "href", "ftp", "mailto"));
 
         assertEquals("<p>Contact me <a rel=\"nofollow\">here</a></p>",
                 TextUtil.stripNewlines(cleanHtml));
     }
 
     @MultiLocaleTest
-    public void whitelistedProtocolShouldBeRetained(Locale locale) {
+    public void safeListedProtocolShouldBeRetained(Locale locale) {
         Locale.setDefault(locale);
 
-        Whitelist whitelist = Whitelist.none()
+        Safelist safelist = Safelist.none()
                 .addTags("a")
                 .addAttributes("a", "href")
                 .addProtocols("a", "href", "something");
 
-        String cleanHtml = Jsoup.clean("<a href=\"SOMETHING://x\"></a>", whitelist);
+        String cleanHtml = Jsoup.clean("<a href=\"SOMETHING://x\"></a>", safelist);
 
         assertEquals("<a href=\"SOMETHING://x\"></a>", TextUtil.stripNewlines(cleanHtml));
     }
 
     @Test public void testDropComments() {
         String h = "<p>Hello<!-- no --></p>";
-        String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
+        String cleanHtml = Jsoup.clean(h, Safelist.relaxed());
         assertEquals("<p>Hello</p>", cleanHtml);
     }
 
     @Test public void testDropXmlProc() {
         String h = "<?import namespace=\"xss\"><p>Hello</p>";
-        String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
+        String cleanHtml = Jsoup.clean(h, Safelist.relaxed());
         assertEquals("<p>Hello</p>", cleanHtml);
     }
 
     @Test public void testDropScript() {
         String h = "<SCRIPT SRC=//ha.ckers.org/.j><SCRIPT>alert(/XSS/.source)</SCRIPT>";
-        String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
+        String cleanHtml = Jsoup.clean(h, Safelist.relaxed());
         assertEquals("", cleanHtml);
     }
 
     @Test public void testDropImageScript() {
         String h = "<IMG SRC=\"javascript:alert('XSS')\">";
-        String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
+        String cleanHtml = Jsoup.clean(h, Safelist.relaxed());
         assertEquals("<img>", cleanHtml);
     }
 
     @Test public void testCleanJavascriptHref() {
         String h = "<A HREF=\"javascript:document.location='http://www.google.com/'\">XSS</A>";
-        String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
+        String cleanHtml = Jsoup.clean(h, Safelist.relaxed());
         assertEquals("<a>XSS</a>", cleanHtml);
     }
 
@@ -128,15 +128,15 @@ public class CleanerTest {
         String validAnchor = "<a href=\"#valid\">Valid anchor</a>";
         String invalidAnchor = "<a href=\"#anchor with spaces\">Invalid anchor</a>";
 
-        // A Whitelist that does not allow anchors will strip them out.
-        String cleanHtml = Jsoup.clean(validAnchor, Whitelist.relaxed());
+        // A Safelist that does not allow anchors will strip them out.
+        String cleanHtml = Jsoup.clean(validAnchor, Safelist.relaxed());
         assertEquals("<a>Valid anchor</a>", cleanHtml);
 
-        cleanHtml = Jsoup.clean(invalidAnchor, Whitelist.relaxed());
+        cleanHtml = Jsoup.clean(invalidAnchor, Safelist.relaxed());
         assertEquals("<a>Invalid anchor</a>", cleanHtml);
 
-        // A Whitelist that allows them will keep them.
-        Whitelist relaxedWithAnchor = Whitelist.relaxed().addProtocols("a", "href", "#");
+        // A Safelist that allows them will keep them.
+        Safelist relaxedWithAnchor = Safelist.relaxed().addProtocols("a", "href", "#");
 
         cleanHtml = Jsoup.clean(validAnchor, relaxedWithAnchor);
         assertEquals(validAnchor, cleanHtml);
@@ -148,13 +148,13 @@ public class CleanerTest {
 
     @Test public void testDropsUnknownTags() {
         String h = "<p><custom foo=true>Test</custom></p>";
-        String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
+        String cleanHtml = Jsoup.clean(h, Safelist.relaxed());
         assertEquals("<p>Test</p>", cleanHtml);
     }
 
     @Test public void testHandlesEmptyAttributes() {
         String h = "<img alt=\"\" src= unknown=''>";
-        String cleanHtml = Jsoup.clean(h, Whitelist.basicWithImages());
+        String cleanHtml = Jsoup.clean(h, Safelist.basicWithImages());
         assertEquals("<img alt=\"\">", cleanHtml);
     }
 
@@ -168,74 +168,74 @@ public class CleanerTest {
         String nok5 = "<p>Test <b><a href='http://example.com/' rel='nofollowme'>OK</a></b></p>";
         String nok6 = "<p>Test <b><a href='http://example.com/'>OK</b></p>"; // missing close tag
         String nok7 = "</div>What";
-        assertTrue(Jsoup.isValid(ok, Whitelist.basic()));
-        assertTrue(Jsoup.isValid(ok1, Whitelist.basic()));
-        assertFalse(Jsoup.isValid(nok1, Whitelist.basic()));
-        assertFalse(Jsoup.isValid(nok2, Whitelist.basic()));
-        assertFalse(Jsoup.isValid(nok3, Whitelist.basic()));
-        assertFalse(Jsoup.isValid(nok4, Whitelist.basic()));
-        assertFalse(Jsoup.isValid(nok5, Whitelist.basic()));
-        assertFalse(Jsoup.isValid(nok6, Whitelist.basic()));
-        assertFalse(Jsoup.isValid(ok, Whitelist.none()));
-        assertFalse(Jsoup.isValid(nok7, Whitelist.basic()));
+        assertTrue(Jsoup.isValid(ok, Safelist.basic()));
+        assertTrue(Jsoup.isValid(ok1, Safelist.basic()));
+        assertFalse(Jsoup.isValid(nok1, Safelist.basic()));
+        assertFalse(Jsoup.isValid(nok2, Safelist.basic()));
+        assertFalse(Jsoup.isValid(nok3, Safelist.basic()));
+        assertFalse(Jsoup.isValid(nok4, Safelist.basic()));
+        assertFalse(Jsoup.isValid(nok5, Safelist.basic()));
+        assertFalse(Jsoup.isValid(nok6, Safelist.basic()));
+        assertFalse(Jsoup.isValid(ok, Safelist.none()));
+        assertFalse(Jsoup.isValid(nok7, Safelist.basic()));
     }
 
     @Test public void testIsValidDocument() {
         String ok = "<html><head></head><body><p>Hello</p></body><html>";
         String nok = "<html><head><script>woops</script><title>Hello</title></head><body><p>Hello</p></body><html>";
 
-        Whitelist relaxed = Whitelist.relaxed();
+        Safelist relaxed = Safelist.relaxed();
         Cleaner cleaner = new Cleaner(relaxed);
         Document okDoc = Jsoup.parse(ok);
         assertTrue(cleaner.isValid(okDoc));
         assertFalse(cleaner.isValid(Jsoup.parse(nok)));
-        assertFalse(new Cleaner(Whitelist.none()).isValid(okDoc));
+        assertFalse(new Cleaner(Safelist.none()).isValid(okDoc));
     }
 
     @Test public void resolvesRelativeLinks() {
         String html = "<a href='/foo'>Link</a><img src='/bar'>";
-        String clean = Jsoup.clean(html, "http://example.com/", Whitelist.basicWithImages());
+        String clean = Jsoup.clean(html, "http://example.com/", Safelist.basicWithImages());
         assertEquals("<a href=\"http://example.com/foo\" rel=\"nofollow\">Link</a>\n<img src=\"http://example.com/bar\">", clean);
     }
 
     @Test public void preservesRelativeLinksIfConfigured() {
         String html = "<a href='/foo'>Link</a><img src='/bar'> <img src='javascript:alert()'>";
-        String clean = Jsoup.clean(html, "http://example.com/", Whitelist.basicWithImages().preserveRelativeLinks(true));
+        String clean = Jsoup.clean(html, "http://example.com/", Safelist.basicWithImages().preserveRelativeLinks(true));
         assertEquals("<a href=\"/foo\" rel=\"nofollow\">Link</a>\n<img src=\"/bar\"> \n<img>", clean);
     }
 
     @Test public void dropsUnresolvableRelativeLinks() {
         String html = "<a href='/foo'>Link</a>";
-        String clean = Jsoup.clean(html, Whitelist.basic());
+        String clean = Jsoup.clean(html, Safelist.basic());
         assertEquals("<a rel=\"nofollow\">Link</a>", clean);
     }
 
     @Test public void handlesCustomProtocols() {
         String html = "<img src='cid:12345' /> <img src='data:gzzt' />";
-        String dropped = Jsoup.clean(html, Whitelist.basicWithImages());
+        String dropped = Jsoup.clean(html, Safelist.basicWithImages());
         assertEquals("<img> \n<img>", dropped);
 
-        String preserved = Jsoup.clean(html, Whitelist.basicWithImages().addProtocols("img", "src", "cid", "data"));
+        String preserved = Jsoup.clean(html, Safelist.basicWithImages().addProtocols("img", "src", "cid", "data"));
         assertEquals("<img src=\"cid:12345\"> \n<img src=\"data:gzzt\">", preserved);
     }
 
     @Test public void handlesAllPseudoTag() {
         String html = "<p class='foo' src='bar'><a class='qux'>link</a></p>";
-        Whitelist whitelist = new Whitelist()
+        Safelist safelist = new Safelist()
                 .addAttributes(":all", "class")
                 .addAttributes("p", "style")
                 .addTags("p", "a");
 
-        String clean = Jsoup.clean(html, whitelist);
+        String clean = Jsoup.clean(html, safelist);
         assertEquals("<p class=\"foo\"><a class=\"qux\">link</a></p>", clean);
     }
 
     @Test public void addsTagOnAttributesIfNotSet() {
         String html = "<p class='foo' src='bar'>One</p>";
-        Whitelist whitelist = new Whitelist()
+        Safelist safelist = new Safelist()
             .addAttributes("p", "class");
-        // ^^ whitelist does not have explicit tag add for p, inferred from add attributes.
-        String clean = Jsoup.clean(html, whitelist);
+        // ^^ safelist does not have explicit tag add for p, inferred from add attributes.
+        String clean = Jsoup.clean(html, safelist);
         assertEquals("<p class=\"foo\">One</p>", clean);
     }
 
@@ -247,8 +247,8 @@ public class CleanerTest {
         os.charset("ascii");
 
         String html = "<div><p>&bernou;</p></div>";
-        String customOut = Jsoup.clean(html, "http://foo.com/", Whitelist.relaxed(), os);
-        String defaultOut = Jsoup.clean(html, "http://foo.com/", Whitelist.relaxed());
+        String customOut = Jsoup.clean(html, "http://foo.com/", Safelist.relaxed(), os);
+        String defaultOut = Jsoup.clean(html, "http://foo.com/", Safelist.relaxed());
         assertNotSame(defaultOut, customOut);
 
         assertEquals("<div><p>&Bscr;</p></div>", customOut); // entities now prefers shorted names if aliased
@@ -258,37 +258,37 @@ public class CleanerTest {
 
         os.charset("ASCII");
         os.escapeMode(Entities.EscapeMode.base);
-        String customOut2 = Jsoup.clean(html, "http://foo.com/", Whitelist.relaxed(), os);
+        String customOut2 = Jsoup.clean(html, "http://foo.com/", Safelist.relaxed(), os);
         assertEquals("<div><p>&#x212c;</p></div>", customOut2);
     }
 
     @Test public void handlesFramesets() {
         String dirty = "<html><head><script></script><noscript></noscript></head><frameset><frame src=\"foo\" /><frame src=\"foo\" /></frameset></html>";
-        String clean = Jsoup.clean(dirty, Whitelist.basic());
+        String clean = Jsoup.clean(dirty, Safelist.basic());
         assertEquals("", clean); // nothing good can come out of that
 
         Document dirtyDoc = Jsoup.parse(dirty);
-        Document cleanDoc = new Cleaner(Whitelist.basic()).clean(dirtyDoc);
+        Document cleanDoc = new Cleaner(Safelist.basic()).clean(dirtyDoc);
         assertNotNull(cleanDoc);
         assertEquals(0, cleanDoc.body().childNodeSize());
     }
 
     @Test public void cleansInternationalText() {
-        assertEquals("привет", Jsoup.clean("привет", Whitelist.none()));
+        assertEquals("привет", Jsoup.clean("привет", Safelist.none()));
     }
 
     @Test
-    public void testScriptTagInWhiteList() {
-        Whitelist whitelist = Whitelist.relaxed();
-        whitelist.addTags( "script" );
-        assertTrue( Jsoup.isValid("Hello<script>alert('Doh')</script>World !", whitelist ) );
+    public void testScriptTagInSafeList() {
+        Safelist safelist = Safelist.relaxed();
+        safelist.addTags( "script" );
+        assertTrue( Jsoup.isValid("Hello<script>alert('Doh')</script>World !", safelist) );
     }
 
     @Test
     public void bailsIfRemovingProtocolThatsNotSet() {
         assertThrows(IllegalArgumentException.class, () -> {
             // a case that came up on the email list
-            Whitelist w = Whitelist.none();
+            Safelist w = Safelist.none();
 
             // note no add tag, and removing protocol without adding first
             w.addAttributes("a", "href");
@@ -298,20 +298,20 @@ public class CleanerTest {
 
     @Test public void handlesControlCharactersAfterTagName() {
         String html = "<a/\06>";
-        String clean = Jsoup.clean(html, Whitelist.basic());
+        String clean = Jsoup.clean(html, Safelist.basic());
         assertEquals("<a rel=\"nofollow\"></a>", clean);
     }
 
     @Test public void handlesAttributesWithNoValue() {
         // https://github.com/jhy/jsoup/issues/973
-        String clean = Jsoup.clean("<a href>Clean</a>", Whitelist.basic());
+        String clean = Jsoup.clean("<a href>Clean</a>", Safelist.basic());
 
         assertEquals("<a rel=\"nofollow\">Clean</a>", clean);
     }
 
     @Test public void handlesNoHrefAttribute() {
         String dirty = "<a>One</a> <a href>Two</a>";
-        Whitelist relaxedWithAnchor = Whitelist.relaxed().addProtocols("a", "href", "#");
+        Safelist relaxedWithAnchor = Safelist.relaxed().addProtocols("a", "href", "#");
         String clean = Jsoup.clean(dirty, relaxedWithAnchor);
         assertEquals("<a>One</a> <a>Two</a>", clean);
     }
@@ -319,7 +319,7 @@ public class CleanerTest {
     @Test public void handlesNestedQuotesInAttribute() {
         // https://github.com/jhy/jsoup/issues/1243 - no repro
         String orig = "<div style=\"font-family: 'Calibri'\">Will (not) fail</div>";
-        Whitelist allow = Whitelist.relaxed()
+        Safelist allow = Safelist.relaxed()
             .addAttributes("div", "style");
 
         String clean = Jsoup.clean(orig, allow);
@@ -328,4 +328,15 @@ public class CleanerTest {
         assertEquals(orig, TextUtil.stripNewlines(clean)); // only difference is pretty print wrap & indent
         assertTrue(isValid);
     }
+
+    @Test public void copiesOutputSettings() {
+        Document orig = Jsoup.parse("<p>test<br></p>");
+        orig.outputSettings().syntax(Document.OutputSettings.Syntax.xml);
+        orig.outputSettings().escapeMode(Entities.EscapeMode.xhtml);
+        Safelist whitelist = Safelist.none().addTags("p", "br");
+
+        Document result = new Cleaner(whitelist).clean(orig);
+        assertEquals(Document.OutputSettings.Syntax.xml, result.outputSettings().syntax());
+        assertEquals("<p>test<br /></p>", result.body().html());
+    }
 }
diff --git a/src/test/java/org/jsoup/safety/CompatibilityTests.java b/src/test/java/org/jsoup/safety/CompatibilityTests.java
new file mode 100644
index 00000000..5d04883d
--- /dev/null
+++ b/src/test/java/org/jsoup/safety/CompatibilityTests.java
@@ -0,0 +1,106 @@
+package org.jsoup.safety;
+
+import org.jsoup.Jsoup;
+import org.jsoup.MultiLocaleExtension;
+import org.jsoup.TextUtil;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Entities;
+import org.junit.jupiter.api.Test;
+
+import java.util.Locale;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ Tests for the deprecated {@link org.jsoup.safety.Whitelist} class source compatibility. Will be removed in
+ <code>v.1.15.1</code>. No net new tests here so safe to blow up.
+ */
+public class CompatibilityTests {
+    @Test
+    public void resolvesRelativeLinks() {
+        String html = "<a href='/foo'>Link</a><img src='/bar'>";
+        String clean = Jsoup.clean(html, "http://example.com/", Whitelist.basicWithImages());
+        assertEquals("<a href=\"http://example.com/foo\" rel=\"nofollow\">Link</a>\n<img src=\"http://example.com/bar\">", clean);
+    }
+
+    @Test
+    public void testDropsUnknownTags() {
+        String h = "<p><custom foo=true>Test</custom></p>";
+        String cleanHtml = Jsoup.clean(h, Whitelist.relaxed());
+        assertEquals("<p>Test</p>", cleanHtml);
+    }
+
+    @Test
+    public void preservesRelativeLinksIfConfigured() {
+        String html = "<a href='/foo'>Link</a><img src='/bar'> <img src='javascript:alert()'>";
+        String clean = Jsoup.clean(html, "http://example.com/", Whitelist.basicWithImages().preserveRelativeLinks(true));
+        assertEquals("<a href=\"/foo\" rel=\"nofollow\">Link</a>\n<img src=\"/bar\"> \n<img>", clean);
+    }
+
+    @Test
+    public void handlesCustomProtocols() {
+        String html = "<img src='cid:12345' /> <img src='data:gzzt' />";
+        String dropped = Jsoup.clean(html, Whitelist.basicWithImages());
+        assertEquals("<img> \n<img>", dropped);
+
+        String preserved = Jsoup.clean(html, Whitelist.basicWithImages().addProtocols("img", "src", "cid", "data"));
+        assertEquals("<img src=\"cid:12345\"> \n<img src=\"data:gzzt\">", preserved);
+    }
+
+    @Test
+    public void handlesFramesets() {
+        String dirty = "<html><head><script></script><noscript></noscript></head><frameset><frame src=\"foo\" /><frame src=\"foo\" /></frameset></html>";
+        String clean = Jsoup.clean(dirty, Whitelist.basic());
+        assertEquals("", clean); // nothing good can come out of that
+
+        Document dirtyDoc = Jsoup.parse(dirty);
+        Document cleanDoc = new Cleaner(Whitelist.basic()).clean(dirtyDoc);
+        assertNotNull(cleanDoc);
+        assertEquals(0, cleanDoc.body().childNodeSize());
+    }
+
+    @Test public void handlesCleanerFromWhitelist() {
+        Cleaner cleaner = new Cleaner(Whitelist.basic());
+        Document doc = Jsoup.parse("<script>Script</script><p>Text</p>");
+        Document clean = cleaner.clean(doc);
+        assertEquals("<p>Text</p>", clean.body().html());
+    }
+
+    @Test
+    public void supplyOutputSettings() {
+        // test that one can override the default document output settings
+        Document.OutputSettings os = new Document.OutputSettings();
+        os.prettyPrint(false);
+        os.escapeMode(Entities.EscapeMode.extended);
+        os.charset("ascii");
+
+        String html = "<div><p>&bernou;</p></div>";
+        String customOut = Jsoup.clean(html, "http://foo.com/", Whitelist.relaxed(), os);
+        String defaultOut = Jsoup.clean(html, "http://foo.com/", Whitelist.relaxed());
+        assertNotSame(defaultOut, customOut);
+
+        assertEquals("<div><p>&Bscr;</p></div>", customOut); // entities now prefers shorted names if aliased
+        assertEquals("<div>\n" +
+            " <p>ℬ</p>\n" +
+            "</div>", defaultOut);
+
+        os.charset("ASCII");
+        os.escapeMode(Entities.EscapeMode.base);
+        String customOut2 = Jsoup.clean(html, "http://foo.com/", Whitelist.relaxed(), os);
+        assertEquals("<div><p>&#x212c;</p></div>", customOut2);
+    }
+
+    @MultiLocaleExtension.MultiLocaleTest
+    public void safeListedProtocolShouldBeRetained(Locale locale) {
+        Locale.setDefault(locale);
+
+        Whitelist safelist = Whitelist.none()
+            .addTags("a")
+            .addAttributes("a", "href")
+            .addProtocols("a", "href", "something");
+
+        String cleanHtml = Jsoup.clean("<a href=\"SOMETHING://x\"></a>", safelist);
+
+        assertEquals("<a href=\"SOMETHING://x\"></a>", TextUtil.stripNewlines(cleanHtml));
+    }
+}
diff --git a/src/test/java/org/jsoup/select/ElementsTest.java b/src/test/java/org/jsoup/select/ElementsTest.java
index c41a01b9..20399e2e 100644
--- a/src/test/java/org/jsoup/select/ElementsTest.java
+++ b/src/test/java/org/jsoup/select/ElementsTest.java
@@ -282,8 +282,8 @@ public class ElementsTest {
 
     @Test public void forms() {
         Document doc = Jsoup.parse("<form id=1><input name=q></form><div /><form id=2><input name=f></form>");
-        Elements els = doc.select("*");
-        assertEquals(9, els.size());
+        Elements els = doc.select("form, div");
+        assertEquals(3, els.size());
 
         List<FormElement> forms = els.forms();
         assertEquals(2, forms.size());
@@ -337,11 +337,6 @@ public class ElementsTest {
         assertEquals(0, doc.select("form").textNodes().size());
     }
 
-    @Test public void formElementsDescendButNotAccumulate() {
-        Document doc = Jsoup.parse("<div><div><form id=1>");
-        assertEquals(1, doc.select("div").forms().size());
-    }
-
     @Test public void classWithHyphen() {
         Document doc = Jsoup.parse("<p class='tab-nav'>Check</p>");
         Elements els = doc.getElementsByClass("tab-nav");
diff --git a/src/test/java/org/jsoup/select/SelectorTest.java b/src/test/java/org/jsoup/select/SelectorTest.java
index 347f80cc..2602c4b5 100644
--- a/src/test/java/org/jsoup/select/SelectorTest.java
+++ b/src/test/java/org/jsoup/select/SelectorTest.java
@@ -843,6 +843,12 @@ public class SelectorTest {
         assertEquals("Two", doc.select("p:matchText + br + *").text());
     }
 
+    @Test public void nthLastChildWithNoParent() {
+        Element el = new Element("p").text("Orphan");
+        Elements els = el.select("p:nth-last-child(1)");
+        assertEquals(0, els.size());
+    }
+
     @Test public void splitOnBr() {
         String html = "<div><p>One<br>Two<br>Three</p></div>";
         Document doc = Jsoup.parse(html);