Fix crash with mixed checkbox lists (dart-lang/markdown#449)
diff --git a/pkgs/markdown/CHANGELOG.md b/pkgs/markdown/CHANGELOG.md index 9742ee2..95a80c5 100644 --- a/pkgs/markdown/CHANGELOG.md +++ b/pkgs/markdown/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/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart index 483a0d8..b111744 100644 --- a/pkgs/markdown/lib/src/block_syntaxes/list_syntax.dart +++ b/pkgs/markdown/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/pkgs/markdown/lib/src/patterns.dart b/pkgs/markdown/lib/src/patterns.dart index f2aa7d8..5602630 100644 --- a/pkgs/markdown/lib/src/patterns.dart +++ b/pkgs/markdown/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/pkgs/markdown/lib/src/version.dart b/pkgs/markdown/lib/src/version.dart index 0773426..e3a5fe6 100644 --- a/pkgs/markdown/lib/src/version.dart +++ b/pkgs/markdown/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/pkgs/markdown/pubspec.yaml b/pkgs/markdown/pubspec.yaml index 4a69ba5..ef7520c 100644 --- a/pkgs/markdown/pubspec.yaml +++ b/pkgs/markdown/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/pkgs/markdown/test/extensions/ordered_list_with_checkboxes.unit b/pkgs/markdown/test/extensions/ordered_list_with_checkboxes.unit new file mode 100644 index 0000000..b45dda5 --- /dev/null +++ b/pkgs/markdown/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/pkgs/markdown/test/extensions/unordered_list_with_checkboxes.unit b/pkgs/markdown/test/extensions/unordered_list_with_checkboxes.unit new file mode 100644 index 0000000..2d7fc2b --- /dev/null +++ b/pkgs/markdown/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/pkgs/markdown/test/markdown_test.dart b/pkgs/markdown/test/markdown_test.dart index a9f6fde..7bbdd98 100644 --- a/pkgs/markdown/test/markdown_test.dart +++ b/pkgs/markdown/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