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>&quot;Smartypants, double quotes&quot; 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](example.png)
+
+[^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');