Footnote support (dart-lang/markdown#441)
diff --git a/pkgs/markdown/lib/markdown.dart b/pkgs/markdown/lib/markdown.dart index 0db8af7..99a85de 100644 --- a/pkgs/markdown/lib/markdown.dart +++ b/pkgs/markdown/lib/markdown.dart
@@ -45,6 +45,7 @@ export 'src/block_syntaxes/empty_block_syntax.dart'; export 'src/block_syntaxes/fenced_blockquote_syntax.dart'; export 'src/block_syntaxes/fenced_code_block_syntax.dart'; +export 'src/block_syntaxes/footnote_def_syntax.dart'; export 'src/block_syntaxes/header_syntax.dart'; export 'src/block_syntaxes/header_with_id_syntax.dart'; export 'src/block_syntaxes/horizontal_rule_syntax.dart';
diff --git a/pkgs/markdown/lib/src/ast.dart b/pkgs/markdown/lib/src/ast.dart index 3fb2561..13490d0 100644 --- a/pkgs/markdown/lib/src/ast.dart +++ b/pkgs/markdown/lib/src/ast.dart
@@ -19,6 +19,7 @@ final List<Node>? children; final Map<String, String> attributes; String? generatedId; + String? footnoteLabel; /// Instantiates a [tag] Element with [children]. Element(this.tag, this.children) : attributes = {};
diff --git a/pkgs/markdown/lib/src/block_syntaxes/footnote_def_syntax.dart b/pkgs/markdown/lib/src/block_syntaxes/footnote_def_syntax.dart new file mode 100644 index 0000000..7b959ff --- /dev/null +++ b/pkgs/markdown/lib/src/block_syntaxes/footnote_def_syntax.dart
@@ -0,0 +1,81 @@ +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 one kind of block, if true footnotes block should end. + static bool _isBlock(Iterable<BlockSyntax> syntaxList, String line) { + return syntaxList.any((s) => s.pattern.hasMatch(line)); + } +}
diff --git a/pkgs/markdown/lib/src/document.dart b/pkgs/markdown/lib/src/document.dart index ff6bc0f..e68579a 100644 --- a/pkgs/markdown/lib/src/document.dart +++ b/pkgs/markdown/lib/src/document.dart
@@ -14,6 +14,12 @@ /// Maintains the context needed to parse a Markdown document. class Document { final Map<String, LinkReference> linkReferences = {}; + + /// Footnote ref count, keys are case-sensitive and added by define syntax. + final footnoteReferences = <String, int>{}; + + /// Footnotes labels by appearing order, are case-insensitive and added by ref syntax. + final footnoteLabels = <String>[]; final Resolver? linkResolver; final Resolver? imageLinkResolver; final bool encodeHtml; @@ -78,7 +84,8 @@ List<Node> parseLineList(List<Line> lines) { final nodes = BlockParser(lines, this).parseLines(); _parseInlineContent(nodes); - return nodes; + // Do filter after parsing inline as we need ref count. + return _filterFootnotes(nodes); } /// Parses the given inline Markdown [text] to a series of AST nodes. @@ -97,6 +104,90 @@ } } } + + /// Footnotes could be defined in arbitrary positions of a document, we need + /// to distinguish them and put them behind; and every footnote definition + /// may have multiple backrefs, we need to append backrefs for it. + List<Node> _filterFootnotes(List<Node> nodes) { + final footnotes = <Element>[]; + final blocks = <Node>[]; + for (final node in nodes) { + if (node is Element && + node.tag == 'li' && + footnoteReferences.containsKey(node.footnoteLabel)) { + final label = node.footnoteLabel; + var count = 0; + if (label != null && (count = footnoteReferences[label] ?? 0) > 0) { + footnotes.add(node); + final children = node.children; + if (children != null) { + _appendBackref(children, Uri.encodeComponent(label), count); + } + } + } else { + blocks.add(node); + } + } + + if (footnotes.isNotEmpty) { + // Sort footnotes by appearing order. + final ordinal = { + for (var i = 0; i < footnoteLabels.length; i++) + 'fn-${footnoteLabels[i]}': i, + }; + footnotes.sort((l, r) { + final idl = l.attributes['id']?.toLowerCase() ?? ''; + final idr = r.attributes['id']?.toLowerCase() ?? ''; + return (ordinal[idl] ?? 0) - (ordinal[idr] ?? 0); + }); + final list = Element('ol', footnotes); + + // Ignore GFM attribute: <data-footnotes>. + final section = Element('section', [list]) + ..attributes['class'] = 'footnotes'; + blocks.add(section); + } + return blocks; + } + + /// Generate backref nodes, append them to footnote definition's last child. + void _appendBackref(List<Node> children, String ref, int count) { + final refs = [ + for (var i = 0; i < count; i++) ...[ + Text(' '), + _ElementExt.footnoteAnchor(ref, i) + ] + ]; + if (children.isEmpty) { + children.addAll(refs); + } else { + final last = children.last; + if (last is Element) { + last.children?.addAll(refs); + } else { + children.last = Element('p', [last, ...refs]); + } + } + } +} + +extension _ElementExt on Element { + static Element footnoteAnchor(String ref, int i) { + final num = '${i + 1}'; + final suffix = i > 0 ? '-$num' : ''; + final e = Element.empty('tag'); + e.match; + return Element('a', [ + Text('\u21a9'), + if (i > 0) + Element('sup', [Text(num)])..attributes['class'] = 'footnote-ref', + ]) + // Ignore GFM's attributes: <data-footnote-backref aria-label="Back to content">. + ..attributes['href'] = '#fnref-$ref$suffix' + ..attributes['class'] = 'footnote-backref'; + } + + String get match => tag; } /// A [link reference
diff --git a/pkgs/markdown/lib/src/extension_set.dart b/pkgs/markdown/lib/src/extension_set.dart index a7bf1ee..660759e 100644 --- a/pkgs/markdown/lib/src/extension_set.dart +++ b/pkgs/markdown/lib/src/extension_set.dart
@@ -1,5 +1,6 @@ import 'block_syntaxes/block_syntax.dart'; import 'block_syntaxes/fenced_code_block_syntax.dart'; +import 'block_syntaxes/footnote_def_syntax.dart'; import 'block_syntaxes/header_with_id_syntax.dart'; import 'block_syntaxes/ordered_list_with_checkbox_syntax.dart'; import 'block_syntaxes/setext_header_with_id_syntax.dart'; @@ -58,6 +59,7 @@ const TableSyntax(), const UnorderedListWithCheckboxSyntax(), const OrderedListWithCheckboxSyntax(), + const FootnoteDefSyntax(), ], ), List<InlineSyntax>.unmodifiable( @@ -80,6 +82,7 @@ const TableSyntax(), const UnorderedListWithCheckboxSyntax(), const OrderedListWithCheckboxSyntax(), + const FootnoteDefSyntax(), ], ), List<InlineSyntax>.unmodifiable(
diff --git a/pkgs/markdown/lib/src/inline_syntaxes/footnote_ref_syntax.dart b/pkgs/markdown/lib/src/inline_syntaxes/footnote_ref_syntax.dart new file mode 100644 index 0000000..32a1eec --- /dev/null +++ b/pkgs/markdown/lib/src/inline_syntaxes/footnote_ref_syntax.dart
@@ -0,0 +1,69 @@ +import '../ast.dart' show Element, Node, Text; +import '../charcode.dart'; +import 'link_syntax.dart' show LinkContext; + +/// The spec of GFM about footnotes is [missing](https://github.com/github/cmark-gfm/issues/283#issuecomment-1378868725). +/// For source code of cmark-gfm, See [noMatch] label of [handle_close_bracket] function in [master@c32ef78](https://github.com/github/cmark-gfm/blob/c32ef78/src/inlines.c#L1236). +/// A Rust implementation is also [available](https://github.com/wooorm/markdown-rs/blob/2498e31eecead798efc649502bbf5f86feaa94be/src/construct/gfm_label_start_footnote.rs). +/// Footnote shares the same syntax with [LinkSyntax], but goes a different branch of handling close bracket. +class FootnoteRefSyntax { + static String? _footnoteLabel(String key) { + if (key.isEmpty || key.codeUnitAt(0) != $caret) { + return null; + } + key = key.substring(1).trim().toLowerCase(); + if (key.isEmpty) { + return null; + } + return key; + } + + static Iterable<Node>? tryCreateFootnoteLink( + LinkContext context, + String text, { + bool? secondary, + }) { + secondary ??= false; + final parser = context.parser; + final key = _footnoteLabel(text); + final refs = parser.document.footnoteReferences; + // `label` is what footnoteReferences stored, it is case sensitive. + final label = + refs.keys.firstWhere((k) => k.toLowerCase() == key, orElse: () => ''); + // `count != null` means footnote was valid. + var count = refs[label]; + // And then check if footnote was matched. + if (key == null || count == null) { + return null; + } + final result = <Node>[]; + // There are 4 cases here: ![^...], [^...], ![...][^...], [...][^...] + if (context.opener.char == $exclamation) { + result.add(Text('!')); + } + refs[label] = ++count; + final labels = parser.document.footnoteLabels; + var pos = labels.indexOf(key); + if (pos < 0) { + pos = labels.length; + labels.add(key); + } + + // `children` are text segments after '[^' before ']'. + final children = context.getChildren(); + if (secondary) { + result.add(Text('[')); + result.addAll(children); + result.add(Text(']')); + } + final id = Uri.encodeComponent(label); + final suffix = count > 1 ? '-$count' : ''; + final link = Element('a', [Text('${pos + 1}')]) + // Ignore GitHub's attribute: <data-footnote-ref>. + ..attributes['href'] = '#fn-$id' + ..attributes['id'] = 'fnref-$id$suffix'; + final sup = Element('sup', [link])..attributes['class'] = 'footnote-ref'; + result.add(sup); + return result; + } +}
diff --git a/pkgs/markdown/lib/src/inline_syntaxes/link_syntax.dart b/pkgs/markdown/lib/src/inline_syntaxes/link_syntax.dart index bc47b49..7499ce4 100644 --- a/pkgs/markdown/lib/src/inline_syntaxes/link_syntax.dart +++ b/pkgs/markdown/lib/src/inline_syntaxes/link_syntax.dart
@@ -8,15 +8,16 @@ import '../inline_parser.dart'; import '../util.dart'; import 'delimiter_syntax.dart'; +import 'footnote_ref_syntax.dart'; /// A helper class holds params of link context. /// Footnote creation needs other info in [_tryCreateReferenceLink]. -class _LinkContext { +class LinkContext { final InlineParser parser; final SimpleDelimiter opener; final List<Node> Function() getChildren; - const _LinkContext(this.parser, this.opener, this.getChildren); + const LinkContext(this.parser, this.opener, this.getChildren); } /// Matches links like `[blah][label]` and `[blah](url)`. @@ -40,7 +41,7 @@ String? tag, required List<Node> Function() getChildren, }) { - final context = _LinkContext(parser, opener, getChildren); + final context = LinkContext(parser, opener, getChildren); final text = parser.source.substring(opener.endPos, parser.pos); // The current character is the `]` that closed the link text. Examine the // next character, to determine what type of link we might have (a '(' @@ -92,7 +93,7 @@ } final label = _parseReferenceLinkLabel(parser); if (label != null) { - return _tryCreateReferenceLink(context, label); + return _tryCreateReferenceLink(context, label, secondary: true); } return null; } @@ -167,9 +168,10 @@ /// /// Returns the nodes if it was successfully created, `null` otherwise. Iterable<Node>? _tryCreateReferenceLink( - _LinkContext context, - String label, - ) { + LinkContext context, + String label, { + bool? secondary, + }) { final parser = context.parser; final getChildren = context.getChildren; final link = _resolveReferenceLink( @@ -180,8 +182,11 @@ if (link != null) { return [link]; } - // TODO: add footnote creation here - return null; + return FootnoteRefSyntax.tryCreateFootnoteLink( + context, + label, + secondary: secondary, + ); } // Tries to create an inline link node.
diff --git a/pkgs/markdown/lib/src/patterns.dart b/pkgs/markdown/lib/src/patterns.dart index 1fdddfc..211782f 100644 --- a/pkgs/markdown/lib/src/patterns.dart +++ b/pkgs/markdown/lib/src/patterns.dart
@@ -50,6 +50,10 @@ final tablePattern = RegExp( r'^[ ]{0,3}\|?([ \t]*:?\-+:?[ \t]*\|)+([ \t]|[ \t]*:?\-+:?[ \t]*)?$'); +/// A line starting with `[^` and contains with `]:`, but without special chars +/// (`\] \r\n\x00\t`) between. Same as [GFM](cmark-gfm/src/scanners.re:318). +final footnotePattern = RegExp(r'(^[ ]{0,3})\[\^([^\] \r\n\x00\t]+)\]:[ \t]*'); + /// A pattern which should never be used. It just satisfies non-nullability of /// pattern fields. final dummyPattern = RegExp('');
diff --git a/pkgs/markdown/test/extensions/footnote_block.unit b/pkgs/markdown/test/extensions/footnote_block.unit new file mode 100644 index 0000000..ae730bf --- /dev/null +++ b/pkgs/markdown/test/extensions/footnote_block.unit
@@ -0,0 +1,288 @@ +>>> footnote reference in footnote definition + +Footnote 1 link[^first]. + +[^first]: footnote reference in footnote definition[^first] + +<<< +<p>Footnote 1 link<sup class="footnote-ref"><a href="#fn-first" id="fnref-first">1</a></sup>.</p> +<section class="footnotes"> +<ol> +<li id="fn-first"> +<p>footnote reference in footnote definition<sup class="footnote-ref"><a href="#fn-first" id="fnref-first-2">1</a></sup> <a href="#fnref-first" class="footnote-backref">↩</a> <a href="#fnref-first-2" class="footnote-backref">↩<sup class="footnote-ref">2</sup></a></p> +</li> +</ol> +</section> +>>> footnote reference cases +[^fifth]: ending with another ']' and different order + +Footnote 1 link[^first]. + +Footnote 2 link[^きゃくちゅう脚注]. + +Footnote 3 link[^p1 p2]. + +Footnote 4 link![^forth]. + +Footnote 5 link![^fifth]]. + +Footnote 6 link![^ sixth ]. + +Footnote 7 link[^きゃくちゅう脚注]. + +[^first]: Here is the footnote definition + +[^きゃくちゅう脚注]: unicode label and duplicated reference. + +[^p1 p2]: p1 p2 + +[^ForTh]: start with '[' and with upper case + +[^sixth]: label-start-with-blank +<<< +<p>Footnote 1 link<sup class="footnote-ref"><a href="#fn-first" id="fnref-first">1</a></sup>.</p> +<p>Footnote 2 link<sup class="footnote-ref"><a href="#fn-%E3%81%8D%E3%82%83%E3%81%8F%E3%81%A1%E3%82%85%E3%81%86%E8%84%9A%E6%B3%A8" id="fnref-%E3%81%8D%E3%82%83%E3%81%8F%E3%81%A1%E3%82%85%E3%81%86%E8%84%9A%E6%B3%A8">2</a></sup>.</p> +<p>Footnote 3 link[^p1 p2].</p> +<p>Footnote 4 link!<sup class="footnote-ref"><a href="#fn-ForTh" id="fnref-ForTh">3</a></sup>.</p> +<p>Footnote 5 link!<sup class="footnote-ref"><a href="#fn-fifth" id="fnref-fifth">4</a></sup>].</p> +<p>Footnote 6 link!<sup class="footnote-ref"><a href="#fn-sixth" id="fnref-sixth">5</a></sup>.</p> +<p>Footnote 7 link<sup class="footnote-ref"><a href="#fn-%E3%81%8D%E3%82%83%E3%81%8F%E3%81%A1%E3%82%85%E3%81%86%E8%84%9A%E6%B3%A8" id="fnref-%E3%81%8D%E3%82%83%E3%81%8F%E3%81%A1%E3%82%85%E3%81%86%E8%84%9A%E6%B3%A8-2">2</a></sup>.</p> +<p>[^p1 p2]: p1 p2</p> +<section class="footnotes"> +<ol> +<li id="fn-first"> +<p>Here is the footnote definition <a href="#fnref-first" class="footnote-backref">↩</a></p> +</li> +<li id="fn-%E3%81%8D%E3%82%83%E3%81%8F%E3%81%A1%E3%82%85%E3%81%86%E8%84%9A%E6%B3%A8"> +<p>unicode label and duplicated reference. <a href="#fnref-%E3%81%8D%E3%82%83%E3%81%8F%E3%81%A1%E3%82%85%E3%81%86%E8%84%9A%E6%B3%A8" class="footnote-backref">↩</a> <a href="#fnref-%E3%81%8D%E3%82%83%E3%81%8F%E3%81%A1%E3%82%85%E3%81%86%E8%84%9A%E6%B3%A8-2" class="footnote-backref">↩<sup class="footnote-ref">2</sup></a></p> +</li> +<li id="fn-ForTh"> +<p>start with '[' and with upper case <a href="#fnref-ForTh" class="footnote-backref">↩</a></p> +</li> +<li id="fn-fifth"> +<p>ending with another ']' and different order <a href="#fnref-fifth" class="footnote-backref">↩</a></p> +</li> +<li id="fn-sixth"> +<p>label-start-with-blank <a href="#fnref-sixth" class="footnote-backref">↩</a></p> +</li> +</ol> +</section> +>>> footnote labels with special chars +empty label[^] and blank label[^ ] + +some[^-] strange[^^] but[^\[] labels[^\[\]] + +[^]: +[^ ]: + +[^-]: valid1 + +[^^]:valid2 + +[^\[]: valid3 + +[^\[\]]: this-is-link-not-footnote +<<< +<p>empty label[^] and blank label[^ ]</p> +<p>some<sup class="footnote-ref"><a href="#fn--" id="fnref--">1</a></sup> strange<sup class="footnote-ref"><a href="#fn-%5E" id="fnref-%5E">2</a></sup> but<sup class="footnote-ref"><a href="#fn-%5C%5B" id="fnref-%5C%5B">3</a></sup> labels<a href="this-is-link-not-footnote">^[]</a></p> +<p>[^]: +[^ ]:</p> +<section class="footnotes"> +<ol> +<li id="fn--"> +<p>valid1 <a href="#fnref--" class="footnote-backref">↩</a></p> +</li> +<li id="fn-%5E"> +<p>valid2 <a href="#fnref-%5E" class="footnote-backref">↩</a></p> +</li> +<li id="fn-%5C%5B"> +<p>valid3 <a href="#fnref-%5C%5B" class="footnote-backref">↩</a></p> +</li> +</ol> +</section> +>>> footnote with paragraph +test footnote[^first]. + +[^first]: Footnote **can have markup** + + and multiple paragraphs. + +"Smartypants, double quotes" and 'single quotes' +<<< +<p>test footnote<sup class="footnote-ref"><a href="#fn-first" id="fnref-first">1</a></sup>.</p> +<p>"Smartypants, double quotes" and 'single quotes'</p> +<section class="footnotes"> +<ol> +<li id="fn-first"> +<p>Footnote <strong>can have markup</strong></p> +<p>and multiple paragraphs. <a href="#fnref-first" class="footnote-backref">↩</a></p> +</li> +</ol> +</section> +>>> footnote adjacent paragraph +Here is a footnote reference,[^1] +[^1]: Here is the footnote. + Subsequent paragraphs +<<< +<p>Here is a footnote reference,<sup class="footnote-ref"><a href="#fn-1" id="fnref-1">1</a></sup></p> +<section class="footnotes"> +<ol> +<li id="fn-1"> +<p>Here is the footnote. +Subsequent paragraphs <a href="#fnref-1" class="footnote-backref">↩</a></p> +</li> +</ol> +</section> +>>> footnote without ref +Here is a footnote reference +[^1]: Here is the footnote. +<<< +<p>Here is a footnote reference</p> +>>> footnote example from github +Here is a simple footnote[^1]. + +A footnote can also have multiple lines[^2]. + +You can also use words, to fit your writing style more closely[^note]. + +[^1]: My reference. +[^2]: Every new line should be prefixed with 2 spaces. + This allows you to have a footnote with multiple lines. +[^note]: + Named footnotes will still render with numbers instead of the text but allow easier identification and linking. + This footnote also has been made with a different syntax using 4 spaces for new lines. +<<< +<p>Here is a simple footnote<sup class="footnote-ref"><a href="#fn-1" id="fnref-1">1</a></sup>.</p> +<p>A footnote can also have multiple lines<sup class="footnote-ref"><a href="#fn-2" id="fnref-2">2</a></sup>.</p> +<p>You can also use words, to fit your writing style more closely<sup class="footnote-ref"><a href="#fn-note" id="fnref-note">3</a></sup>.</p> +<section class="footnotes"> +<ol> +<li id="fn-1"> +<p>My reference. <a href="#fnref-1" class="footnote-backref">↩</a></p> +</li> +<li id="fn-2"> +<p>Every new line should be prefixed with 2 spaces. +This allows you to have a footnote with multiple lines. <a href="#fnref-2" class="footnote-backref">↩</a></p> +</li> +<li id="fn-note"> +<p>Named footnotes will still render with numbers instead of the text but allow easier identification and linking. +This footnote also has been made with a different syntax using 4 spaces for new lines. <a href="#fnref-note" class="footnote-backref">↩</a></p> +</li> +</ol> +</section> +>>> ![^ **image**] without definition should be formatted: unlike github +![^ **image**] +<<< +<p>![^ <strong>image</strong>]</p> +>>> ![^ **image**] with definition should be image: unlike github +![^ **image**] + +[^ **image**]: valid-link +<<< +<p><img src="valid-link" alt="^ image" /></p> +>>> ![^ **image**] with definition should be plain html: unlike github +![^ **image**] + +[^ **image**]: invalid link +<<< +<p>![^ <strong>image</strong>]</p> +<p>[^ <strong>image</strong>]: invalid link</p> +>>> ![ ^**image**] without definition should be formatted +![ ^**image**] +<<< +<p>![ ^<strong>image</strong>]</p> +>>> ![^ ^**image**] without definition should be formatted: unlike github +![^ ^**image**] +<<< +<p>![^ ^<strong>image</strong>]</p> +>>> ![^ ^**image**] with definition should be image: unlike github +![^ ^**image**] + +[^ ^**image**]: valid-link +<<< +<p><img src="valid-link" alt="^ ^image" /></p> +>>> [^ **label**] without definition should be formatted: unlike github +[^ **label**] +<<< +<p>[^ <strong>label</strong>]</p> +>>> [adc][^**link**] without definition should be formatted: unlike github +[adc][^**link**] +<<< +<p>[adc][^<strong>link</strong>]</p> +>>> [adc][^**link**] with definition should be footnotes: +[adc][^**link**] + +[^**link**]: valid-link +<<< +<p>[adc]<sup class="footnote-ref"><a href="#fn-**link**" id="fnref-**link**">1</a></sup></p> +<section class="footnotes"> +<ol> +<li id="fn-**link**"> +<p>valid-link <a href="#fnref-**link**" class="footnote-backref">↩</a></p> +</li> +</ol> +</section> +>>> \[^good] should be text +\[^good] + +[^good]: good +<<< +<p>[^good]</p> +>>>[^ nice] should be link +[^ nice] + +[^ nice ]: good +<<< +<p><a href="good">^ nice</a></p> +>>>[^ nice ] should be text +[^ nice ] + +[^nice ]: good +<<< +<p>[^ nice ]</p> +>>> [^\]] with definition should be link +[^\]] + +[^\]]: good +<<< +<p><a href="good">^]</a></p> +>>> [^a-\nb] with definition should be paragraph +[^a- +b] + +[^a- +b]: good +<<< +<p><a href="good">^a- +b</a></p> +>>> [^a] with error definition should be text +[^a] + +[^a\]: good +<<< +<p>[^a]</p> +>>> [^a] with double definition contain '\]' should be text +[^a] + +[^a\]:]: definition contain '\]' +<<< +<p>[^a]</p> +>>> [^a] with double definition trailing should be footnote +[^a] + +[^a]:]: good +<<< +<p><sup class="footnote-ref"><a href="#fn-a" id="fnref-a">1</a></sup></p> +<section class="footnotes"> +<ol> +<li id="fn-a"> +<p>]: good <a href="#fnref-a" class="footnote-backref">↩</a></p> +</li> +</ol> +</section> +>>> complete image link with definition would be image: unlike github + + +[^image]: image footnote +<<< +<p><img src="example.png" alt="^image" /></p>
diff --git a/pkgs/markdown/test/markdown_test.dart b/pkgs/markdown/test/markdown_test.dart index d78409a..4d21fe8 100644 --- a/pkgs/markdown/test/markdown_test.dart +++ b/pkgs/markdown/test/markdown_test.dart
@@ -60,6 +60,10 @@ 'extensions/strikethrough.unit', inlineSyntaxes: [StrikethroughSyntax()], ); + testFile( + 'extensions/footnote_block.unit', + blockSyntaxes: [const FootnoteDefSyntax()], + ); testDirectory('common_mark'); testDirectory('gfm');