Repository: jhy/jsoup
Pull request #1999 (2023-09-18 23:14:22)

Bump org.apache.maven.plugins:maven-javadoc-plugin from 3.5.0 to 3.6.0
Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.5.0 to 3.6.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/apache/maven-javadoc-plugin/releases">org.apache.maven.plugins:maven-javadoc-plugin's releases</a>.</em></p>
<blockquote>
<h2>3.6.0</h2>
<!-- raw HTML omitted -->
<h2>🚀 New features and improvements</h2>
<ul>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-642">[MJAVADOC-642]</a> - Make offline mode configurable (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/238">#238</a>) <a href="https://github.com/hgschmie"><code>@​hgschmie</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-642">[MJAVADOC-642]</a> - Make offline mode configurable (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/232">#232</a>) <a href="https://github.com/hgschmie"><code>@​hgschmie</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-770">[MJAVADOC-770]</a> - Implement legacy mode for Java 9+ (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/228">#228</a>) <a href="https://github.com/hgschmie"><code>@​hgschmie</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-743">[MJAVADOC-743]</a> - Drop deprecated mojo parameter (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/185">#185</a>) <a href="https://github.com/cstamas"><code>@​cstamas</code></a></li>
</ul>
<h2>🐛 Bug Fixes</h2>
<ul>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-742">[MJAVADOC-742]</a> - Fix resolution of docletArtifacts (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/186">#186</a>) <a href="https://github.com/cstamas"><code>@​cstamas</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-758">[MJAVADOC-758]</a> - correct exception in JavadocUtil (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/230">#230</a>) <a href="https://github.com/hgschmie"><code>@​hgschmie</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-769">[MJAVADOC-769]</a> - fix for transitive filename based modules (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/227">#227</a>) <a href="https://github.com/hgschmie"><code>@​hgschmie</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-767">[MJAVADOC-767]</a> - javadoc creates invalid --patch-module statements (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/225">#225</a>) <a href="https://github.com/hgschmie"><code>@​hgschmie</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-763">[MJAVADOC-763]</a> - remove test that doesn't test what it claims (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/219">#219</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-757">[MJAVADOC-757]</a> - fix Javadoc warnings (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/212">#212</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li>Fix UmlGraph link in site example (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/198">#198</a>) <a href="https://github.com/ParkerM"><code>@​ParkerM</code></a></li>
</ul>
<h2>📦 Dependency updates</h2>
<ul>
<li><a href="https://issues.apache.org/jira/browse/JAVADOC-771">[JAVADOC-771]</a> - Upgrade Parent to 40 (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/234">#234</a>) <a href="https://github.com/hgschmie"><code>@​hgschmie</code></a></li>
<li>Bump org.apache.maven:maven-core from 3.6.0 to 3.8.1 in /src/it/projects/MJAVADOC-568_manifest-splitpackage/mojo (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/226">#226</a>) <a href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li>Bump com.google.guava:guava from 31.1-jre to 32.0.0-jre in /src/it/projects/MJAVADOC-769 (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/229">#229</a>) <a href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-766">[MJAVADOC-766]</a> - Bump org.codehaus.plexus:plexus-archiver from 4.7.1 to 4.8.0 (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/222">#222</a>) <a href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li>Bump hamcrest-core from 1.3 to 2.2 (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/221">#221</a>) <a href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li>Bump guava from 27.0.1-jre to 32.0.0-jre in /src/it/projects/MJAVADOC-555_link-automatic-modules (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/207">#207</a>) <a href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li>Bump javax.servlet-api from 3.1.0 to 4.0.1 (test dependency) (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/220">#220</a>) <a href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-761">[MJAVADOC-761]</a> - commons-io 2.13.0 (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/215">#215</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li>Bump plexus-utils from 3.5.0 to 3.5.1 (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/201">#201</a>) <a href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li>Bump mockito-core from 4.4.0 to 4.11.0 (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/196">#196</a>) <a href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
</ul>
<h2>👻 Maintenance</h2>
<ul>
<li>fix jenkins link (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/237">#237</a>) <a href="https://github.com/hgschmie"><code>@​hgschmie</code></a></li>
<li>Fix build on jenkins (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/236">#236</a>) <a href="https://github.com/slawekjaranowski"><code>@​slawekjaranowski</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-772">[MJAVADOC-772]</a> - Refresh download page (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/235">#235</a>) <a href="https://github.com/slawekjaranowski"><code>@​slawekjaranowski</code></a></li>
<li>Use 3.6.0 as release version (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/233">#233</a>) <a href="https://github.com/hgschmie"><code>@​hgschmie</code></a></li>
<li>Remove superflous whitespace from PR template (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/231">#231</a>) <a href="https://github.com/hgschmie"><code>@​hgschmie</code></a></li>
<li>Bump jetty from 9.4.50.v20221201 to 9.4.51.v20230217 (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/224">#224</a>) <a href="https://github.com/slachiewicz"><code>@​slachiewicz</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-765">[MJAVADOC-765]</a> - Fix build with Java 20 (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/223">#223</a>) <a href="https://github.com/slachiewicz"><code>@​slachiewicz</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-760">[MJAVADOC-760]</a> - declare test dependencies (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/217">#217</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-760">[MJAVADOC-760]</a> - explicitly declare shared utility dependencies (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/216">#216</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li>remove unused private method (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/213">#213</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li>replace plexus with maven shared utils (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/211">#211</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-755">[MJAVADOC-755]</a> - replace IOUtil.close with try with resources (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/209">#209</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MJAVADOC-755">[MJAVADOC-755]</a> - Use java.lang.String instead of StringUtils (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/208">#208</a>) <a href="https://github.com/elharo"><code>@​elharo</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MNG-6829">[MNG-6829]</a> - Replace StringUtils#isEmpty(String) and #isNotEmpty(String) (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/205">#205</a>) <a href="https://github.com/timtebeek"><code>@​timtebeek</code></a></li>
<li>Bump mockito-core from 4.4.0 to 4.11.0 (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/196">#196</a>) <a href="https://github.com/dependabot"><code>@​dependabot</code></a></li>
<li><a href="https://issues.apache.org/jira/browse/MNG-6825">[MNG-6825]</a> - Replace Maven shared StringUtils with Commons Lang3 (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/pull/199">#199</a>) <a href="https://github.com/timtebeek"><code>@​timtebeek</code></a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/apache/maven-javadoc-plugin/commit/7548066d25e6c01055400cca9acf26567c7a74e6"><code>7548066</code></a> [maven-release-plugin] prepare release maven-javadoc-plugin-3.6.0</li>
<li><a href="https://github.com/apache/maven-javadoc-plugin/commit/77adc4747e29bbb1873ae812347a77c0e7648565"><code>77adc47</code></a> [MJAVADOC-642] Make offline mode configurable (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/issues/238">#238</a>)</li>
<li><a href="https://github.com/apache/maven-javadoc-plugin/commit/24362d288bc70c658cdcced4cc0b60b648180dc7"><code>24362d2</code></a> [MJAVADOC-742] Fix resolution of docletArtifacts (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/issues/186">#186</a>)</li>
<li><a href="https://github.com/apache/maven-javadoc-plugin/commit/bee41974b1768e779d9c21c6426776f6a4207b20"><code>bee4197</code></a> fix jenkins link (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/issues/237">#237</a>)</li>
<li><a href="https://github.com/apache/maven-javadoc-plugin/commit/9830bdcb5309db68f86401d7f14c62041ea4fae7"><code>9830bdc</code></a> Fix build on jenkins</li>
<li><a href="https://github.com/apache/maven-javadoc-plugin/commit/6f30bed49a05dc548b4a6c1cffde92204aca14c6"><code>6f30bed</code></a> [MJAVADOC-642] Make offline mode configurable (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/issues/232">#232</a>)</li>
<li><a href="https://github.com/apache/maven-javadoc-plugin/commit/e4023d09cbbd99e27130acefbaa7bd9d0ba6c54f"><code>e4023d0</code></a> [JAVADOC-771] Upgrade Parent to 40 (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/issues/234">#234</a>)</li>
<li><a href="https://github.com/apache/maven-javadoc-plugin/commit/7904e45c82f3385771953896b917e2b589d9c19d"><code>7904e45</code></a> [MJAVADOC-772] Refresh download page</li>
<li><a href="https://github.com/apache/maven-javadoc-plugin/commit/87c24242899b5a1bf01f591533fdbba25dbd238a"><code>87c2424</code></a> Bump org.apache.maven:maven-core (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/issues/226">#226</a>)</li>
<li><a href="https://github.com/apache/maven-javadoc-plugin/commit/83ab01beea0a9cadafa4b2b05b3da35bd0d7dab5"><code>83ab01b</code></a> Use 3.6.0 as release version (<a href="https://redirect.github.com/apache/maven-javadoc-plugin/issues/233">#233</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.5.0...maven-javadoc-plugin-3.6.0">compare view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>


Patch:
diff --git a/CHANGES b/CHANGES
index e82d02d2..b3ad3bdd 100644
--- a/CHANGES
+++ b/CHANGES
@@ -6,13 +6,17 @@ Release 1.16.2 [PENDING]
     matching process by ensuring that simpler evaluations (such as a tag name match) are conducted prior to more
     complex evaluations (such as an attribute regex, or a deep child scan with a :has).
 
+  * Improvement: added support for <svg> and <math> tags (and their children). This includes tag namespaces and case
+    preservation on applicable tags and attributes.
+    <https://github.com/jhy/jsoup/pull/2008>
+
   * Improvement: when converting jsoup Documents to W3C Documents in W3CDom, HTML documents will be placed in the
     `http://www.w3.org/1999/xhtml` namespace by default, per the HTML5 spec. This can be controlled by setting
     `W3CDom#namespaceAware(false)`.
     <https://github.com/jhy/jsoup/pull/1848>
 
   * Improvement: speed optimized the Structural Evaluators by memoizing previous evaluations. Particularly the `~`
-    (any preceeding sibling) and `:nth-of-type` selectors are improved.
+    (any preceding sibling) and `:nth-of-type` selectors are improved.
     <https://github.com/jhy/jsoup/issues/1956>
 
   * Improvement: tweaked the performance of the Element nextElementSibling, previousElementSibling, firstElementSibling,
@@ -50,6 +54,10 @@ Release 1.16.2 [PENDING]
   * Bugfix: in Jsoup.connect(), a DELETE method request did not support a request body.
     <https://github.com/jhy/jsoup/issues/1972>
 
+  * Bugfix: when calling Element.cssSelector() on an extremely deeply nested element, a StackOverflowError could occur.
+    Further, a StackOverflowError may occur when running the query.
+    <https://github.com/jhy/jsoup/issues/2001>
+
   * Change: removed previously deprecated methods Document#normalise, Element#forEach(org.jsoup.helper.Consumer<>),
     Node#forEach(org.jsoup.helper.Consumer<>), and the org.jsoup.helper.Consumer interface; the latter being a
     previously required compatibility shim prior to Android's de-sugaring support.
diff --git a/pom.xml b/pom.xml
index e58fa789..73a2eec0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -92,7 +92,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-javadoc-plugin</artifactId>
-        <version>3.5.0</version>
+        <version>3.6.0</version>
         <configuration>
             <doclint>none</doclint>
             <source>8</source>
diff --git a/src/main/java/org/jsoup/UncheckedIOException.java b/src/main/java/org/jsoup/UncheckedIOException.java
index 65d878f2..dd6a76b5 100644
--- a/src/main/java/org/jsoup/UncheckedIOException.java
+++ b/src/main/java/org/jsoup/UncheckedIOException.java
@@ -17,6 +17,6 @@ public class UncheckedIOException extends java.io.UncheckedIOException {
     }
 
     public IOException ioException() {
-        return (IOException) getCause();
+        return getCause();
     }
 }
diff --git a/src/main/java/org/jsoup/helper/W3CDom.java b/src/main/java/org/jsoup/helper/W3CDom.java
index 231410b0..3070d102 100644
--- a/src/main/java/org/jsoup/helper/W3CDom.java
+++ b/src/main/java/org/jsoup/helper/W3CDom.java
@@ -4,6 +4,7 @@ import org.jsoup.internal.StringUtil;
 import org.jsoup.nodes.Attribute;
 import org.jsoup.nodes.Attributes;
 import org.jsoup.parser.HtmlTreeBuilder;
+import org.jsoup.parser.Parser;
 import org.jsoup.select.NodeTraversor;
 import org.jsoup.select.NodeVisitor;
 import org.jsoup.select.Selector;
@@ -339,9 +340,9 @@ public class W3CDom {
      * Implements the conversion by walking the input.
      */
     protected static class W3CBuilder implements NodeVisitor {
+        // TODO: move the namespace handling stuff into XmlTreeBuilder / HtmlTreeBuilder, now that Tags have namespaces
         private static final String xmlnsKey = "xmlns";
         private static final String xmlnsPrefix = "xmlns:";
-        private static final String xhtmlNs = "http://www.w3.org/1999/xhtml";
 
         private final Document doc;
         private boolean namespaceAware = true;
@@ -358,7 +359,7 @@ public class W3CDom {
             final org.jsoup.nodes.Document inDoc = contextElement.ownerDocument();
             if (namespaceAware && inDoc != null && inDoc.parser().getTreeBuilder() instanceof HtmlTreeBuilder) {
               // as per the WHATWG HTML5 spec § 2.1.3, elements are in the HTML namespace by default
-              namespacesStack.peek().put("", xhtmlNs);
+              namespacesStack.peek().put("", Parser.NamespaceHtml);
             }
           }
 
diff --git a/src/main/java/org/jsoup/internal/FieldsAreNonnullByDefault.java b/src/main/java/org/jsoup/internal/FieldsAreNonnullByDefault.java
index 1aeac517..56d4b869 100644
--- a/src/main/java/org/jsoup/internal/FieldsAreNonnullByDefault.java
+++ b/src/main/java/org/jsoup/internal/FieldsAreNonnullByDefault.java
@@ -7,14 +7,13 @@ import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
-@Documented
-@Nonnull
-@TypeQualifierDefault(ElementType.FIELD)
-@Retention(value = RetentionPolicy.CLASS)
-
 /**
  Indicates that fields types are not nullable, unless otherwise specified by @Nullable.
  @see javax.annotation.ParametersAreNonnullByDefault
  */
+@Documented
+@Nonnull
+@TypeQualifierDefault(ElementType.FIELD)
+@Retention(value = RetentionPolicy.CLASS)
 public @interface FieldsAreNonnullByDefault {
 }
diff --git a/src/main/java/org/jsoup/internal/NonnullByDefault.java b/src/main/java/org/jsoup/internal/NonnullByDefault.java
index 7f6ed84d..869f9fea 100644
--- a/src/main/java/org/jsoup/internal/NonnullByDefault.java
+++ b/src/main/java/org/jsoup/internal/NonnullByDefault.java
@@ -7,14 +7,13 @@ 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.CLASS)
-
 /**
  Indicates that all components (methods, returns, fields) are not nullable, unless otherwise specified by @Nullable.
  @see javax.annotation.ParametersAreNonnullByDefault
  */
+@Documented
+@Nonnull
+@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
+@Retention(value = RetentionPolicy.CLASS)
 public @interface NonnullByDefault {
 }
diff --git a/src/main/java/org/jsoup/internal/ReturnsAreNonnullByDefault.java b/src/main/java/org/jsoup/internal/ReturnsAreNonnullByDefault.java
index 3f004a68..38461351 100644
--- a/src/main/java/org/jsoup/internal/ReturnsAreNonnullByDefault.java
+++ b/src/main/java/org/jsoup/internal/ReturnsAreNonnullByDefault.java
@@ -7,14 +7,13 @@ 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
  */
+@Documented
+@Nonnull
+@TypeQualifierDefault(ElementType.METHOD)
+@Retention(value = RetentionPolicy.RUNTIME)
 public @interface ReturnsAreNonnullByDefault {
 }
diff --git a/src/main/java/org/jsoup/nodes/Attributes.java b/src/main/java/org/jsoup/nodes/Attributes.java
index 76b6590e..f2469526 100644
--- a/src/main/java/org/jsoup/nodes/Attributes.java
+++ b/src/main/java/org/jsoup/nodes/Attributes.java
@@ -12,6 +12,7 @@ import java.util.AbstractSet;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.ConcurrentModificationException;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -320,10 +321,12 @@ public class Attributes implements Iterable<Attribute>, Cloneable {
 
     public Iterator<Attribute> iterator() {
         return new Iterator<Attribute>() {
+            int expectedSize = size;
             int i = 0;
 
             @Override
             public boolean hasNext() {
+                checkModified();
                 while (i < size) {
                     if (isInternalKey(keys[i])) // skip over internal keys
                         i++;
@@ -336,14 +339,20 @@ public class Attributes implements Iterable<Attribute>, Cloneable {
 
             @Override
             public Attribute next() {
+                checkModified();
                 final Attribute attr = new Attribute(keys[i], (String) vals[i], Attributes.this);
                 i++;
                 return attr;
             }
 
+            private void checkModified() {
+                if (size != expectedSize) throw new ConcurrentModificationException("Use Iterator#remove() instead to remove attributes while iterating.");
+            }
+
             @Override
             public void remove() {
                 Attributes.this.remove(--i); // next() advanced, so rewind
+                expectedSize--;
             }
         };
     }
diff --git a/src/main/java/org/jsoup/nodes/Document.java b/src/main/java/org/jsoup/nodes/Document.java
index 9930dc5e..b2c3f7c8 100644
--- a/src/main/java/org/jsoup/nodes/Document.java
+++ b/src/main/java/org/jsoup/nodes/Document.java
@@ -15,7 +15,6 @@ import org.jsoup.select.Selector;
 import javax.annotation.Nullable;
 import java.nio.charset.Charset;
 import java.nio.charset.CharsetEncoder;
-import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -31,17 +30,28 @@ public class Document extends Element {
     private boolean updateMetaCharset = false;
 
     /**
-     Create a new, empty Document.
+     Create a new, empty Document, in the specified namespace.
+     @param namespace the namespace of this Document's root node.
      @param baseUri base URI of document
      @see org.jsoup.Jsoup#parse
      @see #createShell
      */
-    public Document(String baseUri) {
-        super(Tag.valueOf("#root", ParseSettings.htmlDefault), baseUri);
+    public Document(String namespace, String baseUri) {
+        super(Tag.valueOf("#root", namespace, ParseSettings.htmlDefault), baseUri);
         this.location = baseUri;
         this.parser = Parser.htmlParser(); // default, but overridable
     }
 
+    /**
+     Create a new, empty Document, in the HTML namespace.
+     @param baseUri base URI of document
+     @see org.jsoup.Jsoup#parse
+     @see #Document(String namespace, String baseUri)
+     */
+    public Document(String baseUri) {
+        this(Parser.NamespaceHtml, baseUri);
+    }
+
     /**
      Create a valid, empty shell of a document, suitable for adding more elements to.
      @param baseUri baseUri of document
@@ -208,7 +218,7 @@ public class Document extends Element {
      @return new element
      */
     public Element createElement(String tagName) {
-        return new Element(Tag.valueOf(tagName, ParseSettings.preserveCase), this.baseUri());
+        return new Element(Tag.valueOf(tagName, parser.defaultNamespace(), ParseSettings.preserveCase), this.baseUri());
     }
 
     @Override
@@ -312,7 +322,7 @@ public class Document extends Element {
 
     @Override
     public Document shallowClone() {
-        Document clone = new Document(baseUri());
+        Document clone = new Document(this.tag().namespace(), baseUri());
         if (attributes != null)
             clone.attributes = attributes.clone();
         clone.outputSettings = this.outputSettings.clone();
diff --git a/src/main/java/org/jsoup/nodes/Element.java b/src/main/java/org/jsoup/nodes/Element.java
index 3d5f8c15..2205bae1 100644
--- a/src/main/java/org/jsoup/nodes/Element.java
+++ b/src/main/java/org/jsoup/nodes/Element.java
@@ -5,6 +5,7 @@ import org.jsoup.helper.Validate;
 import org.jsoup.internal.NonnullByDefault;
 import org.jsoup.internal.StringUtil;
 import org.jsoup.parser.ParseSettings;
+import org.jsoup.parser.Parser;
 import org.jsoup.parser.Tag;
 import org.jsoup.select.Collector;
 import org.jsoup.select.Elements;
@@ -51,11 +52,21 @@ public class Element extends Node {
     @Nullable Attributes attributes; // field is nullable but all methods for attributes are non-null
 
     /**
-     * Create a new, standalone element.
+     * Create a new, standalone element, in the specified namespace.
      * @param tag tag name
+     * @param namespace namespace for this element
+     */
+    public Element(String tag, String namespace) {
+        this(Tag.valueOf(tag, namespace, ParseSettings.preserveCase), null);
+    }
+
+    /**
+     * Create a new, standalone element, in the HTML namespace.
+     * @param tag tag name
+     * @see #Element(String tag, String namespace)
      */
     public Element(String tag) {
-        this(Tag.valueOf(tag), "", null);
+        this(Tag.valueOf(tag, Parser.NamespaceHtml, ParseSettings.preserveCase), "", null);
     }
 
     /**
@@ -172,8 +183,22 @@ public class Element extends Node {
      * @see Elements#tagName(String)
      */
     public Element tagName(String tagName) {
+        return tagName(tagName, tag.namespace());
+    }
+
+    /**
+     * Change (rename) the tag of this element. For example, convert a {@code <span>} to a {@code <div>} with
+     * {@code el.tagName("div");}.
+     *
+     * @param tagName new tag name for this element
+     * @param namespace the new namespace for this element
+     * @return this element, for chaining
+     * @see Elements#tagName(String)
+     */
+    public Element tagName(String tagName, String namespace) {
         Validate.notEmptyParam(tagName, "tagName");
-        tag = Tag.valueOf(tagName, NodeUtils.parser(this).settings()); // maintains the case option of the original parse
+        Validate.notEmptyParam(namespace, "namespace");
+        tag = Tag.valueOf(tagName, namespace, NodeUtils.parser(this).settings()); // maintains the case option of the original parse
         return this;
     }
 
@@ -679,7 +704,11 @@ public class Element extends Node {
      *  {@code parent.appendElement("h1").attr("id", "header").text("Welcome");}
      */
     public Element appendElement(String tagName) {
-        Element child = new Element(Tag.valueOf(tagName, NodeUtils.parser(this).settings()), baseUri());
+        return appendElement(tagName, tag.namespace());
+    }
+
+    public Element appendElement(String tagName, String namespace) {
+        Element child = new Element(Tag.valueOf(tagName, namespace, NodeUtils.parser(this).settings()), baseUri());
         appendChild(child);
         return child;
     }
@@ -692,7 +721,11 @@ public class Element extends Node {
      *  {@code parent.prependElement("h1").attr("id", "header").text("Welcome");}
      */
     public Element prependElement(String tagName) {
-        Element child = new Element(Tag.valueOf(tagName, NodeUtils.parser(this).settings()), baseUri());
+        return prependElement(tagName, tag.namespace());
+    }
+
+    public Element prependElement(String tagName, String namespace) {
+        Element child = new Element(Tag.valueOf(tagName, namespace, NodeUtils.parser(this).settings()), baseUri());
         prependChild(child);
         return child;
     }
@@ -840,6 +873,16 @@ public class Element extends Node {
             }
         }
 
+        StringBuilder selector = StringUtil.borrowBuilder();
+        Element el = this;
+        while (el != null && !(el instanceof Document)) {
+            selector.insert(0, el.cssSelectorComponent());
+            el = el.parent();
+        }
+        return StringUtil.releaseBuilder(selector);
+    }
+
+    private String cssSelectorComponent() {
         // Escape tagname, and translate HTML namespace ns:tag to CSS namespace syntax ns|tag
         String tagName = escapeCssIdentifier(tagName()).replace("\\:", "|");
         StringBuilder selector = StringUtil.borrowBuilder().append(tagName);
@@ -859,7 +902,7 @@ public class Element extends Node {
             selector.append(String.format(
                 ":nth-child(%d)", elementSiblingIndex() + 1));
 
-        return parent().cssSelector() + StringUtil.releaseBuilder(selector);
+        return StringUtil.releaseBuilder(selector);
     }
 
     /**
@@ -1296,33 +1339,40 @@ public class Element extends Node {
      */
     public String text() {
         final StringBuilder accum = StringUtil.borrowBuilder();
-        NodeTraversor.traverse(new NodeVisitor() {
-            public void head(Node node, int depth) {
-                if (node instanceof TextNode) {
-                    TextNode textNode = (TextNode) node;
-                    appendNormalisedText(accum, textNode);
-                } else if (node instanceof Element) {
-                    Element element = (Element) node;
-                    if (accum.length() > 0 &&
-                        (element.isBlock() || element.isNode("br")) &&
-                        !lastCharIsWhitespace(accum))
-                        accum.append(' ');
-                }
-            }
+        NodeTraversor.traverse(new TextAccumulator(accum), this);
+        return StringUtil.releaseBuilder(accum).trim();
+    }
 
-            public void tail(Node node, int depth) {
-                // make sure there is a space between block tags and immediately following text nodes or inline elements <div>One</div>Two should be "One Two".
-                if (node instanceof Element) {
-                    Element element = (Element) node;
-                    Node next = node.nextSibling();
-                    if (element.isBlock() && (next instanceof TextNode || next instanceof Element && !((Element) next).tag.formatAsBlock()) && !lastCharIsWhitespace(accum))
-                        accum.append(' ');
-                }
+    private static class TextAccumulator implements NodeVisitor {
+        private final StringBuilder accum;
+
+        public TextAccumulator(StringBuilder accum) {
+            this.accum = accum;
+        }
 
+        public void head(Node node, int depth) {
+            if (node instanceof TextNode) {
+                TextNode textNode = (TextNode) node;
+                appendNormalisedText(accum, textNode);
+            } else if (node instanceof Element) {
+                Element element = (Element) node;
+                if (accum.length() > 0 &&
+                    (element.isBlock() || element.isNode("br")) &&
+                    !lastCharIsWhitespace(accum))
+                    accum.append(' ');
             }
-        }, this);
+        }
 
-        return StringUtil.releaseBuilder(accum).trim();
+        public void tail(Node node, int depth) {
+            // make sure there is a space between block tags and immediately following text nodes or inline elements <div>One</div>Two should be "One Two".
+            if (node instanceof Element) {
+                Element element = (Element) node;
+                Node next = node.nextSibling();
+                if (element.isBlock() && (next instanceof TextNode || next instanceof Element && !((Element) next).tag.formatAsBlock()) && !lastCharIsWhitespace(accum))
+                    accum.append(' ');
+            }
+
+        }
     }
 
     /**
diff --git a/src/main/java/org/jsoup/nodes/NodeUtils.java b/src/main/java/org/jsoup/nodes/NodeUtils.java
index e45f5c53..4bd7e019 100644
--- a/src/main/java/org/jsoup/nodes/NodeUtils.java
+++ b/src/main/java/org/jsoup/nodes/NodeUtils.java
@@ -4,14 +4,13 @@ import org.jsoup.helper.Validate;
 import org.jsoup.helper.W3CDom;
 import org.jsoup.parser.HtmlTreeBuilder;
 import org.jsoup.parser.Parser;
-import org.jsoup.select.Elements;
 import org.w3c.dom.NodeList;
 
 import java.util.List;
 
 /**
  * Internal helpers for Nodes, to keep the actual node APIs relatively clean. A jsoup internal class, so don't use it as
- * there is no contract API).
+ * there is no contract API.
  */
 final class NodeUtils {
     /**
diff --git a/src/main/java/org/jsoup/parser/HtmlTreeBuilder.java b/src/main/java/org/jsoup/parser/HtmlTreeBuilder.java
index 2e49e605..fc33a1ac 100644
--- a/src/main/java/org/jsoup/parser/HtmlTreeBuilder.java
+++ b/src/main/java/org/jsoup/parser/HtmlTreeBuilder.java
@@ -1,6 +1,7 @@
 package org.jsoup.parser;
 
 import org.jsoup.helper.Validate;
+import org.jsoup.internal.Normalizer;
 import org.jsoup.internal.StringUtil;
 import org.jsoup.nodes.CDataNode;
 import org.jsoup.nodes.Comment;
@@ -21,6 +22,7 @@ import java.util.List;
 
 import static org.jsoup.internal.StringUtil.inSorted;
 import static org.jsoup.parser.HtmlTreeBuilderState.Constants.InTableFoster;
+import static org.jsoup.parser.HtmlTreeBuilderState.ForeignContent;
 
 /**
  * HTML Tree Builder; creates a DOM from Tokens.
@@ -42,6 +44,8 @@ public class HtmlTreeBuilder extends TreeBuilder {
         "noembed", "noframes", "noscript", "object", "ol", "p", "param", "plaintext", "pre", "script",
         "section", "select", "style", "summary", "table", "tbody", "td", "textarea", "tfoot", "th", "thead",
         "title", "tr", "ul", "wbr", "xmp"};
+    static final String[] TagMathMlTextIntegration = new String[]{"mi", "mn", "mo", "ms", "mtext"};
+    static final String[] TagSvgHtmlIntegration = new String[]{"desc", "foreignObject", "title"};
 
     public static final int MaxScopeSearchDepth = 100; // prevents the parser bogging down in exceptionally broken pages
 
@@ -165,7 +169,86 @@ public class HtmlTreeBuilder extends TreeBuilder {
     @Override
     protected boolean process(Token token) {
         currentToken = token;
-        return this.state.process(token, this);
+
+    if (shouldDispatchToCurrentInsertionMode(token)) {
+            return this.state.process(token, this);
+        } else {
+            return ForeignContent.process(token, this);
+        }
+    }
+
+    boolean shouldDispatchToCurrentInsertionMode(Token token) {
+        // https://html.spec.whatwg.org/multipage/parsing.html#tree-construction
+        // If the stack of open elements is empty
+        if (stack.isEmpty())
+            return true;
+        final Element el = currentElement();
+        final String ns = el.tag().namespace();
+
+        // If the adjusted current node is an element in the HTML namespace
+        if (Parser.NamespaceHtml.equals(ns))
+            return true;
+
+        // If the adjusted current node is a MathML text integration point and the token is a start tag whose tag name is neither "mglyph" nor "malignmark"
+        // If the adjusted current node is a MathML text integration point and the token is a character token
+        if (isMathmlTextIntegration(el)) {
+            if (token.isStartTag()
+                    && !"mglyph".equals(token.asStartTag().normalName)
+                    && !"malignmark".equals(token.asStartTag().normalName))
+                    return true;
+            if (token.isCharacter())
+                    return true;
+        }
+        // If the adjusted current node is a MathML annotation-xml element and the token is a start tag whose tag name is "svg"
+        if (Parser.NamespaceMathml.equals(ns)
+            && el.normalName().equals("annotation-xml")
+            && token.isStartTag()
+            && "svg".equals(token.asStartTag().normalName))
+            return true;
+
+        // If the adjusted current node is an HTML integration point and the token is a start tag
+        // If the adjusted current node is an HTML integration point and the token is a character token
+        if (isHtmlIntegration(el)
+            && (token.isStartTag() || token.isCharacter()))
+            return true;
+
+        // If the token is an end-of-file token
+        return token.isEOF();
+    }
+
+    boolean isMathmlTextIntegration(Element el) {
+        /*
+        A node is a MathML text integration point if it is one of the following elements:
+        A MathML mi element
+        A MathML mo element
+        A MathML mn element
+        A MathML ms element
+        A MathML mtext element
+         */
+        return (Parser.NamespaceMathml.equals(el.tag().namespace())
+            && StringUtil.inSorted(el.normalName(), TagMathMlTextIntegration));
+    }
+
+    boolean isHtmlIntegration(Element el) {
+        /*
+        A node is an HTML integration point if it is one of the following elements:
+        A MathML annotation-xml element whose start tag token had an attribute with the name "encoding" whose value was an ASCII case-insensitive match for the string "text/html"
+        A MathML annotation-xml element whose start tag token had an attribute with the name "encoding" whose value was an ASCII case-insensitive match for the string "application/xhtml+xml"
+        An SVG foreignObject element
+        An SVG desc element
+        An SVG title element
+         */
+        if (Parser.NamespaceMathml.equals(el.tag().namespace())
+            && el.normalName().equals("annotation-xml")) {
+            String encoding = Normalizer.normalize(el.attr("encoding"));
+            if (encoding.equals("text/html") || encoding.equals("application/xhtml+xml"))
+                return true;
+        }
+        if (Parser.NamespaceSvg.equals(el.tag().namespace())
+            && StringUtil.in(el.tagName(), TagSvgHtmlIntegration)) // note using .tagName for case-sensitive hit here of foreignObject
+            return true;
+
+        return false;
     }
 
     boolean process(Token token, HtmlTreeBuilderState state) {
@@ -245,6 +328,23 @@ public class HtmlTreeBuilder extends TreeBuilder {
         return el;
     }
 
+    /**
+     Inserts a foreign element. Preserves the case of the tag name and of the attributes.
+     */
+    Element insertForeign(final Token.StartTag startTag, String namespace) {
+        dedupeAttributes(startTag);
+        Tag tag = tagFor(startTag.name(), namespace, ParseSettings.preserveCase);
+        Element el = new Element(tag, null, ParseSettings.preserveCase.normalizeAttributes(startTag.attributes));
+        insert(el, startTag);
+
+        if (startTag.isSelfClosing()) {
+            tag.setSelfClosing(); // remember this is self-closing for output
+            pop();
+        }
+
+        return el;
+    }
+
 	Element insertStartTag(String startTagName) {
         Element el = new Element(tagFor(startTagName, settings), null);
         insert(el);
@@ -272,7 +372,7 @@ public class HtmlTreeBuilder extends TreeBuilder {
                 if (!tag.isEmpty())
                     tokeniser.error("Tag [%s] cannot be self closing; not a void tag", tag.normalName());
             }
-            else // unknown tag, remember this is self closing for output
+            else // unknown tag, remember this is self-closing for output
                 tag.setSelfClosing();
         }
         return el;
@@ -306,6 +406,7 @@ public class HtmlTreeBuilder extends TreeBuilder {
         insert(characterToken, el);
     }
 
+    /** Inserts the provided character token into the provided element. */
     void insert(Token.Character characterToken, Element el) {
         final Node node;
         final String tagName = el.normalName();
@@ -321,7 +422,7 @@ public class HtmlTreeBuilder extends TreeBuilder {
         onNodeInserted(node, characterToken);
     }
 
-    /** Inserts the provided character token into the provided element. Use when not going onto stack element */
+    /** Inserts the provided Node into the current element. */
     private void insertNode(Node node, @Nullable Token token) {
         // if the stack hasn't been set up yet, elements (doctype, comments) go into the doc
         if (stack.isEmpty())
@@ -331,10 +432,14 @@ public class HtmlTreeBuilder extends TreeBuilder {
         else
             currentElement().appendChild(node);
 
-        // connect form controls to their form element
-        if (node instanceof Element && ((Element) node).tag().isFormListed()) {
-            if (formElement != null)
-                formElement.addElement((Element) node);
+        if (node instanceof Element) {
+            Element el = (Element) node;
+            if (el.tag().isFormListed() && formElement != null)
+                formElement.addElement(el); // connect form controls to their form element
+
+            // in HTML, the xmlns attribute if set must match what the parser set the tag's namespace to
+            if (el.hasAttr("xmlns") && !el.attr("xmlns").equals(el.tag().namespace()))
+                error("Invalid xmlns attribute [%s] on tag [%s]", el.attr("xmlns"), el.tagName());
         }
         onNodeInserted(node, token);
     }
diff --git a/src/main/java/org/jsoup/parser/HtmlTreeBuilderState.java b/src/main/java/org/jsoup/parser/HtmlTreeBuilderState.java
index 3c5352ec..363c9d96 100644
--- a/src/main/java/org/jsoup/parser/HtmlTreeBuilderState.java
+++ b/src/main/java/org/jsoup/parser/HtmlTreeBuilderState.java
@@ -1,5 +1,6 @@
 package org.jsoup.parser;
 
+import org.jsoup.helper.Validate;
 import org.jsoup.internal.StringUtil;
 import org.jsoup.nodes.Attribute;
 import org.jsoup.nodes.Attributes;
@@ -556,13 +557,11 @@ enum HtmlTreeBuilderState {
                     break;
                 case "math":
                     tb.reconstructFormattingElements();
-                    // todo: handle A start tag whose tag name is "math" (i.e. foreign, mathml)
-                    tb.insert(startTag);
+                    tb.insertForeign(startTag, Parser.NamespaceMathml);
                     break;
                 case "svg":
                     tb.reconstructFormattingElements();
-                    // todo: handle A start tag whose tag name is "svg" (xlink, svg)
-                    tb.insert(startTag);
+                    tb.insertForeign(startTag, Parser.NamespaceSvg);
                     break;
                 // static final String[] Headings = new String[]{"h1", "h2", "h3", "h4", "h5", "h6"};
                 case "h1":
@@ -1740,9 +1739,83 @@ enum HtmlTreeBuilderState {
         }
     },
     ForeignContent {
+        // https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inforeign
         boolean process(Token t, HtmlTreeBuilder tb) {
+            switch (t.type) {
+                case Character:
+                    Token.Character c = t.asCharacter();
+                    if (c.getData().equals(nullString))
+                        tb.error(this);
+                    else if (HtmlTreeBuilderState.isWhitespace(c))
+                        tb.insert(c);
+                    else {
+                        tb.insert(c);
+                        tb.framesetOk(false);
+                    }
+                    break;
+                case Comment:
+                    tb.insert(t.asComment());
+                    break;
+                case Doctype:
+                    tb.error(this);
+                    break;
+                case StartTag:
+                    Token.StartTag start = t.asStartTag();
+                    if (StringUtil.in(start.normalName, InForeignToHtml))
+                        return processAsHtml(t, tb);
+                    if (start.normalName.equals("font") && (
+                        start.attributes.hasKeyIgnoreCase("color")
+                            || start.attributes.hasKeyIgnoreCase("face")
+                            || start.attributes.hasKeyIgnoreCase("size")))
+                        return processAsHtml(t, tb);
+
+                    // Any other start:
+                    // (whatwg says to fix up tag name and attribute case per a table - we will preserve original case instead)
+                    tb.insertForeign(start, tb.currentElement().tag().namespace());
+                    // (self-closing handled in insert)
+                    // if self-closing svg script -- level and execution elided
+                    break;
+
+                case EndTag:
+                    Token.EndTag end = t.asEndTag();
+                    if (end.normalName.equals("br") || end.normalName.equals("p"))
+                        return processAsHtml(t, tb);
+                    if (end.normalName.equals("script") && tb.currentElementIs("script", Parser.NamespaceSvg)) {
+                        // script level and execution elided.
+                        tb.pop();
+                        return true;
+                    }
+
+                    // Any other end tag
+                    ArrayList<Element> stack = tb.getStack();
+                    if (stack.isEmpty())
+                        Validate.wtf("Stack unexpectedly empty");
+                    int i = stack.size() - 1;
+                    Element el = stack.get(i);
+                    if (!el.normalName().equals(end.normalName))
+                        tb.error(this);
+                    while (i != 0) {
+                        if (el.normalName().equals(end.normalName)) {
+                            tb.popStackToClose(el.normalName());
+                            return true;
+                        }
+                        i--;
+                        el = stack.get(i);
+                        if (el.tag().namespace().equals(Parser.NamespaceHtml)) {
+                            return processAsHtml(t, tb);
+                        }
+                    }
+                    break;
+
+                case EOF:
+                    // won't come through here, but for completion:
+                    break;
+            }
             return true;
-            // todo: implement. Also; how do we get here?
+        }
+
+        boolean processAsHtml(Token t, HtmlTreeBuilder tb) {
+            return tb.state().process(t, tb);
         }
     };
 
@@ -1817,5 +1890,6 @@ enum HtmlTreeBuilderState {
         static final String[] InCaptionIgnore = new String[]{"body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr"};
         static final String[] InTemplateToHead = new String[] {"base", "basefont", "bgsound", "link", "meta", "noframes", "script", "style", "template", "title"};
         static final String[] InTemplateToTable = new String[] {"caption", "colgroup", "tbody", "tfoot", "thead"};
+        static final String[] InForeignToHtml = new String[] {"b", "big", "blockquote", "body", "br", "center", "code", "dd", "div", "dl", "dt", "em", "embed", "h1", "h2", "h3", "h4", "h5", "h6", "head", "hr", "i", "img", "li", "listing", "menu", "meta", "nobr", "ol", "p", "pre", "ruby", "s", "small", "span", "strike", "strong", "sub", "sup", "table", "tt", "u", "ul", "var"};
     }
 }
diff --git a/src/main/java/org/jsoup/parser/Parser.java b/src/main/java/org/jsoup/parser/Parser.java
index 83789460..123ddc3f 100644
--- a/src/main/java/org/jsoup/parser/Parser.java
+++ b/src/main/java/org/jsoup/parser/Parser.java
@@ -14,6 +14,11 @@ import java.util.List;
  <p>Note that a Parser instance object is not threadsafe. To reuse a Parser configuration in a multi-threaded
  environment, use {@link #newInstance()} to make copies. */
 public class Parser {
+    public static final String NamespaceHtml = "http://www.w3.org/1999/xhtml";
+    public static final String NamespaceXml = "http://www.w3.org/XML/1998/namespace";
+    public static final String NamespaceMathml = "http://www.w3.org/1998/Math/MathML";
+    public static final String NamespaceSvg = "http://www.w3.org/2000/svg";
+
     private TreeBuilder treeBuilder;
     private ParseErrorList errors;
     private ParseSettings settings;
@@ -148,6 +153,10 @@ public class Parser {
         return getTreeBuilder().isContentForTagData(normalName);
     }
 
+    public String defaultNamespace() {
+        return getTreeBuilder().defaultNamespace();
+    }
+
     // static parse functions below
     /**
      * Parse HTML into a Document.
diff --git a/src/main/java/org/jsoup/parser/Tag.java b/src/main/java/org/jsoup/parser/Tag.java
index 97ed5004..0e50c52c 100644
--- a/src/main/java/org/jsoup/parser/Tag.java
+++ b/src/main/java/org/jsoup/parser/Tag.java
@@ -7,15 +7,16 @@ import java.util.HashMap;
 import java.util.Map;
 
 /**
- * HTML Tag capabilities.
+ * Tag capabilities.
  *
  * @author Jonathan Hedley, jonathan@hedley.net
  */
 public class Tag implements Cloneable {
-    private static final Map<String, Tag> tags = new HashMap<>(); // map of known tags
+    private static final Map<String, Tag> Tags = new HashMap<>(); // map of known tags
 
     private String tagName;
-    private String normalName; // always the lower case version of this tag, regardless of case preservation mode
+    private final String normalName; // always the lower case version of this tag, regardless of case preservation mode
+    private String namespace;
     private boolean isBlock = true; // block
     private boolean formatAsBlock = true; // should be formatted as a block
     private boolean empty = false; // can hold nothing; e.g. img
@@ -24,9 +25,10 @@ public class Tag implements Cloneable {
     private boolean formList = false; // a control that appears in forms: input, textarea, output etc
     private boolean formSubmit = false; // a control that can be submitted in a form: input etc
 
-    private Tag(String tagName) {
+    private Tag(String tagName, String namespace) {
         this.tagName = tagName;
         normalName = Normalizer.lowerCase(tagName);
+        this.namespace = namespace;
     }
 
     /**
@@ -46,35 +48,44 @@ public class Tag implements Cloneable {
         return normalName;
     }
 
+    public String namespace() {
+        return namespace;
+    }
+
     /**
      * Get a Tag by name. If not previously defined (unknown), returns a new generic tag, that can do anything.
      * <p>
-     * Pre-defined tags (P, DIV etc) will be ==, but unknown tags are not registered and will only .equals().
+     * Pre-defined tags (p, div etc) will be ==, but unknown tags are not registered and will only .equals().
      * </p>
      * 
-     * @param tagName Name of tag, e.g. "p". Case insensitive.
+     * @param tagName Name of tag, e.g. "p". Case-insensitive.
+     * @param namespace the namespace for the tag.
      * @param settings used to control tag name sensitivity
      * @return The tag, either defined or new generic.
      */
-    public static Tag valueOf(String tagName, ParseSettings settings) {
-        Validate.notNull(tagName);
-        Tag tag = tags.get(tagName);
-
-        if (tag == null) {
-            tagName = settings.normalizeTag(tagName); // the name we'll use
-            Validate.notEmpty(tagName);
-            String normalName = Normalizer.lowerCase(tagName); // the lower-case name to get tag settings off
-            tag = tags.get(normalName);
-
-            if (tag == null) {
-                // not defined: create default; go anywhere, do anything! (incl be inside a <p>)
-                tag = new Tag(tagName);
-                tag.isBlock = false;
-            } else if (settings.preserveTagCase() && !tagName.equals(normalName))  {
+    public static Tag valueOf(String tagName, String namespace, ParseSettings settings) {
+        Validate.notEmpty(tagName);
+        Validate.notNull(namespace);
+        Tag tag = Tags.get(tagName);
+        if (tag != null && tag.namespace.equals(namespace))
+            return tag;
+
+        tagName = settings.normalizeTag(tagName); // the name we'll use
+        Validate.notEmpty(tagName);
+        String normalName = Normalizer.lowerCase(tagName); // the lower-case name to get tag settings off
+        tag = Tags.get(normalName);
+        if (tag != null && tag.namespace.equals(namespace)) {
+            if (settings.preserveTagCase() && !tagName.equals(normalName)) {
                 tag = tag.clone(); // get a new version vs the static one, so name update doesn't reset all
                 tag.tagName = tagName;
             }
+            return tag;
         }
+
+        // not defined: create default; go anywhere, do anything! (incl be inside a <p>)
+        tag = new Tag(tagName, namespace);
+        tag.isBlock = false;
+
         return tag;
     }
 
@@ -86,9 +97,25 @@ public class Tag implements Cloneable {
      *
      * @param tagName Name of tag, e.g. "p". <b>Case sensitive</b>.
      * @return The tag, either defined or new generic.
+     * @see #valueOf(String tagName, String namespace, ParseSettings settings)
      */
     public static Tag valueOf(String tagName) {
-        return valueOf(tagName, ParseSettings.preserveCase);
+        return valueOf(tagName, Parser.NamespaceHtml, ParseSettings.preserveCase);
+    }
+
+    /**
+     * Get a Tag by name. If not previously defined (unknown), returns a new generic tag, that can do anything.
+     * <p>
+     * Pre-defined tags (P, DIV etc) will be ==, but unknown tags are not registered and will only .equals().
+     * </p>
+     *
+     * @param tagName Name of tag, e.g. "p". <b>Case sensitive</b>.
+     * @param settings used to control tag name sensitivity
+     * @return The tag, either defined or new generic.
+     * @see #valueOf(String tagName, String namespace, ParseSettings settings)
+     */
+    public static Tag valueOf(String tagName, ParseSettings settings) {
+        return valueOf(tagName, Parser.NamespaceHtml, settings);
     }
 
     /**
@@ -128,9 +155,9 @@ public class Tag implements Cloneable {
     }
 
     /**
-     * Get if this tag is self closing.
+     * Get if this tag is self-closing.
      *
-     * @return if this tag should be output as self closing.
+     * @return if this tag should be output as self-closing.
      */
     public boolean isSelfClosing() {
         return empty || selfClosing;
@@ -142,7 +169,7 @@ public class Tag implements Cloneable {
      * @return if a known tag
      */
     public boolean isKnownTag() {
-        return tags.containsKey(tagName);
+        return Tags.containsKey(tagName);
     }
 
     /**
@@ -152,7 +179,7 @@ public class Tag implements Cloneable {
      * @return if known HTML tag
      */
     public static boolean isKnownTag(String tagName) {
-        return tags.containsKey(tagName);
+        return Tags.containsKey(tagName);
     }
 
     /**
@@ -247,7 +274,9 @@ public class Tag implements Cloneable {
             "option", "legend", "datalist", "keygen", "output", "progress", "meter", "area", "param", "source", "track",
             "summary", "command", "device", "area", "basefont", "bgsound", "menuitem", "param", "source", "track",
             "data", "bdi", "s", "strike", "nobr",
-            "rb" // deprecated but still known / special handling
+            "rb", // deprecated but still known / special handling
+            "text", // in SVG NS
+            "mi", "mo", "msup", "mn", "mtext" // in MathML NS, to ensure inline
     };
     private static final String[] emptyTags = {
             "meta", "link", "base", "frame", "img", "br", "wbr", "embed", "hr", "input", "keygen", "col", "command",
@@ -270,14 +299,21 @@ public class Tag implements Cloneable {
             "input", "keygen", "object", "select", "textarea"
     };
 
+    private static final Map<String, String[]> namespaces = new HashMap<>();
+    static {
+        namespaces.put(Parser.NamespaceMathml, new String[]{"math", "mi", "mo", "msup", "mn", "mtext"});
+        namespaces.put(Parser.NamespaceSvg, new String[]{"svg", "text"});
+        // We don't need absolute coverage here as other cases will be inferred by the HtmlTreeBuilder
+    }
+
     static {
         // creates
         for (String tagName : blockTags) {
-            Tag tag = new Tag(tagName);
+            Tag tag = new Tag(tagName, Parser.NamespaceHtml);
             register(tag);
         }
         for (String tagName : inlineTags) {
-            Tag tag = new Tag(tagName);
+            Tag tag = new Tag(tagName, Parser.NamespaceHtml);
             tag.isBlock = false;
             tag.formatAsBlock = false;
             register(tag);
@@ -285,37 +321,46 @@ public class Tag implements Cloneable {
 
         // mods:
         for (String tagName : emptyTags) {
-            Tag tag = tags.get(tagName);
+            Tag tag = Tags.get(tagName);
             Validate.notNull(tag);
             tag.empty = true;
         }
 
         for (String tagName : formatAsInlineTags) {
-            Tag tag = tags.get(tagName);
+            Tag tag = Tags.get(tagName);
             Validate.notNull(tag);
             tag.formatAsBlock = false;
         }
 
         for (String tagName : preserveWhitespaceTags) {
-            Tag tag = tags.get(tagName);
+            Tag tag = Tags.get(tagName);
             Validate.notNull(tag);
             tag.preserveWhitespace = true;
         }
 
         for (String tagName : formListedTags) {
-            Tag tag = tags.get(tagName);
+            Tag tag = Tags.get(tagName);
             Validate.notNull(tag);
             tag.formList = true;
         }
 
         for (String tagName : formSubmitTags) {
-            Tag tag = tags.get(tagName);
+            Tag tag = Tags.get(tagName);
             Validate.notNull(tag);
             tag.formSubmit = true;
         }
+
+        // namespace setup
+        for (Map.Entry<String, String[]> ns : namespaces.entrySet()) {
+            for (String tagName : ns.getValue()) {
+                Tag tag = Tags.get(tagName);
+                Validate.notNull(tag);
+                tag.namespace = ns.getKey();
+            }
+        }
     }
 
     private static void register(Tag tag) {
-        tags.put(tag.tagName, tag);
+        Tags.put(tag.tagName, tag);
     }
 }
diff --git a/src/main/java/org/jsoup/parser/Token.java b/src/main/java/org/jsoup/parser/Token.java
index b0fc0af8..e2ab6563 100644
--- a/src/main/java/org/jsoup/parser/Token.java
+++ b/src/main/java/org/jsoup/parser/Token.java
@@ -311,10 +311,11 @@ abstract class Token {
 
         @Override
         public String toString() {
+            String closer = isSelfClosing() ? "/>" : ">";
             if (hasAttributes() && attributes.size() > 0)
-                return "<" + toStringName() + " " + attributes.toString() + ">";
+                return "<" + toStringName() + " " + attributes.toString() + closer;
             else
-                return "<" + toStringName() + ">";
+                return "<" + toStringName() + closer;
         }
     }
 
diff --git a/src/main/java/org/jsoup/parser/TreeBuilder.java b/src/main/java/org/jsoup/parser/TreeBuilder.java
index 77083b2e..6c9a78b5 100644
--- a/src/main/java/org/jsoup/parser/TreeBuilder.java
+++ b/src/main/java/org/jsoup/parser/TreeBuilder.java
@@ -15,6 +15,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import static org.jsoup.parser.Parser.NamespaceHtml;
+
 /**
  * @author Jonathan Hedley
  */
@@ -41,7 +43,7 @@ abstract class TreeBuilder {
         Validate.notNullParam(baseUri, "baseUri");
         Validate.notNull(parser);
 
-        doc = new Document(baseUri);
+        doc = new Document(parser.defaultNamespace(), baseUri);
         doc.parser(parser);
         this.parser = parser;
         settings = parser.settings();
@@ -132,7 +134,7 @@ abstract class TreeBuilder {
     }
 
     /**
-     Checks if the Current Element's normal name equals the supplied name.
+     Checks if the Current Element's normal name equals the supplied name, in the HTML namespace.
      @param normalName name to check
      @return true if there is a current element on the stack, and its name equals the supplied
      */
@@ -140,7 +142,22 @@ abstract class TreeBuilder {
         if (stack.size() == 0)
             return false;
         Element current = currentElement();
-        return current != null && current.normalName().equals(normalName);
+        return current != null && current.normalName().equals(normalName)
+            && current.tag().namespace().equals(NamespaceHtml);
+    }
+
+    /**
+     Checks if the Current Element's normal name equals the supplied name, in the specified namespace.
+     @param normalName name to check
+     @param namespace the namespace
+     @return true if there is a current element on the stack, and its name equals the supplied
+     */
+    protected boolean currentElementIs(String normalName, String namespace) {
+        if (stack.size() == 0)
+            return false;
+        Element current = currentElement();
+        return current != null && current.normalName().equals(normalName)
+            && current.tag().namespace().equals(namespace);
     }
 
     /**
@@ -170,13 +187,27 @@ abstract class TreeBuilder {
         return false;
     }
 
-    protected Tag tagFor(String tagName, ParseSettings settings) {
-        Tag tag = seenTags.get(tagName); // note that we don't normalize the cache key. But tag via valueOf may be normalized.
-        if (tag == null) {
-            tag = Tag.valueOf(tagName, settings);
+    protected Tag tagFor(String tagName, String namespace, ParseSettings settings) {
+        Tag cached = seenTags.get(tagName); // note that we don't normalize the cache key. But tag via valueOf may be normalized.
+        if (cached == null || !cached.namespace().equals(namespace)) {
+            // only return from cache if the namespace is the same. not running nested cache to save double hit on the common flow
+            Tag tag = Tag.valueOf(tagName, namespace, settings);
             seenTags.put(tagName, tag);
+            return tag;
         }
-        return tag;
+        return cached;
+    }
+
+    protected Tag tagFor(String tagName, ParseSettings settings) {
+        return tagFor(tagName, defaultNamespace(), settings);
+    }
+
+    /**
+     Gets the default namespace for this TreeBuilder
+     * @return the default namespace
+     */
+    protected String defaultNamespace() {
+        return NamespaceHtml;
     }
 
     /**
diff --git a/src/main/java/org/jsoup/parser/XmlTreeBuilder.java b/src/main/java/org/jsoup/parser/XmlTreeBuilder.java
index 96607239..e1999f96 100644
--- a/src/main/java/org/jsoup/parser/XmlTreeBuilder.java
+++ b/src/main/java/org/jsoup/parser/XmlTreeBuilder.java
@@ -16,6 +16,8 @@ import java.io.Reader;
 import java.io.StringReader;
 import java.util.List;
 
+import static org.jsoup.parser.Parser.NamespaceXml;
+
 /**
  * Use the {@code XmlTreeBuilder} when you want to parse XML without any of the HTML DOM rules being applied to the
  * document.
@@ -51,6 +53,10 @@ public class XmlTreeBuilder extends TreeBuilder {
         return new XmlTreeBuilder();
     }
 
+    @Override public String defaultNamespace() {
+        return NamespaceXml;
+    }
+
     @Override
     protected boolean process(Token token) {
         // start tag, end tag, doctype, comment, character, eof
@@ -90,15 +96,13 @@ public class XmlTreeBuilder extends TreeBuilder {
 
     Element insert(Token.StartTag startTag) {
         Tag tag = tagFor(startTag.name(), settings);
-        // todo: wonder if for xml parsing, should treat all tags as unknown? because it's not html.
         if (startTag.hasAttributes())
             startTag.attributes.deduplicate(settings);
 
         Element el = new Element(tag, null, settings.normalizeAttributes(startTag.attributes));
         insertNode(el, startTag);
         if (startTag.isSelfClosing()) {
-            if (!tag.isKnownTag()) // unknown tag, remember this is self closing for output. see above.
-                tag.setSelfClosing();
+            tag.setSelfClosing();
         } else {
             stack.add(el);
         }
diff --git a/src/main/java/org/jsoup/safety/Cleaner.java b/src/main/java/org/jsoup/safety/Cleaner.java
index fb12b46e..7b9317ec 100644
--- a/src/main/java/org/jsoup/safety/Cleaner.java
+++ b/src/main/java/org/jsoup/safety/Cleaner.java
@@ -9,6 +9,7 @@ import org.jsoup.nodes.Element;
 import org.jsoup.nodes.Node;
 import org.jsoup.nodes.TextNode;
 import org.jsoup.parser.ParseErrorList;
+import org.jsoup.parser.ParseSettings;
 import org.jsoup.parser.Parser;
 import org.jsoup.parser.Tag;
 import org.jsoup.select.NodeTraversor;
@@ -179,7 +180,7 @@ public class Cleaner {
     private ElementMeta createSafeElement(Element sourceEl) {
         String sourceTag = sourceEl.tagName();
         Attributes destAttrs = new Attributes();
-        Element dest = new Element(Tag.valueOf(sourceTag), sourceEl.baseUri(), destAttrs);
+        Element dest = new Element(Tag.valueOf(sourceTag, sourceEl.tag().namespace(), ParseSettings.preserveCase), sourceEl.baseUri(), destAttrs);
         int numDiscarded = 0;
 
         Attributes sourceAttrs = sourceEl.attributes();
diff --git a/src/main/java/org/jsoup/select/Evaluator.java b/src/main/java/org/jsoup/select/Evaluator.java
index d8929fc3..65040bcf 100644
--- a/src/main/java/org/jsoup/select/Evaluator.java
+++ b/src/main/java/org/jsoup/select/Evaluator.java
@@ -1,7 +1,6 @@
 package org.jsoup.select;
 
 import org.jsoup.helper.Validate;
-import org.jsoup.internal.StringUtil;
 import org.jsoup.nodes.Comment;
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.DocumentType;
@@ -10,6 +9,7 @@ import org.jsoup.nodes.Node;
 import org.jsoup.nodes.PseudoTextElement;
 import org.jsoup.nodes.TextNode;
 import org.jsoup.nodes.XmlDeclaration;
+import org.jsoup.parser.ParseSettings;
 
 import java.util.List;
 import java.util.regex.Matcher;
@@ -969,7 +969,7 @@ public abstract class Evaluator {
             List<TextNode> textNodes = element.textNodes();
             for (TextNode textNode : textNodes) {
                 PseudoTextElement pel = new PseudoTextElement(
-                    org.jsoup.parser.Tag.valueOf(element.tagName()), element.baseUri(), element.attributes());
+                    org.jsoup.parser.Tag.valueOf(element.tagName(), element.tag().namespace(), ParseSettings.preserveCase), element.baseUri(), element.attributes());
                 textNode.replaceWith(pel);
                 pel.appendChild(textNode);
             }
diff --git a/src/main/java/org/jsoup/select/QueryParser.java b/src/main/java/org/jsoup/select/QueryParser.java
index 1e512c02..09f53bdd 100644
--- a/src/main/java/org/jsoup/select/QueryParser.java
+++ b/src/main/java/org/jsoup/select/QueryParser.java
@@ -9,6 +9,7 @@ import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import static org.jsoup.select.StructuralEvaluator.ImmediateParentRun;
 import static org.jsoup.internal.Normalizer.normalize;
 
 /**
@@ -107,7 +108,10 @@ public class QueryParser {
         // for most combinators: change the current eval into an AND of the current eval and the new eval
         switch (combinator) {
             case '>':
-                currentEval = new CombiningEvaluator.And(new StructuralEvaluator.ImmediateParent(currentEval), newEval);
+                ImmediateParentRun run = currentEval instanceof ImmediateParentRun ?
+                        (ImmediateParentRun) currentEval : new ImmediateParentRun(currentEval);
+                run.add(newEval);
+                currentEval = run;
                 break;
             case ' ':
                 currentEval = new CombiningEvaluator.And(new StructuralEvaluator.Parent(currentEval), newEval);
diff --git a/src/main/java/org/jsoup/select/StructuralEvaluator.java b/src/main/java/org/jsoup/select/StructuralEvaluator.java
index 08831802..96ff252e 100644
--- a/src/main/java/org/jsoup/select/StructuralEvaluator.java
+++ b/src/main/java/org/jsoup/select/StructuralEvaluator.java
@@ -1,8 +1,10 @@
 package org.jsoup.select;
 
+import org.jsoup.internal.StringUtil;
 import org.jsoup.nodes.Element;
 import org.jsoup.nodes.Node;
 
+import java.util.ArrayList;
 import java.util.IdentityHashMap;
 
 /**
@@ -139,6 +141,10 @@ abstract class StructuralEvaluator extends Evaluator {
         }
     }
 
+    /**
+     @deprecated replaced by {@link  ImmediateParentRun}
+     */
+    @Deprecated
     static class ImmediateParent extends StructuralEvaluator {
         public ImmediateParent(Evaluator evaluator) {
             super(evaluator);
@@ -163,6 +169,48 @@ abstract class StructuralEvaluator extends Evaluator {
         }
     }
 
+    /**
+     Holds a list of evaluators for one > two > three immediate parent matches, and the final direct evaluator under
+     test. To match, these are effectively ANDed together, starting from the last, matching up to the first.
+     */
+    static class ImmediateParentRun extends Evaluator {
+        final ArrayList<Evaluator> evaluators = new ArrayList<>();
+        int cost = 2;
+
+        public ImmediateParentRun(Evaluator evaluator) {
+            evaluators.add(evaluator);
+            cost += evaluator.cost();
+        }
+
+        void add(Evaluator evaluator) {
+            evaluators.add(evaluator);
+            cost += evaluator.cost();
+        }
+
+        @Override
+        public boolean matches(Element root, Element element) {
+            // evaluate from last to first
+            for (int i = evaluators.size() -1; i >= 0; --i) {
+                if (element == null)
+                    return false;
+                Evaluator eval = evaluators.get(i);
+                if (!eval.matches(root, element))
+                    return false;
+                element = element.parent();
+            }
+            return true;
+        }
+
+        @Override protected int cost() {
+            return cost;
+        }
+
+        @Override
+        public String toString() {
+            return StringUtil.join(evaluators, " > ");
+        }
+    }
+
     static class PreviousSibling extends StructuralEvaluator {
         public PreviousSibling(Evaluator evaluator) {
             super(evaluator);


Test patch:
diff --git a/src/test/java/org/jsoup/nodes/AttributesTest.java b/src/test/java/org/jsoup/nodes/AttributesTest.java
index 58024c6a..bb415c0e 100644
--- a/src/test/java/org/jsoup/nodes/AttributesTest.java
+++ b/src/test/java/org/jsoup/nodes/AttributesTest.java
@@ -3,6 +3,7 @@ package org.jsoup.nodes;
 import org.jsoup.Jsoup;
 import org.junit.jupiter.api.Test;
 
+import java.util.ConcurrentModificationException;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -174,6 +175,49 @@ public class AttributesTest {
         assertFalse(iterator.hasNext());
     }
 
+    @Test
+    public void testIteratorRemove() {
+        String html = "<div 1=1 2=2 3=3 4=4>";
+        Document doc = Jsoup.parse(html);
+        Element el = doc.expectFirst("div");
+        Attributes attrs = el.attributes();
+
+        Iterator<Attribute> iter = attrs.iterator();
+        int seen = 0;
+        while (iter.hasNext()) {
+            seen++;
+            Attribute attr = iter.next();
+            iter.remove();
+        }
+        assertEquals(4, seen);
+        assertEquals(0, attrs.size());
+        assertEquals(0, el.attributesSize());
+    }
+
+    @Test
+    public void testIteratorRemoveConcurrentException() {
+        String html = "<div 1=1 2=2 3=3 4=4>";
+        Document doc = Jsoup.parse(html);
+        Element el = doc.expectFirst("div");
+        Attributes attrs = el.attributes();
+
+        Iterator<Attribute> iter = attrs.iterator();
+        int seen = 0;
+        boolean threw = false;
+        try {
+            while (iter.hasNext()) {
+                seen++;
+                Attribute next = iter.next();
+                el.removeAttr(next.getKey());
+            }
+        } catch (ConcurrentModificationException e) {
+            threw = true;
+        }
+
+        assertEquals(1, seen);
+        assertTrue(threw);
+    }
+
     @Test
     public void removeCaseSensitive() {
         Attributes a = new Attributes();
diff --git a/src/test/java/org/jsoup/nodes/ElementTest.java b/src/test/java/org/jsoup/nodes/ElementTest.java
index 41809af7..7d216136 100644
--- a/src/test/java/org/jsoup/nodes/ElementTest.java
+++ b/src/test/java/org/jsoup/nodes/ElementTest.java
@@ -2566,6 +2566,30 @@ public class ElementTest {
         assertEquals("div", el.cssSelector());
     }
 
+    @Test void cssSelectorDoesntStackOverflow() {
+        // https://github.com/jhy/jsoup/issues/2001
+        Element element = new Element("element");
+        Element root = element;
+
+        // Create a long chain of elements
+        for (int i = 0; i < 5000; i++) {
+            Element elem2 = new Element("element" + i);
+            element.appendChild(elem2);
+            element = elem2;
+        }
+
+        String selector = element.cssSelector(); // would overflow in cssSelector parent() recurse
+        Evaluator eval = QueryParser.parse(selector);
+
+        assertEquals(eval.toString(), selector);
+        assertTrue(selector.startsWith("element > element0 >"));
+        assertTrue(selector.endsWith("8 > element4999"));
+
+        Elements elements = root.select(selector); // would overflow in nested And ImmediateParent chain eval
+        assertEquals(1, elements.size());
+        assertEquals(element, elements.first());
+    }
+
     @Test void orphanSiblings() {
         Element el = new Element("div");
         assertEquals(0, el.siblingElements().size());
diff --git a/src/test/java/org/jsoup/parser/HtmlParserTest.java b/src/test/java/org/jsoup/parser/HtmlParserTest.java
index 4172d0f5..5f9eca9d 100644
--- a/src/test/java/org/jsoup/parser/HtmlParserTest.java
+++ b/src/test/java/org/jsoup/parser/HtmlParserTest.java
@@ -1752,4 +1752,126 @@ public class HtmlParserTest {
 
         assertEquals(textContent, textArea.wholeText());
     }
+
+    @Test void svgParseTest() {
+        String html = "<div><svg viewBox=2><foreignObject><p>One</p></foreignObject></svg></div>";
+        Document doc = Jsoup.parse(html);
+
+        assertHtmlNamespace(doc);
+        Element div = doc.expectFirst("div");
+        assertHtmlNamespace(div);
+
+        Element svg = doc.expectFirst("svg");
+        assertTrue(svg.attributes().hasKey("viewBox"));
+        assertSvgNamespace(svg);
+        assertSvgNamespace(doc.expectFirst("foreignObject"));
+        assertHtmlNamespace(doc.expectFirst("p"));
+
+        String serialized = div.html();
+        assertEquals("<svg viewBox=\"2\">\n" +
+            " <foreignObject>\n" +
+            "  <p>One</p>\n" +
+            " </foreignObject>\n" +
+            "</svg>", serialized);
+    }
+
+    @Test void mathParseText() {
+        String html = "<div><math><mi><p>One</p><svg><text>Blah</text></svg></mi><ms></ms></div>";
+        Document doc = Jsoup.parse(html);
+
+        assertHtmlNamespace(doc.expectFirst("div"));
+        assertMathNamespace(doc.expectFirst("math"));
+        assertMathNamespace(doc.expectFirst("mi"));
+        assertHtmlNamespace(doc.expectFirst("p"));
+        assertSvgNamespace(doc.expectFirst("svg"));
+        assertSvgNamespace(doc.expectFirst("text"));
+        assertMathNamespace(doc.expectFirst("ms"));
+
+        String serialized = doc.expectFirst("div").html();
+        assertEquals("<math>\n" +
+            " <mi>\n" +
+            "  <p>One</p>\n" +
+            "  <svg>\n" +
+            "   <text>Blah</text>\n" +
+            "  </svg></mi><ms></ms>\n" +
+            "</math>", serialized);
+    }
+
+    private static void assertHtmlNamespace(Element el) {
+        assertEquals(Parser.NamespaceHtml, el.tag().namespace());
+    }
+
+    private static void assertSvgNamespace(Element el) {
+        assertEquals(Parser.NamespaceSvg, el.tag().namespace());
+    }
+
+    private static void assertMathNamespace(Element el) {
+        assertEquals(Parser.NamespaceMathml, el.tag().namespace());
+    }
+
+    @Test void mathSvgStyleTest() {
+        String html = "<style><img></style><math><svg><style><img></img></style></svg></math>";
+        Document doc = Jsoup.parse(html);
+
+        Element htmlStyle = doc.expectFirst("style");
+        assertHtmlNamespace(htmlStyle);
+        assertEquals("<img>", htmlStyle.data()); // that's not an element, it's data (textish)
+
+        Element svgStyle = doc.expectFirst("svg style");
+        assertMathNamespace(svgStyle); // in inherited math namespace as not an HTML integration point
+        Element styleImg = svgStyle.expectFirst("img");
+        assertHtmlNamespace(styleImg); // this one is an img tag - in foreign to html elements
+
+        assertMathNamespace(doc.expectFirst("svg"));
+        assertMathNamespace(doc.expectFirst("math"));
+    }
+
+    @Test void xmlnsAttributeError() {
+        String html = "<p><svg></svg></body>";
+        Parser parser = Parser.htmlParser().setTrackErrors(10);
+        Document doc = Jsoup.parse(html, parser);
+        assertEquals(0, doc.parser().getErrors().size());
+
+        String html2 = "<html xmlns='http://www.w3.org/1999/xhtml'><p xmlns='http://www.w3.org/1999/xhtml'><i xmlns='xhtml'></i></body>";
+        Document doc2 = Jsoup.parse(html2, parser);
+        assertEquals(1, doc2.parser().getErrors().size());
+        assertEquals("Invalid xmlns attribute [xhtml] on tag [i]", parser.getErrors().get(0).getErrorMessage());
+    }
+
+    @Test void mathAnnotationSvg() {
+        String html = "<math><svg>"; // not in annotation, svg will be in math ns
+        Document doc = Jsoup.parse(html);
+        assertMathNamespace(doc.expectFirst("math"));
+        assertMathNamespace(doc.expectFirst("svg"));
+
+        String html2 = "<math><annotation-xml><svg>"; // svg will be in svg ns
+        Document doc2 = Jsoup.parse(html2);
+        assertMathNamespace(doc2.expectFirst("math"));
+        assertMathNamespace(doc2.expectFirst("annotation-xml"));
+        assertSvgNamespace(doc2.expectFirst("svg"));
+    }
+
+    @Test void mathHtmlIntegrationPoint() {
+        String html = "<math><div>Hello";
+        Document doc = Jsoup.parse(html);
+        assertMathNamespace(doc.expectFirst("math"));
+        assertHtmlNamespace(doc.expectFirst("div"));
+
+        String html2 = "<math><divv>Hello";
+        Document doc2 = Jsoup.parse(html2);
+        assertMathNamespace(doc2.expectFirst("math"));
+        assertMathNamespace(doc2.expectFirst("divv"));
+
+        String html3 = "<math><annotation-xml><divv>Hello";
+        Document doc3 = Jsoup.parse(html3);
+        assertMathNamespace(doc3.expectFirst("math"));
+        assertMathNamespace(doc3.expectFirst("annotation-xml"));
+        assertMathNamespace(doc3.expectFirst("divv"));
+
+        String html4 = "<math><annotation-xml encoding=text/html><divv>Hello";
+        Document doc4 = Jsoup.parse(html4);
+        assertMathNamespace(doc4.expectFirst("math"));
+        assertMathNamespace(doc4.expectFirst("annotation-xml"));
+        assertHtmlNamespace(doc4.expectFirst("divv"));
+    }
 }
diff --git a/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java b/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java
index 55b828e8..cb09e458 100644
--- a/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java
+++ b/src/test/java/org/jsoup/parser/HtmlTreeBuilderStateTest.java
@@ -45,7 +45,7 @@ public class HtmlTreeBuilderStateTest {
     public void ensureArraysAreSorted() {
         List<Object[]> constants = findConstantArrays(Constants.class);
         ensureSorted(constants);
-        assertEquals(39, constants.size());
+        assertEquals(40, constants.size());
     }
 
     @Test public void ensureTagSearchesAreKnownTags() {
diff --git a/src/test/java/org/jsoup/parser/HtmlTreeBuilderTest.java b/src/test/java/org/jsoup/parser/HtmlTreeBuilderTest.java
index 10e6c254..dae8e460 100644
--- a/src/test/java/org/jsoup/parser/HtmlTreeBuilderTest.java
+++ b/src/test/java/org/jsoup/parser/HtmlTreeBuilderTest.java
@@ -18,7 +18,7 @@ public class HtmlTreeBuilderTest {
     public void ensureSearchArraysAreSorted() {
         List<Object[]> constants = HtmlTreeBuilderStateTest.findConstantArrays(HtmlTreeBuilder.class);
         HtmlTreeBuilderStateTest.ensureSorted(constants);
-        assertEquals(8, constants.size());
+        assertEquals(10, constants.size());
     }
 
     @Test
diff --git a/src/test/java/org/jsoup/parser/TagTest.java b/src/test/java/org/jsoup/parser/TagTest.java
index 65a794d8..999cfd07 100644
--- a/src/test/java/org/jsoup/parser/TagTest.java
+++ b/src/test/java/org/jsoup/parser/TagTest.java
@@ -81,4 +81,26 @@ public class TagTest {
         assertTrue(Tag.isKnownTag("div"));
         assertFalse(Tag.isKnownTag("explain"));
     }
+
+    @Test public void knownSvgNamespace() {
+        Tag svgHtml = Tag.valueOf("svg"); // no namespace specified, defaults to html, so not the known tag
+        Tag svg = Tag.valueOf("svg", Parser.NamespaceSvg, ParseSettings.htmlDefault);
+
+        assertEquals(Parser.NamespaceHtml, svgHtml.namespace());
+        assertEquals(Parser.NamespaceSvg, svg.namespace());
+
+        assertFalse(svgHtml.isBlock()); // generated
+        assertTrue(svg.isBlock()); // known
+    }
+
+    @Test public void unknownTagNamespace() {
+        Tag fooHtml = Tag.valueOf("foo"); // no namespace specified, defaults to html
+        Tag foo = Tag.valueOf("foo", Parser.NamespaceSvg, ParseSettings.htmlDefault);
+
+        assertEquals(Parser.NamespaceHtml, fooHtml.namespace());
+        assertEquals(Parser.NamespaceSvg, foo.namespace());
+
+        assertFalse(fooHtml.isBlock()); // generated
+        assertFalse(foo.isBlock()); // generated
+    }
 }
diff --git a/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java b/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java
index d359a528..2499f3f5 100644
--- a/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java
+++ b/src/test/java/org/jsoup/parser/XmlTreeBuilderTest.java
@@ -127,7 +127,7 @@ public class XmlTreeBuilderTest {
     public void testDoesHandleEOFInTag() {
         String html = "<img src=asdf onerror=\"alert(1)\" x=";
         Document xmlDoc = Jsoup.parse(html, "", Parser.xmlParser());
-        assertEquals("<img src=\"asdf\" onerror=\"alert(1)\" x=\"\" />", xmlDoc.html());
+        assertEquals("<img src=\"asdf\" onerror=\"alert(1)\" x=\"\"></img>", xmlDoc.html());
     }
 
     @Test
@@ -312,6 +312,36 @@ public class XmlTreeBuilderTest {
         assertEquals("FOO", t3.getName());
         assertSame(t1, t2);
         assertSame(t3, t4);
+    }
+
+    @Test void rootHasXmlSettings() {
+        Document doc = Jsoup.parse("<foo>", Parser.xmlParser());
+        ParseSettings settings = doc.parser().settings();
+        assertTrue(settings.preserveTagCase());
+        assertTrue(settings.preserveAttributeCase());
+        assertEquals(Parser.NamespaceXml, doc.parser().defaultNamespace());
+    }
+
+    @Test void xmlNamespace() {
+        String xml = "<foo><bar><div><svg><math>Qux</bar></foo>";
+        Document doc = Jsoup.parse(xml, Parser.xmlParser());
 
+        assertXmlNamespace(doc);
+        Elements els = doc.select("*");
+        for (Element el : els) {
+            assertXmlNamespace(el);
+        }
+
+        Document clone = doc.clone();
+        assertXmlNamespace(clone);
+        assertXmlNamespace(clone.expectFirst("bar"));
+
+        Document shallow = doc.shallowClone();
+        assertXmlNamespace(shallow);
     }
+
+    private static void assertXmlNamespace(Element el) {
+        assertEquals(Parser.NamespaceXml, el.tag().namespace(), String.format("Element %s not in XML namespace", el.tagName()));
+    }
+
 }
diff --git a/src/test/java/org/jsoup/select/QueryParserTest.java b/src/test/java/org/jsoup/select/QueryParserTest.java
index 8c4db234..ae2f3448 100644
--- a/src/test/java/org/jsoup/select/QueryParserTest.java
+++ b/src/test/java/org/jsoup/select/QueryParserTest.java
@@ -12,7 +12,6 @@ import static org.junit.jupiter.api.Assertions.*;
  * @author Jonathan Hedley
  */
 public class QueryParserTest {
-
     @Test public void testConsumeSubQuery() {
         Document doc = Jsoup.parse("<html><head>h</head><body>" +
                 "<li><strong>l1</strong></li>" +
@@ -25,6 +24,17 @@ public class QueryParserTest {
         assertEquals("l2", doc.select(">body>p>strong,>body>*>li>strong").text());
     }
 
+    @Test public void testImmediateParentRun() {
+        String query = "div > p > bold.brass";
+        Evaluator eval1 = QueryParser.parse(query);
+        assertEquals(query, eval1.toString());
+
+        StructuralEvaluator.ImmediateParentRun run = (StructuralEvaluator.ImmediateParentRun) eval1;
+        assertTrue(run.evaluators.get(0) instanceof Evaluator.Tag);
+        assertTrue(run.evaluators.get(1) instanceof Evaluator.Tag);
+        assertTrue(run.evaluators.get(2) instanceof CombiningEvaluator.And);
+    }
+
     @Test public void testOrGetsCorrectPrecedence() {
         // tests that a selector "a b, c d, e f" evals to (a AND b) OR (c AND d) OR (e AND f)"
         // top level or, three child ands
@@ -42,17 +52,20 @@ public class QueryParserTest {
     }
 
     @Test public void testParsesMultiCorrectly() {
-        String query = ".foo > ol, ol > li + li";
+        String query = ".foo.qux > ol.bar, ol > li + li";
         Evaluator eval = QueryParser.parse(query);
         assertTrue(eval instanceof CombiningEvaluator.Or);
         CombiningEvaluator.Or or = (CombiningEvaluator.Or) eval;
         assertEquals(2, or.evaluators.size());
 
-        CombiningEvaluator.And andLeft = (CombiningEvaluator.And) or.evaluators.get(0);
+        StructuralEvaluator.ImmediateParentRun run = (StructuralEvaluator.ImmediateParentRun) or.evaluators.get(0);
         CombiningEvaluator.And andRight = (CombiningEvaluator.And) or.evaluators.get(1);
 
-        assertEquals(".foo > ol", andLeft.toString());
-        assertEquals(2, andLeft.evaluators.size());
+        assertEquals(".foo.qux > ol.bar", run.toString());
+        assertEquals(2, run.evaluators.size());
+        Evaluator runAnd = run.evaluators.get(0);
+        assertTrue(runAnd instanceof CombiningEvaluator.And);
+        assertEquals(".foo.qux", runAnd.toString());
         assertEquals("ol > li + li", andRight.toString());
         assertEquals(2, andRight.evaluators.size());
         assertEquals(query, eval.toString());