sectionsOf function
Splits a markdown body into {h: heading, a: anchor, b: body} records.
Text before the first heading becomes a leading section with no anchor
or heading, so a page's intro is searchable too. maxSectionLength caps
how much of each section's body is kept, since the index is downloaded
whole.
Implementation
List<Map<String, Object?>> sectionsOf(
String body, {
int maxSectionLength = 1200,
}) {
final sections = <Map<String, Object?>>[];
var heading = '';
var anchor = '';
final buffer = StringBuffer();
var inFence = false;
void flush() {
final text = plainText(buffer.toString());
buffer.clear();
if (heading.isEmpty && text.isEmpty) return;
sections.add({
if (heading.isNotEmpty) 'h': heading,
if (anchor.isNotEmpty) 'a': anchor,
'b': text.length > maxSectionLength
? text.substring(0, maxSectionLength)
: text,
});
}
// Commented-out prose is not on the page, so a `##` inside `<!-- -->` is not
// a section. This has to happen before the split, not in [plainText]: by
// the time that runs, a commented heading has already become an index
// entry deep-linking to an anchor the built page does not contain.
final visible = body.replaceAll(RegExp('<!--.*?-->', dotAll: true), '');
for (final line in visible.split('\n')) {
if (line.trimLeft().startsWith('```')) {
inFence = !inFence;
continue;
}
// Headings inside a fenced block are shell comments, not sections.
final match = inFence
? null
: RegExp(r'^(#{2,3})\s+(.*)$').firstMatch(line);
if (match == null) {
buffer.writeln(line);
continue;
}
flush();
heading = plainText(match.group(2)!);
anchor = anchorFor(match.group(2)!);
}
flush();
return sections;
}