parseBlocks function

List<MdNode> parseBlocks(
  1. List<String> lines,
  2. bool useDollar, [
  3. MarkdownBlockRegistry? registry,
  4. MarkdownDefinitions defs = MarkdownDefinitions.empty,
])

Implementation

List<MdNode> parseBlocks(
  List<String> lines,
  bool useDollar, [
  MarkdownBlockRegistry? registry,
  MarkdownDefinitions defs = MarkdownDefinitions.empty,
]) {
  final out = <MdNode>[];
  final n = lines.length;
  var i = 0;
  // Where the last comment scanned for closed. A later `<!--` line before
  // that point closes there too, so it is not scanned again — each paragraph
  // starting `<!--` with the `-->` far below would otherwise rescan to it.
  ({int next, String trailing})? commentEnd;
  var commentFrom = -1;

  while (i < n) {
    final raw = lines[i];
    if (isBlank(raw)) {
      i += 1;
      continue;
    }
    final trimmed = raw.trimLeft();

    final custom = registry?.match(lines, i);
    if (custom != null) {
      out.add(custom.node);
      i = custom.endLine;
      continue;
    }

    // Fenced code block ```lang ... ``` or ~~~lang ... ~~~. It closes on a
    // run of the same character at least as long as the opener, so a ````
    // fence can show a ``` fence inside it.
    final fence = fenceOpen(trimmed);
    if (fence != null) {
      i += 1;
      final code = <String>[];
      var closed = false;
      while (i < n) {
        if (isFenceClose(lines[i], fence)) {
          closed = true;
          i += 1;
          break;
        }
        code.add(lines[i]);
        i += 1;
      }
      out.add(
        MdCodeBlock(
          language: fence.info,
          code: code.join('\n'),
          closed: closed,
        ),
      );
      continue;
    }

    // <!-- comment --> on lines of its own — hidden, blank lines and all.
    // Unclosed, it runs to the end, as an HTML block does: a comment still
    // streaming in stays hidden.
    if (startsHtmlComment(trimmed)) {
      final cached = commentEnd;
      final ({int next, String trailing}) end;
      if (cached != null &&
          i > commentFrom &&
          i < cached.next - 1 &&
          !lines[i].contains('-->', lines[i].indexOf('<!--') + 4)) {
        end = cached;
      } else {
        end = htmlCommentEnd(lines, i);
        commentEnd = end;
        commentFrom = i;
      }
      if (end.trailing.trim().isEmpty) {
        i = end.next;
        continue;
      }
    }

    // [label]: url "title" — consumed; the references resolve to it.
    final linkDefinition = linkDefinitionAt(raw);
    if (linkDefinition != null && defs.link(linkDefinition.label) != null) {
      i += 1;
      continue;
    }

    // [^label]: text — consecutive definitions render as one list.
    final footnotes = _parseFootnotes(lines, i, useDollar, registry, defs);
    if (footnotes != null) {
      out.add(MdFootnoteDefinitions(footnotes: footnotes.footnotes));
      i = footnotes.next;
      continue;
    }

    // Block LaTeX \[ ... \]
    if (trimmed.startsWith('\\[')) {
      var first = trimmed;
      while (first.startsWith('\\[')) {
        first = first.substring(2);
      }
      final end = first.indexOf('\\]');
      if (end != -1) {
        out.add(MdBlockLatex(tex: first.substring(0, end).trim()));
        i += 1;
        continue;
      }
      final content = <String>[];
      if (first.trim().isNotEmpty) {
        content.add(first);
      }
      i += 1;
      while (i < n) {
        final lineEnd = lines[i].indexOf('\\]');
        if (lineEnd != -1) {
          final pre = lines[i].substring(0, lineEnd);
          if (pre.trim().isNotEmpty) {
            content.add(pre);
          }
          i += 1;
          break;
        }
        content.add(lines[i]);
        i += 1;
      }
      out.add(MdBlockLatex(tex: content.join('\n').trim()));
      continue;
    }

    // Heading
    final heading = isHeading(trimmed);
    if (heading != null) {
      out.add(
        MdHeading(
          level: heading.level,
          children: parseInline(heading.content, useDollar, defs),
        ),
      );
      i += 1;
      continue;
    }

    // Horizontal rule
    if (isHr(trimmed)) {
      out.add(const MdHorizontalRule());
      i += 1;
      continue;
    }

    // Blockquote
    if (trimmed.startsWith('>')) {
      final inner = <String>[];
      while (i < n) {
        if (isBlank(lines[i])) {
          break;
        }
        final t = lines[i].trimLeft();
        if (t.startsWith('>')) {
          var stripped = t.substring(1);
          if (stripped.startsWith(' ')) {
            stripped = stripped.substring(1);
          }
          inner.add(stripped);
        } else {
          // A lazy continuation line continues the quote's paragraph — but
          // `---` or `===` after a quote ends it. Taken lazily they turned the
          // quote's last line into a setext heading inside the quote.
          if (isHr(t) || setextUnderlineLevel(t) != null) {
            break;
          }
          inner.add(t);
        }
        i += 1;
      }
      final alertType = inner.isEmpty
          ? null
          : MarkdownAlertType.fromMarker(inner.first);
      out.add(
        MdBlockQuote(
          children: parseBlocks(inner, useDollar, registry, defs),
          alert: alertType == null
              ? null
              : MdAlert(
                  type: alertType,
                  children: parseBlocks(
                    inner.sublist(1),
                    useDollar,
                    registry,
                    defs,
                  ),
                ),
        ),
      );
      continue;
    }

    // Table
    final tableEnd = _tryTable(lines, i, useDollar, out, defs);
    if (tableEnd != null) {
      i = tableEnd;
      continue;
    }

    // Checkbox / radio
    final checkbox = checkboxMarker(trimmed);
    if (checkbox != null) {
      out.add(
        MdCheckbox(
          checked: checkbox.checked,
          children: parseInline(checkbox.content, useDollar, defs),
        ),
      );
      i += 1;
      continue;
    }
    final radio = radioMarker(trimmed);
    if (radio != null) {
      out.add(
        MdRadio(
          selected: radio.selected,
          children: parseInline(radio.content, useDollar, defs),
        ),
      );
      i += 1;
      continue;
    }

    // Lists
    if (unorderedMarker(trimmed) != null) {
      final r = _parseListInner(lines, i, false, useDollar, registry, defs);
      out.add(MdUnorderedList(items: r.items));
      i = r.next;
      continue;
    }
    if (orderedMarker(trimmed) != null) {
      final r = _parseListInner(lines, i, true, useDollar, registry, defs);
      out.add(MdOrderedList(start: r.start, items: r.items));
      i = r.next;
      continue;
    }

    // Paragraph: gather consecutive non-blank, non-block lines. A `===` or
    // `---` line under it makes it a setext heading instead.
    final para = <String>[];
    int? setextLevel;
    while (i < n) {
      if (isBlank(lines[i])) {
        break;
      }
      if (para.isNotEmpty) {
        setextLevel = setextUnderlineLevel(lines[i]);
        if (setextLevel != null) {
          i += 1;
          break;
        }
      }
      final t = lines[i].trimLeft();
      if (_startsBlock(t) ||
          (para.isNotEmpty && registry?.match(lines, i) != null)) {
        break;
      }
      para.add(t);
      i += 1;
    }
    // Joined on the newline, not on a space. CommonMark folds a single
    // newline inside a paragraph into a space, and this package has never
    // done that: a model writes a list of bullet glyphs, or a run of short
    // lines, and folding them produced one long wrapped line. Folding also
    // swallowed the two breaks CommonMark does define — two trailing spaces
    // and a trailing backslash — because the newline they mark was gone
    // before the inline parser ran.
    final content = parseInline(para.join('\n'), useDollar, defs);
    out.add(
      setextLevel != null
          ? MdHeading(level: setextLevel, children: content)
          : MdParagraph(children: content),
    );
  }

  return out;
}