mod constants;
mod ctx;
mod elements;
mod ext;
mod heading;
mod serializer;
mod text_utils;
use tendril::StrTendril;
use crate::node::NodeRef;
pub fn serialize_md(
root_node: &NodeRef,
include_node: bool,
skip_tags: Option<&[&str]>,
) -> StrTendril {
serializer::MDSerializer::new(root_node, skip_tags).serialize(include_node)
}
#[cfg(test)]
mod tests {
use crate::Document;
use super::*;
#[track_caller]
fn html_2md_compare(html_contents: &str, expected: &str) {
let doc = Document::from(html_contents);
let root_node = &doc.root();
let md_text = serialize_md(root_node, false, None);
assert_eq!(md_text.as_ref(), expected);
}
#[test]
fn test_heading_closing_hashes() {
html_2md_compare("<h2>Title #</h2>", "## Title \\#");
html_2md_compare("<h1>#</h1>", "# \\#");
html_2md_compare("<h2>A ### </h2>", "## A \\###");
html_2md_compare("<h1>C# and F#</h1>", "# C# and F#");
html_2md_compare("<h2>Title #<br></h2>", "## Title \\#");
}
#[test]
fn test_headings() {
let contents = r"<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<h3><span>III.</span> Heading With Span</h3>
<h3><span></span>Early years (2006–2009)</h3>
<h3><span> </span> Early years (2006–2009)</h3>
<hr>";
let expected = "\n\n# Heading 1\n\n\
## Heading 2\n\n\
### Heading 3\n\n\
#### Heading 4\n\n\
##### Heading 5\n\n\
###### Heading 6\n\n\
### III. Heading With Span\n\n\
### Early years (2006–2009)\n\n\
### Early years (2006–2009)\n\n\
---\n\n";
let doc = Document::from(contents);
let body_sel = &doc.select("body");
let body_node = body_sel.nodes().first().unwrap();
let md_text = serialize_md(body_node, true, None);
assert_eq!(md_text.as_ref(), expected);
}
#[test]
fn test_escaping() {
html_2md_compare("<p>Foo. Bar!</p>", "Foo. Bar!");
html_2md_compare(
"<p>Call (555) 123-4567, see {a: 1} and https://example.com/x.</p>",
"Call (555) 123-4567, see {a: 1} and https://example.com/x.",
);
html_2md_compare(
"<p>see # tags, a > b, x - y</p>",
"see # tags, a > b, x - y",
);
html_2md_compare("<p># not a heading</p>", "\\# not a heading");
html_2md_compare("<p>> not a quote</p>", "\\> not a quote");
html_2md_compare("<p>- not a list</p>", "\\- not a list");
html_2md_compare("<p>#tag-name</p>", "\\#tag-name");
html_2md_compare("<p>+1-2</p>", "\\+1-2");
html_2md_compare("<p>>foo>bar</p>", "\\>foo>bar");
html_2md_compare("<p><span>foo </span><span>#bar</span></p>", "foo #bar");
html_2md_compare("<p>Title<br>===</p>", "Title \n\\===");
html_2md_compare("<p>=> a == b</p>", "=> a == b");
html_2md_compare(
"<p>2024. not an ordered list</p>",
"2024\\. not an ordered list",
);
html_2md_compare("<p>see 2024. below</p>", "see 2024. below");
html_2md_compare("<p>Wow!</p>", "Wow!");
html_2md_compare("<p>not ![an image]</p>", "not \\!\\[an image\\]");
html_2md_compare(
"<p>a *b* c _d_ e`f` [g]</p>",
"a \\*b\\* c \\_d\\_ e\\`f\\` \\[g\\]",
);
html_2md_compare("<p>a | b</p>", "a \\| b");
}
#[test]
fn test_tilde_escaping() {
html_2md_compare("<p>~~~</p><p>code?</p>", "\\~\\~\\~\n\ncode?");
html_2md_compare("<p>~~strike~~</p>", "\\~\\~strike\\~\\~");
html_2md_compare(
"<p>run me@h:~/a$ make then me@h:~/b$ go</p>",
"run me@h:\\~/a$ make then me@h:\\~/b$ go",
);
html_2md_compare("<p><code>~/a</code></p>", "`~/a`");
}
#[test]
fn test_italic() {
let contents = r"<h4><i>Italic Text</i></h4>";
let expected = "#### *Italic Text*";
html_2md_compare(contents, expected);
}
#[test]
fn test_span_italic() {
let contents = r"<span>It`s like <i>that</i></span>";
let expected = r"It\`s like *that*";
html_2md_compare(contents, expected);
}
#[test]
fn test_bold_italic() {
let contents = r"<span>It`s like <b><i>that</i></b></span>";
let expected = r"It\`s like ***that***";
html_2md_compare(contents, expected);
}
#[test]
fn test_simple_code() {
let contents = r"<span>It`s like <code>that</code></span>";
let expected = r"It\`s like `that`";
html_2md_compare(contents, expected);
}
#[test]
fn test_code_edge_spaces() {
html_2md_compare("<p><code> a </code></p>", "` a `");
html_2md_compare("<p><code> a</code></p>", "` a`");
}
#[test]
fn test_false_multiline_code() {
let contents = r"<span>
It`s like
<code>
that
</code>
</span>";
let expected = r"It\`s like `that`";
html_2md_compare(contents, expected);
}
#[test]
fn test_multiline_code() {
let contents = r"<code>$ cargo new hello
Created binary (application) `hello` package
$ cd hello</code>";
let expected = r"```
$ cargo new hello
Created binary (application) `hello` package
$ cd hello
```";
html_2md_compare(contents, expected);
}
#[test]
fn test_code_span_with_backticks() {
html_2md_compare("<p><code>a `b` c</code></p>", "``a `b` c``");
html_2md_compare("<p><code>`leading</code></p>", "`` `leading ``");
html_2md_compare("<p><code>trailing`</code></p>", "`` trailing` ``");
html_2md_compare("<p><code>```</code></p>", "```` ``` ````");
html_2md_compare("<p><code>go.sum</code></p>", "`go.sum`");
}
#[test]
fn test_ul() {
let contents = "<h3>Pizza Margherita Ingredients</h3>\
<ul>\
<li>Pizza Dough</li>\
<li>Mozzarella cheese</li>\
<li>Tomatoes</li>\
<li>Olive Oil</li>\
<li><i>Basil</i></li>\
<li><b>Salt</b></li>\
</ul>";
let expected = "### Pizza Margherita Ingredients\n\n\
- Pizza Dough\n\
- Mozzarella cheese\n\
- Tomatoes\n\
- Olive Oil\n\
- *Basil*\n\
- **Salt**";
html_2md_compare(contents, expected);
}
#[test]
fn test_ol() {
let contents = "<h3>Pizza Margherita Ingredients</h3>\
<ol>\
<li>Pizza Dough</li>\
<li>Mozzarella cheese</li>\
<li>Tomatoes</li>\
<li>Olive Oil</li>\
<li><i>Basil</i></li>\
<li><b>Salt</b></li>\
</ol>";
let expected = "### Pizza Margherita Ingredients\n\n\
1. Pizza Dough\n\
2. Mozzarella cheese\n\
3. Tomatoes\n\
4. Olive Oil\n\
5. *Basil*\n\
6. **Salt**";
html_2md_compare(contents, expected);
}
#[test]
fn test_bad_ol() {
let contents = "<h3>Pizza Margherita Ingredients</h3>\
<ol>\
<li>Pizza Dough</li>\
<li>Mozzarella cheese</li>\
<li>Tomatoes</li>\
<li>Olive Oil</li>\
<div><i>Basil</i></div>\
<li><b>Salt</b></li>\
</ol>";
let expected = "### Pizza Margherita Ingredients\n\n\
1. Pizza Dough\n\
2. Mozzarella cheese\n\
3. Tomatoes\n\
4. Olive Oil\n\
\n*Basil*\n\n\
5. **Salt**";
html_2md_compare(contents, expected);
}
#[test]
fn test_ol_inline() {
let contents = "
<ol>\
<li>Item 1</li>\
<li>Item 2</li>\
<li>Item 3\
<div>\
<ol>\
<li>Item 3-1</li>\
<li>Item 3-2</li>\
<li>Item 3-3\
<ol>\
<li>Item 3-3-1</li>\
<li>Item 3-3-2</li>\
<li>Item 3-3-3</li>\
</ol>
</li>\
</ol>\
</div>
</li>\
</ol>";
let expected = "\
1. Item 1
2. Item 2
3. Item 3
1. Item 3-1
2. Item 3-2
3. Item 3-3
1. Item 3-3-1
2. Item 3-3-2
3. Item 3-3-3";
html_2md_compare(contents, expected);
}
#[test]
fn test_ul_inline() {
let contents = "
<ul>\
<li>Item 1</li>\
<li>Item 2</li>\
<li>Item 3\
<div>\
<ul>\
<li>Item 3-1</li>\
<li>Item 3-2</li>\
<li>Item 3-3\
<ul>\
<li>Item 3-3-1</li>\
<li>Item 3-3-2</li>\
<li>Item 3-3-3</li>\
</ul>
</li>\
</ul>\
</div>
</li>\
</ul>";
let expected = "\
- Item 1
- Item 2
- Item 3
- Item 3-1
- Item 3-2
- Item 3-3
- Item 3-3-1
- Item 3-3-2
- Item 3-3-3";
html_2md_compare(contents, expected);
}
#[test]
fn test_list_with_paragraphs() {
let contents = "<ol>
<li>
<p>Paragraph 1-1</p>
<p>Paragraph 1-2</p>
</li>
<li><p>Paragraph 2-1</p><p>Paragraph 2-2</p></li>
<li><p>Paragraph 3-1</p></li>
</ol>
<p>Another Paragraph</p>";
let expected = "\
1. Paragraph 1-1
Paragraph 1-2
2. Paragraph 2-1
Paragraph 2-2
3. Paragraph 3-1
Another Paragraph";
html_2md_compare(contents, expected);
}
#[test]
fn test_nested_list_in_item_with_paragraphs() {
html_2md_compare(
"<ul><li><p>a</p><ul><li><p>b</p></li><li><p>c</p></li></ul></li></ul>",
"- a\n\n - b\n\n - c",
);
html_2md_compare(
"<ul><li><p>a</p><ul><li>b</li><li>c</li></ul></li></ul>",
"- a\n\n - b\n - c",
);
}
#[test]
fn test_ordered_list_start_and_value() {
html_2md_compare("<ol start=\"5\"><li>a</li><li>b</li></ol>", "5. a\n6. b");
html_2md_compare(
"<ol><li>a</li><li value=\"9\">b</li><li>c</li></ol>",
"1. a\n9. b\n10. c",
);
html_2md_compare("<ol start=\"x\"><li>a</li></ol>", "1. a");
html_2md_compare("<ol start=\" 5abc\"><li>a</li></ol>", "5. a");
html_2md_compare("<ol start=\"+3\"><li>a</li></ol>", "3. a");
html_2md_compare("<ol start=\"-2\"><li>a</li><li>b</li></ol>", "0. a\n1. b");
html_2md_compare("<ol><li>a</li><li value=\"-1\">b</li></ol>", "1. a\n0. b");
html_2md_compare(
"<ol start=\"1234567890\"><li>a</li><li>b</li></ol>",
"999999999. a\n999999999. b",
);
html_2md_compare(
"<ol start=\"18446744073709551615\"><li>a</li></ol>",
"999999999. a",
);
html_2md_compare(
"<ol start=\" 00000018446744073709551615\"><li>a</li></ol>",
"999999999. a",
);
html_2md_compare("<ol start=\"000000\"><li>a</li></ol>", "0. a");
html_2md_compare(
"<ol><li>a</li><li value=\"1234567890\">b</li></ol>",
"1. a\n999999999. b",
);
html_2md_compare(
"<ol start=\"999999999\"><li>2)</li></ol>",
"999999999. 2\\)",
);
html_2md_compare(
"<ol start=\"123456789\"><li>- x</li></ol>",
"123456789. \\- x",
);
html_2md_compare(
"<ol start=\"123456789\"><li>a - x</li></ol>",
"123456789. a - x",
);
html_2md_compare(
"<ol start=\"999999998\"><li>a</li><li>2)</li></ol>",
"999999998. a\n999999999. 2\\)",
);
}
#[test]
fn test_split_ordered_list_marker() {
html_2md_compare("<p><span>1</span>. foo</p>", "1\\. foo");
html_2md_compare("<p>1<span>. not a list</span></p>", "1\\. not a list");
html_2md_compare("<ul><li><span>2</span>) x</li></ul>", "- 2\\) x");
html_2md_compare(
"<ul><li>a<ul><li><span>3</span>. x</li></ul></li></ul>",
"- a\n - 3\\. x",
);
html_2md_compare("<ol><li><span>3</span>. x</li></ol>", "1. 3\\. x");
html_2md_compare(
"<ol start=\"12\"><li><span>3</span>) x</li></ol>",
"12. 3\\) x",
);
html_2md_compare("<p><b>see</b> <span>1</span>. foo</p>", "**see** 1. foo");
html_2md_compare("<p>In <span>2024</span>. Then</p>", "In 2024. Then");
html_2md_compare("<p><span>1</span>.5 stars</p>", "1.5 stars");
html_2md_compare("<p><span>10000000000</span>. foo</p>", "10000000000. foo");
}
#[test]
fn test_nested_list_under_wide_marker() {
let md = "100. a\n 1. x";
html_2md_compare("<ol start=\"100\"><li>a<ol><li>x</li></ol></li></ol>", md);
html_2md_compare(
"<ol start=\"98\"><li>a</li><li>b</li><li>c<ul><li>n</li></ul></li></ol>",
"98. a\n99. b\n100. c\n - n",
);
}
#[test]
fn test_nested_list_not_starting_at_one() {
html_2md_compare(
"<ul><li>a<ol start=\"2\"><li>b</li></ol></li></ul>",
"- a\n\n 2. b",
);
html_2md_compare(
"<ul><li>a<ol><li value=\"3\">b</li></ol></li></ul>",
"- a\n\n 3. b",
);
}
#[test]
fn test_nested_list_starting_with_empty_item() {
html_2md_compare(
"<ul><li>a<ul><li></li><li>b</li></ul></li></ul>",
"- a\n\n -\n - b",
);
html_2md_compare(
"<ul><li>a<ul><li><ul><li>c</li></ul></li></ul></li></ul>",
"- a\n\n -\n - c",
);
}
#[test]
fn test_nested_list_item_paragraphs() {
html_2md_compare(
"<ul><li>a<ul><li><p>b</p><p>c</p></li></ul></li></ul>",
"- a\n - b\n\n c",
);
}
#[test]
fn test_list_item_code_block() {
html_2md_compare(
"<ul><li><p>a</p><pre><code>b\n\nc\n</code></pre><p>d</p></li></ul>",
"- a\n\n ```\n b\n\n c\n ```\n\n d",
);
html_2md_compare(
"<ol start=\"10\"><li><pre><code>x\n</code></pre></li></ol>",
"10. ```\n x\n ```",
);
html_2md_compare(
"<ul><li>a<ul><li><p>b</p><pre><code>c\n</code></pre></li></ul></li></ul>",
"- a\n - b\n\n ```\n c\n ```",
);
html_2md_compare(
"<ul><li>Run:<pre><code>make\n</code></pre></li></ul>",
"- Run:\n ```\n make\n ```",
);
}
#[test]
fn test_list_item_blockquote() {
html_2md_compare(
"<ul><li><p>a</p><blockquote><p>b</p><p>c</p></blockquote><p>d</p></li></ul>",
"- a\n\n > b\n > \n > c\n\n d",
);
html_2md_compare(
"<ol start=\"10\"><li><blockquote><p>x</p><p>y</p></blockquote></li></ol>",
"10. > x\n > \n > y",
);
html_2md_compare(
"<ul><li>a<blockquote><p>b</p></blockquote></li></ul>",
"- a\n > b",
);
}
#[test]
fn test_list_item_thematic_break() {
html_2md_compare("<ul><li><hr></li><li>two</li></ul>", "-\n ---\n- two");
html_2md_compare(
"<ul><li><p>a</p><hr><p>b</p></li><li>c</li></ul>",
"- a\n\n ---\n b\n\n- c",
);
html_2md_compare("<ul><li>a<hr>b</li></ul>", "- a\n\n ---\n b");
html_2md_compare(
"<ol start=\"3\"><li><hr></li><li><p>a</p><hr></li></ol>",
"3.\n ---\n4. a\n\n ---",
);
html_2md_compare(
"<table><tr><td><ul><li>a<hr>b</li></ul></td></tr></table>",
"| |\n| - |\n| + a<br>b |",
);
}
#[test]
fn test_list_tight_after_item_block() {
html_2md_compare(
"<ul><li>a</li><li><pre><code>b\n</code></pre></li><li>c</li></ul>",
"- a\n- ```\n b\n ```\n- c",
);
html_2md_compare(
"<ul><li>a<blockquote><p>b</p></blockquote> <pre><code>c\n</code></pre> </li><li>d</li></ul>",
"- a\n > b\n ```\n c\n ```\n- d",
);
html_2md_compare(
"<ul><li><blockquote><p>a</p></blockquote></li><li>b</li></ul>",
"- > a\n- b",
);
html_2md_compare(
"<ul><li><blockquote><p>a</p></blockquote><blockquote><p>b</p></blockquote></li></ul>",
"- > a\n\n > b",
);
html_2md_compare(
"<ul><li>a</li><li><blockquote><p>b</p></blockquote></li></ul>c",
"- a\n- > b\n\nc",
);
}
#[test]
fn test_list_item_heading() {
html_2md_compare(
"<ul><li><h1>Foo</h1></li><li><h2>Bar</h2>baz</li></ul>",
"- # Foo\n- ## Bar\n baz",
);
html_2md_compare(
"<ol><li><h3>a #</h3><p>b</p></li><li>c</li></ol>",
"1. ### a \\#\n b\n\n2. c",
);
}
#[test]
fn test_list_item_nested_blocks() {
html_2md_compare(
"<ul><li><div><p>a</p></div></li><li>two</li></ul>",
"- a\n\n- two",
);
html_2md_compare(
"<ul><li><div><p>a</p><p>b</p></div></li><li>two</li></ul>",
"- a\n\n b\n\n- two",
);
html_2md_compare(
"<ol start=\"5\"><li><div><p>a</p></div></li><li>b</li></ol>",
"5. a\n\n6. b",
);
html_2md_compare(
"<ul><li><div><section><p>a</p><blockquote><p>b</p></blockquote></section></div></li></ul>",
"- a\n\n > b",
);
html_2md_compare(
"<ul><li><div><p>a</p><!-- c --><p>b</p></div></li></ul>",
"- a\n\n b",
);
html_2md_compare(
"<ul><li><div>\n <p>a</p>\n <p>b</p>\n</div></li><li>two</li></ul>",
"- a\n\n b\n\n- two",
);
}
#[test]
fn test_list_item_nested_blocks_after_inline() {
html_2md_compare("<ul><li>text<div><p>a</p></div></li></ul>", "- text\n a");
html_2md_compare(
"<ul><li>text<div><h2>h</h2></div></li></ul>",
"- text\n ## h",
);
html_2md_compare(
"<ul><li><a href=\"/u\">label</a><div><p>a</p></div></li></ul>",
"- [label](/u)\n a",
);
html_2md_compare(
"<ul><li>text<br><div><p>a</p></div></li></ul>",
"- text \n a",
);
html_2md_compare(
"<ul><li><div><p>a</p></div>tail<div><p>b</p></div></li></ul>",
"- a\n\n tail\n b",
);
}
#[test]
fn test_list_item_nested_blocks_other_wrappers() {
html_2md_compare(
"<ul><li><article><p>a</p><p>b</p></article></li></ul>",
"- a\n\n b",
);
html_2md_compare(
"<ul><li><div><pre>x\ny</pre></div></li></ul>",
"- ```\n x\n y\n ```",
);
html_2md_compare(
"<ul><li><div><p>a</p><table><tr><td>t</td></tr></table></div></li></ul>",
"-\n\na\n\n\n| |\n| - |\n| t |",
);
html_2md_compare(
"<ul><li><div><p>a</p><hr><p>b</p></div></li></ul>",
"-\n\na\n\n---\n\nb",
);
}
#[test]
fn test_list_item_nested_blocks_skip_tags() {
let doc = Document::from("<ul><li><div><section><p>a</p></section></div></li></ul>");
assert_eq!(doc.md(Some(&["section"])).as_ref(), "-");
let doc = Document::from("<ul><li><div><p>a</p></div></li></ul>");
assert_eq!(doc.md(Some(&["p"])).as_ref(), "-");
}
#[test]
fn test_list_item_text_after_block() {
html_2md_compare(
"<ul><li><p>a</p>b <em>c</em></li><li>d</li></ul>",
"- a\n\n b *c*\n- d",
);
html_2md_compare(
"<ul><li><pre><code>a\n</code></pre>b</li><li>c</li></ul>",
"- ```\n a\n ```\n\n b\n- c",
);
html_2md_compare(
"<ul><li><p>a</p># b<br>+ c</li><li>+ d<p>e</p></li></ul>",
"- a\n\n \\# b \n \\+ c\n- \\+ d\n e",
);
}
#[test]
fn test_list_item_block_syntax_text() {
html_2md_compare("<ul><li>1. Preheat oven</li></ul>", "- 1\\. Preheat oven");
html_2md_compare("<ul><li># tag</li></ul>", "- \\# tag");
html_2md_compare("<ul><li>- x</li></ul>", "- \\- x");
html_2md_compare("<ul><li>+ x</li></ul>", "- \\+ x");
html_2md_compare("<ul><li>> x</li></ul>", "- \\> x");
html_2md_compare("<ol><li># tag</li></ol>", "1. \\# tag");
html_2md_compare("<ul><li><p>a</p><p># b</p></li></ul>", "- a\n\n \\# b");
html_2md_compare("<ul><li>a #b</li></ul>", "- a #b");
}
#[test]
fn test_list_item_comment() {
html_2md_compare("<ul><li><p>a</p><!-- c --><p>b</p></li></ul>", "- a\n\n b");
html_2md_compare(
"<ol><li><p>a</p><!-- c --><p>b</p></li></ol>",
"1. a\n\n b",
);
html_2md_compare(
"<ul><li><pre>a</pre><!-- c --></li><li>b</li></ul>",
"- ```\n a\n ```\n- b",
);
html_2md_compare(
"<ul><li>x<ul><li><p>n</p><!-- c --><p>m</p></li></ul></li></ul>",
"- x\n - n\n\n m",
);
}
#[test]
fn test_nbsp_is_not_collapsed() {
html_2md_compare("<p>a b</p>", "a\u{a0}\u{a0}b");
html_2md_compare("<p> a </p>", "\u{a0}a\u{a0}");
html_2md_compare("<p>a <span> </span> b</p>", "a \u{a0} b");
}
#[test]
fn test_paragraphs() {
let contents =
"<p>To create paragraphs, use a blank line to separate one or more lines of text.</p>
<p>I really like using <span>Markdown</span><span> text</span>.</p>
<p>I think I'll use it to format all of my documents from now on.</p>";
let expected = "To create paragraphs, use a blank line to separate one or more lines of text.\n\n\
I really like using Markdown text.\n\n\
I think I'll use it to format all of my documents from now on.";
html_2md_compare(contents, expected);
}
#[test]
fn test_links() {
let simple_contents = r#"<p>My favorite search engine is <a href="https://duckduckgo.com">Duck Duck Go</a>.</p>"#;
let simple_expected =
r"My favorite search engine is [Duck Duck Go](https://duckduckgo.com).";
html_2md_compare(simple_contents, simple_expected);
let title_contents = r#"<p>My favorite search engine is <a href="https://duckduckgo.com" title="Duck Duck Go">Duck Duck Go</a>.</p>"#;
let title_expected = r#"My favorite search engine is [Duck Duck Go](https://duckduckgo.com "Duck Duck Go")."#;
html_2md_compare(title_contents, title_expected);
let bold_contents = r#"<p>My favorite search engine is <b><a href="https://duckduckgo.com">Duck Duck Go</a></b>.</p>"#;
let bold_expected =
r"My favorite search engine is **[Duck Duck Go](https://duckduckgo.com)**.";
html_2md_compare(bold_contents, bold_expected);
let bold_ignored_contents = r#"<p>My favorite search engine is <a href="https://duckduckgo.com"><b>Duck Duck Go</b></a>.</p>"#;
let bold_ignored_expected =
r"My favorite search engine is [Duck Duck Go](https://duckduckgo.com).";
html_2md_compare(bold_ignored_contents, bold_ignored_expected);
let ignored_contents = r#"<p>My favorite search engine is <a href="https://duckduckgo.com"><div>Duck Duck Go</div></a>.</p>"#;
let ignored_expected =
"My favorite search engine is\n\n[Duck Duck Go](https://duckduckgo.com)
.";
html_2md_compare(ignored_contents, ignored_expected);
let no_href_contents = r"<p>My favorite search engine is <a>Duck Duck Go</a>.</p>";
let no_href_expected = "My favorite search engine is Duck Duck Go.";
html_2md_compare(no_href_contents, no_href_expected);
let complex_contents =
r#"<a href="https://duckduckgo.com" title="My "Search"">Duck Duck Go</a>"#;
let comptex_expected = r#"[Duck Duck Go](https://duckduckgo.com "My \"Search\"")"#;
html_2md_compare(complex_contents, comptex_expected);
let content = r##"<p>See <a href="http://x">[Link]</a> and <a href="http://y">utm_source</a></p>
<h2>What I wanted <a href="#w">#</a></h2>"##;
let expected = concat!(
"See [\\[Link\\]](http://x) and [utm\\_source](http://y)\n\n",
"## What I wanted [#](#w)"
);
html_2md_compare(content, expected);
}
#[test]
fn test_bang_before_link_or_image() {
html_2md_compare(
r#"<p>Wow!<a href="https://e.com">x</a></p>"#,
r"Wow\",
);
html_2md_compare(
r#"<p>Wow\!<a href="https://e.com">x</a></p>"#,
r"Wow\\\",
);
html_2md_compare(
r#"<p>Wow!<a href="u"><img src="i.png" alt="a"></a></p>"#,
r"Wow\](u)",
);
html_2md_compare(
r#"<p><b>Wow!<b><a href="u">x</a></b></b></p>"#,
r"**Wow\**",
);
html_2md_compare(
r#"<p><b>Wow!</b><b><a href="u">x</a></b></p>"#,
r"**Wow\**",
);
html_2md_compare(r#"<p>Wow!<b><a href="u">x</a></b></p>"#, r"Wow!**[x](u)**");
}
#[test]
fn test_images() {
let simple_contents = r#"<p>Image: <img src="/path/to/img.jpg" alt="Alt text"></p>"#;
let simple_expected = "Image: ";
html_2md_compare(simple_contents, simple_expected);
let simple_contents =
r#"<p>Image: <img src="/path/to/img.jpg" alt="Alt text" title="Title"></p>"#;
let simple_expected = r#"Image: "#;
html_2md_compare(simple_contents, simple_expected);
let simple_contents = r#"<p>Image: <img src="/path/to/img.jpg"></p>"#;
let simple_expected = r"Image: ";
html_2md_compare(simple_contents, simple_expected);
let simple_contents = r#"<p>Image: <img alt="Alt text" title="Title"></p>"#;
let simple_expected = "Image:";
html_2md_compare(simple_contents, simple_expected);
}
#[test]
fn test_img_src_fallbacks() {
html_2md_compare(
"<p><img srcset=\"https://i.e.com/p.png 1x, https://i.e.com/p2x.png 2x\" alt=\"pic\"></p>",
"",
);
html_2md_compare(
"<p><img srcset=\"https://i.e.com/p.png, https://i.e.com/p2x.png 2x\" alt=\"pic\"></p>",
"",
);
html_2md_compare(
"<p><img data-src=\"https://i.e.com/p.png\" alt=\"pic\"></p>",
"",
);
html_2md_compare(
"<p><img src=\"https://i.e.com/s.png\" data-src=\"https://i.e.com/d.png\" alt=\"pic\"></p>",
"",
);
html_2md_compare(
"<p><img src=\"\" data-src=\"https://i.e.com/p.png\" alt=\"pic\"></p>",
"",
);
html_2md_compare("<p><img alt=\"pic\"></p>", "");
html_2md_compare("<p><img src=\"\" alt=\"pic\"></p>", "");
}
#[test]
fn test_whitespace_only_link() {
html_2md_compare(
"<h2>VMAF <a href=\"#vmaf\"> </a></h2><p>body</p>",
"## VMAF\n\nbody",
);
html_2md_compare("<p>x<a href=\"/p\"> </a>y</p>", "x y");
html_2md_compare("<p><a href=\"/p\"> </a># x</p>", "\\# x");
}
#[test]
fn test_linked_image() {
html_2md_compare(
r#"<p><a href="https://e.com"><img src="https://i.e.com/p.png" alt="pic"></a></p>"#,
"[](https://e.com)",
);
html_2md_compare(
r#"<p><a href="https://e.com">see <img src="https://i.e.com/p.png" alt="pic"></a></p>"#,
"[see ](https://e.com)",
);
html_2md_compare(r#"<p><a href="https://e.com"></a>x</p>"#, "x");
let simple_contents =
r#"<p>Image: <img src="/path/to/img.jpg" alt="Alt text" title='A "Great" Photo'></p>"#;
let simple_expected = r#"Image: "#;
html_2md_compare(simple_contents, simple_expected);
}
#[test]
fn test_link_wrapping_blocks() {
html_2md_compare(
"<a href=\"/post\"><img src=\"a.jpg\" alt=\"i\"><h3>Title</h3><p>Blurb</p></a>",
"[ Title Blurb](/post)",
);
html_2md_compare(
"<a href=\"/p\"><img src=\"a.jpg\"><ul><li>a</li><li>b<ol><li>c</li></ol></li></ul></a>",
"[ a b c](/p)",
);
html_2md_compare(
"<a href=\"/p\"><img src=\"a.jpg\"><blockquote><p>q</p><p>r</p></blockquote></a>",
"[ q r](/p)",
);
html_2md_compare(
"<a href=\"/p\"><img src=\"a.jpg\"><table><tr><th>h</th><th>i</th></tr><tr><td>c</td><td>d</td></tr></table></a>",
"[ h i c d](/p)",
);
html_2md_compare(
"<a href=\"/p\"><img src=\"a.jpg\"><pre><code class=\"language-rust\"># comment\n```\n# x</code></pre></a>",
"[ ````# comment ``` # x````](/p)",
);
html_2md_compare(
"<a href=\"/p\"><img src=\"a.jpg\">a<br><br>b</a>",
"[a<br><br>b](/p)",
);
html_2md_compare(
"<table><tr><th>h</th></tr><tr><td><a href=\"/p\"><img src=\"a.jpg\"><h3>T</h3><p>B</p><br>x</a></td></tr></table>",
"| h |\n| - |\n| [ T B<br>x](/p) |",
);
}
#[test]
fn test_link_destination_and_alt_escaping() {
html_2md_compare(
"<p><a href=\"https://e.com/x(y)z\">x</a></p>",
"[x](https://e.com/x(y)z)",
);
html_2md_compare(
"<p><a href=\"https://e.com/x)y\">x</a></p>",
"[x](<https://e.com/x)y>)",
);
html_2md_compare(
"<p><a href=\"https://e.com/a b.png\">x</a></p>",
"[x](<https://e.com/a b.png>)",
);
html_2md_compare(
"<p><a href=\"https://e.com/a>b\">x</a></p>",
"[x](https://e.com/a>b)",
);
html_2md_compare(
r#"<p><a href="https://e.com/a<b>">x</a></p>"#,
r"[x](<https://e.com/a\<b\>>)",
);
html_2md_compare(
"<p><img src=\"https://i.e.com/p.png\" alt=\"a [b] c\"></p>",
"![a \\[b\\] c](https://i.e.com/p.png)",
);
html_2md_compare(
r#"<p><a href="https://e.com/a<b">x</a></p>"#,
r"[x](<https://e.com/a\<b>)",
);
html_2md_compare(
"<p><a href=\"https://e.com/a\nb\">x</a></p>",
"[x](<https://e.com/a%0Ab>)",
);
html_2md_compare(
r#"<p><a href="https://e.com/a b">x</a></p>"#,
r"[x](<https://e.com/a%0Db>)",
);
html_2md_compare("<p><a href=\"a b\\\">x</a></p>", "[x](<a b\\\\>)");
html_2md_compare("<p><a href=\"a\\*b\">x</a></p>", "[x](a\\\\*b)");
html_2md_compare("<p><a href=\"a\tb\">x</a></p>", "[x](<a\tb>)");
html_2md_compare("<p><img src=\"i.png\" alt=\"a\\\"></p>", "");
html_2md_compare(
"<p><img src=\"i.png\" alt=\"*y*\"></p>",
"",
);
}
#[test]
fn test_pre_code() {
let simple_contents = "<pre>\
<span>fn</span> <span>main</span><span>()</span><span> </span><span>{</span>\n\
<span> </span><span>println!</span><span>(</span><span>\"Hello, World!\"</span><span>);</span>\n\
<span>}</span>\
</pre>";
let simple_expected = "```
fn main() {
println!(\"Hello, World!\");
}
```";
html_2md_compare(simple_contents, simple_expected);
}
#[test]
fn test_pre_code_with_data_lang_attribute() {
let simple_contents = "<pre data-lang=\"rust\">\
<span>fn</span> <span>main</span><span>()</span><span> </span><span>{</span>\n\
<span> </span><span>println!</span><span>(</span><span>\"Hello, World!\"</span><span>);</span>\n\
<span>}</span>\n\
</pre>";
let simple_expected = "```rust
fn main() {
println!(\"Hello, World!\");
}
```";
html_2md_compare(simple_contents, simple_expected);
}
#[test]
fn test_pre_code_with_data_lang_attribute_in_parent_tag() {
let simple_contents = "<div data-lang=\"rust\"><pre>\
<span>fn</span> <span>main</span><span>()</span><span> </span><span>{</span>\n\
<span> </span><span>println!</span><span>(</span><span>\"Hello, World!\"</span><span>);</span>\n\
<span>}</span>\n\
</pre></div>";
let simple_expected = "```rust
fn main() {
println!(\"Hello, World!\");
}
```";
html_2md_compare(simple_contents, simple_expected);
}
#[test]
fn test_pre_code_with_language_css_class_in_child_code_tag() {
let contents = "<pre><code class=\"language-rust something else\">\
<span>fn</span> <span>main</span><span>()</span><span> </span><span>{</span>\n\
<span> </span><span>println!</span><span>(</span><span>\"Hello, World!\"</span><span>);</span>\n\
<span>}</span>\n\
</code></pre>";
let expected = "```rust
fn main() {
println!(\"Hello, World!\");
}
```";
html_2md_compare(contents, expected);
}
#[test]
fn test_pre_empty() {
html_2md_compare("<pre><code></code></pre>", "```\n```");
}
#[test]
fn test_blockquote() {
let simple_contents = "<blockquote><p>Quoted text</p></blockquote>";
let simple_expected = "> Quoted text";
html_2md_compare(simple_contents, simple_expected);
let complex_contents = "<blockquote>
<p>
Who has seen the wind?<br>
Neither I nor you:<br>
But when the leaves hang trembling,<br>
The wind is passing through.
</p>
<p>
Who has seen the wind?<br>
Neither you nor I:<br>
But when the trees bow down their heads,<br>
The wind is passing by.
</p>
</blockquote>
<p><i>Christina Rossetti</i></p>";
let complex_expected = r"> Who has seen the wind?
> Neither I nor you:
> But when the leaves hang trembling,
> The wind is passing through.
>
> Who has seen the wind?
> Neither you nor I:
> But when the trees bow down their heads,
> The wind is passing by.
*Christina Rossetti*";
html_2md_compare(complex_contents, complex_expected);
let empty_contents = "<blockquote></blockquote>";
let empty_expected = "";
html_2md_compare(empty_contents, empty_expected);
}
#[test]
fn test_inline_blockquote() {
let contents = "<blockquote>
<p>
Who has seen the wind?<br>
Neither I nor you:<br>
But when the leaves hang trembling,<br>
The wind is passing through.
</p>
<blockquote>
<p>
Who has seen the wind?<br>
Neither you nor I:<br>
But when the trees bow down their heads,<br>
The wind is passing by.
</p>
</blockquote>
</blockquote>";
let expected = r"> Who has seen the wind?
> Neither I nor you:
> But when the leaves hang trembling,
> The wind is passing through.
>
> > Who has seen the wind?
> > Neither you nor I:
> > But when the trees bow down their heads,
> > The wind is passing by.";
html_2md_compare(contents, expected);
}
#[test]
fn test_table() {
let contents = "<table>
<tr>
<th>Column 1</th>
<th>Column 2</th>
<th>Column 3</th>
</tr>
<tr>
<td>R 1, <i>C 1</i></td>
<td>R 1, <i>C 2</i></td>
<td>R 1, <i>C 3</i></td>
</tr>
<tr>
<td>R 2, <i>C 1</i></td>
<td>R 2, <i>C 2</i></td>
<td>R 2, <i>C 3</i></td>
</tr>
</table>";
let expected = "| Column 1 | Column 2 | Column 3 |
| -------- | -------- | -------- |
| R 1, *C 1* | R 1, *C 2* | R 1, *C 3* |
| R 2, *C 1* | R 2, *C 2* | R 2, *C 3* |";
html_2md_compare(contents, expected);
}
#[test]
fn test_table_inside_table() {
let contents = "<table>
<tr>
<td>
<table>
<tr>
<th>Column 1</th>
<th>Column 2</th>
<th>Column 3</th>
</tr>
<tr>
<td>R 1, <i>C 1</i></td>
<td>R 1, <i>C 2</i></td>
<td>R 1, <i>C 3</i></td>
</tr>
<tr>
<td>R 2, <i>C 1</i></td>
<td>R 2, <i>C 2</i></td>
<td>R 2, <i>C 3</i></td>
</tr>
</table>
</td>
</tr>
</table>";
let expected = "| Column 1 | Column 2 | Column 3 |
| -------- | -------- | -------- |
| R 1, *C 1* | R 1, *C 2* | R 1, *C 3* |
| R 2, *C 1* | R 2, *C 2* | R 2, *C 3* |";
html_2md_compare(contents, expected);
}
#[test]
fn test_table_without_headings() {
let contents = "<table>
<tr>
<td>R 1, <i>C 1</i></td>
<td>R 1, <i>C 2</i></td>
<td>R 1, <i>C 3</i></td>
</tr>
<tr>
<td>R 2, <i>C 1</i></td>
<td>R 2, <i>C 2</i></td>
<td>R 2, <i>C 3</i></td>
</tr>
</table>";
let expected = "| | | |
| - | - | - |
| R 1, *C 1* | R 1, *C 2* | R 1, *C 3* |
| R 2, *C 1* | R 2, *C 2* | R 2, *C 3* |";
html_2md_compare(contents, expected);
}
#[test]
fn test_table_skip() {
let contents = "<table>
<tr>
<td>R 1, <i>C 1</i></td>
<td>R 1, <i>C 2</i></td>
<td>R 1, <i>C 3</i></td>
</tr>
<tr>
<td>R 2, <i>C 1</i></td>
<td>R 2, <i>C 2</i></td>
</tr>
</table>";
let expected = "R 1, *C 1* R 1, *C 2* R 1, *C 3*
R 2, *C 1* R 2, *C 2*";
html_2md_compare(contents, expected);
}
#[test]
fn test_table_empty() {
let contents = "<table>
<tr></tr>
<tr></tr>
</table>";
let expected = "";
html_2md_compare(contents, expected);
}
#[test]
fn test_table_with_list() {
let contents = "<table>
<tr>
<td>1</td>
<td>
<ul><li>Lemon</li></ul>
<ul><li>Lime</li></ul>
<ul><li>Grapefruit</li></ul>
<ul><li>Orange</li></ul>
</td>
</tr>
</table>";
let expected =
"| | |\n| - | - |\n| 1 | + Lemon<br>+ Lime<br>+ Grapefruit<br>+ Orange |";
html_2md_compare(contents, expected);
}
#[test]
fn test_table_cell_blocks() {
html_2md_compare(
"<table><tr><th>H</th></tr><tr><td><p>para1</p><p>para2</p></td></tr></table>",
"| H |\n| - |\n| para1<br>para2 |",
);
}
#[test]
fn test_table_cell_trailing_br() {
html_2md_compare(
"<table><tr><th>a</th></tr><tr><td>x<br></td></tr></table>",
"| a |\n| - |\n| x |",
);
html_2md_compare(
"<table><tr><th>H</th></tr><tr><td>use <br></td></tr></table>",
"| H |\n| - |\n| use \\<br> |",
);
html_2md_compare(
"<table><tr><th>H</th></tr><tr><td><p>a\\</p></td></tr></table>",
"| H |\n| - |\n| a\\\\ |",
);
html_2md_compare(
"<table><tr><th>H</th></tr><tr><td><p>use <br></p><p>x</p></td></tr></table>",
"| H |\n| - |\n| use \\<br><br>x |",
);
}
#[test]
fn test_table_row_headers() {
let contents = "<table>
<tr><th>Lib</th><th>First</th></tr>
<tr><th>Rustls</th><td>14</td></tr>
<tr><th>BoringSSL</th><td>2</td></tr>
</table>";
let expected = "| Lib | First |\n| --- | ----- |\n| Rustls | 14 |\n| BoringSSL | 2 |";
html_2md_compare(contents, expected);
let contents = "<table>
<tr><th>Rustls</th><td>14</td></tr>
<tr><th>BoringSSL</th><td>2</td></tr>
</table>";
let expected = "| | |\n| - | - |\n| Rustls | 14 |\n| BoringSSL | 2 |";
html_2md_compare(contents, expected);
}
#[test]
fn test_table_headings_after_empty_row() {
let contents = "<table>
<tr></tr>
<tr><th>A</th><th>B</th></tr>
<tr><td>1</td><td>2</td></tr>
</table>";
html_2md_compare(contents, "| A | B |\n| - | - |\n| 1 | 2 |");
}
#[test]
fn test_table_only_headings() {
let contents = "<table><tr><th>a</th><th>b</th></tr></table>";
html_2md_compare(contents, "| a | b |\n| - | - |");
}
#[test]
fn test_table_empty_th() {
let contents = "<table>
<tr>
<th></th>
<th>x</th>
</tr>
<tr>
<td>a</td>
<td>b</td>
</tr>
</table>";
let expected = "| | x |\n| - | - |\n| a | b |";
html_2md_compare(contents, expected);
}
#[test]
fn test_table_cell_escape() {
let contents = r#"<table>
<tr>
<th>a</th>
</tr>
<tr>
<td><code>a|b</code> <a href="http://x/a|b">l</a></td>
</tr>
</table>"#;
let expected = "| a |\n| - |\n| `a\\|b` [l](http://x/a\\|b) |";
html_2md_compare(contents, expected);
let contents = r"<table>
<tr>
<th><b>a|b</b></th>
</tr>
<tr>
<td><i>c|d</i></td>
</tr>
</table>";
let expected = "| **a\\|b** |\n| -------- |\n| *c\\|d* |";
html_2md_compare(contents, expected);
}
#[test]
fn test_skip_tags_default() {
let contents = "
<style>p {color: blue;}</style>
<p>I really like using <b>Markdown</b>.</p>
<p>I think I'll use it to format all of my documents from now on.</p>";
let expected = "I really like using **Markdown**.\n\n\
I think I'll use it to format all of my documents from now on.";
html_2md_compare(contents, expected);
}
#[test]
fn test_skip_tags() {
let contents = "<style>p {color: blue;}</style>\
<div><h1>Content Heading<h1></div>\
<p>I really like using Markdown.</p>\
<p>I think I'll use it to format all of my documents from now on.</p>";
let expected = "p {color: blue;}\n\n\
I really like using Markdown.\n\n\
I think I'll use it to format all of my documents from now on.";
let doc = Document::fragment(contents);
let html_node = &doc.root();
let md_text = serialize_md(html_node, false, Some(&["div"]));
assert_eq!(md_text.as_ref(), expected);
}
#[test]
fn test_text_after_list() {
html_2md_compare("<ul><li>a</li></ul>b", "- a\n\nb");
html_2md_compare(
"<ul><li>a</li></ul><pre><code>b\n</code></pre>",
"- a\n\n```\nb\n```",
);
html_2md_compare(
"<ul><li>a<ul><li>b</li></ul></li><li>c</li></ul>d",
"- a\n - b\n- c\n\nd",
);
}
#[test]
fn test_linebreak_after_lists() {
let contents = r"Influenced
<ul>
<li>Idris (programming language)</li>
<li>Project Verona</li>
<li>Spark</li>
<li>Swift</li>
<li>V</li>
<li>Zig</li>
</ul>
<p><b>Rust</b> is a general-purpose programming language</p>";
let expected = "Influenced\n\n\
- Idris (programming language)
- Project Verona
- Spark
- Swift
- V
- Zig
**Rust** is a general-purpose programming language";
html_2md_compare(contents, expected);
}
#[test]
fn test_pre_code_without_new_line() {
let simple_contents = r#"<pre>
<span>fn</span> <span>main</span><span>()</span><span> </span><span>{</span>
<span> </span><span>println!</span><span>(</span><span>"Hello, World!"</span><span>);</span>
<span>}</span></pre>"#;
let simple_expected = "```
fn main() {
println!(\"Hello, World!\");
}
```";
html_2md_compare(simple_contents, simple_expected);
}
#[test]
fn test_pre_with_interior_backticks() {
html_2md_compare(
"<pre><code>```bash\nls\n```</code></pre>",
"````\n```bash\nls\n```\n````",
);
html_2md_compare("<pre><code>a`b</code></pre>", "```\na`b\n```");
}
#[test]
fn test_empty_and_adjacent_code_spans() {
html_2md_compare("<p>a<code></code>b</p>", "ab");
html_2md_compare("<p><code> </code>x</p>", "` `x");
html_2md_compare("<p>a<code></code>|<code> </code>b</p>", "a\\|` `b");
let md = "`x` `y`";
html_2md_compare("<p><code>x</code><code>y</code></p>", md);
}
#[test]
fn test_code_span_with_elements() {
html_2md_compare("<p><code><code>x</code></code></p>", "`x`");
html_2md_compare("<p><code><a href=\"/u\">~</a></code></p>", "`~`");
html_2md_compare("<p><code>a <b>*b*</b> c</code></p>", "`a *b* c`");
html_2md_compare("<p><code>a<br>b</code></p>", "`a b`");
html_2md_compare("<p><code>x_1<span>[y]</span></code></p>", "`x_1[y]`");
html_2md_compare("<p><code>a<script>s</script></code></p>", "`a`");
}
#[test]
fn test_deeply_nested_code() {
let depth = 100;
let html = format!(
"<p>{}x{}</p>",
"<code>".repeat(depth),
"</code>".repeat(depth)
);
let doc = Document::from(html.as_str());
let md = serialize_md(&doc.root(), false, None);
assert_eq!(md.as_ref(), "`x`");
}
#[test]
fn test_pre_with_trailing_newline() {
let md = "```\na\nb\n```";
html_2md_compare("<pre>a\nb\n</pre>", md);
}
#[test]
fn test_emphasis_boundary_whitespace() {
html_2md_compare(
"<p><strong>The Rundown: </strong>Body text</p>",
"**The Rundown:** Body text",
);
html_2md_compare(
"<p><em> Leading</em> and <strong>trailing </strong></p>",
"*Leading* and **trailing**",
);
html_2md_compare(
"<p><strong><em>both </em>bold</strong></p>",
"***both* bold**",
);
html_2md_compare("<p>a<strong> </strong>b</p>", "a b");
html_2md_compare("Hello<strong> world</strong>.", "Hello **world**.");
html_2md_compare("x<em> y</em>z", "x *y*z");
html_2md_compare("a <em> x</em>", "a *x*");
html_2md_compare("<p><strong> <em> x</em></strong></p>", "***x***");
html_2md_compare("a<strong> <em> x</em></strong>", "a ***x***");
}
#[test]
fn test_nested_same_type_emphasis() {
html_2md_compare("<p><i><i>b</i></i></p>", "*b*");
html_2md_compare("<p><b>a<span><b>b</b></span>c</b></p>", "**abc**");
html_2md_compare("<p><em>a</em><em><i>b</i></em>c</p>", "*ab*c");
html_2md_compare("<p><b>a</b><b><b>b</b> c</b></p>", "**ab c**");
html_2md_compare("<p><b>a</b> <b> <b>b</b> c</b></p>", "**a b c**");
html_2md_compare("<p><b>a<i><b>b</b></i></b></p>", "**a*b***");
html_2md_compare(r"<p><b>a*<i><b>b</b></i></b></p>", r"**a\**b***");
html_2md_compare("<p><em>a</em><br><em>b</em></p>", "*a* \n*b*");
html_2md_compare(r"<p>a*b</p>", r"a\*b");
html_2md_compare("<p><b>a</b><em>b</em></p>", "**a***b*");
html_2md_compare("<p><b>a</b> <em>b</em></p>", "**a** *b*");
}
}