blob: 3e59dcf71fdd710b46de4130ac0839760a7605ad [file] [log] [blame]
import '../ast.dart' show Element, Node;
import '../block_parser.dart' show BlockParser;
import '../line.dart';
import '../patterns.dart' show dummyPattern, emptyPattern, footnotePattern;
import 'block_syntax.dart' show BlockSyntax;
/// The spec of GFM about footnotes is [missing](https://github.com/github/cmark-gfm/issues/283#issuecomment-1378868725).
/// For online source code of cmark-gfm, see [master@c32ef78](https://github.com/github/cmark-gfm/blob/c32ef78/src/blocks.c#L1212).
/// A Rust implementation is also [available](https://github.com/wooorm/markdown-rs/blob/2498e31eecead798efc649502bbf5f86feaa94be/src/construct/gfm_footnote_definition.rs).
/// Footnote definition could contain multiple line-children and children could
/// be separated by one empty line.
/// Its first child-line would be the remaining part of the first line after
/// taking definition leading, combining with other child lines parsed by
/// [parseChildLines], is fed into [BlockParser].
class FootnoteDefSyntax extends BlockSyntax {
const FootnoteDefSyntax();
@override
RegExp get pattern => footnotePattern;
@override
Node? parse(BlockParser parser) {
final current = parser.current.content;
final match = pattern.firstMatch(current)!;
final label = match[2]!;
final refs = parser.document.footnoteReferences;
refs[label] = 0;
final id = Uri.encodeComponent(label);
parser.advance();
final lines = [
Line(current.substring(match[0]!.length)),
...parseChildLines(parser),
];
final children = BlockParser(lines, parser.document).parseLines();
return Element('li', children)
..attributes['id'] = 'fn-$id'
..footnoteLabel = label;
}
@override
List<Line> parseChildLines(BlockParser parser) {
final children = <String>[];
// As one empty line should not split footnote definition, use this flag.
var shouldBeBlock = false;
late final syntaxList = parser.blockSyntaxes
.where((s) => !_excludingPattern.contains(s.pattern));
// Every line is footnote's children util two blank lines or a block.
while (!parser.isDone) {
final line = parser.current.content;
if (line.trim().isEmpty) {
children.add(line);
parser.advance();
shouldBeBlock = true;
continue;
} else if (line.startsWith(' ')) {
children.add(line.substring(4));
parser.advance();
shouldBeBlock = false;
} else if (shouldBeBlock || _isBlock(syntaxList, line)) {
break;
} else {
children.add(line);
parser.advance();
}
}
return children.map(Line.new).toList(growable: false);
}
/// Patterns that would be used to decide if one line is a block.
static final _excludingPattern = {
emptyPattern,
dummyPattern,
};
/// Whether this line is any kind of block.
/// If `true`, the footnote block should end.
static bool _isBlock(Iterable<BlockSyntax> syntaxList, String line) {
return syntaxList.any((s) => s.pattern.hasMatch(line));
}
}