Refactor list syntax (dart-lang/markdown#499)

* Refactor ListSyntax

* Update CHANGELOG.md

* Some cleanup and fixing

* Add a test case for gitHubFlavored ExtensionSet

* Use `=>` syntax for `canParse`
diff --git a/pkgs/markdown/CHANGELOG.md b/pkgs/markdown/CHANGELOG.md
index fead92f..91da8b7 100644
--- a/pkgs/markdown/CHANGELOG.md
+++ b/pkgs/markdown/CHANGELOG.md
@@ -19,6 +19,8 @@
 * Add a new public method `parse(String text)` for `Document`.
 * Add a new public method `parseLineList(List<Line> text)` for `Document`.
 * Add a new type: `Line`.
+* Add a new optional parameter `parentSyntax` for `parseLines()` of
+  `BlockParser`, which can be used when parsing nested blocks.
 
 ## 6.0.1
 
diff --git a/pkgs/markdown/benchmark/output.html b/pkgs/markdown/benchmark/output.html
index e3831bd..200e4bd 100644
--- a/pkgs/markdown/benchmark/output.html
+++ b/pkgs/markdown/benchmark/output.html
@@ -290,7 +290,7 @@
 void main() {
   group(&quot;complicated algorithm tests&quot;, () {
     // ...
-  }, skip: &quot;the algorithm isn&#39;t quite right&quot;);
+  }, skip: &quot;the algorithm isn't quite right&quot;);
 
   test(&quot;error-checking test&quot;, () {
     // ...
diff --git a/pkgs/markdown/lib/src/block_parser.dart b/pkgs/markdown/lib/src/block_parser.dart
index 5d930fe..023e9cb 100644
--- a/pkgs/markdown/lib/src/block_parser.dart
+++ b/pkgs/markdown/lib/src/block_parser.dart
@@ -110,7 +110,14 @@
     return regex.hasMatch(next!.content);
   }
 
-  List<Node> parseLines() {
+  /// The parent [BlockSyntax] when it is running inside a nested syntax.
+  BlockSyntax? get parentSyntax => _parentSyntax;
+  BlockSyntax? _parentSyntax;
+
+  List<Node> parseLines({
+    BlockSyntax? parentSyntax,
+  }) {
+    _parentSyntax = parentSyntax;
     final blocks = <Node>[];
 
     // If the `_pos` does not change before and after `parse()`, never try to
diff --git a/pkgs/markdown/lib/src/block_syntaxes/code_block_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/code_block_syntax.dart
index fdcb5f1..38c9951 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/code_block_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/code_block_syntax.dart
@@ -34,7 +34,11 @@
           pattern.hasMatch(parser.current.content) != true) {
         break;
       }
-      childLines.add(Line(parser.current.content.dedent().text));
+
+      childLines.add(Line(
+        parser.current.content.dedent().text,
+        tabRemaining: parser.current.tabRemaining,
+      ));
 
       parser.advance();
     }
@@ -49,7 +53,9 @@
     // The Markdown tests expect a trailing newline.
     childLines.add(Line(''));
 
-    var content = childLines.map((e) => e.content).join('\n');
+    var content = childLines
+        .map((e) => e.content.prependSpace(e.tabRemaining ?? 0))
+        .join('\n');
     if (parser.document.encodeHtml) {
       content = escapeHtml(content, escapeApos: false);
     }
diff --git a/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart
index 1acb080..77f6791 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart
@@ -4,8 +4,11 @@
 
 import '../ast.dart';
 import '../block_parser.dart';
+import '../charcode.dart';
 import '../line.dart';
 import '../patterns.dart';
+import '../text_parser.dart';
+import '../util.dart';
 import 'block_syntax.dart';
 import 'ordered_list_with_checkbox_syntax.dart';
 import 'unordered_list_with_checkbox_syntax.dart';
@@ -25,6 +28,11 @@
 /// Base class for both ordered and unordered lists.
 abstract class ListSyntax extends BlockSyntax {
   @override
+  bool canParse(BlockParser parser) =>
+      pattern.hasMatch(parser.current.content) &&
+      !hrPattern.hasMatch(parser.current.content);
+
+  @override
   bool canEndBlock(BlockParser parser) {
     // An empty list cannot interrupt a paragraph. See
     // https://spec.commonmark.org/0.29/#example-255.
@@ -32,12 +40,27 @@
     // which accepts a [BlockParser], but this would be a breaking change,
     // so we're going with this temporarily.
     final match = pattern.firstMatch(parser.current.content)!;
-    // The seventh group, in both [olPattern] and [ulPattern] is the text
-    // after the delimiter.
-    return match[7]?.isNotEmpty ?? false;
-  }
 
-  String get listTag;
+    // Allow only lists starting with 1 to interrupt paragraphs, if it is an
+    // ordered list. See https://spec.commonmark.org/0.30/#example-304.
+    // But there shuold be an exception for nested ordered lists, for example:
+    // ```
+    // 1. one
+    // 2. two
+    //   3. three
+    //   4. four
+    // 5. five
+    // ```
+    if (parser.parentSyntax is! ListSyntax &&
+        match[1] != null &&
+        match[1] != '1') {
+      return false;
+    }
+
+    // An empty list item cannot interrupt a paragraph. See
+    // https://spec.commonmark.org/0.30/#example-285
+    return match[2]?.isNotEmpty ?? false;
+  }
 
   const ListSyntax();
 
@@ -47,14 +70,14 @@
     headerPattern,
     hrPattern,
     indentPattern,
-    ulPattern,
-    olPattern
+    listPattern,
   ];
 
-  static final _whitespaceRe = RegExp('[ \t]*');
-
   @override
   Node parse(BlockParser parser) {
+    final match = pattern.firstMatch(parser.current.content);
+    final ordered = match![1] != null;
+
     final taskListParserEnabled = this is UnorderedListWithCheckboxSyntax ||
         this is OrderedListWithCheckboxSyntax;
     final items = <ListItem>[];
@@ -92,72 +115,117 @@
     }
 
     String? listMarker;
-    String? indent;
+    int? indent;
     // In case the first number in an ordered list is not 1, use it as the
     // "start".
     int? startNumber;
 
+    int? blankLines;
+
     while (!parser.isDone) {
-      final leadingSpace =
-          _whitespaceRe.matchAsPrefix(parser.current.content)!.group(0)!;
-      final leadingExpandedTabLength = _expandedTabLength(leadingSpace);
+      final currentIndent = parser.current.content.indentation() +
+          (parser.current.tabRemaining ?? 0);
+
       if (parser.current.isBlankLine) {
-        if (parser.next?.isBlankLine ?? true) {
-          // Two blank lines ends a list.
+        childLines.add(parser.current);
+
+        if (blankLines != null) {
+          blankLines++;
+        }
+      } else if (indent != null && indent <= currentIndent) {
+        // A list item can begin with at most one blank line. See:
+        // https://spec.commonmark.org/0.30/#example-280
+        if (blankLines != null && blankLines > 1) {
           break;
         }
-        // Add a blank line to the current list item.
-        childLines.add(Line(''));
-      } else if (indent != null && indent.length <= leadingExpandedTabLength) {
-        // Strip off indent and add to current item.
-        final line = parser.current.content
-            .replaceFirst(leadingSpace, ' ' * leadingExpandedTabLength)
-            .replaceFirst(indent, '');
-        childLines.add(Line(parseTaskListItem(line)));
+
+        final indentedLine = parser.current.content.dedent(indent);
+
+        childLines.add(Line(
+          blankLines == null
+              ? indentedLine.text
+              : parseTaskListItem(indentedLine.text),
+          tabRemaining: indentedLine.tabRemaining,
+        ));
       } else if (tryMatch(hrPattern)) {
         // Horizontal rule takes precedence to a new list item.
         break;
-      } else if (tryMatch(ulPattern) || tryMatch(olPattern)) {
+      } else if (tryMatch(listPattern)) {
+        blankLines = null;
         final match = possibleMatch!;
-        final precedingWhitespace = match[1]!;
-        final digits = match[2] ?? '';
-        if (startNumber == null && digits.isNotEmpty) {
-          startNumber = int.parse(digits);
+        final textParser = TextParser(parser.current.content);
+        var precedingWhitespaces = textParser.moveThroughWhitespace();
+        final markerStart = textParser.pos;
+        final digits = match[1] ?? '';
+        if (digits.isNotEmpty) {
+          startNumber ??= int.parse(digits);
+          textParser.advanceBy(digits.length);
         }
-        final marker = match[3]!;
-        final firstWhitespace = match[5] ?? '';
-        final restWhitespace = match[6] ?? '';
-        final content = match[7] ?? '';
-        final isBlank = content.isEmpty;
-        if (listMarker != null && listMarker != marker) {
-          // Changing the bullet or ordered list delimiter starts a new list.
+        textParser.advance();
+
+        // See https://spec.commonmark.org/0.30/#ordered-list-marker
+        final marker = textParser.substring(
+          markerStart,
+          textParser.pos,
+        );
+
+        var isBlank = true;
+        var contentWhitespances = 0;
+        var containsTab = false;
+        int? contentBlockStart;
+
+        if (!textParser.isDone) {
+          containsTab = textParser.charAt() == $tab;
+          // Skip the first whitespace.
+          textParser.advance();
+          contentBlockStart = textParser.pos;
+          if (!textParser.isDone) {
+            contentWhitespances = textParser.moveThroughWhitespace();
+
+            if (!textParser.isDone) {
+              isBlank = false;
+            }
+          }
+        }
+
+        // Changing the bullet or ordered list delimiter starts a new list.
+        if (listMarker != null && listMarker.last() != marker.last()) {
           break;
         }
+
+        // End the current list item and start a new one.
+        endItem();
+
+        // Start a new list item, the last item will be ended up outside of the
+        // `while` loop.
         listMarker = marker;
-        final markerAsSpaces = ' ' * (digits.length + marker.length);
+        precedingWhitespaces += digits.length + 2;
         if (isBlank) {
-          // See http://spec.commonmark.org/0.28/#list-items under "3. Item
-          // starting with a blank line."
-          //
-          // If the list item starts with a blank line, the final piece of the
-          // indentation is just a single space.
-          indent = '$precedingWhitespace$markerAsSpaces ';
-        } else if (restWhitespace.length >= 4) {
-          // See http://spec.commonmark.org/0.28/#list-items under "2. Item
-          // starting with indented code."
+          // See https://spec.commonmark.org/0.30/#example-278.
+          blankLines = 1;
+          indent = precedingWhitespaces;
+        } else if (contentWhitespances >= 4) {
+          // See https://spec.commonmark.org/0.30/#example-270.
           //
           // If the list item starts with indented code, we need to _not_ count
           // any indentation past the required whitespace character.
-          indent = precedingWhitespace + markerAsSpaces + firstWhitespace;
+          indent = precedingWhitespaces;
         } else {
-          indent = precedingWhitespace +
-              markerAsSpaces +
-              firstWhitespace +
-              restWhitespace;
+          indent = precedingWhitespaces + contentWhitespances;
         }
-        // End the current list item and start a new one.
-        endItem();
-        childLines.add(Line(parseTaskListItem('$restWhitespace$content')));
+
+        var content = contentBlockStart != null && !isBlank
+            ? parseTaskListItem(textParser.substring(contentBlockStart))
+            : '';
+
+        if (content.isEmpty && containsTab) {
+          content = content.prependSpace(2);
+        }
+
+        childLines.add(Line(
+          content,
+          tabRemaining: containsTab ? 2 : null,
+        ));
       } else if (BlockSyntax.isAtBlockEnd(parser)) {
         // Done with the list.
         break;
@@ -195,7 +263,7 @@
       }
 
       final itemParser = BlockParser(item.lines, parser.document);
-      final children = itemParser.parseLines();
+      final children = itemParser.parseLines(parentSyntax: this);
       final itemElement = checkboxToInsert == null
           ? Element('li', children)
           : (Element('li', [checkboxToInsert, ...children])
@@ -227,8 +295,8 @@
       }
     }
 
-    final listElement = Element(listTag, itemNodes);
-    if (listTag == 'ol' && startNumber != 1) {
+    final listElement = Element(ordered ? 'ol' : 'ul', itemNodes);
+    if (ordered && startNumber != 1) {
       listElement.attributes['start'] = '$startNumber';
     }
 
@@ -259,12 +327,4 @@
     }
     return anyEmpty;
   }
-
-  static int _expandedTabLength(String input) {
-    var length = 0;
-    for (final char in input.codeUnits) {
-      length += char == 0x9 ? 4 - (length % 4) : 1;
-    }
-    return length;
-  }
 }
diff --git a/pkgs/markdown/lib/src/block_syntaxes/ordered_list_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/ordered_list_syntax.dart
index 61570a3..53c4730 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/ordered_list_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/ordered_list_syntax.dart
@@ -8,10 +8,7 @@
 /// Parses ordered lists.
 class OrderedListSyntax extends ListSyntax {
   @override
-  RegExp get pattern => olPattern;
-
-  @override
-  String get listTag => 'ol';
+  RegExp get pattern => listPattern;
 
   const OrderedListSyntax();
 }
diff --git a/pkgs/markdown/lib/src/block_syntaxes/setext_header_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/setext_header_syntax.dart
index c9aa4ec..d4777a5 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/setext_header_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/setext_header_syntax.dart
@@ -65,7 +65,6 @@
           headerPattern.hasMatch(line) ||
           blockquotePattern.hasMatch(line) ||
           hrPattern.hasMatch(line) ||
-          ulPattern.hasMatch(line) ||
-          olPattern.hasMatch(line) ||
+          listPattern.hasMatch(line) ||
           emptyPattern.hasMatch(line));
 }
diff --git a/pkgs/markdown/lib/src/block_syntaxes/unordered_list_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/unordered_list_syntax.dart
index 357e029..35dd670 100644
--- a/pkgs/markdown/lib/src/block_syntaxes/unordered_list_syntax.dart
+++ b/pkgs/markdown/lib/src/block_syntaxes/unordered_list_syntax.dart
@@ -9,7 +9,7 @@
 /// Parses unordered lists.
 class UnorderedListSyntax extends ListSyntax {
   @override
-  RegExp get pattern => ulPattern;
+  RegExp get pattern => listPattern;
 
   @override
   bool canParse(BlockParser parser) {
@@ -26,8 +26,5 @@
     return pattern.hasMatch(parser.current.content);
   }
 
-  @override
-  String get listTag => 'ul';
-
   const UnorderedListSyntax();
 }
diff --git a/pkgs/markdown/lib/src/patterns.dart b/pkgs/markdown/lib/src/patterns.dart
index 2bd696b..03e618c 100644
--- a/pkgs/markdown/lib/src/patterns.dart
+++ b/pkgs/markdown/lib/src/patterns.dart
@@ -34,91 +34,17 @@
 /// SETEXT should win.
 final hrPattern = RegExp(r'^ {0,3}([-*_])[ \t]*\1[ \t]*\1(?:\1|[ \t])*$');
 
-// why `{1}`?
-const _checkbox = r'\[[ xX]{1}\]';
-
-const _groupedWhitespaceAndEverything = r'([ \t])([ \t]*)(.*)';
-
-const _oneToNineDigits = r'\d{1,9}';
-
-const _zeroToFourWhitespace = r'[ \t]{0,4}';
-
-const _zeroToThreeSpaces = '[ ]{0,3}';
-
-/// A line starting with one of these markers: `-`, `*`, `+`.
+/// **Unordered list**
+/// A line starting with one of these markers: `-`, `*`, `+`. May have up to
+/// three leading spaces before the marker and any number of spaces or tabs
+/// after.
 ///
-/// May have up to three leading spaces before the marker and any number of
-/// spaces or tabs after.
+/// **Ordered list**
 ///
-/// Contains a dummy group at `[2]`, so that the groups in [ulPattern] and
-/// [olPattern] match up; in both, `[2]` is the length of the number that begins
-/// the list marker.
-final ulPattern = RegExp(''
-    '^($_zeroToThreeSpaces)'
-    // Empty group for group number alignment with [olPattern].
-    '()'
-    '([*+-])'
-    '($_groupedWhitespaceAndEverything)?\$');
-
-/// Similar to [ulPattern] but with a GitHub-style checkbox
-/// (`'[ ]'|'[x]'|'[X]'`) following the number.
-///
-/// The checkbox will be grabbed by group `[5]` and [ulPattern]'s groups
-/// `[4]`, `[5]`, and `[6]` are all shifted 2 places to be `[6]`, `[7]`, and
-/// `[8]`.
-final ulWithCheckBoxPattern = RegExp(''
-    '^($_zeroToThreeSpaces)'
-    // Empty group for group number alignment with [olWithCheckBoxPattern].
-    '()'
-    '([*+-])'
-    '($_zeroToFourWhitespace)'
-    '($_checkbox)'
-    '($_groupedWhitespaceAndEverything)?\$');
-
-/// Similar to [ulWithCheckBoxPattern] but the checkbox is optional.
-// TODO(srawlins): This is temporary tech debt. I think we will collapse
-// [ulPattern] and [ulWithCheckBoxPattern] into this one pattern.
-final ulWithPossibleCheckboxPattern = RegExp(''
-    '^($_zeroToThreeSpaces)'
-    // Empty group for group number alignment with [olWithCheckBoxPattern].
-    '()'
-    '([*+-])'
-    '(($_zeroToFourWhitespace)($_checkbox))?'
-    // [7], [8], [9], and [10].
-    '($_groupedWhitespaceAndEverything)?\$');
-
 /// A line starting with a number like `123.`. May have up to three leading
 /// spaces before the marker and any number of spaces or tabs after.
-final olPattern = RegExp(''
-    '^($_zeroToThreeSpaces)'
-    '($_oneToNineDigits)'
-    r'([\.)])'
-    '($_groupedWhitespaceAndEverything)?\$');
-
-/// Similar to [olPattern] but with a GitHub-style checkbox
-/// (`'[ ]'|'[x]'|'[X]'`) following the number.
-///
-/// The checkbox will be grabbed by group `[5]` and [olPattern]'s groups
-/// `[4]`, `[5]`, and `[6]` are all shifted 2 places to be `[6]`, `[7]`, and
-/// `[8]`.
-final olWithCheckBoxPattern = RegExp(''
-    '^($_zeroToThreeSpaces)'
-    '($_oneToNineDigits)'
-    r'([\.)])'
-    '($_zeroToFourWhitespace)'
-    '($_checkbox)'
-    '($_groupedWhitespaceAndEverything)?\$');
-
-/// Similar to [olWithCheckBoxPattern] but the checkbox is optional.
-// TODO(srawlins): This is temporary tech debt. I think we will collapse
-// [olPattern] and [olWithCheckBoxPattern] into this one pattern.
-final olWithPossibleCheckboxPattern = RegExp(''
-    '^($_zeroToThreeSpaces)'
-    '($_oneToNineDigits)'
-    r'([\.)])'
-    '(($_zeroToFourWhitespace)($_checkbox))?'
-    // [7], [8], [9], and [10].
-    '($_groupedWhitespaceAndEverything)?\$');
+final listPattern =
+    RegExp(r'^[ ]{0,3}(?:(\d{1,9})[\.)]|[*+-])(?:[ \t]+(.*))?$');
 
 /// A line of hyphens separated by at least one pipe.
 final tablePattern = RegExp(
diff --git a/pkgs/markdown/lib/src/text_parser.dart b/pkgs/markdown/lib/src/text_parser.dart
new file mode 100644
index 0000000..1492268
--- /dev/null
+++ b/pkgs/markdown/lib/src/text_parser.dart
@@ -0,0 +1,57 @@
+// 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 'charcode.dart';
+
+/// A parser to parse a segment of source text.
+class TextParser {
+  final String source;
+
+  TextParser(this.source);
+
+  /// The current read position.
+  var _position = 0;
+  int get pos => _position;
+
+  /// Whether the read position has reached the end of [source].
+  bool get isDone => _position == length;
+
+  /// The length of [source].
+  int get length => source.length;
+
+  /// Walk the parser forward through any whitespace.
+  ///
+  /// Set [multiLine] `true` to support multiline, otherwise it will stop before
+  /// the line feed [$lf].
+  int moveThroughWhitespace({bool multiLine = false}) {
+    var i = 0;
+    while (!isDone) {
+      final char = charAt();
+      if (char != $space &&
+          char != $tab &&
+          char != $vt &&
+          char != $cr &&
+          char != $ff &&
+          !(multiLine == true && char == $lf)) {
+        return i;
+      }
+
+      i++;
+      advance();
+    }
+    return i;
+  }
+
+  int charAt([int? position]) => source.codeUnitAt(position ?? _position);
+
+  /// Moves the read position one character ahead.
+  void advance() => advanceBy(1);
+
+  /// Moves the read position for [length] characters. [length] can be negative.
+  void advanceBy(int length) {
+    _position += length;
+  }
+
+  /// Substrings the [source] and returns a [String].
+  String substring(int start, [int? end]) => source.substring(start, end);
+}
diff --git a/pkgs/markdown/lib/src/util.dart b/pkgs/markdown/lib/src/util.dart
index 22ca19c..4073445 100644
--- a/pkgs/markdown/lib/src/util.dart
+++ b/pkgs/markdown/lib/src/util.dart
@@ -136,6 +136,20 @@
 }
 
 extension StringExtensions on String {
+  /// Calculates the length of indentation a `String` has.
+  ///
+  // The behavior of tabs: https://spec.commonmark.org/0.30/#tabs
+  int indentation() {
+    var length = 0;
+    for (final char in codeUnits) {
+      if (char != $space && char != $tab) {
+        break;
+      }
+      length += char == $tab ? 4 - (length % 4) : 1;
+    }
+    return length;
+  }
+
   /// Removes up to [length] characters of leading whitespace.
   // The way of handling tabs: https://spec.commonmark.org/0.30/#tabs
   DedentedText dedent([int length = 4]) {
@@ -172,11 +186,17 @@
     return DedentedText(substring(start), tabRemaining);
   }
 
+  /// Adds [width] of spaces to the beginning of this string.
+  String prependSpace(int width) => '${" " * width}$this';
+
   /// Whether this string contains only whitespaces.
   bool get isBlank => trim().isEmpty;
 
   /// Converts this string to a list of [Line].
   List<Line> toLines() => LineSplitter.split(this).map(Line.new).toList();
+
+  /// Returns the last character.
+  String last([int n = 1]) => substring(length - n);
 }
 
 /// A class that describes a dedented text.
diff --git a/pkgs/markdown/test/common_mark/list_items.unit b/pkgs/markdown/test/common_mark/list_items.unit
index 7f9119f..b6554e0 100644
--- a/pkgs/markdown/test/common_mark/list_items.unit
+++ b/pkgs/markdown/test/common_mark/list_items.unit
@@ -115,9 +115,11 @@
   bar
 <<<
 <ul>
-<li>foo</li>
-</ul>
+<li>
+<p>foo</p>
 <p>bar</p>
+</li>
+</ul>
 >>> List items - 263
 1.  foo
 
@@ -152,11 +154,12 @@
 <li>
 <p>Foo</p>
 <pre><code>bar
+
+
+baz
 </code></pre>
 </li>
 </ul>
-<pre><code>  baz
-</code></pre>
 >>> List items - 265
 123456789. ok
 <<<
@@ -312,10 +315,9 @@
   foo
 <<<
 <ul>
-<li>
-<p>foo</p>
-</li>
+<li></li>
 </ul>
+<p>foo</p>
 >>> List items - 281
 - foo
 -
diff --git a/pkgs/markdown/test/common_mark/lists.unit b/pkgs/markdown/test/common_mark/lists.unit
index 7eb42df..986f2aa 100644
--- a/pkgs/markdown/test/common_mark/lists.unit
+++ b/pkgs/markdown/test/common_mark/lists.unit
@@ -36,10 +36,8 @@
 The number of windows in my house is
 14.  The number of doors is 6.
 <<<
-<p>The number of windows in my house is</p>
-<ol start="14">
-<li>The number of doors is 6.</li>
-</ol>
+<p>The number of windows in my house is
+14.  The number of doors is 6.</p>
 >>> Lists - 305
 The number of windows in my house is
 1.  The number of doors is 6.
@@ -63,9 +61,9 @@
 <li>
 <p>bar</p>
 </li>
-</ul>
-<ul>
-<li>baz</li>
+<li>
+<p>baz</p>
+</li>
 </ul>
 >>> Lists - 307
 - foo
@@ -80,14 +78,15 @@
 <ul>
 <li>bar
 <ul>
-<li>baz</li>
+<li>
+<p>baz</p>
+<p>bim</p>
+</li>
 </ul>
 </li>
 </ul>
 </li>
 </ul>
-<pre><code>  bim
-</code></pre>
 >>> Lists - 308
 - foo
 - bar
@@ -275,11 +274,12 @@
 <li>a</li>
 <li>
 <pre><code>b
+
+
 </code></pre>
 </li>
+<li>c</li>
 </ul>
-<pre><code>- c
-</code></pre>
 >>> Lists - 319
 - a
   - b
diff --git a/pkgs/markdown/test/common_mark/tabs.unit b/pkgs/markdown/test/common_mark/tabs.unit
index cee429d..1239481 100644
--- a/pkgs/markdown/test/common_mark/tabs.unit
+++ b/pkgs/markdown/test/common_mark/tabs.unit
@@ -50,7 +50,7 @@
 <<<
 <ul>
 <li>
-<pre><code>foo
+<pre><code>  foo
 </code></pre>
 </li>
 </ul>
diff --git a/pkgs/markdown/test/extensions/ordered_list_with_checkboxes.unit b/pkgs/markdown/test/extensions/ordered_list_with_checkboxes.unit
index 84cf86e..3dce7b7 100644
--- a/pkgs/markdown/test/extensions/ordered_list_with_checkboxes.unit
+++ b/pkgs/markdown/test/extensions/ordered_list_with_checkboxes.unit
@@ -45,21 +45,19 @@
 <li>five</li>
 </ol>
 >>> mixed leading spaces
-1.[ ] zero
-2. [ ] one
-3.  [ ] two
-4.   [ ] three
-5.    [ ] four
-6.     [ ] five
+1. [ ] zero
+2.  [ ] one
+3.   [ ] two
+4.    [ ] three
+5.     [ ] four
 <<<
-<p>1.[ ] zero</p>
-<ol start="2" class="contains-task-list">
+<ol class="contains-task-list">
+<li class="task-list-item"><input type="checkbox"></input>zero</li>
 <li class="task-list-item"><input type="checkbox"></input>one</li>
 <li class="task-list-item"><input type="checkbox"></input>two</li>
 <li class="task-list-item"><input type="checkbox"></input>three</li>
-<li class="task-list-item"><input type="checkbox"></input>four</li>
 <li>
-<pre><code>[ ] five
+<pre><code>[ ] four
 </code></pre>
 </li>
 </ol>
\ No newline at end of file
diff --git a/pkgs/markdown/test/gfm/list_items.unit b/pkgs/markdown/test/gfm/list_items.unit
index fae2958..bb2cb72 100644
--- a/pkgs/markdown/test/gfm/list_items.unit
+++ b/pkgs/markdown/test/gfm/list_items.unit
@@ -115,9 +115,11 @@
   bar
 <<<
 <ul>
-<li>foo</li>
-</ul>
+<li>
+<p>foo</p>
 <p>bar</p>
+</li>
+</ul>
 >>> List items - 241
 1.  foo
 
@@ -152,11 +154,12 @@
 <li>
 <p>Foo</p>
 <pre><code>bar
+
+
+baz
 </code></pre>
 </li>
 </ul>
-<pre><code>  baz
-</code></pre>
 >>> List items - 243
 123456789. ok
 <<<
@@ -312,10 +315,9 @@
   foo
 <<<
 <ul>
-<li>
-<p>foo</p>
-</li>
+<li></li>
 </ul>
+<p>foo</p>
 >>> List items - 259
 - foo
 -
diff --git a/pkgs/markdown/test/gfm/lists.unit b/pkgs/markdown/test/gfm/lists.unit
index 9911dea..80558b7 100644
--- a/pkgs/markdown/test/gfm/lists.unit
+++ b/pkgs/markdown/test/gfm/lists.unit
@@ -36,10 +36,8 @@
 The number of windows in my house is
 14.  The number of doors is 6.
 <<<
-<p>The number of windows in my house is</p>
-<ol start="14">
-<li>The number of doors is 6.</li>
-</ol>
+<p>The number of windows in my house is
+14.  The number of doors is 6.</p>
 >>> Lists - 285
 The number of windows in my house is
 1.  The number of doors is 6.
@@ -63,9 +61,9 @@
 <li>
 <p>bar</p>
 </li>
-</ul>
-<ul>
-<li>baz</li>
+<li>
+<p>baz</p>
+</li>
 </ul>
 >>> Lists - 287
 - foo
@@ -80,14 +78,15 @@
 <ul>
 <li>bar
 <ul>
-<li>baz</li>
+<li>
+<p>baz</p>
+<p>bim</p>
+</li>
 </ul>
 </li>
 </ul>
 </li>
 </ul>
-<pre><code>  bim
-</code></pre>
 >>> Lists - 288
 - foo
 - bar
@@ -275,11 +274,12 @@
 <li>a</li>
 <li>
 <pre><code>b
+
+
 </code></pre>
 </li>
+<li>c</li>
 </ul>
-<pre><code>- c
-</code></pre>
 >>> Lists - 299
 - a
   - b
diff --git a/pkgs/markdown/test/gfm/tabs.unit b/pkgs/markdown/test/gfm/tabs.unit
index cee429d..1239481 100644
--- a/pkgs/markdown/test/gfm/tabs.unit
+++ b/pkgs/markdown/test/gfm/tabs.unit
@@ -50,7 +50,7 @@
 <<<
 <ul>
 <li>
-<pre><code>foo
+<pre><code>  foo
 </code></pre>
 </li>
 </ul>
diff --git a/pkgs/markdown/test/markdown_test.dart b/pkgs/markdown/test/markdown_test.dart
index 58b42c6..f299f79 100644
--- a/pkgs/markdown/test/markdown_test.dart
+++ b/pkgs/markdown/test/markdown_test.dart
@@ -234,4 +234,21 @@
         ''',
         inlineOnly: true);
   });
+
+  group('ExtensionSet', () {
+    test(
+      '3 asterisks separated with spaces horizontal rule while it is '
+      'gitHubFlavored',
+      () {
+        // Because `gitHubFlavored` will put `UnorderedListWithCheckboxSyntax`
+        // before `HorizontalRuleSyntax`, the `* * *` will be parsed into an
+        // empty unordered list if `ListSyntax` does not skip the horizontal
+        // rule structure.
+        expect(
+          markdownToHtml('* * *', extensionSet: ExtensionSet.gitHubFlavored),
+          '<hr />\n',
+        );
+      },
+    );
+  });
 }
diff --git a/pkgs/markdown/test/util_test.dart b/pkgs/markdown/test/util_test.dart
index 50a9453..5a2108a 100644
--- a/pkgs/markdown/test/util_test.dart
+++ b/pkgs/markdown/test/util_test.dart
@@ -52,6 +52,29 @@
       ]);
     });
   });
+
+  group('String.indentation()', () {
+    test('only spaces', () {
+      expect('   '.indentation(), 3);
+      expect('    '.indentation(), 4);
+      expect('     '.indentation(), 5);
+    });
+
+    test('spaces and tabs', () {
+      expect('\t  '.indentation(), 6);
+      expect(' \t '.indentation(), 5);
+      expect('  \t'.indentation(), 4);
+      expect('\t\t  '.indentation(), 10);
+      expect(' \t\t '.indentation(), 9);
+      expect('  \t\t'.indentation(), 8);
+    });
+
+    test('spaces, tabs and non whitespace characters', () {
+      expect('\t  foo'.indentation(), 6);
+      expect(' \t foo'.indentation(), 5);
+      expect('  \tfoo'.indentation(), 4);
+    });
+  });
 }
 
 extension on Line {
diff --git a/pkgs/markdown/tool/common_mark_stats.json b/pkgs/markdown/tool/common_mark_stats.json
index 8ff5f7c..12a63e8 100644
--- a/pkgs/markdown/tool/common_mark_stats.json
+++ b/pkgs/markdown/tool/common_mark_stats.json
@@ -527,9 +527,9 @@
   "259": "strict",
   "260": "strict",
   "261": "strict",
-  "262": "fail",
+  "262": "strict",
   "263": "strict",
-  "264": "fail",
+  "264": "strict",
   "265": "strict",
   "266": "strict",
   "267": "strict",
@@ -545,7 +545,7 @@
   "277": "strict",
   "278": "strict",
   "279": "strict",
-  "280": "fail",
+  "280": "strict",
   "281": "strict",
   "282": "strict",
   "283": "strict",
@@ -571,10 +571,10 @@
   "301": "strict",
   "302": "strict",
   "303": "strict",
-  "304": "fail",
+  "304": "strict",
   "305": "strict",
-  "306": "fail",
-  "307": "fail",
+  "306": "strict",
+  "307": "strict",
   "308": "loose",
   "309": "loose",
   "310": "strict",
@@ -585,7 +585,7 @@
   "315": "strict",
   "316": "strict",
   "317": "loose",
-  "318": "fail",
+  "318": "strict",
   "319": "strict",
   "320": "strict",
   "321": "strict",
@@ -671,7 +671,7 @@
   "4": "strict",
   "5": "strict",
   "6": "loose",
-  "7": "loose",
+  "7": "strict",
   "8": "strict",
   "9": "strict",
   "10": "strict",
diff --git a/pkgs/markdown/tool/common_mark_stats.txt b/pkgs/markdown/tool/common_mark_stats.txt
index f5fbe3b..eb0ac59 100644
--- a/pkgs/markdown/tool/common_mark_stats.txt
+++ b/pkgs/markdown/tool/common_mark_stats.txt
@@ -14,8 +14,8 @@
    1 of    1 – 100.0%  Inlines
   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
+  48 of   48 – 100.0%  List items
+  26 of   26 – 100.0%  Lists
    8 of    8 – 100.0%  Paragraphs
    1 of    1 – 100.0%  Precedence
   21 of   21 – 100.0%  Raw HTML
@@ -24,5 +24,5 @@
   11 of   11 – 100.0%  Tabs
    3 of    3 – 100.0%  Textual content
   19 of   19 – 100.0%  Thematic breaks
- 637 of  652 –  97.7%  TOTAL
- 618 of  637 –  97.0%  TOTAL Strict
+ 644 of  652 –  98.8%  TOTAL
+ 626 of  644 –  97.2%  TOTAL Strict
diff --git a/pkgs/markdown/tool/gfm_stats.json b/pkgs/markdown/tool/gfm_stats.json
index 820d869..43cd122 100644
--- a/pkgs/markdown/tool/gfm_stats.json
+++ b/pkgs/markdown/tool/gfm_stats.json
@@ -540,9 +540,9 @@
   "237": "strict",
   "238": "strict",
   "239": "strict",
-  "240": "fail",
+  "240": "strict",
   "241": "strict",
-  "242": "fail",
+  "242": "strict",
   "243": "strict",
   "244": "strict",
   "245": "strict",
@@ -558,7 +558,7 @@
   "255": "strict",
   "256": "strict",
   "257": "strict",
-  "258": "fail",
+  "258": "strict",
   "259": "strict",
   "260": "strict",
   "261": "strict",
@@ -584,10 +584,10 @@
   "281": "strict",
   "282": "strict",
   "283": "strict",
-  "284": "fail",
+  "284": "strict",
   "285": "strict",
-  "286": "fail",
-  "287": "fail",
+  "286": "strict",
+  "287": "strict",
   "288": "loose",
   "289": "loose",
   "290": "strict",
@@ -598,7 +598,7 @@
   "295": "strict",
   "296": "strict",
   "297": "loose",
-  "298": "fail",
+  "298": "strict",
   "299": "strict",
   "300": "strict",
   "301": "strict",
@@ -698,7 +698,7 @@
   "4": "strict",
   "5": "strict",
   "6": "loose",
-  "7": "loose",
+  "7": "strict",
   "8": "strict",
   "9": "strict",
   "10": "strict",
diff --git a/pkgs/markdown/tool/gfm_stats.txt b/pkgs/markdown/tool/gfm_stats.txt
index dbeadd0..c14547e 100644
--- a/pkgs/markdown/tool/gfm_stats.txt
+++ b/pkgs/markdown/tool/gfm_stats.txt
@@ -16,8 +16,8 @@
    1 of    1 – 100.0%  Inlines
   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
+  48 of   48 – 100.0%  List items
+  26 of   26 – 100.0%  Lists
    8 of    8 – 100.0%  Paragraphs
    1 of    1 – 100.0%  Precedence
   21 of   21 – 100.0%  Raw HTML
@@ -28,5 +28,5 @@
   11 of   11 – 100.0%  Tabs
    3 of    3 – 100.0%  Textual content
   19 of   19 – 100.0%  Thematic breaks
- 655 of  671 –  97.6%  TOTAL
- 635 of  655 –  96.9%  TOTAL Strict
+ 662 of  671 –  98.7%  TOTAL
+ 643 of  662 –  97.1%  TOTAL Strict