use markdown::{ mdast::{FootnoteDefinition, FootnoteReference, Node, Paragraph, Root, Text}, message, to_html, to_html_with_options, to_mdast, unist::Position, CompileOptions, Options, ParseOptions, }; use pretty_assertions::assert_eq; #[test] fn gfm_footnote() -> Result<(), message::Message> { assert_eq!( to_html("A call.[^a]\n\n[^a]: whatevs"), "

A call.^a

\n", "should ignore footnotes by default" ); assert_eq!( to_html_with_options("A call.[^a]\n\n[^a]: whatevs", &Options::gfm())?, "

A call.1

Footnotes

  1. whatevs ↩

", "should support footnotes" ); assert_eq!( to_html_with_options( "Noot.[^a]\n\n[^a]: dingen", &Options { parse: ParseOptions::gfm(), compile: CompileOptions { gfm_footnote_label: Some("Voetnoten".into()), gfm_footnote_back_label: Some("Terug naar de inhoud".into()), ..CompileOptions::gfm() } } )?, "

Noot.1

Voetnoten

  1. dingen ↩

", "should support `options.gfm_footnote_label`, `options.gfm_footnote_back_label`" ); assert_eq!( to_html_with_options( "[^a]\n\n[^a]: b", &Options { parse: ParseOptions::gfm(), compile: CompileOptions { gfm_footnote_label_tag_name: Some("h1".into()), ..CompileOptions::gfm() } } )?, "

1

Footnotes

  1. b ↩

", "should support `options.gfm_footnote_label_tag_name`" ); assert_eq!( to_html_with_options( "[^a]\n\n[^a]: b", &Options { parse: ParseOptions::gfm(), compile: CompileOptions { gfm_footnote_label_attributes: Some("class=\"footnote-heading\"".into()), ..CompileOptions::gfm() } } )?, "

1

Footnotes

  1. b ↩

", "should support `options.gfm_footnote_label_attributes`" ); assert_eq!( to_html_with_options( "[^a]\n\n[^a]: b", &Options { parse: ParseOptions::gfm(), compile: CompileOptions { gfm_footnote_clobber_prefix: Some("".into()), ..CompileOptions::gfm() } } )?, "

1

Footnotes

  1. b ↩

", "should support `options.gfm_footnote_clobber_prefix`" ); assert_eq!( to_html_with_options("A paragraph.\n\n[^a]: whatevs", &Options::gfm())?, "

A paragraph.

\n", "should ignore definitions w/o calls" ); assert_eq!( to_html_with_options("a[^b]", &Options::gfm())?, "

a[^b]

", "should ignore calls w/o definitions" ); assert_eq!( to_html_with_options("a[^b]\n\n[^b]: c\n[^b]: d", &Options::gfm())?, "

a1

Footnotes

  1. c ↩

", "should use the first of duplicate definitions" ); assert_eq!( to_html_with_options("a[^b], c[^b]\n\n[^b]: d", &Options::gfm())?, "

a1, c1

Footnotes

  1. d ↩ ↩2

", "should supports multiple calls to the same definition" ); assert_eq!( to_html_with_options("![^a](b)", &Options::gfm())?, "

!^a

", "should not support images starting w/ `^` (but see it as a link?!, 1)" ); assert_eq!( to_html_with_options("![^a][b]\n\n[b]: c", &Options::gfm())?, "

!^a

\n", "should not support images starting w/ `^` (but see it as a link?!, 2)" ); assert_eq!( to_html_with_options("[^]()", &Options::gfm())?, "

^

", "should support an empty link with caret" ); assert_eq!( to_html_with_options("![^]()", &Options::gfm())?, "

!^

", "should support an empty image with caret (as link)" ); // assert_eq!( to_html_with_options("Call.[^a\\+b].\n\n[^a\\+b]: y", &Options::gfm())?, "

Call.1.

Footnotes

  1. y ↩

", "should support a character escape in a call / definition" ); assert_eq!( to_html_with_options("Call.[^a©b].\n\n[^a©b]: y", &Options::gfm())?, "

Call.1.

Footnotes

  1. y ↩

", "should support a character reference in a call / definition" ); // // assert_eq!( to_html_with_options("Call.[^a\\]b].\n\n[^a\\]b]: y", &Options::gfm())?, "

Call.1.

Footnotes

  1. y ↩

", "should support a useful character escape in a call / definition" ); assert_eq!( to_html_with_options("Call.[^a[b].\n\n[^a[b]: y", &Options::gfm())?, "

Call.1.

Footnotes

  1. y ↩

", "should support a useful character reference in a call / definition" ); assert_eq!( to_html_with_options("Call.[^a\\+b].\n\n[^a+b]: y", &Options::gfm())?, "

Call.[^a+b].

\n", "should match calls to definitions on the source of the label, not on resolved escapes" ); assert_eq!( to_html_with_options("Call.[^a[b].\n\n[^a\\[b]: y", &Options::gfm())?, "

Call.[^a[b].

\n", "should match calls to definitions on the source of the label, not on resolved references" ); assert_eq!( to_html_with_options("[^1].\n\n[^1]: a\nb", &Options::gfm())?, "

1.

Footnotes

  1. a b ↩

", "should support lazyness (1)" ); assert_eq!( to_html_with_options("[^1].\n\n> [^1]: a\nb", &Options::gfm())?, "

1.

Footnotes

  1. a b ↩

", "should support lazyness (2)" ); assert_eq!( to_html_with_options("[^1].\n\n> [^1]: a\n> b", &Options::gfm())?, "

1.

Footnotes

  1. a b ↩

", "should support lazyness (3)" ); assert_eq!( to_html_with_options("[^1].\n\n[^1]: a\n\n > b", &Options::gfm())?, "

1.

Footnotes

  1. a

    b

    ↩
", "should support lazyness (4)" ); // 999 `x` characters. let max = "x".repeat(999); assert_eq!( to_html_with_options(format!("Call.[^{}].\n\n[^{}]: y", max, max).as_str(), &Options::gfm())?, format!("

Call.1.

Footnotes

  1. y ↩

", max, max, max, max), "should support 999 characters in a call / definition" ); assert_eq!( to_html_with_options( format!("Call.[^a{}].\n\n[^a{}]: y", max, max).as_str(), &Options::gfm() )?, format!("

Call.[^a{}].

\n

[^a{}]: y

", max, max), "should not support 1000 characters in a call / definition" ); assert_eq!( to_html_with_options( "[^a]\n\n[^a]: b\n \n c", &Options::gfm() )?, "

1

Footnotes

  1. b

    c ↩

\n", "should support blank lines in footnote definitions" ); assert_eq!( to_html_with_options( r"a![i](#) a\![i](#) a![i][] a![^1] [^1] ^1] [^1]: b [i]: c", &Options::gfm() )?, r##"

ai a!i ai a!1 1 ^1]

Footnotes

  1. b ↩ ↩2

"##, "should match bang/caret interplay like GitHub" ); assert_eq!( to_html_with_options("a![^1]", &Options::gfm())?, "

a![^1]

", "should match bang/caret interplay (undefined) like GitHub" ); assert_eq!( to_html_with_options( r###"a![^1] [^1]: b "###, &Options::gfm() )?, r##"

a!1

Footnotes

  1. b ↩

"##, "should match bang/caret like GitHub" ); assert_eq!( to_html_with_options( r###"Calls may not be empty: [^]. Calls cannot contain whitespace only: [^ ]. Calls cannot contain whitespace at all: [^ ], [^ ], [^ ]. Calls can contain other characters, such as numbers [^1234567890], or [^^] even another caret. [^]: empty [^ ]: space [^ ]: tab [^ ]: line feed [^1234567890]: numbers [^^]: caret "###, &Options::gfm() )?, r##"

Calls may not be empty: ^.

Calls cannot contain whitespace only: ^ .

Calls cannot contain whitespace at all: ^ , ^ , ^ .

Calls can contain other characters, such as numbers 1, or 2 even another caret.

^ : line feed

Footnotes

  1. numbers ↩

  2. caret ↩

"##, "should match calls like GitHub" ); // Note: // * GH does not support line ending in call. // See: // Here line endings don’t make text disappear. assert_eq!( to_html_with_options( r###"[^a]: # b [^c d]: # e [^f g]: # h [^i j]: # k [^ l]: # l [^m ]: # m xxx[^a], [^c d], [^f g], [^i j], [^ l], [^m ] --- Some calls.[^ w][^x ][^y][^z] [^w]: # w [^x]: # x [^ y]: # y [^x ]: # z "###, &Options::gfm() )?, r##"

[^c d]: # e

[^f g]: # h

[^i j]: # k

[^ l]: # l

[^m ]: # m

xxx1, [^c d], [^f g], [^i j], [^ l], [^m ]


Some calls.23[^y][^z]

[^ y]: # y

3: # z

Footnotes

  1. b

    ↩
  2. w

    ↩
  3. x

    ↩ ↩2
"##, "should match whitespace in calls like GitHub (except for the bugs)" ); assert_eq!( to_html_with_options( r###"[^*emphasis*] [^**strong**] [^`code`] [^www.example.com] [^https://example.com] [^://example.com] [^[link](#)] [^![image](#)] [^*emphasis*]: a [^**strong**]: a [^`code`]: a [^www.example.com]: a [^https://example.com]: a [^://example.com]: a [^[link](#)]: a [^![image](#)]: a "###, &Options::gfm() )?, // Note: // * GH does not support colons. // See: // Here identifiers that include colons *do* work (so they’re added below). // * GH does not support footnote-like brackets around an image. // See: // Here images are fine. r##"

1

2

3

4

5

6

[^link]

[^image]

[^link]: a

[^image]: a

Footnotes

  1. a ↩

  2. a ↩

  3. a ↩

  4. a ↩

  5. a ↩

  6. a ↩

"##, "should match construct identifiers like GitHub (except for its bugs)" ); assert_eq!( to_html_with_options( r###"Call[^1][^2][^3][^4] > [^1]: Defined in a block quote. > > More. [^2]: Directly after a block quote. * [^3]: Defined in a list item. More. [^4]: Directly after a list item. "###, &Options::gfm() )?, r##"

Call1234

More.

  • More.

Footnotes

  1. Defined in a block quote. ↩

  2. Directly after a block quote. ↩

  3. Defined in a list item. ↩

  4. Directly after a list item. ↩

"##, "should match containers like GitHub" ); assert_eq!( to_html_with_options( r###"[^1][^2][^3][^4] [^1]: Paragraph …continuation # Heading [^2]: Paragraph …continuation “code”, which is paragraphs… …because of the indent! [^3]: Paragraph …continuation > block quote [^4]: Paragraph …continuation - list "###, &Options::gfm() )?, r##"

1234

Heading

block quote

  • list

Footnotes

  1. Paragraph …continuation ↩

  2. Paragraph …continuation

    “code”, which is paragraphs…

    …because of the indent! ↩

  3. Paragraph …continuation ↩

  4. Paragraph …continuation ↩

"##, "should match continuation like GitHub" ); assert_eq!( to_html_with_options( r###"Call[^1][^2][^3][^4][^5]. [^1]: --- [^2]: Paragraph. [^3]: Lazy? [^4]: Another blank. [^5]: Lazy! "###, &Options::gfm() )?, r##"

Call12345.

Lazy?

Lazy!

Footnotes


  1. ↩
  2. Paragraph. ↩

  3. ↩
  4. Another blank. ↩

  5. ↩
"##, "should match definitions initial blank like GitHub" ); assert_eq!( to_html_with_options( r###"Note![^0][^1][^2][^3][^4][^5][^6][^7][^8][^9][^10] [^0]: alpha [^1]: bravo [^2]: charlie indented delta [^3]: echo [^4]: foxtrot [^5]:> golf [^6]: > hotel [^7]: > india [^8]: # juliett [^9]: --- [^10]:- - - kilo "###, &Options::gfm() )?, r##"

Note!1234567891011

Footnotes

  1. alpha ↩

  2. bravo ↩

  3. charlie indented delta ↩

  4. echo ↩

  5. foxtrot ↩

  6. golf

    ↩
  7. hotel

    ↩
  8. india

    ↩
  9. juliett

    ↩

  10. ↩
        • kilo
    ↩
"##, "should match definitions like GitHub" ); assert_eq!( to_html_with_options( r###"Call[^1][^1] [^1]: Recursion[^1][^1] "###, &Options::gfm() )?, r##"

Call11

Footnotes

  1. Recursion11 ↩ ↩2 ↩3 ↩4

"##, "should match duplicate calls and recursion like GitHub" ); assert_eq!( to_html_with_options( r###"Call[^1] [^1]: a [^1]: b "###, &Options::gfm() )?, r##"

Call1

Footnotes

  1. a ↩

"##, "should match duplicate definitions like GitHub" ); // Note: // * GH “supports” footnotes inside links. // This breaks an HTML parser, as it is not allowed. // See: // CommonMark has mechanisms in place to prevent links in links. // These mechanisms are in place here too. assert_eq!( to_html_with_options( r###"*emphasis[^1]* **strong[^2]** `code[^3]` ![image[^4]](#) [link[^5]](#) [^1]: a [^2]: b [^3]: c [^4]: d [^5]: e "###, &Options::gfm() )?, r##"

emphasis1

strong2

code[^3]

image

[link4](#)

Footnotes

  1. a ↩

  2. b ↩

  3. d ↩

  4. e ↩

"##, "should match footnotes in constructs like GitHub (without the bugs)" ); assert_eq!( to_html_with_options( r###"What are these![^1], ![^2][], and ![this][^3]. [^1]: a [^2]: b [^3]: c "###, &Options::gfm() )?, r##"

What are these!1, !2[], and ![this]3.

Footnotes

  1. a ↩

  2. b ↩

  3. c ↩

"##, "should match images/footnotes like GitHub" ); assert_eq!( to_html_with_options( r###"[^0][^1][^2][^3][^4][^5] [^0]: Paragraph …continuation [^1]: Another [^2]: Paragraph …continuation # Heading [^3]: Paragraph …continuation “code”, which is paragraphs… …because of the indent! [^4]: Paragraph …continuation > block quote [^5]: Paragraph …continuation * list "###, &Options::gfm() )?, r##"

123456

Heading

block quote

  • list

Footnotes

  1. Paragraph …continuation ↩

  2. Another ↩

  3. Paragraph …continuation ↩

  4. Paragraph …continuation “code”, which is paragraphs…

    …because of the indent! ↩

  5. Paragraph …continuation ↩

  6. Paragraph …continuation ↩

"##, "should match interrupt like GitHub" ); assert_eq!( to_html_with_options( r###"What are these[^1], [^2][], and [this][^3]. [^1]: a [^2]: b [^3]: c "###, &Options::gfm() )?, r##"

What are these1, 2[], and [this]3.

Footnotes

  1. a ↩

  2. b ↩

  3. c ↩

"##, "should match links/footnotes like GitHub" ); assert_eq!( to_html_with_options( r###"[^1][^2][^3][^4] [^1]: Paragraph # Heading [^2]: Paragraph “code”, which is paragraphs… …because of the indent! [^3]: Paragraph > block quote [^4]: Paragraph - list "###, &Options::gfm() )?, r##"

1234

Heading

block quote

  • list

Footnotes

  1. Paragraph ↩

  2. Paragraph

    “code”, which is paragraphs…

    …because of the indent! ↩

  3. Paragraph ↩

  4. Paragraph ↩

"##, "should match many blank lines/no indent like GitHub" ); assert_eq!( to_html_with_options( r###"[^1][^2][^3][^4] [^1]: Paragraph # Heading [^2]: Paragraph code more code [^3]: Paragraph > block quote [^4]: Paragraph - list "###, &Options::gfm() )?, r##"

1234

Footnotes

  1. Paragraph

    Heading

    ↩
  2. Paragraph

    code
    
    
    more code
    
    ↩
  3. Paragraph

    block quote

    ↩
  4. Paragraph

    • list
    ↩
"##, "should match many blank lines like GitHub" ); assert_eq!( to_html_with_options( r###"Note![^1][^2][^3][^4] - [^1]: Paragraph > [^2]: Paragraph [^3]: [^4]: Paragraph "###, &Options::gfm() )?, r##"

Note!1234

Footnotes

  1. Paragraph ↩

  2. Paragraph ↩

  3. ↩
  4. Paragraph ↩

"##, "should match nest like GitHub" ); assert_eq!( to_html_with_options( r###"[^1][^2][^3][^4] [^1]: Paragraph # Heading [^2]: Paragraph “code”, which is paragraphs… …because of the indent! [^3]: Paragraph > block quote [^4]: Paragraph - list "###, &Options::gfm() )?, r##"

1234

Heading

block quote

  • list

Footnotes

  1. Paragraph ↩

  2. Paragraph

    “code”, which is paragraphs…

    …because of the indent! ↩

  3. Paragraph ↩

  4. Paragraph ↩

"##, "should match normal blank lines/no indent like GitHub" ); assert_eq!( to_html_with_options( r###"[^1][^2][^3][^4] [^1]: Paragraph # Heading [^2]: Paragraph code more code [^3]: Paragraph > block quote [^4]: Paragraph - list "###, &Options::gfm() )?, r##"

1234

Footnotes

  1. Paragraph

    Heading

    ↩
  2. Paragraph

    code
    
    more code
    
    ↩
  3. Paragraph

    block quote

    ↩
  4. Paragraph

    • list
    ↩
"##, "should match normal blank lines like GitHub" ); assert_eq!( to_html_with_options( r###"Here is a footnote reference,[^1] and another.[^longnote] [^1]: Here is the footnote. [^longnote]: Here’s one with multiple blocks. Subsequent paragraphs are indented to show that they belong to the previous footnote. { some.code } The whole paragraph can be indented, or just the first line. In this way, multi-paragraph footnotes work like multi-paragraph list items. This paragraph won’t be part of the note, because it isn’t indented. "###, &Options::gfm() )?, r##"

Here is a footnote reference,1 and another.2

This paragraph won’t be part of the note, because it isn’t indented.

Footnotes

  1. Here is the footnote. ↩

  2. Here’s one with multiple blocks.

    Subsequent paragraphs are indented to show that they belong to the previous footnote.

    { some.code }
    

    The whole paragraph can be indented, or just the first line. In this way, multi-paragraph footnotes work like multi-paragraph list items. ↩

"##, "should match pandoc like GitHub" ); assert_eq!( to_html_with_options( r###"Call[^1][^2][^3][^4][^5][^6][^7][^8][^9][^10][^11][^12]. [^1]: 5 [^2]: 4 [^3]: 3 [^4]: 2 [^5]: 1 [^6]: 0 *** [^7]: 3 5 [^8]: 3 4 [^9]: 3 3 [^10]: 2 5 [^11]: 2 4 [^12]: 2 3 "###, &Options::gfm() )?, r##"

Call[^1][^2]12345678910.

 [^1]: 5

[^2]: 4

3

3

Footnotes

  1. 3 ↩

  2. 2 ↩

  3. 1 ↩

  4. 0 ↩

  5. 3

    5 ↩

  6. 3

    4 ↩

  7. 3 ↩

  8. 2

    5 ↩

  9. 2

    4 ↩

  10. 2 ↩

"##, "should match prefix before like GitHub" ); assert_eq!( to_html_with_options( r###"Call[^1][^2][^3][^4][^5][^6][^7][^8][^9]. [^1]: a 8 [^2]: a 7 [^3]: a 6 [^4]: a 5 [^5]: a 4 [^6]: a 3 [^7]: a 2 [^8]: a 1 [^9]: a 0 "###, &Options::gfm() )?, r##"

Call123456789.

3

2

1

0

Footnotes

  1. a

    8
    
    ↩
  2. a

    7 ↩

  3. a

    6 ↩

  4. a

    5 ↩

  5. a

    4 ↩

  6. a ↩

  7. a ↩

  8. a ↩

  9. a ↩

"##, "should match prefix like GitHub" ); assert_eq!( to_html_with_options( r###"Here is a short reference,[1], a collapsed one,[2][], and a full [one][3]. [1]: a [2]: b [3]: c "###, &Options::gfm() )?, r#"

Here is a short reference,1, a collapsed one,2, and a full one.

"#, "should match references and definitions like GitHub" ); assert_eq!( to_mdast("[^a]: b\n\tc\n\nd [^a] e.", &ParseOptions::gfm())?, Node::Root(Root { children: vec![ Node::FootnoteDefinition(FootnoteDefinition { children: vec![Node::Paragraph(Paragraph { children: vec![Node::Text(Text { value: "b\nc".into(), position: Some(Position::new(1, 7, 6, 2, 6, 10)) })], position: Some(Position::new(1, 7, 6, 2, 6, 10)) })], identifier: "a".into(), label: Some("a".into()), position: Some(Position::new(1, 1, 0, 3, 1, 11)) }), Node::Paragraph(Paragraph { children: vec![ Node::Text(Text { value: "d ".into(), position: Some(Position::new(4, 1, 12, 4, 3, 14)) }), Node::FootnoteReference(FootnoteReference { identifier: "a".into(), label: Some("a".into()), position: Some(Position::new(4, 3, 14, 4, 7, 18)) }), Node::Text(Text { value: " e.".into(), position: Some(Position::new(4, 7, 18, 4, 10, 21)) }) ], position: Some(Position::new(4, 1, 12, 4, 10, 21)) }) ], position: Some(Position::new(1, 1, 0, 4, 10, 21)) }), "should support GFM footnotes as `FootnoteDefinition`, `FootnoteReference`s in mdast" ); Ok(()) }