Fix crash with mixed checkbox lists (#449)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9742ee2..95a80c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 6.0.1-dev
+
+* Fix a crash in checkbox lists when mixing checkbox items with
+ non-checkbox items.
+
## 6.0.0
* Require Dart 2.17
diff --git a/lib/src/block_syntaxes/list_syntax.dart b/lib/src/block_syntaxes/list_syntax.dart
index 483a0d8..b111744 100644
--- a/lib/src/block_syntaxes/list_syntax.dart
+++ b/lib/src/block_syntaxes/list_syntax.dart
@@ -14,11 +14,11 @@
final List<String> lines;
}
-/// Invisible string used to placehold for an *unchecked* CheckBox.
+/// 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.
+/// 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}';
@@ -57,8 +57,11 @@
Node parse(BlockParser parser) {
final items = <ListItem>[];
var childLines = <String>[];
- final isCheckboxListSubclass =
- (listTag == 'ol_with_checkbox' || listTag == 'ul_with_checkbox');
+ // 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';
void endItem() {
if (childLines.isNotEmpty) {
@@ -83,7 +86,7 @@
final leadingSpace =
_whitespaceRe.matchAsPrefix(parser.current)!.group(0)!;
final leadingExpandedTabLength = _expandedTabLength(leadingSpace);
- if (tryMatch(emptyPattern)) {
+ if (emptyPattern.hasMatch(parser.current)) {
if (emptyPattern.hasMatch(parser.next ?? '')) {
// Two blank lines ends a list.
break;
@@ -96,46 +99,38 @@
.replaceFirst(leadingSpace, ' ' * leadingExpandedTabLength)
.replaceFirst(indent, '');
childLines.add(line);
- } else if (tryMatch(hrPattern)) {
+ } else if (hrPattern.hasMatch(parser.current)) {
// Horizontal rule takes precedence to a new list item.
break;
- } else if ((isCheckboxListSubclass &&
- (tryMatch(ulWithCheckBoxPattern) ||
- tryMatch(olWithCheckBoxPattern))) ||
- tryMatch(ulPattern) ||
- tryMatch(olPattern)) {
+ } else if (tryMatch(ulWithPossibleCheckboxPattern) ||
+ tryMatch(olWithPossibleCheckboxPattern)) {
// We know we have a valid [possibleMatch] now, so capture it.
- final successfulMatch = possibleMatch!;
- // The checkbox "subclass" patterns ([ulWithCheckBoxPattern] and
- // [olWithCheckBoxPattern]) have 2 extra capturing groups at the 5
- // position to capture the checkbox. These shift the other groups
- // forward by 2 slots.
- final cbGroupOffset = isCheckboxListSubclass ? 2 : 0;
- final precedingWhitespace = successfulMatch[1]!;
- final digits = successfulMatch[2] ?? '';
+ final match = possibleMatch!;
+ final precedingWhitespace = match[1]!;
+ final digits = match[2] ?? '';
if (startNumber == null && digits.isNotEmpty) {
startNumber = int.parse(digits);
}
- final marker = successfulMatch[3]!;
+ final marker = match[3]!;
// [checkBoxIndicator] is always empty unless a checkbox was found.
- String checkBoxIndicator = '';
- if (isCheckboxListSubclass) {
- // Look at checkbox capture group and get checkbox state.
+ 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.
- final String checkboxGroup = successfulMatch[5]!.toLowerCase();
- if (checkboxGroup == '[ ]') {
- checkBoxIndicator = indicatorForUncheckedCheckBox;
- } else if (checkboxGroup == '[x]') {
- checkBoxIndicator = indicatorForCheckedCheckBox;
+ if (checkbox == '[ ]') {
+ checkboxIndicator = indicatorForUncheckedCheckBox;
+ } else if (checkbox == '[x]' || checkbox == '[X]') {
+ checkboxIndicator = indicatorForCheckedCheckBox;
}
}
- final firstWhitespace = successfulMatch[5 + cbGroupOffset] ?? '';
- final restWhitespace = successfulMatch[6 + cbGroupOffset] ?? '';
- final content = successfulMatch[7 + cbGroupOffset] ?? '';
+ final firstWhitespace = match[8] ?? '';
+ final restWhitespace = match[9] ?? '';
+ final content = match[10] ?? '';
final isBlank = content.isEmpty;
if (listMarker != null && listMarker != marker) {
// Changing the bullet or ordered list delimiter starts a new list.
@@ -165,7 +160,7 @@
}
// End the current list item and start a new one.
endItem();
- childLines.add('$checkBoxIndicator$restWhitespace$content');
+ childLines.add('$checkboxIndicator$restWhitespace$content');
} else if (BlockSyntax.isAtBlockEnd(parser)) {
// Done with the list.
break;
@@ -197,7 +192,7 @@
// 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 (isCheckboxListSubclass) {
+ if (isCheckboxList) {
if (children.isNotEmpty) {
if (children.first.textContent
.startsWith(indicatorForCheckedCheckBox)) {
diff --git a/lib/src/patterns.dart b/lib/src/patterns.dart
index f2aa7d8..5602630 100644
--- a/lib/src/patterns.dart
+++ b/lib/src/patterns.dart
@@ -31,32 +31,91 @@
/// SETEXT should win.
final hrPattern = RegExp(r'^ {0,3}([-*_])[ \t]*\1[ \t]*\1(?:\1|[ \t])*$');
-/// 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.
+// 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: `-`, `*`, `+`.
+///
+/// May have up to three leading spaces before the marker and any number of
+/// spaces or tabs after.
///
/// 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(r'^([ ]{0,3})()([*+-])(([ \t])([ \t]*)(.*))?$');
+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 `[5,6,7]` are all
-/// shifted 2 places to be `[7,8,9]`
-final ulWithCheckBoxPattern = RegExp(
- r'^([ ]{0,3})()([*+-])([ \t]{0,4})(\[[ xX]{1}\])(([ \t])([ \t]*)(.*))?$');
+/// 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(r'^([ ]{0,3})(\d{1,9})([\.)])(([ \t])([ \t]*)(.*))?$');
+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 `[5,6,7]` are all
-/// shifted 2 places to be `[7,8,9]`
-final olWithCheckBoxPattern = RegExp(
- r'^([ ]{0,3})(\d{1,9})([\.)])([ \t]{0,4})(\[[ xX]{1}\])(([ \t])([ \t]*)(.*))?$');
+/// 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)?\$');
/// A line of hyphens separated by at least one pipe.
final tablePattern = RegExp(
diff --git a/lib/src/version.dart b/lib/src/version.dart
index 0773426..e3a5fe6 100644
--- a/lib/src/version.dart
+++ b/lib/src/version.dart
@@ -1,2 +1,2 @@
// Generated code. Do not modify.
-const packageVersion = '6.0.0';
+const packageVersion = '6.0.1-dev';
diff --git a/pubspec.yaml b/pubspec.yaml
index 4a69ba5..ef7520c 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: markdown
-version: 6.0.0
+version: 6.0.1-dev
description: A portable Markdown library written in Dart that can parse
Markdown into HTML.
diff --git a/test/extensions/ordered_list_with_checkboxes.unit b/test/extensions/ordered_list_with_checkboxes.unit
new file mode 100644
index 0000000..b45dda5
--- /dev/null
+++ b/test/extensions/ordered_list_with_checkboxes.unit
@@ -0,0 +1,65 @@
+>>> checkbox with space
+1. [ ] one
+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>
+</ol>
+>>> empty checkbox
+1. [] one
+2. [] two
+<<<
+<ol>
+<li>[] one</li>
+<li>[] 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>
+</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>
+</ol>
+>>> mixed checkboxes
+1. [ ] one
+2. [] two
+3. [x] three
+4. [X] four
+5. five
+<<<
+<ol class="contains-task-list">
+<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>five</li>
+</ol>
+>>> mixed leading spaces
+1.[ ] zero
+2. [ ] one
+3. [ ] two
+4. [ ] three
+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>
+<li>
+<pre><code>[ ] five
+</code></pre>
+</li>
+</ol>
\ No newline at end of file
diff --git a/test/extensions/unordered_list_with_checkboxes.unit b/test/extensions/unordered_list_with_checkboxes.unit
new file mode 100644
index 0000000..2d7fc2b
--- /dev/null
+++ b/test/extensions/unordered_list_with_checkboxes.unit
@@ -0,0 +1,65 @@
+>>> checkbox with space
+* [ ] one
+* [ ] 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>
+</ul>
+>>> empty checkbox
+* [] one
+* [] two
+<<<
+<ul>
+<li>[] one</li>
+<li>[] 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>
+</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>
+</ul>
+>>> mixed checkboxes
+* [ ] one
+* [] two
+* [x] three
+* [X] four
+* five
+<<<
+<ul class="contains-task-list">
+<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>five</li>
+</ul>
+>>> mixed leading spaces
+*[ ] zero
+* [ ] one
+* [ ] two
+* [ ] three
+* [ ] four
+* [ ] five
+<<<
+<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>
+<pre><code>[ ] five
+</code></pre>
+</li>
+</ul>
\ No newline at end of file
diff --git a/test/markdown_test.dart b/test/markdown_test.dart
index a9f6fde..7bbdd98 100644
--- a/test/markdown_test.dart
+++ b/test/markdown_test.dart
@@ -10,7 +10,11 @@
void main() async {
await testDirectory('original');
- // Block syntax extensions
+ // Block syntax extensions.
+ testFile(
+ 'extensions/fenced_blockquotes.unit',
+ blockSyntaxes: [const FencedBlockquoteSyntax()],
+ );
testFile(
'extensions/fenced_code_blocks.unit',
blockSyntaxes: [const FencedCodeBlockSyntax()],
@@ -20,6 +24,10 @@
blockSyntaxes: [const HeaderWithIdSyntax()],
);
testFile(
+ 'extensions/ordered_list_with_checkboxes.unit',
+ blockSyntaxes: [const OrderedListWithCheckBoxSyntax()],
+ );
+ testFile(
'extensions/setext_headers_with_ids.unit',
blockSyntaxes: [const SetextHeaderWithIdSyntax()],
);
@@ -28,8 +36,8 @@
blockSyntaxes: [const TableSyntax()],
);
testFile(
- 'extensions/fenced_blockquotes.unit',
- blockSyntaxes: [const FencedBlockquoteSyntax()],
+ 'extensions/unordered_list_with_checkboxes.unit',
+ blockSyntaxes: [const UnorderedListWithCheckBoxSyntax()],
);
// Inline syntax extensions