| 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)); |
| } |
| } |