inlineSafeLength function
How much of source can be shown without its styling changing later.
Returns source's length when nothing is pending, so the common case costs
one scan and no allocation.
Deliberately a scan for unbalanced delimiters rather than a second parse: it runs on the tail segment for every chunk that arrives, and it only has to be conservative. Holding a character that would not have changed costs a few milliseconds of latency; showing one that does change is the artefact this exists to remove.
Implementation
int inlineSafeLength(String source, {bool holdMathDollars = false}) {
var limit = source.length;
// Each delimiter carries its own patience. Holding is only ever worth it
// while the closer is plausibly still coming.
void holdAt(int index, int cap) {
if (index < 0 || index >= limit) {
return;
}
if (source.length - index > cap) {
return;
}
limit = index;
}
// A code span runs to its closing backtick and swallows any other delimiter
// on the way, so it is resolved first and the rest is checked outside it.
var i = 0;
var lastOpenTick = -1;
final outside = StringBuffer();
while (i < source.length) {
if (source.codeUnitAt(i) == 0x60 /* ` */ ) {
final close = source.indexOf('`', i + 1);
if (close == -1) {
lastOpenTick = i;
break;
}
// Keep the offsets aligned without copying the span's contents.
outside.write(' ' * (close + 1 - i));
i = close + 1;
continue;
}
outside.writeCharCode(source.codeUnitAt(i));
i += 1;
}
holdAt(lastOpenTick, proseDelimiterHold);
// A `*` that begins a line and is followed by whitespace is a list bullet,
// and a line made only of `*`/`-`/`_` is a thematic break — neither is an
// emphasis delimiter, and counting them as one made every streamed `*`
// bullet invisible for the length of the leash. They are blanked out
// before the emphasis scans, offsets preserved.
final rest = _maskLineMarkers(outside.toString());
// Paired delimiters: an odd count means the last one is still open.
for (final token in const ['**', '~~', r'$$']) {
holdAt(_lastUnpaired(rest, token), proseDelimiterHold);
}
// Single `*` for italic, but only the ones that are not part of `**`.
holdAt(_lastUnpairedStar(rest), proseDelimiterHold);
// Delimiters with distinct open and close forms.
for (final pair in const [(r'\(', r'\)'), (r'\[', r'\]'), ('<u>', '</u>')]) {
final open = rest.lastIndexOf(pair.$1);
if (open != -1 && rest.indexOf(pair.$2, open + pair.$1.length) == -1) {
holdAt(open, markupDelimiterHold);
}
}
// A link or image: `[label](href)` is prose until its closing paren.
final bracket = rest.lastIndexOf('[');
if (bracket != -1) {
final closeBracket = rest.indexOf(']', bracket + 1);
if (closeBracket == -1) {
holdAt(bracket, markupDelimiterHold);
} else if (closeBracket + 1 < rest.length &&
rest.codeUnitAt(closeBracket + 1) == 0x28 /* ( */ &&
rest.indexOf(')', closeBracket + 1) == -1) {
holdAt(bracket, markupDelimiterHold);
}
}
// A table row is atomic: a line beginning with `|` is held until its
// newline arrives, so the parser only ever sees complete rows. And while
// the delimiter row is still arriving, the header is held with it — a
// partial delimiter is invalid, so without this the table formed on one
// cut (`|-`), fell apart on the next (`|:`), and re-formed, several times
// per row.
final lastNewline = source.lastIndexOf('\n');
final lastLine = source.substring(lastNewline + 1);
if (lastLine.trimLeft().startsWith('|')) {
var holdIndex = lastNewline + 1;
if (lastNewline >= 0 && _partialTableSeparator.hasMatch(lastLine.trim())) {
final prevNewline = source.lastIndexOf('\n', lastNewline - 1);
final prevLine = source.substring(prevNewline + 1, lastNewline);
if (prevLine.trimLeft().startsWith('|')) {
holdIndex = prevNewline + 1;
}
}
holdAt(holdIndex, markupDelimiterHold);
}
// Single-`$` maths, only when the caller treats `$…$` as maths at all —
// to everyone else a dollar is a dollar.
if (holdMathDollars) {
holdAt(_lastUnpairedDollar(rest), markupDelimiterHold);
}
// A trailing character that is only the first half of an opener: `\` may
// become `\(` and `<` may become `<u>`. Un-held it is shown, revealed, and
// then vanishes when the second half lands — the one leak the rules above
// cannot see. The very next character decides, so this holds a couple of
// characters at most and cannot stall.
final partial = _partialTrailingOpener.firstMatch(source);
if (partial != null) {
holdAt(partial.start, proseDelimiterHold);
}
return limit;
}