parse method

  1. @override
Node parse(
  1. BlockParser parser
)
override

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;
}