Fix HTML escape issues (#484)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c6672f..38d261b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
   character references.
 * Add a new syntax `SoftLineBreakSyntax` to remove the single space before the
   line ending.
+* Add a new syntax `EscapeHtmlSyntax` to encode (`"`), (`<`), (`>`) and (`&`).
 * Add an option `caseSensitive` to `TextSyntax`.
 
 ## 6.0.1
diff --git a/benchmark/output.html b/benchmark/output.html
index 16689fd..e3831bd 100644
--- a/benchmark/output.html
+++ b/benchmark/output.html
@@ -7,58 +7,58 @@
 sample of real-world markdown:</p>
 <p>Tests are specified using the top-level <a href="https://pub.dev/documentation/test_core/latest/test_core/test.html"><code>test()</code></a> function, and test
 assertions are made using <a href="https://pub.dev/documentation/test_api/latest/test_api/expect.html"><code>expect()</code></a>:</p>
-<pre><code class="language-dart">import "package:test/test.dart";
+<pre><code class="language-dart">import &quot;package:test/test.dart&quot;;
 
 void main() {
-  test("String.split() splits the string on the delimiter", () {
-    var string = "foo,bar,baz";
-    expect(string.split(","), equals(["foo", "bar", "baz"]));
+  test(&quot;String.split() splits the string on the delimiter&quot;, () {
+    var string = &quot;foo,bar,baz&quot;;
+    expect(string.split(&quot;,&quot;), equals([&quot;foo&quot;, &quot;bar&quot;, &quot;baz&quot;]));
   });
 
-  test("String.trim() removes surrounding whitespace", () {
-    var string = "  foo ";
-    expect(string.trim(), equals("foo"));
+  test(&quot;String.trim() removes surrounding whitespace&quot;, () {
+    var string = &quot;  foo &quot;;
+    expect(string.trim(), equals(&quot;foo&quot;));
   });
 }
 </code></pre>
 <p>Tests can be grouped together using the [<code>group()</code>] function. Each group's
 description is added to the beginning of its test's descriptions.</p>
-<pre><code class="language-dart">import "package:test/test.dart";
+<pre><code class="language-dart">import &quot;package:test/test.dart&quot;;
 
 void main() {
-  group("String", () {
-    test(".split() splits the string on the delimiter", () {
-      var string = "foo,bar,baz";
-      expect(string.split(","), equals(["foo", "bar", "baz"]));
+  group(&quot;String&quot;, () {
+    test(&quot;.split() splits the string on the delimiter&quot;, () {
+      var string = &quot;foo,bar,baz&quot;;
+      expect(string.split(&quot;,&quot;), equals([&quot;foo&quot;, &quot;bar&quot;, &quot;baz&quot;]));
     });
 
-    test(".trim() removes surrounding whitespace", () {
-      var string = "  foo ";
-      expect(string.trim(), equals("foo"));
+    test(&quot;.trim() removes surrounding whitespace&quot;, () {
+      var string = &quot;  foo &quot;;
+      expect(string.trim(), equals(&quot;foo&quot;));
     });
   });
 
-  group("int", () {
-    test(".remainder() returns the remainder of division", () {
+  group(&quot;int&quot;, () {
+    test(&quot;.remainder() returns the remainder of division&quot;, () {
       expect(11.remainder(3), equals(2));
     });
 
-    test(".toRadixString() returns a hex string", () {
-      expect(11.toRadixString(16), equals("b"));
+    test(&quot;.toRadixString() returns a hex string&quot;, () {
+      expect(11.toRadixString(16), equals(&quot;b&quot;));
     });
   });
 }
 </code></pre>
 <p>Any matchers from the <a href="https://pub.dev/documentation/matcher/latest/matcher/matcher-library.html"><code>matcher</code></a> package can be used with <code>expect()</code>
 to do complex validations:</p>
-<pre><code class="language-dart">import "package:test/test.dart";
+<pre><code class="language-dart">import &quot;package:test/test.dart&quot;;
 
 void main() {
-  test(".split() splits the string on the delimiter", () {
-    expect("foo,bar,baz", allOf([
-      contains("foo"),
-      isNot(startsWith("bar")),
-      endsWith("baz")
+  test(&quot;.split() splits the string on the delimiter&quot;, () {
+    expect(&quot;foo,bar,baz&quot;, allOf([
+      contains(&quot;foo&quot;),
+      isNot(startsWith(&quot;bar&quot;)),
+      endsWith(&quot;baz&quot;)
     ]));
   });
 }
@@ -66,9 +66,9 @@
 <h2>Running Tests</h2>
 <p>A single test file can be run just using <code>pub run test:test path/to/test.dart</code>
 (on Dart 1.10, this can be shortened to <code>pub run test path/to/test.dart</code>).</p>
-<p><img src="https://raw.githubusercontent.com/dart-lang/test/master/image/test1.gif" alt="Single file being run via pub run"" /></p>
+<p><img src="https://raw.githubusercontent.com/dart-lang/test/master/image/test1.gif" alt="Single file being run via pub run&quot;" /></p>
 <p>Many tests can be run at a time using <code>pub run test:test path/to/dir</code>.</p>
-<p><img src="https://raw.githubusercontent.com/dart-lang/test/master/image/test2.gif" alt="Directory being run via "pub run"." /></p>
+<p><img src="https://raw.githubusercontent.com/dart-lang/test/master/image/test2.gif" alt="Directory being run via &quot;pub run&quot;." /></p>
 <p>It's also possible to run a test on the Dart VM only by invoking it using <code>dart path/to/test.dart</code>, but this doesn't load the full test runner and will be
 missing some features.</p>
 <p>The test runner considers any file that ends with <code>_test.dart</code> to be a test
@@ -78,7 +78,7 @@
 well by passing <code>pub run test:test -p chrome path/to/test.dart</code>.
 <code>test</code> will take care of starting the browser and loading the tests, and all
 the results will be reported on the command line just like for VM tests. In
-fact, you can even run tests on both platforms with a single command: <code>pub run test:test -p "chrome,vm" path/to/test.dart</code>.</p>
+fact, you can even run tests on both platforms with a single command: <code>pub run test:test -p &quot;chrome,vm&quot; path/to/test.dart</code>.</p>
 <h3>Restricting Tests to Certain Platforms</h3>
 <p>Some test files only make sense to run on particular platforms. They may use
 <code>dart:html</code> or <code>dart:io</code>, they might test Windows' particular filesystem
@@ -86,17 +86,17 @@
 <a href="https://pub.dev/documentation/test_api/latest/test_api/TestOn-class.html"><code>@TestOn</code></a> annotation makes it easy to declare exactly which platforms
 a test file should run on. Just put it at the top of your file, before any
 <code>library</code> or <code>import</code> declarations:</p>
-<pre><code class="language-dart">@TestOn("vm")
+<pre><code class="language-dart">@TestOn(&quot;vm&quot;)
 
-import "dart:io";
+import &quot;dart:io&quot;;
 
-import "package:test/test.dart";
+import &quot;package:test/test.dart&quot;;
 
 void main() {
   // ...
 }
 </code></pre>
-<p>The string you pass to <code>@TestOn</code> is what's called a "platform selector", and it
+<p>The string you pass to <code>@TestOn</code> is what's called a &quot;platform selector&quot;, and it
 specifies exactly which platforms a test can run on. It can be as simple as the
 name of a platform, or a more complex Dart-like boolean expression involving
 these platform names.</p>
@@ -171,16 +171,16 @@
 </li>
 </ul>
 <p>For example, if you wanted to run a test on every browser but Chrome, you would
-write <code>@TestOn("browser &amp;&amp; !chrome")</code>.</p>
+write <code>@TestOn(&quot;browser &amp;&amp; !chrome&quot;)</code>.</p>
 <h2>Asynchronous Tests</h2>
 <p>Tests written with <code>async</code>/<code>await</code> will work automatically. The test runner
 won't consider the test finished until the returned <code>Future</code> completes.</p>
-<pre><code class="language-dart">import "dart:async";
+<pre><code class="language-dart">import &quot;dart:async&quot;;
 
-import "package:test/test.dart";
+import &quot;package:test/test.dart&quot;;
 
 void main() {
-  test("new Future.value() returns the value", () async {
+  test(&quot;new Future.value() returns the value&quot;, () async {
     var value = await new Future.value(10);
     expect(value, equals(10));
   });
@@ -190,12 +190,12 @@
 asynchrony. The <a href="https://pub.dev/documentation/test_api/latest/test_api/completion.html"><code>completion()</code></a> matcher can be used to test
 <code>Futures</code>; it ensures that the test doesn't finish until the <code>Future</code> completes,
 and runs a matcher against that <code>Future</code>'s value.</p>
-<pre><code class="language-dart">import "dart:async";
+<pre><code class="language-dart">import &quot;dart:async&quot;;
 
-import "package:test/test.dart";
+import &quot;package:test/test.dart&quot;;
 
 void main() {
-  test("new Future.value() returns the value", () {
+  test(&quot;new Future.value() returns the value&quot;, () {
     expect(new Future.value(10), completion(equals(10)));
   });
 }
@@ -203,14 +203,14 @@
 <p>The <a href="https://pub.dev/documentation/test_api/latest/test_api/throwsA.html"><code>throwsA()</code></a> matcher and the various <code>throwsExceptionType</code>
 matchers work with both synchronous callbacks and asynchronous <code>Future</code>s. They
 ensure that a particular type of exception is thrown:</p>
-<pre><code class="language-dart">import "dart:async";
+<pre><code class="language-dart">import &quot;dart:async&quot;;
 
-import "package:test/test.dart";
+import &quot;package:test/test.dart&quot;;
 
 void main() {
-  test("new Future.error() throws the error", () {
-    expect(new Future.error("oh no"), throwsA(equals("oh no")));
-    expect(new Future.error(new StateError("bad state")), throwsStateError);
+  test(&quot;new Future.error() throws the error&quot;, () {
+    expect(new Future.error(&quot;oh no&quot;), throwsA(equals(&quot;oh no&quot;)));
+    expect(new Future.error(new StateError(&quot;bad state&quot;)), throwsStateError);
   });
 }
 </code></pre>
@@ -219,12 +219,12 @@
 times, and will cause the test to fail if it's called too often; second, it
 keeps the test from finishing until the function is called the requisite number
 of times.</p>
-<pre><code class="language-dart">import "dart:async";
+<pre><code class="language-dart">import &quot;dart:async&quot;;
 
-import "package:test/test.dart";
+import &quot;package:test/test.dart&quot;;
 
 void main() {
-  test("Stream.fromIterable() emits the values in the iterable", () {
+  test(&quot;Stream.fromIterable() emits the values in the iterable&quot;, () {
     var stream = new Stream.fromIterable([1, 2, 3]);
 
     stream.listen(expectAsync((number) {
@@ -242,11 +242,11 @@
 <p>They must have the same name as the test, with <code>.dart</code> replaced by <code>.html</code>.</p>
 </li>
 <li>
-<p>They must contain a <code>link</code> tag with <code>rel="x-dart-test"</code> and an <code>href</code>
+<p>They must contain a <code>link</code> tag with <code>rel=&quot;x-dart-test&quot;</code> and an <code>href</code>
 attribute pointing to the test script.</p>
 </li>
 <li>
-<p>They must contain <code>&lt;script src="packages/test/dart.js"&gt;&lt;/script&gt;</code>.</p>
+<p>They must contain <code>&lt;script src=&quot;packages/test/dart.js&quot;&gt;&lt;/script&gt;</code>.</p>
 </li>
 </ul>
 <p>For example, if you had a test called <code>custom_html_test.dart</code>, you might write
@@ -256,8 +256,8 @@
 &lt;html&gt;
   &lt;head&gt;
     &lt;title&gt;Custom HTML Test&lt;/title&gt;
-    &lt;link rel="x-dart-test" href="custom_html_test.dart"&gt;
-    &lt;script src="packages/test/dart.js"&gt;&lt;/script&gt;
+    &lt;link rel=&quot;x-dart-test&quot; href=&quot;custom_html_test.dart&quot;&gt;
+    &lt;script src=&quot;packages/test/dart.js&quot;&gt;&lt;/script&gt;
   &lt;/head&gt;
   &lt;body&gt;
     // ...
@@ -267,15 +267,15 @@
 <h2>Configuring Tests</h2>
 <h3>Skipping Tests</h3>
 <p>If a test, group, or entire suite isn't working yet and you just want it to stop
-complaining, you can mark it as "skipped". The test or tests won't be run, and,
+complaining, you can mark it as &quot;skipped&quot;. The test or tests won't be run, and,
 if you supply a reason why, that reason will be printed. In general, skipping
 tests indicates that they should run but is temporarily not working. If they're
 is fundamentally incompatible with a platform, <a href="https://pub.dev/documentation/test_api/latest/test_api/TestOn-class.html"><code>@TestOn</code>/<code>testOn</code></a>
 should be used instead.</p>
 <p>To skip a test suite, put a <code>@Skip</code> annotation at the top of the file:</p>
-<pre><code class="language-dart">@Skip("currently failing (see issue 1234)")
+<pre><code class="language-dart">@Skip(&quot;currently failing (see issue 1234)&quot;)
 
-import "package:test/test.dart";
+import &quot;package:test/test.dart&quot;;
 
 void main() {
   // ...
@@ -285,16 +285,16 @@
 include it, but it's a good idea to document why the test isn't running.</p>
 <p>Groups and individual tests can be skipped by passing the <code>skip</code> parameter. This
 can be either <code>true</code> or a String describing why the test is skipped. For example:</p>
-<pre><code class="language-dart">import "package:test/test.dart";
+<pre><code class="language-dart">import &quot;package:test/test.dart&quot;;
 
 void main() {
-  group("complicated algorithm tests", () {
+  group(&quot;complicated algorithm tests&quot;, () {
     // ...
-  }, skip: "the algorithm isn't quite right");
+  }, skip: &quot;the algorithm isn&#39;t quite right&quot;);
 
-  test("error-checking test", () {
+  test(&quot;error-checking test&quot;, () {
     // ...
-  }, skip: "TODO: add error-checking.");
+  }, skip: &quot;TODO: add error-checking.&quot;);
 }
 </code></pre>
 <h3>Timeouts</h3>
@@ -303,7 +303,7 @@
 for a test suite, put a <code>@Timeout</code> annotation at the top of the file:</p>
 <pre><code class="language-dart">@Timeout(const Duration(seconds: 45))
 
-import "package:test/test.dart";
+import &quot;package:test/test.dart&quot;;
 
 void main() {
   // ...
@@ -314,20 +314,20 @@
 set the timeout to one and a half times as long as the default—45 seconds.</p>
 <p>Timeouts can be set for tests and groups using the <code>timeout</code> parameter. This
 parameter takes a <code>Timeout</code> object just like the annotation. For example:</p>
-<pre><code class="language-dart">import "package:test/test.dart";
+<pre><code class="language-dart">import &quot;package:test/test.dart&quot;;
 
 void main() {
-  group("slow tests", () {
+  group(&quot;slow tests&quot;, () {
     // ...
 
-    test("even slower test", () {
+    test(&quot;even slower test&quot;, () {
       // ...
     }, timeout: new Timeout.factor(2))
   }, timeout: new Timeout(new Duration(minutes: 1)));
 }
 </code></pre>
 <p>Nested timeouts apply in order from outermost to innermost. That means that
-"even slower test" will take two minutes to time out, since it multiplies the
+&quot;even slower test&quot; will take two minutes to time out, since it multiplies the
 group's timeout by 2.</p>
 <h3>Platform-Specific Configuration</h3>
 <p>Sometimes a test may need to be configured differently for different platforms.
@@ -337,16 +337,16 @@
 and <code>group()</code>. For example:</p>
 <pre><code class="language-dart">@OnPlatform(const {
   // Give Windows some extra wiggle-room before timing out.
-  "windows": const Timeout.factor(2)
+  &quot;windows&quot;: const Timeout.factor(2)
 })
 
-import "package:test/test.dart";
+import &quot;package:test/test.dart&quot;;
 
 void main() {
-  test("do a thing", () {
+  test(&quot;do a thing&quot;, () {
     // ...
   }, onPlatform: {
-    "safari": new Skip("Safari is currently broken (see #1234)")
+    &quot;safari&quot;: new Skip(&quot;Safari is currently broken (see #1234)&quot;)
   });
 }
 </code></pre>
diff --git a/lib/markdown.dart b/lib/markdown.dart
index 62d7576..3e435af 100644
--- a/lib/markdown.dart
+++ b/lib/markdown.dart
@@ -72,6 +72,7 @@
 export 'src/inline_syntaxes/email_autolink_syntax.dart';
 export 'src/inline_syntaxes/emoji_syntax.dart';
 export 'src/inline_syntaxes/emphasis_syntax.dart';
+export 'src/inline_syntaxes/escape_html_syntax.dart';
 export 'src/inline_syntaxes/escape_syntax.dart';
 export 'src/inline_syntaxes/image_syntax.dart';
 export 'src/inline_syntaxes/inline_html_syntax.dart';
diff --git a/lib/src/inline_parser.dart b/lib/src/inline_parser.dart
index c9ebbb6..d69af2b 100644
--- a/lib/src/inline_parser.dart
+++ b/lib/src/inline_parser.dart
@@ -11,6 +11,7 @@
 import 'inline_syntaxes/delimiter_syntax.dart';
 import 'inline_syntaxes/email_autolink_syntax.dart';
 import 'inline_syntaxes/emphasis_syntax.dart';
+import 'inline_syntaxes/escape_html_syntax.dart';
 import 'inline_syntaxes/escape_syntax.dart';
 import 'inline_syntaxes/image_syntax.dart';
 import 'inline_syntaxes/inline_syntax.dart';
@@ -27,8 +28,6 @@
     EmailAutolinkSyntax(),
     AutolinkSyntax(),
     LineBreakSyntax(),
-    // Allow any punctuation to be escaped.
-    EscapeSyntax(),
     // "*" surrounded by spaces is left alone.
     TextSyntax(r' \* ', startCharacter: $space),
     // "_" surrounded by spaces is left alone.
@@ -47,12 +46,6 @@
     // Leave already-encoded HTML entities alone. Ensures we don't turn
     // "&amp;" into "&amp;amp;"
     TextSyntax('&[#a-zA-Z0-9]*;', startCharacter: $ampersand),
-    // Encode "&".
-    TextSyntax('&', sub: '&amp;', startCharacter: $ampersand),
-    // Encode "<".
-    TextSyntax('<', sub: '&lt;', startCharacter: $lt),
-    // Encode ">".
-    TextSyntax('>', sub: '&gt;', startCharacter: $gt),
   ]);
 
   /// The string of Markdown being parsed.
@@ -95,6 +88,7 @@
     if (document.withDefaultInlineSyntaxes) {
       // Custom link resolvers go after the generic text syntax.
       syntaxes.addAll([
+        EscapeSyntax(),
         DecodeHtmlSyntax(),
         LinkSyntax(linkResolver: document.linkResolver),
         ImageSyntax(linkResolver: document.imageLinkResolver)
@@ -104,6 +98,10 @@
     }
 
     if (encodeHtml) {
+      syntaxes.add(EscapeHtmlSyntax());
+    }
+
+    if (encodeHtml) {
       syntaxes.addAll(_htmlSyntaxes);
     }
   }
diff --git a/lib/src/inline_syntaxes/autolink_syntax.dart b/lib/src/inline_syntaxes/autolink_syntax.dart
index 05eb41c..073c1cc 100644
--- a/lib/src/inline_syntaxes/autolink_syntax.dart
+++ b/lib/src/inline_syntaxes/autolink_syntax.dart
@@ -16,7 +16,10 @@
     final url = match[1]!;
     final text = parser.encodeHtml ? escapeHtml(url) : url;
     final anchor = Element.text('a', text);
-    anchor.attributes['href'] = Uri.encodeFull(url);
+
+    final destination = normalizeLinkDestination(url);
+    anchor.attributes['href'] =
+        parser.encodeHtml ? escapeHtml(destination) : destination;
     parser.addNode(anchor);
 
     return true;
diff --git a/lib/src/inline_syntaxes/decode_html_syntax.dart b/lib/src/inline_syntaxes/decode_html_syntax.dart
index 348a6b2..1af15d5 100644
--- a/lib/src/inline_syntaxes/decode_html_syntax.dart
+++ b/lib/src/inline_syntaxes/decode_html_syntax.dart
@@ -2,6 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import '../assets/html_entities.dart';
 import '../ast.dart';
 import '../charcode.dart';
 import '../inline_parser.dart';
@@ -27,9 +28,7 @@
       return false;
     }
 
-    // TODO(Zhiguang): Enable HTML entity decoding when working on HTML escape
-    // issues.
-    if (match[1] != null) {
+    if (match[1] != null && htmlEntitiesMap[match.match] == null) {
       return false;
     }
 
@@ -40,7 +39,11 @@
 
   @override
   bool onMatch(InlineParser parser, Match match) {
-    final decodedText = decodeHtmlCharacterFromMatch(match);
+    var decodedText = decodeHtmlCharacterFromMatch(match);
+
+    if (parser.encodeHtml) {
+      decodedText = escapeHtml(decodedText);
+    }
 
     parser.addNode(Text(decodedText));
     return true;
diff --git a/lib/src/inline_syntaxes/escape_html_syntax.dart b/lib/src/inline_syntaxes/escape_html_syntax.dart
new file mode 100644
index 0000000..565f870
--- /dev/null
+++ b/lib/src/inline_syntaxes/escape_html_syntax.dart
@@ -0,0 +1,20 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import '../ast.dart';
+import '../inline_parser.dart';
+import '../util.dart';
+import 'inline_syntax.dart';
+
+/// Encodes (`"`), (`<`), (`>`) and (`&`).
+class EscapeHtmlSyntax extends InlineSyntax {
+  EscapeHtmlSyntax() : super('["<>&]');
+  @override
+  bool onMatch(InlineParser parser, Match match) {
+    final text = escapeHtml(match[0]!);
+    parser.addNode(Text(text));
+
+    return true;
+  }
+}
diff --git a/lib/src/inline_syntaxes/escape_syntax.dart b/lib/src/inline_syntaxes/escape_syntax.dart
index 48a6426..30e6ea4 100644
--- a/lib/src/inline_syntaxes/escape_syntax.dart
+++ b/lib/src/inline_syntaxes/escape_syntax.dart
@@ -5,34 +5,29 @@
 import '../ast.dart';
 import '../charcode.dart';
 import '../inline_parser.dart';
+import '../patterns.dart';
 import '../util.dart';
 import 'inline_syntax.dart';
 
-/// Escape punctuation preceded by a backslash.
+/// Escape ASCII punctuation preceded by a backslash.
+///
+/// Backslashes before other characters are treated as literal backslashes.
+// See https://spec.commonmark.org/0.30/#backslash-escapes.
 class EscapeSyntax extends InlineSyntax {
-  EscapeSyntax() : super(r'''\\[!"#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~]''');
+  EscapeSyntax()
+      : super('\\\\([$asciiPunctuationEscaped])', startCharacter: $backslash);
 
   @override
   bool onMatch(InlineParser parser, Match match) {
     final chars = match.match;
-    final char = chars.codeUnitAt(1);
-    // Insert the substitution. Why these three charactes are replaced with
-    // their equivalent HTML entity referenced appears to be missing from the
-    // CommonMark spec, but is very present in all of the examples.
-    // https://talk.commonmark.org/t/entity-ification-of-quotes-and-brackets-missing-from-spec/3207
-    if (parser.encodeHtml) {
-      if (char == $double_quote) {
-        parser.addNode(Text('&quot;'));
-      } else if (char == $lt) {
-        parser.addNode(Text('&lt;'));
-      } else if (char == $gt) {
-        parser.addNode(Text('&gt;'));
-      } else {
-        parser.addNode(Text(chars[1]));
-      }
+
+    if ('&"<>'.contains(match[1]!) && parser.encodeHtml) {
+      final text = escapeHtml(match[1]!);
+      parser.addNode(Text(text));
+      return true;
     } else {
       parser.addNode(Text(chars[1]));
+      return true;
     }
-    return true;
   }
 }
diff --git a/lib/src/inline_syntaxes/image_syntax.dart b/lib/src/inline_syntaxes/image_syntax.dart
index 3c13679..cb47ae0 100644
--- a/lib/src/inline_syntaxes/image_syntax.dart
+++ b/lib/src/inline_syntaxes/image_syntax.dart
@@ -27,8 +27,7 @@
     element.attributes['src'] = destination;
     element.attributes['alt'] = children.map((node) => node.textContent).join();
     if (title != null && title.isNotEmpty) {
-      element.attributes['title'] =
-          escapeAttribute(title.replaceAll('&', '&amp;'));
+      element.attributes['title'] = normalizeLinkTitle(title);
     }
     return element;
   }
diff --git a/lib/src/inline_syntaxes/link_syntax.dart b/lib/src/inline_syntaxes/link_syntax.dart
index 0546b93..bc47b49 100644
--- a/lib/src/inline_syntaxes/link_syntax.dart
+++ b/lib/src/inline_syntaxes/link_syntax.dart
@@ -152,9 +152,13 @@
   }) {
     final children = getChildren();
     final element = Element('a', children);
-    element.attributes['href'] = escapeAttribute(destination);
+    element.attributes['href'] = normalizeLinkDestination(
+      escapePunctuation(destination),
+    );
     if (title != null && title.isNotEmpty) {
-      element.attributes['title'] = escapeAttribute(title);
+      element.attributes['title'] = normalizeLinkTitle(
+        escapePunctuation(title),
+      );
     }
     return element;
   }
diff --git a/lib/src/patterns.dart b/lib/src/patterns.dart
index a99cc5e..269689f 100644
--- a/lib/src/patterns.dart
+++ b/lib/src/patterns.dart
@@ -202,9 +202,13 @@
     caseSensitive: false);
 
 /// ASCII punctuation characters.
-// see https://spec.commonmark.org/0.30/#unicode-whitespace-character.
+// See https://spec.commonmark.org/0.30/#unicode-whitespace-character.
 const asciiPunctuationCharacters = r'''!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~''';
 
+/// ASCII punctuation characters with some characters escaped, in order to be
+// used in the RegExp character set.
+const asciiPunctuationEscaped = r'''!"#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~''';
+
 /// A pattern to match HTML entity references and numeric character references.
 // https://spec.commonmark.org/0.30/#entity-and-numeric-character-references
 final htmlCharactersPattern = RegExp(
diff --git a/lib/src/util.dart b/lib/src/util.dart
index a4861e2..02e26ab 100644
--- a/lib/src/util.dart
+++ b/lib/src/util.dart
@@ -9,81 +9,20 @@
 import 'charcode.dart';
 import 'patterns.dart';
 
-String escapeHtml(String html) =>
-    const HtmlEscape(HtmlEscapeMode.element).convert(html);
-
-String escapeHtmlAttribute(String text) =>
-    const HtmlEscape(HtmlEscapeMode.attribute).convert(text);
-
-/// Escapes the contents of [value], so that it may be used as an HTML
-/// attribute.
-///
-/// Based on http://spec.commonmark.org/0.28/#backslash-escapes.
-String escapeAttribute(String value) {
-  final result = StringBuffer();
-  int ch;
-  for (var i = 0; i < value.codeUnits.length; i++) {
-    ch = value.codeUnitAt(i);
-    if (ch == $backslash) {
-      i++;
-      if (i == value.codeUnits.length) {
-        result.writeCharCode(ch);
-        break;
-      }
-      ch = value.codeUnitAt(i);
-      switch (ch) {
-        case $quote:
-          result.write('&quot;');
-          break;
-        case $exclamation:
-        case $hash:
-        case $dollar:
-        case $percent:
-        case $ampersand:
-        case $apostrophe:
-        case $lparen:
-        case $rparen:
-        case $asterisk:
-        case $plus:
-        case $comma:
-        case $dash:
-        case $dot:
-        case $slash:
-        case $colon:
-        case $semicolon:
-        case $lt:
-        case $equal:
-        case $gt:
-        case $question:
-        case $at:
-        case $lbracket:
-        case $backslash:
-        case $rbracket:
-        case $caret:
-        case $underscore:
-        case $backquote:
-        case $lbrace:
-        case $bar:
-        case $rbrace:
-        case $tilde:
-          result.writeCharCode(ch);
-          break;
-        default:
-          result.write('%5C');
-          result.writeCharCode(ch);
-      }
-    } else if (ch == $quote) {
-      result.write('%22');
-    } else {
-      result.writeCharCode(ch);
-    }
-  }
-  return result.toString();
-}
-
 /// One or more whitespace, for compressing.
 final _oneOrMoreWhitespacePattern = RegExp('[ \n\r\t]+');
 
+/// Escapes (`'`), (`"`), (`<`), (`>`) and (`&`) characters.
+String escapeHtml(String html) => const HtmlEscape(HtmlEscapeMode(
+      escapeApos: true,
+      escapeLtGt: true,
+      escapeQuot: true,
+    )).convert(html);
+
+/// Escapes (`"`), (`<`) and (`>`) characters.
+String escapeHtmlAttribute(String text) =>
+    const HtmlEscape(HtmlEscapeMode.attribute).convert(text);
+
 /// "Normalizes" a link label, according to the [CommonMark spec].
 ///
 /// [CommonMark spec] https://spec.commonmark.org/0.30/#link-label
@@ -98,6 +37,34 @@
   return text;
 }
 
+/// Normalizes a link destination, including the process of HTML characters
+/// decoding  and percent encoding.
+// See the description of these examples:
+// https://spec.commonmark.org/0.30/#example-501
+// https://spec.commonmark.org/0.30/#example-502
+String normalizeLinkDestination(String destination) {
+  // Decode first, because the destination might have been partly encoded.
+  // For example https://spec.commonmark.org/0.30/#example-502.
+  // With this function, `foo%20b&auml;` will be parsed in the following steps:
+  // 1. foo b&auml;
+  // 2. foo bä
+  // 3. foo%20b%C3%A4
+  try {
+    destination = Uri.decodeFull(destination);
+  } catch (_) {}
+  return Uri.encodeFull(decodeHtmlCharacters(destination));
+}
+
+/// Normalizes a link title, including the process of HTML characters decoding
+/// and HTML characters escaping.
+// See the description of these examples:
+// https://spec.commonmark.org/0.30/#example-505
+// https://spec.commonmark.org/0.30/#example-506
+// https://spec.commonmark.org/0.30/#example-507
+// https://spec.commonmark.org/0.30/#example-508
+String normalizeLinkTitle(String title) =>
+    escapeHtmlAttribute(decodeHtmlCharacters(title));
+
 ///  Decodes HTML entity and numeric character references, for example decode
 /// `&#35` to `#`.
 String decodeHtmlCharacters(String input) =>
diff --git a/test/common_mark/autolinks.unit b/test/common_mark/autolinks.unit
index 5547760..8b5b887 100644
--- a/test/common_mark/autolinks.unit
+++ b/test/common_mark/autolinks.unit
@@ -5,7 +5,7 @@
 >>> Autolinks - 594
 <http://foo.bar.baz/test?q=hello&id=22&boolean>
 <<<
-<p><a href="http://foo.bar.baz/test?q=hello&id=22&boolean">http://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean</a></p>
+<p><a href="http://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean">http://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean</a></p>
 >>> Autolinks - 595
 <irc://foo.bar:2233/baz>
 <<<
diff --git a/test/common_mark/backslash_escapes.unit b/test/common_mark/backslash_escapes.unit
index 7042aa8..2c499ed 100644
--- a/test/common_mark/backslash_escapes.unit
+++ b/test/common_mark/backslash_escapes.unit
@@ -1,7 +1,7 @@
 >>> Backslash escapes - 12
 \!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~
 <<<
-<p>!&quot;#$%&'()*+,-./:;&lt;=&gt;?@[\]^_`{|}~</p>
+<p>!&quot;#$%&amp;'()*+,-./:;&lt;=&gt;?@[\]^_`{|}~</p>
 >>> Backslash escapes - 13
 \	\A\a\ \3\φ\«
 <<<
@@ -18,14 +18,14 @@
 \&ouml; not a character entity
 <<<
 <p>*not emphasized*
-&lt;br/> not a tag
+&lt;br/&gt; not a tag
 [not a link](/foo)
 `not code`
 1. not a list
 * not a list
 # not a heading
-[foo]: /url "not a reference"
-&ouml; not a character entity</p>
+[foo]: /url &quot;not a reference&quot;
+&amp;ouml; not a character entity</p>
 >>> Backslash escapes - 15
 \\*emphasis*
 <<<
diff --git a/test/common_mark/code_spans.unit b/test/common_mark/code_spans.unit
index 6864bf7..4c664ab 100644
--- a/test/common_mark/code_spans.unit
+++ b/test/common_mark/code_spans.unit
@@ -70,7 +70,7 @@
 >>> Code spans - 343
 `<a href="`">`
 <<<
-<p><code>&lt;a href="</code>"&gt;`</p>
+<p><code>&lt;a href=&quot;</code>&quot;&gt;`</p>
 >>> Code spans - 344
 <a href="`">`
 <<<
diff --git a/test/common_mark/emphasis_and_strong_emphasis.unit b/test/common_mark/emphasis_and_strong_emphasis.unit
index 67155bf..514b651 100644
--- a/test/common_mark/emphasis_and_strong_emphasis.unit
+++ b/test/common_mark/emphasis_and_strong_emphasis.unit
@@ -9,7 +9,7 @@
 >>> Emphasis and strong emphasis - 352
 a*"foo"*
 <<<
-<p>a*"foo"*</p>
+<p>a*&quot;foo&quot;*</p>
 >>> Emphasis and strong emphasis - 353
 * a *
 <<<
@@ -33,7 +33,7 @@
 >>> Emphasis and strong emphasis - 358
 a_"foo"_
 <<<
-<p>a_"foo"_</p>
+<p>a_&quot;foo&quot;_</p>
 >>> Emphasis and strong emphasis - 359
 foo_bar_
 <<<
@@ -49,7 +49,7 @@
 >>> Emphasis and strong emphasis - 362
 aa_"bb"_cc
 <<<
-<p>aa_"bb"_cc</p>
+<p>aa_&quot;bb&quot;_cc</p>
 >>> Emphasis and strong emphasis - 363
 foo-_(bar)_
 <<<
@@ -119,7 +119,7 @@
 >>> Emphasis and strong emphasis - 379
 a**"foo"**
 <<<
-<p>a**"foo"**</p>
+<p>a**&quot;foo&quot;**</p>
 >>> Emphasis and strong emphasis - 380
 foo**bar**
 <<<
@@ -141,7 +141,7 @@
 >>> Emphasis and strong emphasis - 384
 a__"foo"__
 <<<
-<p>a__"foo"__</p>
+<p>a__&quot;foo&quot;__</p>
 >>> Emphasis and strong emphasis - 385
 foo__bar__
 <<<
@@ -183,7 +183,7 @@
 >>> Emphasis and strong emphasis - 394
 **foo "*bar*" foo**
 <<<
-<p><strong>foo "<em>bar</em>" foo</strong></p>
+<p><strong>foo &quot;<em>bar</em>&quot; foo</strong></p>
 >>> Emphasis and strong emphasis - 395
 **foo**bar
 <<<
diff --git a/test/common_mark/entity_and_numeric_character_references.unit b/test/common_mark/entity_and_numeric_character_references.unit
index da96678..2859435 100644
--- a/test/common_mark/entity_and_numeric_character_references.unit
+++ b/test/common_mark/entity_and_numeric_character_references.unit
@@ -3,9 +3,9 @@
 &frac34; &HilbertSpace; &DifferentialD;
 &ClockwiseContourIntegral; &ngE;
 <<<
-<p>&nbsp; &amp; &copy; &AElig; &Dcaron;
-&frac34; &HilbertSpace; &DifferentialD;
-&ClockwiseContourIntegral; &ngE;</p>
+<p>&amp; © Æ Ď
+¾ ℋ ⅆ
+∲ ≧̸</p>
 >>> Entity and numeric character references - 26
 &#35; &#1234; &#992; &#0;
 <<<
@@ -13,17 +13,17 @@
 >>> Entity and numeric character references - 27
 &#X22; &#XD06; &#xcab;
 <<<
-<p>" ആ ಫ</p>
+<p>&quot; ആ ಫ</p>
 >>> Entity and numeric character references - 28
 &nbsp &x; &#; &#x;
 &#87654321;
 &#abcdef0;
 &ThisIsNotDefined; &hi?;
 <<<
-<p>&amp;nbsp &x; &#; &#x;
-&#87654321;
-&#abcdef0;
-&ThisIsNotDefined; &amp;hi?;</p>
+<p>&amp;nbsp &amp;x; &amp;#; &amp;#x;
+&amp;#87654321;
+&amp;#abcdef0;
+&amp;ThisIsNotDefined; &amp;hi?;</p>
 >>> Entity and numeric character references - 29
 &copy
 <<<
@@ -31,7 +31,7 @@
 >>> Entity and numeric character references - 30
 &MadeUpEntity;
 <<<
-<p>&MadeUpEntity;</p>
+<p>&amp;MadeUpEntity;</p>
 >>> Entity and numeric character references - 31
 <a href="&ouml;&ouml;.html">
 <<<
@@ -39,13 +39,13 @@
 >>> Entity and numeric character references - 32
 [foo](/f&ouml;&ouml; "f&ouml;&ouml;")
 <<<
-<p><a href="/f&ouml;&ouml;" title="f&ouml;&ouml;">foo</a></p>
+<p><a href="/f%C3%B6%C3%B6" title="föö">foo</a></p>
 >>> Entity and numeric character references - 33
 [foo]
 
 [foo]: /f&ouml;&ouml; "f&ouml;&ouml;"
 <<<
-<p><a href="/f&ouml;&ouml;" title="f&ouml;&ouml;">foo</a></p>
+<p><a href="/f%C3%B6%C3%B6" title="föö">foo</a></p>
 >>> Entity and numeric character references - 34
 ``` f&ouml;&ouml;
 foo
diff --git a/test/common_mark/images.unit b/test/common_mark/images.unit
index f1cf602..158ca40 100644
--- a/test/common_mark/images.unit
+++ b/test/common_mark/images.unit
@@ -100,7 +100,7 @@
 [[foo]]: /url "title"
 <<<
 <p>![[foo]]</p>
-<p>[[foo]]: /url "title"</p>
+<p>[[foo]]: /url &quot;title&quot;</p>
 >>> Images - 590
 ![Foo]
 
diff --git a/test/common_mark/link_reference_definitions.unit b/test/common_mark/link_reference_definitions.unit
index 46bbcf1..8d9ae8f 100644
--- a/test/common_mark/link_reference_definitions.unit
+++ b/test/common_mark/link_reference_definitions.unit
@@ -72,7 +72,7 @@
 
 [foo]
 <<<
-<p><a href="<>">foo</a></p>
+<p><a href="%3C%3E">foo</a></p>
 >>> Link reference definitions - 201
 [foo]: <bar>(baz)
 
@@ -84,7 +84,7 @@
 
 [foo]
 <<<
-<p>[foo]: /url\bar*baz "foo&quot;bar\baz"</p>
+<p>[foo]: /url\bar*baz &quot;foo&quot;bar\baz&quot;</p>
 <p>[foo]</p>
 >>> Link reference definitions - 203
 [foo]
@@ -110,7 +110,7 @@
 
 [αγω]
 <<<
-<p><a href="/φου">αγω</a></p>
+<p><a href="/%CF%86%CE%BF%CF%85">αγω</a></p>
 >>> Link reference definitions - 207
 [foo]: /url
 <<<
@@ -125,18 +125,18 @@
 >>> Link reference definitions - 209
 [foo]: /url "title" ok
 <<<
-<p>[foo]: /url "title" ok</p>
+<p>[foo]: /url &quot;title&quot; ok</p>
 >>> Link reference definitions - 210
 [foo]: /url
 "title" ok
 <<<
-<p>"title" ok</p>
+<p>&quot;title&quot; ok</p>
 >>> Link reference definitions - 211
     [foo]: /url "title"
 
 [foo]
 <<<
-<pre><code>[foo]: /url "title"
+<pre><code>[foo]: /url &quot;title&quot;
 </code></pre>
 <p>[foo]</p>
 >>> Link reference definitions - 212
diff --git a/test/common_mark/links.unit b/test/common_mark/links.unit
index 5e99f65..4015350 100644
--- a/test/common_mark/links.unit
+++ b/test/common_mark/links.unit
@@ -99,7 +99,7 @@
 >>> Links - 502
 [link](foo%20b&auml;)
 <<<
-<p><a href="foo%20b&auml;">link</a></p>
+<p><a href="foo%20b%C3%A4">link</a></p>
 >>> Links - 503
 [link]("title")
 <<<
@@ -115,19 +115,19 @@
 >>> Links - 505
 [link](/url "title \"&quot;")
 <<<
-<p><a href="/url" title="title %22&quot;">link</a></p>
+<p><a href="/url" title="title &quot;&quot;">link</a></p>
 >>> Links - 506
 [link](/url "title")
 <<<
-<p><a href="/url %22title%22">link</a></p>
+<p><a href="/url%C2%A0%22title%22">link</a></p>
 >>> Links - 507
 [link](/url "title "and" title")
 <<<
-<p>[link](/url "title "and" title")</p>
+<p>[link](/url &quot;title &quot;and&quot; title&quot;)</p>
 >>> Links - 508
 [link](/url 'title "and" title')
 <<<
-<p><a href="/url" title="title %22and%22 title">link</a></p>
+<p><a href="/url" title="title &quot;and&quot; title">link</a></p>
 >>> Links - 509
 [link](   /uri
   "title"  )
diff --git a/test/common_mark/raw_html.unit b/test/common_mark/raw_html.unit
index cde09c4..64315b0 100644
--- a/test/common_mark/raw_html.unit
+++ b/test/common_mark/raw_html.unit
@@ -29,11 +29,11 @@
 >>> Raw HTML - 618
 <a h*#ref="hi">
 <<<
-<p>&lt;a h*#ref="hi"&gt;</p>
+<p>&lt;a h*#ref=&quot;hi&quot;&gt;</p>
 >>> Raw HTML - 619
 <a href="hi'> <a href=hi'>
 <<<
-<p>&lt;a href="hi'&gt; &lt;a href=hi'&gt;</p>
+<p>&lt;a href=&quot;hi'&gt; &lt;a href=hi'&gt;</p>
 >>> Raw HTML - 620
 < a><
 foo><bar/ >
@@ -43,7 +43,7 @@
 <p>&lt; a&gt;&lt;
 foo&gt;&lt;bar/ &gt;
 &lt;foo bar=baz
-bim!bop /></p>
+bim!bop /&gt;</p>
 >>> Raw HTML - 621
 <a href='bar'title=title>
 <<<
@@ -55,7 +55,7 @@
 >>> Raw HTML - 623
 </a href="foo">
 <<<
-<p>&lt;/a href="foo"&gt;</p>
+<p>&lt;/a href=&quot;foo&quot;&gt;</p>
 >>> Raw HTML - 624
 foo <!-- this is a
 comment - with hyphen -->
@@ -96,4 +96,4 @@
 >>> Raw HTML - 632
 <a href="\"">
 <<<
-<p>&lt;a href="&quot;"&gt;</p>
+<p>&lt;a href=&quot;&quot;&quot;&gt;</p>
diff --git a/test/common_mark/setext_headings.unit b/test/common_mark/setext_headings.unit
index 8fae410..dca5f93 100644
--- a/test/common_mark/setext_headings.unit
+++ b/test/common_mark/setext_headings.unit
@@ -99,8 +99,8 @@
 <<<
 <h2>`Foo</h2>
 <p>`</p>
-<h2>&lt;a title="a lot</h2>
-<p>of dashes"/></p>
+<h2>&lt;a title=&quot;a lot</h2>
+<p>of dashes&quot;/&gt;</p>
 >>> Setext headings - 92
 > Foo
 ---
diff --git a/test/extensions/inline_html.unit b/test/extensions/inline_html.unit
index d02d1fd..1540118 100644
--- a/test/extensions/inline_html.unit
+++ b/test/extensions/inline_html.unit
@@ -14,4 +14,4 @@
 Text <a href="_foo_">And "_foo_"</a>.
 
 <<<
-<p>Text <a href="_foo_">And "<em>foo</em>"</a>.</p>
+<p>Text <a href="_foo_">And &quot;<em>foo</em>&quot;</a>.</p>
diff --git a/test/gfm/autolinks.unit b/test/gfm/autolinks.unit
index 4de9122..6df5296 100644
--- a/test/gfm/autolinks.unit
+++ b/test/gfm/autolinks.unit
@@ -5,7 +5,7 @@
 >>> Autolinks - 603
 <http://foo.bar.baz/test?q=hello&id=22&boolean>
 <<<
-<p><a href="http://foo.bar.baz/test?q=hello&id=22&boolean">http://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean</a></p>
+<p><a href="http://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean">http://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean</a></p>
 >>> Autolinks - 604
 <irc://foo.bar:2233/baz>
 <<<
diff --git a/test/gfm/autolinks_extension.unit b/test/gfm/autolinks_extension.unit
index 024fde1..ff69a53 100644
--- a/test/gfm/autolinks_extension.unit
+++ b/test/gfm/autolinks_extension.unit
@@ -36,7 +36,7 @@
 www.google.com/search?q=commonmark&hl;
 <<<
 <p><a href="http://www.google.com/search?q=commonmark&amp;hl=en">www.google.com/search?q=commonmark&amp;hl=en</a></p>
-<p><a href="http://www.google.com/search?q=commonmark">www.google.com/search?q=commonmark</a>&hl;</p>
+<p><a href="http://www.google.com/search?q=commonmark">www.google.com/search?q=commonmark</a>&amp;hl;</p>
 >>> Autolinks (extension) - 627
 www.commonmark.org/he<lp
 <<<
diff --git a/test/gfm/backslash_escapes.unit b/test/gfm/backslash_escapes.unit
index f5cce5c..0e9074a 100644
--- a/test/gfm/backslash_escapes.unit
+++ b/test/gfm/backslash_escapes.unit
@@ -1,7 +1,7 @@
 >>> Backslash escapes - 308
 \!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~
 <<<
-<p>!&quot;#$%&'()*+,-./:;&lt;=&gt;?@[\]^_`{|}~</p>
+<p>!&quot;#$%&amp;'()*+,-./:;&lt;=&gt;?@[\]^_`{|}~</p>
 >>> Backslash escapes - 309
 \	\A\a\ \3\φ\«
 <<<
@@ -18,14 +18,14 @@
 \&ouml; not a character entity
 <<<
 <p>*not emphasized*
-&lt;br/> not a tag
+&lt;br/&gt; not a tag
 [not a link](/foo)
 `not code`
 1. not a list
 * not a list
 # not a heading
-[foo]: /url "not a reference"
-&ouml; not a character entity</p>
+[foo]: /url &quot;not a reference&quot;
+&amp;ouml; not a character entity</p>
 >>> Backslash escapes - 311
 \\*emphasis*
 <<<
diff --git a/test/gfm/code_spans.unit b/test/gfm/code_spans.unit
index f620371..ebf1f64 100644
--- a/test/gfm/code_spans.unit
+++ b/test/gfm/code_spans.unit
@@ -70,7 +70,7 @@
 >>> Code spans - 353
 `<a href="`">`
 <<<
-<p><code>&lt;a href="</code>"&gt;`</p>
+<p><code>&lt;a href=&quot;</code>&quot;&gt;`</p>
 >>> Code spans - 354
 <a href="`">`
 <<<
diff --git a/test/gfm/emphasis_and_strong_emphasis.unit b/test/gfm/emphasis_and_strong_emphasis.unit
index b4222a6..3cde638 100644
--- a/test/gfm/emphasis_and_strong_emphasis.unit
+++ b/test/gfm/emphasis_and_strong_emphasis.unit
@@ -9,7 +9,7 @@
 >>> Emphasis and strong emphasis - 362
 a*"foo"*
 <<<
-<p>a*"foo"*</p>
+<p>a*&quot;foo&quot;*</p>
 >>> Emphasis and strong emphasis - 363
 * a *
 <<<
@@ -33,7 +33,7 @@
 >>> Emphasis and strong emphasis - 368
 a_"foo"_
 <<<
-<p>a_"foo"_</p>
+<p>a_&quot;foo&quot;_</p>
 >>> Emphasis and strong emphasis - 369
 foo_bar_
 <<<
@@ -49,7 +49,7 @@
 >>> Emphasis and strong emphasis - 372
 aa_"bb"_cc
 <<<
-<p>aa_"bb"_cc</p>
+<p>aa_&quot;bb&quot;_cc</p>
 >>> Emphasis and strong emphasis - 373
 foo-_(bar)_
 <<<
@@ -119,7 +119,7 @@
 >>> Emphasis and strong emphasis - 389
 a**"foo"**
 <<<
-<p>a**"foo"**</p>
+<p>a**&quot;foo&quot;**</p>
 >>> Emphasis and strong emphasis - 390
 foo**bar**
 <<<
@@ -141,7 +141,7 @@
 >>> Emphasis and strong emphasis - 394
 a__"foo"__
 <<<
-<p>a__"foo"__</p>
+<p>a__&quot;foo&quot;__</p>
 >>> Emphasis and strong emphasis - 395
 foo__bar__
 <<<
@@ -183,7 +183,7 @@
 >>> Emphasis and strong emphasis - 404
 **foo "*bar*" foo**
 <<<
-<p><strong>foo "<em>bar</em>" foo</strong></p>
+<p><strong>foo &quot;<em>bar</em>&quot; foo</strong></p>
 >>> Emphasis and strong emphasis - 405
 **foo**bar
 <<<
diff --git a/test/gfm/entity_and_numeric_character_references.unit b/test/gfm/entity_and_numeric_character_references.unit
index be80572..2a6248e 100644
--- a/test/gfm/entity_and_numeric_character_references.unit
+++ b/test/gfm/entity_and_numeric_character_references.unit
@@ -3,9 +3,9 @@
 &frac34; &HilbertSpace; &DifferentialD;
 &ClockwiseContourIntegral; &ngE;
 <<<
-<p>&nbsp; &amp; &copy; &AElig; &Dcaron;
-&frac34; &HilbertSpace; &DifferentialD;
-&ClockwiseContourIntegral; &ngE;</p>
+<p>&amp; © Æ Ď
+¾ ℋ ⅆ
+∲ ≧̸</p>
 >>> Entity and numeric character references - 322
 &#35; &#1234; &#992; &#0;
 <<<
@@ -13,17 +13,17 @@
 >>> Entity and numeric character references - 323
 &#X22; &#XD06; &#xcab;
 <<<
-<p>" ആ ಫ</p>
+<p>&quot; ആ ಫ</p>
 >>> Entity and numeric character references - 324
 &nbsp &x; &#; &#x;
 &#987654321;
 &#abcdef0;
 &ThisIsNotDefined; &hi?;
 <<<
-<p>&amp;nbsp &x; &#; &#x;
-&#987654321;
-&#abcdef0;
-&ThisIsNotDefined; &amp;hi?;</p>
+<p>&amp;nbsp &amp;x; &amp;#; &amp;#x;
+&amp;#987654321;
+&amp;#abcdef0;
+&amp;ThisIsNotDefined; &amp;hi?;</p>
 >>> Entity and numeric character references - 325
 &copy
 <<<
@@ -31,7 +31,7 @@
 >>> Entity and numeric character references - 326
 &MadeUpEntity;
 <<<
-<p>&MadeUpEntity;</p>
+<p>&amp;MadeUpEntity;</p>
 >>> Entity and numeric character references - 327
 <a href="&ouml;&ouml;.html">
 <<<
@@ -39,13 +39,13 @@
 >>> Entity and numeric character references - 328
 [foo](/f&ouml;&ouml; "f&ouml;&ouml;")
 <<<
-<p><a href="/f&ouml;&ouml;" title="f&ouml;&ouml;">foo</a></p>
+<p><a href="/f%C3%B6%C3%B6" title="föö">foo</a></p>
 >>> Entity and numeric character references - 329
 [foo]
 
 [foo]: /f&ouml;&ouml; "f&ouml;&ouml;"
 <<<
-<p><a href="/f&ouml;&ouml;" title="f&ouml;&ouml;">foo</a></p>
+<p><a href="/f%C3%B6%C3%B6" title="föö">foo</a></p>
 >>> Entity and numeric character references - 330
 ``` f&ouml;&ouml;
 foo
diff --git a/test/gfm/images.unit b/test/gfm/images.unit
index 838a07c..e0bfb90 100644
--- a/test/gfm/images.unit
+++ b/test/gfm/images.unit
@@ -100,7 +100,7 @@
 [[foo]]: /url "title"
 <<<
 <p>![[foo]]</p>
-<p>[[foo]]: /url "title"</p>
+<p>[[foo]]: /url &quot;title&quot;</p>
 >>> Images - 599
 ![Foo]
 
diff --git a/test/gfm/link_reference_definitions.unit b/test/gfm/link_reference_definitions.unit
index 16f86fb..442c431 100644
--- a/test/gfm/link_reference_definitions.unit
+++ b/test/gfm/link_reference_definitions.unit
@@ -72,7 +72,7 @@
 
 [foo]
 <<<
-<p><a href="<>">foo</a></p>
+<p><a href="%3C%3E">foo</a></p>
 >>> Link reference definitions - 170
 [foo]: <bar>(baz)
 
@@ -84,7 +84,7 @@
 
 [foo]
 <<<
-<p>[foo]: /url\bar*baz "foo&quot;bar\baz"</p>
+<p>[foo]: /url\bar*baz &quot;foo&quot;bar\baz&quot;</p>
 <p>[foo]</p>
 >>> Link reference definitions - 172
 [foo]
@@ -110,7 +110,7 @@
 
 [αγω]
 <<<
-<p><a href="/φου">αγω</a></p>
+<p><a href="/%CF%86%CE%BF%CF%85">αγω</a></p>
 >>> Link reference definitions - 176
 [foo]: /url
 <<<
@@ -125,18 +125,18 @@
 >>> Link reference definitions - 178
 [foo]: /url "title" ok
 <<<
-<p>[foo]: /url "title" ok</p>
+<p>[foo]: /url &quot;title&quot; ok</p>
 >>> Link reference definitions - 179
 [foo]: /url
 "title" ok
 <<<
-<p>"title" ok</p>
+<p>&quot;title&quot; ok</p>
 >>> Link reference definitions - 180
     [foo]: /url "title"
 
 [foo]
 <<<
-<pre><code>[foo]: /url "title"
+<pre><code>[foo]: /url &quot;title&quot;
 </code></pre>
 <p>[foo]</p>
 >>> Link reference definitions - 181
diff --git a/test/gfm/links.unit b/test/gfm/links.unit
index b2d8e7f..38a861e 100644
--- a/test/gfm/links.unit
+++ b/test/gfm/links.unit
@@ -87,7 +87,7 @@
 >>> Links - 511
 [link](foo%20b&auml;)
 <<<
-<p><a href="foo%20b&auml;">link</a></p>
+<p><a href="foo%20b%C3%A4">link</a></p>
 >>> Links - 512
 [link]("title")
 <<<
@@ -103,19 +103,19 @@
 >>> Links - 514
 [link](/url "title \"&quot;")
 <<<
-<p><a href="/url" title="title %22&quot;">link</a></p>
+<p><a href="/url" title="title &quot;&quot;">link</a></p>
 >>> Links - 515
 [link](/url "title")
 <<<
-<p><a href="/url %22title%22">link</a></p>
+<p><a href="/url%C2%A0%22title%22">link</a></p>
 >>> Links - 516
 [link](/url "title "and" title")
 <<<
-<p>[link](/url "title "and" title")</p>
+<p>[link](/url &quot;title &quot;and&quot; title&quot;)</p>
 >>> Links - 517
 [link](/url 'title "and" title')
 <<<
-<p><a href="/url" title="title %22and%22 title">link</a></p>
+<p><a href="/url" title="title &quot;and&quot; title">link</a></p>
 >>> Links - 518
 [link](   /uri
   "title"  )
diff --git a/test/gfm/raw_html.unit b/test/gfm/raw_html.unit
index 1802dba..bc3a388 100644
--- a/test/gfm/raw_html.unit
+++ b/test/gfm/raw_html.unit
@@ -29,11 +29,11 @@
 >>> Raw HTML - 638
 <a h*#ref="hi">
 <<<
-<p>&lt;a h*#ref="hi"&gt;</p>
+<p>&lt;a h*#ref=&quot;hi&quot;&gt;</p>
 >>> Raw HTML - 639
 <a href="hi'> <a href=hi'>
 <<<
-<p>&lt;a href="hi'&gt; &lt;a href=hi'&gt;</p>
+<p>&lt;a href=&quot;hi'&gt; &lt;a href=hi'&gt;</p>
 >>> Raw HTML - 640
 < a><
 foo><bar/ >
@@ -43,7 +43,7 @@
 <p>&lt; a&gt;&lt;
 foo&gt;&lt;bar/ &gt;
 &lt;foo bar=baz
-bim!bop /></p>
+bim!bop /&gt;</p>
 >>> Raw HTML - 641
 <a href='bar'title=title>
 <<<
@@ -55,7 +55,7 @@
 >>> Raw HTML - 643
 </a href="foo">
 <<<
-<p>&lt;/a href="foo"&gt;</p>
+<p>&lt;/a href=&quot;foo&quot;&gt;</p>
 >>> Raw HTML - 644
 foo <!-- this is a
 comment - with hyphen -->
@@ -96,4 +96,4 @@
 >>> Raw HTML - 652
 <a href="\"">
 <<<
-<p>&lt;a href="&quot;"&gt;</p>
+<p>&lt;a href=&quot;&quot;&quot;&gt;</p>
diff --git a/test/gfm/setext_headings.unit b/test/gfm/setext_headings.unit
index 5909731..4306bf4 100644
--- a/test/gfm/setext_headings.unit
+++ b/test/gfm/setext_headings.unit
@@ -99,8 +99,8 @@
 <<<
 <h2>`Foo</h2>
 <p>`</p>
-<h2>&lt;a title="a lot</h2>
-<p>of dashes"/></p>
+<h2>&lt;a title=&quot;a lot</h2>
+<p>of dashes&quot;/&gt;</p>
 >>> Setext headings - 62
 > Foo
 ---
diff --git a/test/markdown_test.dart b/test/markdown_test.dart
index 35fde7e..58b42c6 100644
--- a/test/markdown_test.dart
+++ b/test/markdown_test.dart
@@ -74,7 +74,7 @@
     validateCore('Unicode ellipsis as punctuation', '''
 "Connecting dot **A** to **B.**…"
 ''', '''
-<p>"Connecting dot <strong>A</strong> to <strong>B.</strong>…"</p>
+<p>&quot;Connecting dot <strong>A</strong> to <strong>B.</strong>…&quot;</p>
 ''');
   });
 
diff --git a/test/original/autolinks.unit b/test/original/autolinks.unit
index 804b65c..d3658e8 100644
--- a/test/original/autolinks.unit
+++ b/test/original/autolinks.unit
@@ -7,4 +7,4 @@
 <http://foo.com/?a=1&b=2>
 
 <<<
-<p><a href="http://foo.com/?a=1&b=2">http://foo.com/?a=1&amp;b=2</a></p>
+<p><a href="http://foo.com/?a=1&amp;b=2">http://foo.com/?a=1&amp;b=2</a></p>
diff --git a/test/original/backslash_escapes.unit b/test/original/backslash_escapes.unit
index 05a8c99..e3446f3 100644
--- a/test/original/backslash_escapes.unit
+++ b/test/original/backslash_escapes.unit
@@ -5,7 +5,7 @@
 and \~.
 
 <<<
-<p>Punctuations like ! and &quot; and # and $ and % and & and ' and ( and )
+<p>Punctuations like ! and &quot; and # and $ and % and &amp; and ' and ( and )
 and * and + and , and - and . and / and : and ; and &lt; and = and &gt;
 and ? and @ and [ and \ and ] and ^ and _ and ` and { and | and }
 and ~.</p>
diff --git a/test/original/inline_links.unit b/test/original/inline_links.unit
index 4d910c8..0a40407 100644
--- a/test/original/inline_links.unit
+++ b/test/original/inline_links.unit
@@ -84,4 +84,4 @@
 links [are](<http://example.com> "title\") awesome
 
 <<<
-<p>links [are](<a href="http://example.com">http://example.com</a> "title&quot;) awesome</p>
\ No newline at end of file
+<p>links [are](<a href="http://example.com">http://example.com</a> &quot;title&quot;) awesome</p>
\ No newline at end of file
diff --git a/tool/common_mark_stats.json b/tool/common_mark_stats.json
index 4e814d3..65181ec 100644
--- a/tool/common_mark_stats.json
+++ b/tool/common_mark_stats.json
@@ -21,7 +21,7 @@
  },
  "Autolinks": {
   "593": "strict",
-  "594": "loose",
+  "594": "strict",
   "595": "strict",
   "596": "strict",
   "597": "strict",
@@ -41,9 +41,9 @@
   "611": "strict"
  },
  "Backslash escapes": {
-  "12": "loose",
+  "12": "strict",
   "13": "strict",
-  "14": "loose",
+  "14": "strict",
   "15": "strict",
   "16": "strict",
   "17": "strict",
@@ -101,7 +101,7 @@
   "340": "strict",
   "341": "strict",
   "342": "strict",
-  "343": "loose",
+  "343": "strict",
   "344": "strict",
   "345": "strict",
   "346": "strict",
@@ -112,17 +112,17 @@
  "Emphasis and strong emphasis": {
   "350": "strict",
   "351": "strict",
-  "352": "loose",
+  "352": "strict",
   "353": "fail",
   "354": "strict",
   "355": "strict",
   "356": "strict",
   "357": "strict",
-  "358": "loose",
+  "358": "strict",
   "359": "strict",
   "360": "strict",
   "361": "strict",
-  "362": "loose",
+  "362": "strict",
   "363": "strict",
   "364": "strict",
   "365": "strict",
@@ -139,12 +139,12 @@
   "376": "strict",
   "377": "strict",
   "378": "strict",
-  "379": "loose",
+  "379": "strict",
   "380": "strict",
   "381": "strict",
   "382": "strict",
   "383": "strict",
-  "384": "loose",
+  "384": "strict",
   "385": "strict",
   "386": "strict",
   "387": "strict",
@@ -154,7 +154,7 @@
   "391": "strict",
   "392": "strict",
   "393": "strict",
-  "394": "loose",
+  "394": "strict",
   "395": "strict",
   "396": "strict",
   "397": "strict",
@@ -245,13 +245,13 @@
  "Entity and numeric character references": {
   "25": "loose",
   "26": "strict",
-  "27": "loose",
-  "28": "loose",
+  "27": "strict",
+  "28": "strict",
   "29": "strict",
-  "30": "loose",
+  "30": "strict",
   "31": "strict",
-  "32": "fail",
-  "33": "fail",
+  "32": "strict",
+  "33": "strict",
   "34": "strict",
   "35": "strict",
   "36": "strict",
@@ -374,7 +374,7 @@
   "586": "strict",
   "587": "strict",
   "588": "strict",
-  "589": "loose",
+  "589": "strict",
   "590": "strict",
   "591": "strict",
   "592": "strict"
@@ -411,12 +411,12 @@
   "203": "strict",
   "204": "strict",
   "205": "strict",
-  "206": "fail",
+  "206": "strict",
   "207": "loose",
   "208": "strict",
-  "209": "loose",
-  "210": "loose",
-  "211": "loose",
+  "209": "strict",
+  "210": "strict",
+  "211": "strict",
   "212": "strict",
   "213": "strict",
   "214": "strict",
@@ -447,13 +447,13 @@
   "499": "strict",
   "500": "strict",
   "501": "strict",
-  "502": "fail",
+  "502": "strict",
   "503": "strict",
   "504": "strict",
-  "505": "fail",
-  "506": "fail",
-  "507": "loose",
-  "508": "fail",
+  "505": "strict",
+  "506": "strict",
+  "507": "strict",
+  "508": "strict",
   "509": "strict",
   "510": "strict",
   "511": "strict",
@@ -615,12 +615,12 @@
   "615": "strict",
   "616": "strict",
   "617": "strict",
-  "618": "loose",
-  "619": "loose",
-  "620": "loose",
+  "618": "strict",
+  "619": "strict",
+  "620": "strict",
   "621": "strict",
   "622": "strict",
-  "623": "loose",
+  "623": "strict",
   "624": "strict",
   "625": "strict",
   "626": "strict",
@@ -629,7 +629,7 @@
   "629": "strict",
   "630": "strict",
   "631": "strict",
-  "632": "loose"
+  "632": "strict"
  },
  "Setext headings": {
   "80": "strict",
@@ -643,7 +643,7 @@
   "88": "strict",
   "89": "strict",
   "90": "strict",
-  "91": "loose",
+  "91": "strict",
   "92": "strict",
   "93": "fail",
   "94": "strict",
diff --git a/tool/common_mark_stats.txt b/tool/common_mark_stats.txt
index 4b6f751..72b98de 100644
--- a/tool/common_mark_stats.txt
+++ b/tool/common_mark_stats.txt
@@ -5,15 +5,15 @@
   25 of   25 – 100.0%  Block quotes
   22 of   22 – 100.0%  Code spans
  130 of  131 –  99.2%  Emphasis and strong emphasis
-  15 of   17 –  88.2%  Entity and numeric character references
+  17 of   17 – 100.0%  Entity and numeric character references
   29 of   29 – 100.0%  Fenced code blocks
   15 of   15 – 100.0%  Hard line breaks
   44 of   44 – 100.0%  HTML blocks
   21 of   22 –  95.5%  Images
   11 of   12 –  91.7%  Indented code blocks
    1 of    1 – 100.0%  Inlines
-  20 of   27 –  74.1%  Link reference definitions
-  85 of   90 –  94.4%  Links
+  21 of   27 –  77.8%  Link reference definitions
+  89 of   90 –  98.9%  Links
   45 of   48 –  93.8%  List items
   22 of   26 –  84.6%  Lists
    8 of    8 – 100.0%  Paragraphs
@@ -24,5 +24,5 @@
   11 of   11 – 100.0%  Tabs
    3 of    3 – 100.0%  Textual content
   19 of   19 – 100.0%  Thematic breaks
- 626 of  652 –  96.0%  TOTAL
- 573 of  626 –  91.5%  TOTAL Strict
+ 633 of  652 –  97.1%  TOTAL
+ 604 of  633 –  95.4%  TOTAL Strict
diff --git a/tool/gfm_stats.json b/tool/gfm_stats.json
index ff4d34a..657aaff 100644
--- a/tool/gfm_stats.json
+++ b/tool/gfm_stats.json
@@ -21,7 +21,7 @@
  },
  "Autolinks": {
   "602": "strict",
-  "603": "loose",
+  "603": "strict",
   "604": "strict",
   "605": "strict",
   "606": "strict",
@@ -46,7 +46,7 @@
   "623": "strict",
   "624": "strict",
   "625": "strict",
-  "626": "loose",
+  "626": "strict",
   "627": "strict",
   "628": "strict",
   "629": "strict",
@@ -54,9 +54,9 @@
   "631": "strict"
  },
  "Backslash escapes": {
-  "308": "loose",
+  "308": "strict",
   "309": "strict",
-  "310": "loose",
+  "310": "strict",
   "311": "strict",
   "312": "strict",
   "313": "strict",
@@ -114,7 +114,7 @@
   "350": "strict",
   "351": "strict",
   "352": "strict",
-  "353": "loose",
+  "353": "strict",
   "354": "strict",
   "355": "strict",
   "356": "strict",
@@ -128,17 +128,17 @@
  "Emphasis and strong emphasis": {
   "360": "strict",
   "361": "strict",
-  "362": "loose",
+  "362": "strict",
   "363": "fail",
   "364": "strict",
   "365": "strict",
   "366": "strict",
   "367": "strict",
-  "368": "loose",
+  "368": "strict",
   "369": "strict",
   "370": "strict",
   "371": "strict",
-  "372": "loose",
+  "372": "strict",
   "373": "strict",
   "374": "strict",
   "375": "strict",
@@ -155,12 +155,12 @@
   "386": "strict",
   "387": "strict",
   "388": "strict",
-  "389": "loose",
+  "389": "strict",
   "390": "strict",
   "391": "strict",
   "392": "strict",
   "393": "strict",
-  "394": "loose",
+  "394": "strict",
   "395": "strict",
   "396": "strict",
   "397": "strict",
@@ -170,7 +170,7 @@
   "401": "strict",
   "402": "strict",
   "403": "strict",
-  "404": "loose",
+  "404": "strict",
   "405": "strict",
   "406": "strict",
   "407": "strict",
@@ -261,13 +261,13 @@
  "Entity and numeric character references": {
   "321": "loose",
   "322": "strict",
-  "323": "loose",
-  "324": "loose",
+  "323": "strict",
+  "324": "strict",
   "325": "strict",
-  "326": "loose",
+  "326": "strict",
   "327": "strict",
-  "328": "fail",
-  "329": "fail",
+  "328": "strict",
+  "329": "strict",
   "330": "strict",
   "331": "strict",
   "332": "strict",
@@ -389,7 +389,7 @@
   "595": "strict",
   "596": "strict",
   "597": "strict",
-  "598": "loose",
+  "598": "strict",
   "599": "strict",
   "600": "strict",
   "601": "strict"
@@ -426,12 +426,12 @@
   "172": "strict",
   "173": "strict",
   "174": "strict",
-  "175": "fail",
+  "175": "strict",
   "176": "loose",
   "177": "strict",
-  "178": "loose",
-  "179": "loose",
-  "180": "loose",
+  "178": "strict",
+  "179": "strict",
+  "180": "strict",
   "181": "strict",
   "182": "strict",
   "183": "strict",
@@ -460,13 +460,13 @@
   "508": "strict",
   "509": "strict",
   "510": "strict",
-  "511": "fail",
+  "511": "strict",
   "512": "strict",
   "513": "strict",
-  "514": "fail",
-  "515": "fail",
-  "516": "loose",
-  "517": "fail",
+  "514": "strict",
+  "515": "strict",
+  "516": "strict",
+  "517": "strict",
   "518": "strict",
   "519": "strict",
   "520": "strict",
@@ -628,12 +628,12 @@
   "635": "strict",
   "636": "strict",
   "637": "strict",
-  "638": "loose",
-  "639": "loose",
-  "640": "loose",
+  "638": "strict",
+  "639": "strict",
+  "640": "strict",
   "641": "strict",
   "642": "strict",
-  "643": "loose",
+  "643": "strict",
   "644": "strict",
   "645": "strict",
   "646": "strict",
@@ -642,7 +642,7 @@
   "649": "strict",
   "650": "strict",
   "651": "strict",
-  "652": "loose"
+  "652": "strict"
  },
  "Setext headings": {
   "50": "strict",
@@ -656,7 +656,7 @@
   "58": "strict",
   "59": "strict",
   "60": "strict",
-  "61": "loose",
+  "61": "strict",
   "62": "strict",
   "63": "fail",
   "64": "strict",
diff --git a/tool/gfm_stats.txt b/tool/gfm_stats.txt
index 5cb3981..aea55ba 100644
--- a/tool/gfm_stats.txt
+++ b/tool/gfm_stats.txt
@@ -7,15 +7,15 @@
   22 of   22 – 100.0%  Code spans
    0 of    1 –   0.0%  Disallowed Raw HTML (extension)
  130 of  131 –  99.2%  Emphasis and strong emphasis
-  15 of   17 –  88.2%  Entity and numeric character references
+  17 of   17 – 100.0%  Entity and numeric character references
   29 of   29 – 100.0%  Fenced code blocks
   15 of   15 – 100.0%  Hard line breaks
   43 of   43 – 100.0%  HTML blocks
   21 of   22 –  95.5%  Images
   11 of   12 –  91.7%  Indented code blocks
    1 of    1 – 100.0%  Inlines
-  21 of   28 –  75.0%  Link reference definitions
-  82 of   87 –  94.3%  Links
+  22 of   28 –  78.6%  Link reference definitions
+  86 of   87 –  98.9%  Links
   45 of   48 –  93.8%  List items
   22 of   26 –  84.6%  Lists
    8 of    8 – 100.0%  Paragraphs
@@ -28,5 +28,5 @@
   11 of   11 – 100.0%  Tabs
    3 of    3 – 100.0%  Textual content
   19 of   19 – 100.0%  Thematic breaks
- 644 of  671 –  96.0%  TOTAL
- 589 of  644 –  91.5%  TOTAL Strict
+ 651 of  671 –  97.0%  TOTAL
+ 621 of  651 –  95.4%  TOTAL Strict