parse method
Implementation
@override
Node parse(BlockParser parser) {
var items = <ListItem>[];
var childLines = <String>[];
var offset = 0; //offset of the first line of [childLines]
void endItem() {
if (childLines.isNotEmpty) {
items.add(ListItem(childLines, offset + parser.offset));
childLines = <String>[];
}
}
void addChildLine(String line) {
if (childLines.isEmpty) offset = parser._pos;
childLines.add(line);
}
late Match? match;
bool tryMatch(RegExp pattern) {
match = pattern.firstMatch(parser.current);
return match != null;
}
String? listMarker;
String? indent;
// In case the first number in an ordered list is not 1, use it as the
// "start".
int? startNumber;
while (!parser.isDone) {
var leadingSpace = _whitespaceRe.matchAsPrefix(parser.current)!.group(0)!;
var leadingExpandedTabLength = _expandedTabLength(leadingSpace);
if (tryMatch(_emptyPattern)) {
if (_emptyPattern.hasMatch(parser.next ?? '')) {
// Two blank lines ends a list.
break;
}
// Add a blank line to the current list item.
addChildLine('');
} else if (indent != null && indent.length <= leadingExpandedTabLength) {
// Strip off indent and add to current item.
var line = parser.current
.replaceFirst(leadingSpace, ' ' * leadingExpandedTabLength)
.replaceFirst(indent, '');
addChildLine(line);
} else if (tryMatch(_hrPattern)) {
// Horizontal rule takes precedence to a new list item.
break;
} else if (tryMatch(_ulPattern) || tryMatch(_olPattern)) {
var precedingWhitespace = match![1]!;
var digits = match![2] ?? '';
if (startNumber == null && digits.isNotEmpty) {
startNumber = int.parse(digits);
}
var marker = match![3]!;
var firstWhitespace = match![5] ?? '';
var restWhitespace = match![6] ?? '';
var content = match![7] ?? '';
var isBlank = content.isEmpty;
if (listMarker != null && listMarker != marker) {
// Changing the bullet or ordered list delimiter starts a new list.
break;
}
listMarker = marker;
var markerAsSpaces = ' ' * (digits.length + marker.length);
if (isBlank) {
// See http://spec.commonmark.org/0.28/#list-items under "3. Item
// starting with a blank line."
//
// If the list item starts with a blank line, the final piece of the
// indentation is just a single space.
indent = precedingWhitespace + markerAsSpaces + ' ';
} else if (restWhitespace.length >= 4) {
// See http://spec.commonmark.org/0.28/#list-items under "2. Item
// starting with indented code."
//
// If the list item starts with indented code, we need to _not_ count
// any indentation past the required whitespace character.
indent = precedingWhitespace + markerAsSpaces + firstWhitespace;
} else {
indent = precedingWhitespace +
markerAsSpaces +
firstWhitespace +
restWhitespace;
}
// End the current list item and start a new one.
endItem();
addChildLine(restWhitespace + content);
} 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 == '')) {
parser.encounteredBlankLine = true;
break;
}
// Anything else is paragraph continuation text.
addChildLine(parser.current);
}
parser.advance();
}
endItem();
var itemNodes = <Element>[];
items.forEach(_removeLeadingEmptyLine);
var anyEmptyLines = _removeTrailingEmptyLines(items);
var anyEmptyLinesBetweenBlocks = false;
for (var item in items) {
var hasCheck = false,
checked = false;
if (item.lines.isNotEmpty) {
final m = _reCheckbox.firstMatch(item.lines[0]);
if (m != null) {
hasCheck = true;
checked = m[1] == 'x';
item.lines[0] = m[2]!;
}
}
var itemParser = parser.document.getBlockParser(item.lines)
..offset = item.offset
.._inList = true;
var children = itemParser.parseLines();
if (hasCheck) {
final check = Element('input', null);
if (checked) check.attributes['checked'] = 'checked';
if (parser.document.checkable) {
check.attributes['data-line'] = '${item.offset}';
} else {
check.attributes['disabled'] = 'disabled';
}
check
..attributes['class'] = 'todo'
..attributes['type'] = 'checkbox';
var child = children.first;
(child is Element && child.tag == 'p' ? child.children: children)
?.insert(0, check);
}
final li = Element('li', children);
if (hasCheck) {
li.attributes['class'] = 'todo';
}
itemNodes.add(li);
anyEmptyLinesBetweenBlocks =
anyEmptyLinesBetweenBlocks || itemParser.encounteredBlankLine;
}
// Must strip paragraph tags if the list is "tight".
// http://spec.commonmark.org/0.28/#lists
var listIsTight = !anyEmptyLines && !anyEmptyLinesBetweenBlocks;
if (listIsTight) {
// We must post-process the list items, converting any top-level paragraph
// elements to just text elements.
for (var item in itemNodes) {
var children = item.children;
if (children != null) {
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child is Element && child.tag == 'p') {
children.removeAt(i);
children.insertAll(i, child.children!);
}
}
}
}
}
final ul = Element(listTag, itemNodes);
if (listTag == 'ol' && startNumber != 1) {
ul.attributes['start'] = '$startNumber';
}
return ul;
}