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