extern crate markdown; use markdown::{ mdast::{FootnoteDefinition, FootnoteReference, Node, Paragraph, Root, Text}, to_html, to_html_with_options, to_mdast, unist::Position, CompileOptions, Constructs, Options, ParseOptions, }; use pretty_assertions::assert_eq; #[test] fn gfm_footnote() -> Result<(), String> { let gfm = Options { parse: ParseOptions { constructs: Constructs::gfm(), ..ParseOptions::default() }, ..Options::default() }; 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", &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 { constructs: Constructs::gfm(), ..ParseOptions::default() }, compile: CompileOptions { gfm_footnote_label: Some("Voetnoten".into()), gfm_footnote_back_label: Some("Terug naar de inhoud".into()), ..CompileOptions::default() } } )?, "

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 { constructs: Constructs::gfm(), ..ParseOptions::default() }, compile: CompileOptions { gfm_footnote_label_tag_name: Some("h1".into()), ..CompileOptions::default() } } )?, "

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 { constructs: Constructs::gfm(), ..ParseOptions::default() }, compile: CompileOptions { gfm_footnote_label_attributes: Some("class=\"footnote-heading\"".into()), ..CompileOptions::default() } } )?, "

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 { constructs: Constructs::gfm(), ..ParseOptions::default() }, compile: CompileOptions { gfm_footnote_clobber_prefix: Some("".into()), ..CompileOptions::default() } } )?, "

1

Footnotes

  1. b

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

A paragraph.

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

a[^b]

", "should ignore calls w/o definitions" ); assert_eq!( to_html_with_options("a[^b]\n\n[^b]: c\n[^b]: d", &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", &gfm)?, "

a1, c1

Footnotes

  1. d 2

", "should supports multiple calls to the same definition" ); assert_eq!( to_html_with_options("![^a](b)", &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", &gfm)?, "

!^a

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

^

", "should support an empty link with caret" ); assert_eq!( to_html_with_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", &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", &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", &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", &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", &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", &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", &gfm)?, "

1.

Footnotes

  1. a b

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

1.

Footnotes

  1. a b

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

1.

Footnotes

  1. a b

", "should support lazyness (3)" ); assert_eq!( to_html_with_options("[^1].\n\n[^1]: a\n\n > b", &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(), &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(), &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", &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"###, &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]", &gfm)?, "

a![^1]

", "should match bang/caret interplay (undefined) like GitHub" ); assert_eq!( to_html_with_options( r###"a![^1] [^1]: b "###, &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 "###, &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 "###, &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 "###, &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. "###, &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 "###, &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! "###, &gfm )?, r###"

Call12345.

Lazy?

Lazy!

Footnotes


  1. Paragraph.

  2. Another blank.

"###, "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 "###, &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


        • kilo
"###, "should match definitions like GitHub" ); assert_eq!( to_html_with_options( r###"Call[^1][^1] [^1]: Recursion[^1][^1] "###, &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 "###, &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 "###, &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 "###, &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 "###, &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 "###, &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 "###, &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 "###, &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 "###, &gfm )?, r###"

Note!1234

Footnotes

  1. Paragraph

  2. Paragraph

  3. 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 "###, &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 "###, &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. "###, &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 "###, &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 "###, &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 "###, &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.", &gfm.parse)?, 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(()) }