parse method
Implementation
@override
Node parse(BlockParser parser) {
final match = pattern.firstMatch(parser.current.content);
final ordered = match![1] != null;
final taskListParserEnabled = this is UnorderedListWithCheckboxSyntax ||
this is OrderedListWithCheckboxSyntax;
final items = <ListItem>[];
var childLines = <Line>[];
TaskListItemState? taskListItemState;
var offset = 0; //offset of the first line of [childLines]
void endItem() {
if (childLines.isNotEmpty) {
items.add(ListItem(childLines, taskListItemState: taskListItemState,
offset: offset + parser.offset));
childLines = <Line>[];
}
}
void addChildLine(Line line) {
if (childLines.isEmpty) offset = parser.pos;
childLines.add(line);
}
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.content);
return possibleMatch != null;
}
String? listMarker;
int? indent;
// In case the first number in an ordered list is not 1, use it as the
// "start".
int? startNumber;
int? blankLines;
while (!parser.isDone) {
final currentIndent = parser.current.content.indentation() +
(parser.current.tabRemaining ?? 0);
if (parser.current.isBlankLine) {
addChildLine(parser.current);
if (blankLines != null) {
blankLines++;
}
} else if (indent != null && indent <= currentIndent) {
// A list item can begin with at most one blank line. See:
// https://spec.commonmark.org/0.30/#example-280
if (blankLines != null && blankLines > 1) {
break;
}
final indentedLine = parser.current.content.dedent(indent);
addChildLine(Line(
blankLines == null
? indentedLine.text
: parseTaskListItem(indentedLine.text),
tabRemaining: indentedLine.tabRemaining,
));
} else if (tryMatch(hrPattern)) {
// Horizontal rule takes precedence to a new list item.
break;
} else if (tryMatch(listPattern)) {
blankLines = null;
final match = possibleMatch!;
final textParser = TextParser(parser.current.content);
var precedingWhitespaces = textParser.moveThroughWhitespace();
final markerStart = textParser.pos;
final digits = match[1] ?? '';
if (digits.isNotEmpty) {
startNumber ??= int.parse(digits);
textParser.advanceBy(digits.length);
}
textParser.advance();
// See https://spec.commonmark.org/0.30/#ordered-list-marker
final marker = textParser.substring(
markerStart,
textParser.pos,
);
var isBlank = true;
var contentWhitespances = 0;
var containsTab = false;
int? contentBlockStart;
if (!textParser.isDone) {
containsTab = textParser.charAt() == $tab;
// Skip the first whitespace.
textParser.advance();
contentBlockStart = textParser.pos;
if (!textParser.isDone) {
contentWhitespances = textParser.moveThroughWhitespace();
if (!textParser.isDone) {
isBlank = false;
}
}
}
// Changing the bullet or ordered list delimiter starts a new list.
if (listMarker != null && listMarker.last() != marker.last()) {
break;
}
// End the current list item and start a new one.
endItem();
// Start a new list item, the last item will be ended up outside of the
// `while` loop.
listMarker = marker;
precedingWhitespaces += digits.length + 2;
if (isBlank) {
// See https://spec.commonmark.org/0.30/#example-278.
blankLines = 1;
indent = precedingWhitespaces;
} else if (contentWhitespances >= 4) {
// See https://spec.commonmark.org/0.30/#example-270.
//
// If the list item starts with indented code, we need to _not_ count
// any indentation past the required whitespace character.
indent = precedingWhitespaces;
} else {
indent = precedingWhitespaces + contentWhitespances;
}
taskListItemState = null;
var content = contentBlockStart != null && !isBlank
? parseTaskListItem(textParser.substring(contentBlockStart))
: '';
if (content.isEmpty && containsTab) {
content = content.prependSpace(2);
}
addChildLine(Line(
content,
tabRemaining: containsTab ? 2 : null,
));
} else if (BlockSyntax.isAtBlockEnd(parser)) {
// Done with the list.
break;
} else {
// If the previous item is a blank line, this means we're done with the
// list and are starting a new top-level paragraph.
if (childLines.isNotEmpty && childLines.last.isBlankLine) {
parser.encounteredBlankLine = true;
break;
}
// Anything else is paragraph continuation text.
addChildLine(parser.current);
}
parser.advance();
}
endItem();
final itemNodes = <Element>[];
items.forEach(_removeLeadingEmptyLine);
final anyEmptyLines = _removeTrailingEmptyLines(items);
var anyEmptyLinesBetweenBlocks = false;
var containsTaskList = false;
const taskListClass = 'task-list-item';
for (final item in items) {
Element? checkboxToInsert;
if (item.taskListItemState != null) {
containsTaskList = true;
checkboxToInsert = Element.withTag('input')
..attributes['type'] = 'checkbox';
if (parser.document.checkable) {
checkboxToInsert.attributes['data-line'] = '${item.offset}';
} else {
checkboxToInsert.attributes['disabled'] = 'disabled';
}
if (item.taskListItemState == TaskListItemState.checked) {
checkboxToInsert.attributes['checked'] = 'true';
}
}
final itemParser = parser.document.getBlockParser(item.lines,
offset: item.offset);
final children = itemParser.parseLines(parentSyntax: this);
final itemElement = checkboxToInsert == null
? Element('li', children)
: (Element('li', _addCheckbox(children, checkboxToInsert))
..attributes['class'] = taskListClass);
itemNodes.add(itemElement);
anyEmptyLinesBetweenBlocks =
anyEmptyLinesBetweenBlocks || itemParser.encounteredBlankLine;
}
// Must strip paragraph tags if the list is "tight".
// https://spec.commonmark.org/0.30/#lists
final listIsTight = !anyEmptyLines && !anyEmptyLinesBetweenBlocks;
if (listIsTight) {
// We must post-process the list items, converting any top-level paragraph
// elements to just text elements.
for (final item in itemNodes) {
final isTaskList = item.attributes['class'] == taskListClass;
final children = item.children;
if (children != null) {
Node? lastNode;
for (var i = 0; i < children.length; i++) {
final child = children[i];
if (child is Element && child.tag == 'p') {
final childContent = child.children!;
if (lastNode is Element && !isTaskList) {
childContent.insert(0, Text('\n'));
}
children
..removeAt(i)
..insertAll(i, childContent);
}
lastNode = child;
}
}
}
}
final listElement = Element(ordered ? 'ol' : 'ul', itemNodes);
if (ordered && startNumber != 1) {
listElement.attributes['start'] = '$startNumber';
}
if (containsTaskList) {
listElement.attributes['class'] = 'contains-task-list';
}
return listElement;
}