parseInline function
Implementation
List<MdNode> parseInline(String text, bool useDollar) {
final n = text.length;
// Most runs of an assistant's prose contain no markup at all. Finding that
// out costs one scan, and skips the buffer, the delimiter tables and the
// dispatch loop entirely.
var plainUntil = 0;
while (plainUntil < n && !_canStartConstruct(text.codeUnitAt(plainUntil))) {
plainUntil += 1;
}
if (plainUntil == n) {
return n == 0 ? <MdNode>[] : <MdNode>[MdText(text: text)];
}
final delims = _Delims(text);
final nodes = <MdNode>[];
final buf = StringBuffer();
var i = 0;
void flush() {
if (buf.isNotEmpty) {
nodes.add(MdText(text: buf.toString()));
buf.clear();
}
}
while (i < n) {
final c = text.codeUnitAt(i);
// Fast path: a run of characters that cannot begin a construct is copied
// through in one piece. This is the bulk of ordinary prose, and skipping
// the dispatch chain for it is what keeps the parser's cost close to a
// scan.
if (!_canStartConstruct(c)) {
var j = i + 1;
while (j < n && !_canStartConstruct(text.codeUnitAt(j))) {
j += 1;
}
buf.write(text.substring(i, j));
i = j;
continue;
}
var matched = false;
// 
if (c == _bang &&
i + 1 < n &&
text.codeUnitAt(i + 1) == _openBracket &&
delims.bracket.containsKey(i + 1)) {
final r = _tryImage(text, i, delims);
if (r != null) {
flush();
nodes.add(r.node);
i = r.next;
matched = true;
}
}
// [text](url) or [123] source tag
if (!matched && c == _openBracket) {
final link = _tryLink(text, i, useDollar, delims);
if (link != null) {
flush();
nodes.add(link.node);
i = link.next;
matched = true;
} else {
final tag = _trySourceTag(text, i);
if (tag != null) {
flush();
nodes.add(tag.node);
i = tag.next;
matched = true;
}
}
}
// **bold** / *italic*
if (!matched && c == _star) {
// Emphasis is decided by the length of the *run* of asterisks, not by
// the first one. Reading `***both***` as `**` starting at the second
// asterisk left a stray `*` inside the bold, and closing a single `*`
// with `indexOf('*')` landed on the opening half of a nested `**`, which
// dropped the bold and cut the italic into pieces.
var run = 1;
while (i + run < n && text.codeUnitAt(i + run) == _star) {
run += 1;
}
// `***x***` is both.
if (run >= 3) {
final close = _nextStarRun(text, i + run, 3);
if (close != -1) {
final inner = text.substring(i + 3, close);
if (inner.trim().isNotEmpty) {
flush();
nodes.add(
MdBold(
children: [MdItalic(children: parseInline(inner, useDollar))],
),
);
i = close + 3;
matched = true;
}
}
}
if (!matched && run == 2) {
final close = _nextStarRun(text, i + 2, 2);
if (close != -1) {
final inner = text.substring(i + 2, close);
if (inner.trim().isNotEmpty) {
flush();
nodes.add(MdBold(children: parseInline(inner, useDollar)));
i = close + 2;
matched = true;
}
}
}
if (!matched && run == 1) {
// Only a lone asterisk closes an italic; a `**` inside it opens a
// bold, which the recursive parse below then claims.
final close = _nextStarRun(text, i + 1, 1, exact: true);
if (close != -1) {
final inner = text.substring(i + 1, close);
if (inner.trim().isNotEmpty) {
flush();
nodes.add(MdItalic(children: parseInline(inner, useDollar)));
i = close + 1;
matched = true;
}
}
}
}
// ~~strike~~
if (!matched &&
c == _tilde &&
i + 1 < n &&
text.codeUnitAt(i + 1) == _tilde) {
final end = text.indexOf('~~', i + 2);
if (end != -1) {
flush();
nodes.add(
MdStrike(
children: parseInline(text.substring(i + 2, end), useDollar),
),
);
i = end + 2;
matched = true;
}
}
// `code`
if (!matched && c == _backtick) {
final end = text.indexOf('`', i + 1);
if (end != -1) {
flush();
nodes.add(MdInlineCode(text: text.substring(i + 1, end)));
i = end + 1;
matched = true;
}
}
// <u>underline</u>
if (!matched && c == _lt && text.startsWith('<u>', i)) {
final end = text.indexOf('</u>', i + 3);
if (end != -1) {
flush();
nodes.add(
MdUnderline(
children: parseInline(text.substring(i + 3, end), useDollar),
),
);
i = end + 4;
matched = true;
}
}
// \[ block latex \] in an inline position.
//
// The block parser claims `\[` only when it opens a line, so block maths
// written mid-sentence — or after a list marker, `1. Result: \[ x^2 \]` —
// used to survive as literal text. The syntax is recognised wherever it
// appears; it still renders as a block, because that is what it is.
if (!matched &&
c == _backslash &&
i + 1 < n &&
text.codeUnitAt(i + 1) == _openBracket) {
final end = text.indexOf('\\]', i + 2);
if (end != -1) {
flush();
nodes.add(MdBlockLatex(tex: text.substring(i + 2, end).trim()));
i = end + 2;
matched = true;
}
}
// \( inline latex \)
if (!matched &&
c == _backslash &&
i + 1 < n &&
text.codeUnitAt(i + 1) == _openParen) {
final end = text.indexOf('\\)', i + 2);
if (end != -1) {
flush();
nodes.add(MdInlineLatex(tex: text.substring(i + 2, end).trim()));
i = end + 2;
matched = true;
}
}
// $$ … $$ / $ … $ (only when enabled)
if (!matched && useDollar && c == _dollar) {
if (i + 1 < n && text.codeUnitAt(i + 1) == _dollar) {
final end = text.indexOf(r'$$', i + 2);
if (end != -1) {
flush();
nodes.add(MdInlineLatex(tex: text.substring(i + 2, end).trim()));
i = end + 2;
matched = true;
}
}
if (!matched) {
final end = text.indexOf(r'$', i + 1);
if (end != -1) {
final inner = text.substring(i + 1, end);
if (inner.trim().isNotEmpty) {
flush();
nodes.add(MdInlineLatex(tex: inner.trim()));
i = end + 1;
matched = true;
}
}
}
}
// \| — the GFM escape for a literal pipe. Table cells are split before
// this runs (see _splitPipes in block_parser.dart), which is what lets a
// pipe reach a cell at all; here the backslash is dropped so the reader
// sees `|`. Only `|` is unescaped: a general \X rule would change how
// \*, \_ and friends render across every document.
if (!matched &&
c == _backslash &&
i + 1 < n &&
text.codeUnitAt(i + 1) == _pipe) {
buf.writeCharCode(_pipe);
i += 2;
matched = true;
}
if (!matched) {
buf.writeCharCode(c);
i += 1;
}
}
flush();
return nodes;
}