linkDefinitionIn function
A link reference definition starting at line start of lines, or null.
[label]: destination "optional title" — up to three spaces of indent, a
label that is not blank and holds no unescaped bracket, then the
destination and title, either of which may sit on the next line. Nothing
may follow the title on its line; when something does, and the title
started a line of its own, the definition ends before it. A label starting
with ^ is a footnote, not a link. next is the line after it.
Implementation
({String label, MdLinkDefinition definition, int next})? linkDefinitionIn(
List<String> lines,
int start,
) {
if (!startsAfterIndent(lines[start], 0x5B /* [ */)) {
return null;
}
// A definition spans no blank line. Ten lines is far more than any real one
// needs, and bounds the work for a long paragraph that only starts with `[`.
final run = StringBuffer(lines[start]);
for (var k = start + 1; k < lines.length && k < start + 10; k++) {
if (isBlank(lines[k])) {
break;
}
run
..write('\n')
..write(lines[k]);
}
final s = run.toString();
final n = s.length;
var p = firstNonSpace(s) + 1; // past `[`
final labelStart = p;
while (p < n) {
final c = s.codeUnitAt(p);
if (c == 0x5C /* \ */ && p + 1 < n) {
p += 2;
continue;
}
if (c == 0x5B /* [ */ ) {
return null;
}
if (c == 0x5D /* ] */ ) {
break;
}
p += 1;
}
if (p >= n || p + 1 >= n || s.codeUnitAt(p + 1) != 0x3A /* : */ ) {
return null;
}
final label = s.substring(labelStart, p);
if (label.trim().isEmpty || label.startsWith('^') || label.length > 999) {
return null;
}
p += 2;
p = _skipSpace(s, p, newline: true);
// The destination.
final String url;
if (p < n && s.codeUnitAt(p) == 0x3C /* < */ ) {
var q = p + 1;
while (q < n) {
final c = s.codeUnitAt(q);
if (c == 0x5C /* \ */ && q + 1 < n) {
q += 2;
continue;
}
if (c == 0x0A || c == 0x3C /* < */ || c == 0x3E /* > */ ) {
break;
}
q += 1;
}
if (q >= n || s.codeUnitAt(q) != 0x3E /* > */ ) {
return null;
}
url = s.substring(p + 1, q);
p = q + 1;
} else {
var q = p;
var depth = 0;
while (q < n) {
final c = s.codeUnitAt(q);
if (c == 0x5C /* \ */ && q + 1 < n && s.codeUnitAt(q + 1) != 0x0A) {
q += 2;
continue;
}
if (c <= 0x20) {
break;
}
if (c == 0x28 /* ( */ ) {
depth += 1;
if (depth > 32) {
return null;
}
} else if (c == 0x29 /* ) */ ) {
if (depth == 0) {
break;
}
depth -= 1;
}
q += 1;
}
if (q == p || depth != 0) {
return null;
}
url = s.substring(p, q);
p = q;
}
final destinationEnd = p;
int lineAfter(int offset) {
var count = 0;
for (var k = 0; k < offset && k < n; k++) {
if (s.codeUnitAt(k) == 0x0A) {
count += 1;
}
}
return start + count + 1;
}
// Where a definition without a title would end: the destination must end
// its line.
final afterDestination = _skipSpace(s, p, newline: false);
final untitledEnd =
afterDestination >= n || s.codeUnitAt(afterDestination) == 0x0A
? afterDestination
: -1;
({String label, MdLinkDefinition definition, int next})? untitled() =>
untitledEnd == -1
? null
: (
label: label,
definition: MdLinkDefinition(url: _decode(url)),
next: lineAfter(untitledEnd),
);
// The title, separated from the destination by whitespace.
p = _skipSpace(s, p, newline: true);
if (p >= n || p == destinationEnd) {
return untitled();
}
final open = s.codeUnitAt(p);
final close = switch (open) {
0x22 /* " */ => 0x22,
0x27 /* ' */ => 0x27,
0x28 /* ( */ => 0x29,
_ => -1,
};
if (close == -1) {
return untitled();
}
var q = p + 1;
while (q < n) {
final c = s.codeUnitAt(q);
if (c == 0x5C /* \ */ && q + 1 < n) {
q += 2;
continue;
}
if (c == close) {
break;
}
if (open == 0x28 && c == 0x28) {
return untitled();
}
q += 1;
}
if (q >= n) {
return untitled();
}
final title = s.substring(p + 1, q);
final end = _skipSpace(s, q + 1, newline: false);
if (end < n && s.codeUnitAt(end) != 0x0A) {
return untitled();
}
return (
label: label,
definition: MdLinkDefinition(url: _decode(url), title: _decode(title)),
next: lineAfter(end),
);
}