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
",
"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
",
"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
",
"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
",
"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
",
"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
",
"should use the first of duplicate definitions"
);
assert_eq!(
to_html_with_options("a[^b], c[^b]\n\n[^b]: d", &gfm)?,
"a1, c1
",
"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.
",
"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.
",
"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.
",
"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.
",
"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.
",
"should support lazyness (1)"
);
assert_eq!(
to_html_with_options("[^1].\n\n> [^1]: a\nb", &gfm)?,
"1.
",
"should support lazyness (2)"
);
assert_eq!(
to_html_with_options("[^1].\n\n> [^1]: a\n> b", &gfm)?,
"1.
",
"should support lazyness (3)"
);
assert_eq!(
to_html_with_options("[^1].\n\n[^1]: a\n\n > b", &gfm)?,
"1.
",
"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.
", 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
\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###"a
a!i
a
a!1
1
^1]
"###,
"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
"###,
"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
"###,
"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
"###,
"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]
[^]
[^link]: a
[^]: 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.
"###,
"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
"###,
"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!
"###,
"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
"###,
"should match definitions like GitHub"
);
assert_eq!(
to_html_with_options(
r###"Call[^1][^1]
[^1]: Recursion[^1][^1]
"###,
&gfm
)?,
r###"Call11
"###,
"should match duplicate calls and recursion like GitHub"
);
assert_eq!(
to_html_with_options(
r###"Call[^1]
[^1]: a
[^1]: b
"###,
&gfm
)?,
r###"Call1
"###,
"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]
[link4](#)
"###,
"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.
"###,
"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
"###,
"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.
"###,
"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
"###,
"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
"###,
"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
"###,
"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
"###,
"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
"###,
"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.
"###,
"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
"###,
"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
"###,
"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(())
}