Repository: superlistapp/super_editor

Issue:
[SuperEditorMarkdown] - Incorrect document when deserializing a markdown with a unordered list using "-" followed by a task
**Minimal Reproduction Code**
The following unit test captures the issue:

```dart
      test('unordered list item with a dash followed by a task', () {
        const markdown = '''
- list item node 
- [ ] task node
''';

        final document = deserializeMarkdownToDocument(markdown);

        expect(document.nodeCount, 2);
        expect(document.getNodeAt(0)!, isA<ListItemNode>());
        expect(document.getNodeAt(1)!, isA<TaskNode>());
      });
```

**Actual behavior**
Creates a document with 2 unordered list item nodes.

**Expected behavior**
Creates a document with an unordered list item and a task




Discussion: Cc @matthew-carroll 

Fix patch:
diff --git a/super_editor_markdown/lib/src/markdown_to_document_parsing.dart b/super_editor_markdown/lib/src/markdown_to_document_parsing.dart
index 3b547c573c..7cfc88cfab 100644
--- a/super_editor_markdown/lib/src/markdown_to_document_parsing.dart
+++ b/super_editor_markdown/lib/src/markdown_to_document_parsing.dart
@@ -36,7 +36,7 @@ MutableDocument deserializeMarkdownToDocument(
         const _ParagraphWithAlignmentSyntax(),
       ],
       const _EmptyLinePreservingParagraphSyntax(),
-      const _TaskSyntax(),
+      const md.UnorderedListWithCheckboxSyntax(),
     ],
   );
   final blockParser = md.BlockParser(markdownLines, markdownDoc);
@@ -63,8 +63,8 @@ MutableDocument deserializeMarkdownToDocument(
 
   // Add 1 hanging line for every 2 blank lines at the end, need this to preserve behavior pre markdown 7.2.1
   final hangingEmptyLines = markdownLines.reversed.takeWhile((md.Line l) => l.isBlankLine);
-  if(hangingEmptyLines.isNotEmpty && documentNodes.lastOrNull is ListItemNode) {
-    for(var i = 0; i < hangingEmptyLines.length ~/ 2; i++) {
+  if (hangingEmptyLines.isNotEmpty && documentNodes.lastOrNull is ListItemNode) {
+    for (var i = 0; i < hangingEmptyLines.length ~/ 2; i++) {
       documentNodes.add(ParagraphNode(id: Editor.createNodeId(), text: AttributedText()));
     }
   }
@@ -201,6 +201,15 @@ class _MarkdownToDocument implements md.NodeVisitor {
           throw Exception('Tried to parse a markdown list item but the list item type was null');
         }
 
+        if (element.attributes['class'] == 'task-list-item') {
+          // We handle task deserialization using the built-in `UnorderedListWithCheckboxSyntax`. It's parsed
+          // as a list item with a checkbox input element.
+          _addTask(element);
+
+          // Skip any child elements because we already added the task node.
+          return false;
+        }
+
         // Mark that we are visiting a list item.
         _listItemVisitedCount += 1;
 
@@ -223,9 +232,6 @@ class _MarkdownToDocument implements md.NodeVisitor {
       case 'hr':
         _addHorizontalRule();
         break;
-      case 'task':
-        _addTask(element);
-        break;
     }
 
     return true;
@@ -383,11 +389,19 @@ class _MarkdownToDocument implements md.NodeVisitor {
   }
 
   void _addTask(md.Element element) {
+    bool checked = false;
+    if (element.children != null && //
+        element.children!.isNotEmpty &&
+        element.children!.first is md.Element &&
+        (element.children!.first as md.Element).tag == 'input') {
+      checked = (element.children!.first as md.Element).attributes['checked'] == 'true';
+    }
+
     _content.add(
       TaskNode(
         id: Editor.createNodeId(),
         text: _parseInlineText(element.textContent),
-        isComplete: element.attributes['completed'] == 'true',
+        isComplete: checked,
       ),
     );
   }
@@ -536,7 +550,6 @@ abstract class ElementToNodeConverter {
 ///
 /// This [DelimiterSyntax] produces `Element`s with a `u` tag.
 class UnderlineSyntax extends md.DelimiterSyntax {
-
   /// According to the docs:
   ///
   /// https://pub.dev/documentation/markdown/latest/markdown/DelimiterSyntax-class.html
@@ -547,7 +560,7 @@ class UnderlineSyntax extends md.DelimiterSyntax {
   ///
   /// https://github.com/dart-lang/markdown/blob/d53feae0760a4f0aae5ffdfb12d8e6acccf14b40/lib/src/inline_syntaxes/delimiter_syntax.dart#L67
   /// https://github.com/dart-lang/markdown/blob/d53feae0760a4f0aae5ffdfb12d8e6acccf14b40/lib/src/inline_syntaxes/delimiter_syntax.dart#L319
-  static final _tags = [ md.DelimiterTag("u", 1) ];
+  static final _tags = [md.DelimiterTag("u", 1)];
 
   UnderlineSyntax() : super('¬', requiresDelimiterRun: true, allowIntraWord: true, tags: _tags);
 
@@ -560,7 +573,7 @@ class UnderlineSyntax extends md.DelimiterSyntax {
     required String tag,
   }) {
     final element = md.Element('u', getChildren());
-    return [ element ];
+    return [element];
   }
 }
 
@@ -580,7 +593,6 @@ class SingleStrikethroughSyntax extends md.DelimiterSyntax {
         );
 }
 
-
 /// Parses a paragraph preceded by an alignment token.
 class _ParagraphWithAlignmentSyntax extends _EmptyLinePreservingParagraphSyntax {
   /// This pattern matches the text aligment notation.
@@ -803,57 +815,6 @@ class _LineBreakSeparatedElement extends md.Element {
   }
 }
 
-/// A [md.BlockSyntax] that parses tasks.
-///
-/// A compled task starts with `- [x] ` followed by the task's content.
-///
-/// An incomplete task starts with `- [ ] ` followed by the task's content.
-///
-/// Tasks can have multiple lines of content.
-class _TaskSyntax extends md.BlockSyntax {
-  const _TaskSyntax();
-
-  /// Parses the first line of a task.
-  ///
-  /// `- [x] ` or `- [ ]` followed by any text.
-  @override
-  RegExp get pattern => RegExp(r'^- \[( |x)\] (.*)');
-
-  @override
-  md.Node? parse(md.BlockParser parser) {
-    final match = pattern.firstMatch(parser.current.content);
-    if (match == null) {
-      return null;
-    }
-
-    final completionToken = match.group(1)!;
-    final taskDescriptionFirstLine = match.group(2)!;
-
-    final buffer = StringBuffer(taskDescriptionFirstLine);
-
-    // Move to the second line.
-    parser.advance();
-
-    // Consume the following lines until we:
-    // - reach the end of the input OR
-    // - find a blank line OR
-    // - find the start of another block element (including another task)
-    while (!parser.isDone &&
-        !_blankLinePattern.hasMatch(parser.current.content) &&
-        !_standardNonParagraphBlockSyntaxes.any((syntax) => syntax.pattern.hasMatch(parser.current.content))) {
-      buffer.write('\n');
-      buffer.write(parser.current.content);
-
-      parser.advance();
-    }
-
-    return md.Element(
-      'task',
-      [md.Text(buffer.toString())],
-    )..attributes['completed'] = (completionToken == 'x').toString();
-  }
-}
-
 /// Parses a header preceded by an alignment token.
 ///
 /// Headers are represented by `_ParagraphWithAlignmentSyntax`s and therefore
@@ -943,7 +904,7 @@ const List<md.BlockSyntax> _standardNonParagraphBlockSyntaxes = [
   md.FencedCodeBlockSyntax(),
   md.BlockquoteSyntax(),
   md.HorizontalRuleSyntax(),
-  _TaskSyntax(),
+  md.UnorderedListWithCheckboxSyntax(),
   md.UnorderedListSyntax(),
   md.OrderedListSyntax(),
 ];


Test patch:
diff --git a/super_editor_markdown/test/super_editor_markdown_test.dart b/super_editor_markdown/test/super_editor_markdown_test.dart
index eb9c715699..b1c4a94759 100644
--- a/super_editor_markdown/test/super_editor_markdown_test.dart
+++ b/super_editor_markdown/test/super_editor_markdown_test.dart
@@ -1285,6 +1285,35 @@ This is some code
         expect((document.getNodeAt(2)! as ListItemNode).text.toPlainText(), 'list item 3');
       });
 
+      test('unordered list items mixed with task items', () {
+        const markdown = '''
+- list item node 
+- [ ] task node
+- [x] completed task node
+- second list item node 
+- [ ] another task node
+- third list item node
+- fourth list item node 
+''';
+
+        final document = deserializeMarkdownToDocument(markdown);
+
+        expect(document.nodeCount, 7);
+        expect(document.getNodeAt(0)!, isA<ListItemNode>());
+        expect(document.getNodeAt(1)!, isA<TaskNode>());
+        expect((document.getNodeAt(1) as TaskNode).text.toPlainText(), 'task node');
+        expect((document.getNodeAt(1) as TaskNode).isComplete, isFalse);
+        expect(document.getNodeAt(2)!, isA<TaskNode>());
+        expect((document.getNodeAt(2) as TaskNode).text.toPlainText(), 'completed task node');
+        expect((document.getNodeAt(2) as TaskNode).isComplete, isTrue);
+        expect(document.getNodeAt(3)!, isA<ListItemNode>());
+        expect(document.getNodeAt(4)!, isA<TaskNode>());
+        expect((document.getNodeAt(4) as TaskNode).text.toPlainText(), 'another task node');
+        expect((document.getNodeAt(4) as TaskNode).isComplete, isFalse);
+        expect(document.getNodeAt(5)!, isA<ListItemNode>());
+        expect(document.getNodeAt(6)!, isA<ListItemNode>());
+      });
+
       test('ordered list', () {
         const markdown = '''
  1. list item 1