Rewrite checkbox(task list) extension (#450)

* revert to old version

* Parse tasklist items

* Add test cases

* Change package import to path import

* Add a comment to UnorderedListSyntax

* Some optimisation

1. Remove enableCheckbox option from ListSyntax
2. Rename UnorderedListWithCheckBoxSyntax to UnOrderedListWithCheckboxSyntax
3. Rename OrderedListWithCheckBoxSyntax to OrderedListWithCheckboxSyntax

* Fix a prefer_interpolation_to_compose_strings issue

* Fix the requested changes

* Put an accidentally deleted test case back

* Rename successfulMatch to match

* Rename UnOrderedListWithCheckboxSyntax to UnorderedListWithCheckboxSyntax
diff --git a/lib/src/block_syntaxes/list_syntax.dart b/lib/src/block_syntaxes/list_syntax.dart
index b111744..ce46166 100644
--- a/lib/src/block_syntaxes/list_syntax.dart
+++ b/lib/src/block_syntaxes/list_syntax.dart
@@ -6,21 +6,20 @@
 import '../block_parser.dart';
 import '../patterns.dart';
 import 'block_syntax.dart';
+import 'ordered_list_with_checkbox_syntax.dart';
+import 'unordered_list_with_checkbox_syntax.dart';
 
 class ListItem {
-  ListItem(this.lines);
+  const ListItem(
+    this.lines, {
+    this.taskListItemState,
+  });
 
-  bool forceBlock = false;
   final List<String> lines;
+  final TaskListItemState? taskListItemState;
 }
 
-/// Invisible string used to placehold for an *unchecked* Checkbox.
-/// The character is Unicode Zero Width Space (U+200B).
-const indicatorForUncheckedCheckBox = '\u{200B}';
-
-/// Invisible string used to placehold for a *checked* Checkbox.
-/// This is 2 Unicode Zero Width Space (U+200B) characters.
-const indicatorForCheckedCheckBox = '\u{200B}\u{200B}';
+enum TaskListItemState { checked, unchecked }
 
 /// Base class for both ordered and unordered lists.
 abstract class ListSyntax extends BlockSyntax {
@@ -55,21 +54,36 @@
 
   @override
   Node parse(BlockParser parser) {
+    final taskListParserEnabled = this is UnorderedListWithCheckboxSyntax ||
+        this is OrderedListWithCheckboxSyntax;
     final items = <ListItem>[];
     var childLines = <String>[];
-    // TODO(https://github.com/dart-lang/markdown/issues/448) make some fixes to
-    // this area of the code. I think we can follow the same pattern as deciding
-    //whether a list item is a forced block.
-    final isCheckboxList =
-        listTag == 'ol_with_checkbox' || listTag == 'ul_with_checkbox';
+    TaskListItemState? taskListItemState;
 
     void endItem() {
       if (childLines.isNotEmpty) {
-        items.add(ListItem(childLines));
+        items.add(ListItem(childLines, taskListItemState: taskListItemState));
         childLines = <String>[];
       }
     }
 
+    String parseTaskListItem(String text) {
+      final pattern = RegExp(r'^ {0,3}\[([ xX])\][ \t]');
+
+      if (taskListParserEnabled && pattern.hasMatch(text)) {
+        return text.replaceFirstMapped(pattern, ((match) {
+          taskListItemState = match[1] == ' '
+              ? TaskListItemState.unchecked
+              : TaskListItemState.checked;
+
+          return '';
+        }));
+      } else {
+        taskListItemState = null;
+        return text;
+      }
+    }
+
     late Match? possibleMatch;
     bool tryMatch(RegExp pattern) {
       possibleMatch = pattern.firstMatch(parser.current);
@@ -98,13 +112,11 @@
         final line = parser.current
             .replaceFirst(leadingSpace, ' ' * leadingExpandedTabLength)
             .replaceFirst(indent, '');
-        childLines.add(line);
-      } else if (hrPattern.hasMatch(parser.current)) {
+        childLines.add(parseTaskListItem(line));
+      } else if (tryMatch(hrPattern)) {
         // Horizontal rule takes precedence to a new list item.
         break;
-      } else if (tryMatch(ulWithPossibleCheckboxPattern) ||
-          tryMatch(olWithPossibleCheckboxPattern)) {
-        // We know we have a valid [possibleMatch] now, so capture it.
+      } else if (tryMatch(ulPattern) || tryMatch(olPattern)) {
         final match = possibleMatch!;
         final precedingWhitespace = match[1]!;
         final digits = match[2] ?? '';
@@ -112,25 +124,9 @@
           startNumber = int.parse(digits);
         }
         final marker = match[3]!;
-        // [checkBoxIndicator] is always empty unless a checkbox was found.
-        String checkboxIndicator = '';
-        final checkbox = match[6];
-        final isCheckboxItem = checkbox != null && isCheckboxList;
-        if (isCheckboxItem) {
-          // If we find a checked or unchecked checkbox then we will
-          // set [checkBoxIndicator] to one of our invisible
-          // codes that we can later detect to know if we need to insert
-          // a check or unchecked checkbox when we are inserting the
-          // listitem li node.
-          if (checkbox == '[ ]') {
-            checkboxIndicator = indicatorForUncheckedCheckBox;
-          } else if (checkbox == '[x]' || checkbox == '[X]') {
-            checkboxIndicator = indicatorForCheckedCheckBox;
-          }
-        }
-        final firstWhitespace = match[8] ?? '';
-        final restWhitespace = match[9] ?? '';
-        final content = match[10] ?? '';
+        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.
@@ -160,7 +156,7 @@
         }
         // End the current list item and start a new one.
         endItem();
-        childLines.add('$checkboxIndicator$restWhitespace$content');
+        childLines.add(parseTaskListItem('$restWhitespace$content'));
       } else if (BlockSyntax.isAtBlockEnd(parser)) {
         // Done with the list.
         break;
@@ -184,34 +180,27 @@
     items.forEach(_removeLeadingEmptyLine);
     final anyEmptyLines = _removeTrailingEmptyLines(items);
     var anyEmptyLinesBetweenBlocks = false;
+    var containsTaskList = false;
 
     for (final item in items) {
-      final itemParser = BlockParser(item.lines, parser.document);
-      final children = itemParser.parseLines();
-      // If this is a checkbox sublass of ListSyntax then we must check
-      // for possible invisible checkbox placeholder characters at
-      // the start of first node's text to see if we need to insert a checkbox.
       Element? checkboxToInsert;
-      if (isCheckboxList) {
-        if (children.isNotEmpty) {
-          if (children.first.textContent
-              .startsWith(indicatorForCheckedCheckBox)) {
-            checkboxToInsert = Element.withTag('input')
-              ..attributes['type'] = 'checkbox'
-              ..attributes['checked'] = 'true';
-          } else if (children.first.textContent
-              .startsWith(indicatorForUncheckedCheckBox)) {
-            checkboxToInsert = Element.withTag('input')
-              ..attributes['type'] = 'checkbox';
-          }
+      if (item.taskListItemState != null) {
+        containsTaskList = true;
+        checkboxToInsert = Element.withTag('input')
+          ..attributes['type'] = 'checkbox';
+        if (item.taskListItemState == TaskListItemState.checked) {
+          checkboxToInsert.attributes['checked'] = 'true';
         }
       }
-      if (checkboxToInsert != null) {
-        itemNodes.add(Element('li', [checkboxToInsert, ...children])
-          ..attributes['class'] = 'task-list-item');
-      } else {
-        itemNodes.add(Element('li', children));
-      }
+
+      final itemParser = BlockParser(item.lines, parser.document);
+      final children = itemParser.parseLines();
+      final itemElement = checkboxToInsert == null
+          ? Element('li', children)
+          : (Element('li', [checkboxToInsert, ...children])
+            ..attributes['class'] = 'task-list-item');
+
+      itemNodes.add(itemElement);
       anyEmptyLinesBetweenBlocks =
           anyEmptyLinesBetweenBlocks || itemParser.encounteredBlankLine;
     }
@@ -237,21 +226,15 @@
       }
     }
 
-    if (listTag == 'ol_with_checkbox') {
-      final olWithCheckbox = Element('ol', itemNodes)
-        ..attributes['class'] = 'contains-task-list';
-      if (startNumber != 1) {
-        olWithCheckbox.attributes['start'] = '$startNumber';
-      }
-      return olWithCheckbox;
-    } else if (listTag == 'ul_with_checkbox') {
-      return Element('ul', itemNodes)
-        ..attributes['class'] = 'contains-task-list';
-    } else if (listTag == 'ol' && startNumber != 1) {
-      return Element(listTag, itemNodes)..attributes['start'] = '$startNumber';
-    } else {
-      return Element(listTag, itemNodes);
+    final listElement = Element(listTag, itemNodes);
+    if (listTag == 'ol' && startNumber != 1) {
+      listElement.attributes['start'] = '$startNumber';
     }
+
+    if (containsTaskList) {
+      listElement.attributes['class'] = 'contains-task-list';
+    }
+    return listElement;
   }
 
   void _removeLeadingEmptyLine(ListItem item) {
diff --git a/lib/src/block_syntaxes/ordered_list_with_checkbox_syntax.dart b/lib/src/block_syntaxes/ordered_list_with_checkbox_syntax.dart
index 2f8a66a..8d865e8 100644
--- a/lib/src/block_syntaxes/ordered_list_with_checkbox_syntax.dart
+++ b/lib/src/block_syntaxes/ordered_list_with_checkbox_syntax.dart
@@ -2,16 +2,9 @@
 // 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 '../patterns.dart';
-import 'list_syntax.dart';
+import 'ordered_list_syntax.dart';
 
-/// Parses ordered lists.
-class OrderedListWithCheckBoxSyntax extends ListSyntax {
-  @override
-  RegExp get pattern => olWithCheckBoxPattern;
-
-  @override
-  String get listTag => 'ol_with_checkbox';
-
-  const OrderedListWithCheckBoxSyntax();
+/// Parses ordered lists with checkboxes.
+class OrderedListWithCheckboxSyntax extends OrderedListSyntax {
+  const OrderedListWithCheckboxSyntax();
 }
diff --git a/lib/src/block_syntaxes/unordered_list_syntax.dart b/lib/src/block_syntaxes/unordered_list_syntax.dart
index 6b1ae10..66bdf0f 100644
--- a/lib/src/block_syntaxes/unordered_list_syntax.dart
+++ b/lib/src/block_syntaxes/unordered_list_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 '../block_parser.dart';
 import '../patterns.dart';
 import 'list_syntax.dart';
 
@@ -11,6 +12,21 @@
   RegExp get pattern => ulPattern;
 
   @override
+  bool canParse(BlockParser parser) {
+    // Check if it matches `hrPattern`, otherwise it will produce an infinite
+    // loop if put `UnorderedListSyntax` or `UnorderedListWithCheckboxSyntax`
+    // bofore `HorizontalRuleSyntax` and parse:
+    // ```
+    // * * *
+    // ```
+    if (hrPattern.hasMatch(parser.current)) {
+      return false;
+    }
+
+    return pattern.hasMatch(parser.current);
+  }
+
+  @override
   String get listTag => 'ul';
 
   const UnorderedListSyntax();
diff --git a/lib/src/block_syntaxes/unordered_list_with_checkbox_syntax.dart b/lib/src/block_syntaxes/unordered_list_with_checkbox_syntax.dart
index 795d8a7..5f3ddf0 100644
--- a/lib/src/block_syntaxes/unordered_list_with_checkbox_syntax.dart
+++ b/lib/src/block_syntaxes/unordered_list_with_checkbox_syntax.dart
@@ -2,16 +2,9 @@
 // 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 '../patterns.dart';
-import 'list_syntax.dart';
+import 'unordered_list_syntax.dart';
 
-/// Parses unordered lists.
-class UnorderedListWithCheckBoxSyntax extends ListSyntax {
-  @override
-  RegExp get pattern => ulWithCheckBoxPattern;
-
-  @override
-  String get listTag => 'ul_with_checkbox';
-
-  const UnorderedListWithCheckBoxSyntax();
+/// Parses unordered lists with checkboxes.
+class UnorderedListWithCheckboxSyntax extends UnorderedListSyntax {
+  const UnorderedListWithCheckboxSyntax();
 }
diff --git a/lib/src/extension_set.dart b/lib/src/extension_set.dart
index 7f2be48..bbbd8ef 100644
--- a/lib/src/extension_set.dart
+++ b/lib/src/extension_set.dart
@@ -60,8 +60,8 @@
         const HeaderWithIdSyntax(),
         const SetextHeaderWithIdSyntax(),
         const TableSyntax(),
-        const UnorderedListWithCheckBoxSyntax(),
-        const OrderedListWithCheckBoxSyntax(),
+        const UnorderedListWithCheckboxSyntax(),
+        const OrderedListWithCheckboxSyntax(),
       ],
     ),
     List<InlineSyntax>.unmodifiable(
@@ -82,8 +82,8 @@
       <BlockSyntax>[
         const FencedCodeBlockSyntax(),
         const TableSyntax(),
-        const UnorderedListWithCheckBoxSyntax(),
-        const OrderedListWithCheckBoxSyntax(),
+        const UnorderedListWithCheckboxSyntax(),
+        const OrderedListWithCheckboxSyntax(),
       ],
     ),
     List<InlineSyntax>.unmodifiable(
diff --git a/test/extensions/ordered_list_with_checkboxes.unit b/test/extensions/ordered_list_with_checkboxes.unit
index b45dda5..84cf86e 100644
--- a/test/extensions/ordered_list_with_checkboxes.unit
+++ b/test/extensions/ordered_list_with_checkboxes.unit
@@ -3,8 +3,8 @@
 2. [ ] two
 <<<
 <ol class="contains-task-list">
-<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>one</li>
+<li class="task-list-item"><input type="checkbox"></input>two</li>
 </ol>
 >>> empty checkbox
 1. [] one
@@ -19,16 +19,16 @@
 2. [x] two
 <<<
 <ol class="contains-task-list">
-<li class="task-list-item"><input type="checkbox" checked="true"></input>​​one</li>
-<li class="task-list-item"><input type="checkbox" checked="true"></input>​​two</li>
+<li class="task-list-item"><input type="checkbox" checked="true"></input>one</li>
+<li class="task-list-item"><input type="checkbox" checked="true"></input>two</li>
 </ol>
 >>> checkbox with X
 1. [X] one
 2. [X] two
 <<<
 <ol class="contains-task-list">
-<li class="task-list-item"><input type="checkbox" checked="true"></input>​​one</li>
-<li class="task-list-item"><input type="checkbox" checked="true"></input>​​two</li>
+<li class="task-list-item"><input type="checkbox" checked="true"></input>one</li>
+<li class="task-list-item"><input type="checkbox" checked="true"></input>two</li>
 </ol>
 >>> mixed checkboxes
 1. [ ] one
@@ -38,10 +38,10 @@
 5. five
 <<<
 <ol class="contains-task-list">
-<li class="task-list-item"><input type="checkbox"></input>​one</li>
+<li class="task-list-item"><input type="checkbox"></input>one</li>
 <li>[] two</li>
-<li class="task-list-item"><input type="checkbox" checked="true"></input>​​three</li>
-<li class="task-list-item"><input type="checkbox" checked="true"></input>​​four</li>
+<li class="task-list-item"><input type="checkbox" checked="true"></input>three</li>
+<li class="task-list-item"><input type="checkbox" checked="true"></input>four</li>
 <li>five</li>
 </ol>
 >>> mixed leading spaces
@@ -52,12 +52,12 @@
 5.    [ ] four
 6.     [ ] five
 <<<
-<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>
+<p>1.[ ] zero</p>
+<ol start="2" class="contains-task-list">
+<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
 </code></pre>
diff --git a/test/extensions/unordered_list_with_checkboxes.unit b/test/extensions/unordered_list_with_checkboxes.unit
index 2d7fc2b..170b6de 100644
--- a/test/extensions/unordered_list_with_checkboxes.unit
+++ b/test/extensions/unordered_list_with_checkboxes.unit
@@ -3,8 +3,8 @@
 * [ ] two
 <<<
 <ul class="contains-task-list">
-<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>one</li>
+<li class="task-list-item"><input type="checkbox"></input>two</li>
 </ul>
 >>> empty checkbox
 * [] one
@@ -19,16 +19,16 @@
 * [x] two
 <<<
 <ul class="contains-task-list">
-<li class="task-list-item"><input type="checkbox" checked="true"></input>​​one</li>
-<li class="task-list-item"><input type="checkbox" checked="true"></input>​​two</li>
+<li class="task-list-item"><input type="checkbox" checked="true"></input>one</li>
+<li class="task-list-item"><input type="checkbox" checked="true"></input>two</li>
 </ul>
 >>> checkbox with X
 * [X] one
 * [X] two
 <<<
 <ul class="contains-task-list">
-<li class="task-list-item"><input type="checkbox" checked="true"></input>​​one</li>
-<li class="task-list-item"><input type="checkbox" checked="true"></input>​​two</li>
+<li class="task-list-item"><input type="checkbox" checked="true"></input>one</li>
+<li class="task-list-item"><input type="checkbox" checked="true"></input>two</li>
 </ul>
 >>> mixed checkboxes
 * [ ] one
@@ -38,10 +38,10 @@
 * five
 <<<
 <ul class="contains-task-list">
-<li class="task-list-item"><input type="checkbox"></input>​one</li>
+<li class="task-list-item"><input type="checkbox"></input>one</li>
 <li>[] two</li>
-<li class="task-list-item"><input type="checkbox" checked="true"></input>​​three</li>
-<li class="task-list-item"><input type="checkbox" checked="true"></input>​​four</li>
+<li class="task-list-item"><input type="checkbox" checked="true"></input>three</li>
+<li class="task-list-item"><input type="checkbox" checked="true"></input>four</li>
 <li>five</li>
 </ul>
 >>> mixed leading spaces
@@ -52,12 +52,12 @@
 *    [ ] four
 *     [ ] five
 <<<
+<p>*[ ] zero</p>
 <ul 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 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
 </code></pre>
diff --git a/test/markdown_test.dart b/test/markdown_test.dart
index 7bbdd98..0b05c73 100644
--- a/test/markdown_test.dart
+++ b/test/markdown_test.dart
@@ -25,7 +25,7 @@
   );
   testFile(
     'extensions/ordered_list_with_checkboxes.unit',
-    blockSyntaxes: [const OrderedListWithCheckBoxSyntax()],
+    blockSyntaxes: [const OrderedListWithCheckboxSyntax()],
   );
   testFile(
     'extensions/setext_headers_with_ids.unit',
@@ -37,7 +37,7 @@
   );
   testFile(
     'extensions/unordered_list_with_checkboxes.unit',
-    blockSyntaxes: [const UnorderedListWithCheckBoxSyntax()],
+    blockSyntaxes: [const UnorderedListWithCheckboxSyntax()],
   );
 
   // Inline syntax extensions