parseBlocks function
List<MdNode>
parseBlocks(
- List<
String> lines, - bool useDollar, [
- MarkdownBlockRegistry? registry,
- 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;
}