mod support; use moli_html2md::{Converter, Options, convert}; use support::{Tree, rendered_html}; fn markdown(html: &str) -> String { let dom = Tree::parse(html); let before = dom.clone(); let result = convert(&dom, dom.root); assert_eq!(dom, before); result } #[test] fn hacker_news_layout_tables_preserve_story_and_metadata_order() { let result = markdown(include_str!("fixtures/hacker-news-layout.html")); assert_eq!( result, include_str!("fixtures/hacker-news-layout.md").trim_end() ); let html = rendered_html(&result); // The simple navigation row becomes a table; nested/spanning story layout // still expands into blocks in the original content order. assert_eq!(html.matches("").count(), 1); assert!(!html.contains("
")); assert_eq!(html.matches("before

\n
\n\n
KeyValue
a|bc|d
\n

after

\n" ); } #[test] fn wrapping_a_table_preserves_its_content() { let data = "
onetwo
threefour
"; assert_eq!( markdown(&format!("
{data}
")), markdown(data) ); assert_eq!( markdown(data), "| | |\n| --- | --- |\n| one | two |\n| three | four |" ); } #[test] fn presentational_tables_expand_into_blocks() { for role in ["presentation", "none", " PRESENTATION ", "\tNoNe\r\n"] { for header in [ "", "NameValue", "NameValue", ] { let html = format!( "{header}
Title
onetwo|three
end
" ); let heading = if header.is_empty() { "" } else { "Name\n\nValue\n\n" }; assert_eq!( markdown(&html), format!("**Title**\n\n{heading}[one](/one)\n\n`two|three`\n\nend"), "{html}" ); } } } #[test] fn presentational_table_roles_do_not_propagate_to_nested_tables() { for role in ["presentation", "none"] { let inner = "
NameValue
onetwo
"; let html = format!( "
before{inner}after
" ); assert_eq!( markdown(&html), "before\n\n| Name | Value |\n| --- | --- |\n| one | two |\n\nafter", "{html}" ); } } #[test] fn other_table_roles_do_not_change_conversion() { for role in [ "", "table", "grid", "treegrid", "presentationish", "none-other", ] { let html = format!( "
NameValue
onetwo
" ); assert_eq!( markdown(&html), "| Name | Value |\n| --- | --- |\n| one | two |" ); } } #[test] fn table_cells_preserve_block_syntax_and_code_whitespace() { let html = "
intro

Heading

  a\n  b\n
quote
  • item
end
"; assert_eq!( markdown(html), "intro\n\n## Heading\n\n```\n a\n b\n```\n\n> quote\n\n- item\n\nend" ); } #[test] fn captions_sections_headers_and_spacer_rows_preserve_content_order() { for (html, expected) in [ ( "
ab
cd
", "| | |\n| --- | --- |\n| a | b |\n| c | d |", ), ( "
Data
ab
cd
", "Data\n\n| | |\n| --- | --- |\n| a | b |\n| c | d |", ), ( "
ab
cd
end
", "| a | b |\n| --- | --- |\n| c | d |\n| end |", ), ( "
ab
cd
", "| a | b |\n| --- | --- |\n| c | d |", ), ] { assert_eq!(markdown(html), expected, "{html}"); } } #[test] fn data_table_outputs_gfm_and_preserves_literal_underscores() { let actual = markdown(include_str!("fixtures/data-table.html")); assert_eq!(actual, include_str!("fixtures/data-table.md").trim_end()); assert_eq!( rendered_html(&actual), "\n\n\n
NameAge
Alice30
Bob_under12
\n" ); } #[test] fn headerless_data_table_keeps_every_data_row_under_empty_headers() { let actual = markdown(include_str!("fixtures/headerless-data-table.html")); assert_eq!( actual, include_str!("fixtures/headerless-data-table.md").trim_end() ); assert_eq!( rendered_html(&actual), "\n\n\n
苹果5 元
香蕉3 元
\n" ); } #[test] fn headerless_tables_use_the_widest_row_without_discarding_cells() { let actual = markdown( "
one
twothree
four
", ); assert_eq!( actual, "| | |\n| ---: | :---: |\n| one |\n| two | three |\n| four |" ); let rendered = rendered_html(&actual); assert_eq!(rendered.matches("three")); } #[test] fn recognizes_headers_through_sections_whitespace_and_comments() { for header in [ "\nNameAge\n", "\nNameAge\n", " \n NameAge", ] { let html = format!("{header}
Alice30
"); assert_eq!( markdown(&html), "| Name | Age |\n| --- | --- |\n| Alice | 30 |", "{html}" ); } assert_eq!( markdown( "
NameAge
Alice30
" ), "| | |\n| --- | --- |\n| Name | Age |\n| Alice | 30 |" ); } #[test] fn preserves_caption_alignment_empty_cells_and_short_rows() { let html = "
People
NameRoleAge
Alice30
Bob
"; let actual = markdown(html); assert_eq!( actual, "**People**\n\n| Name | Role | Age |\n| :--- | :---: | ---: |\n| Alice | | 30 |\n| Bob |\n| | | |" ); let rendered = rendered_html(&actual); assert!( rendered.contains("Name"), "{rendered}" ); assert!( rendered.contains("Role"), "{rendered}" ); assert!( rendered.contains("Age"), "{rendered}" ); assert_eq!(rendered.matches("second

*third*

fourth |" ); assert!( rendered_html(&actual) .contains("first
second

third

fourth") ); } #[test] fn literal_pipes_survive_in_text_code_links_and_image_attributes() { for slashes in 0..=3 { let content = format!("a{}|b", "\\".repeat(slashes)); let html = format!( "
TextCodeLinkImage
{content}{content}{content}{content}
" ); let actual = markdown(&html); let rendered = rendered_html(&actual); assert_eq!(rendered.matches("").count(), 4, "{actual}"); assert_eq!(rendered.matches("").count(), 4, "{actual}"); assert!( rendered.contains(&format!("{content}")), "{rendered}" ); assert!( rendered.contains(&format!("{content}")), "{rendered}" ); assert!( rendered.contains("href=\"/a%7Cb\" title=\"a|b\""), "{rendered}" ); assert!( rendered.contains(&format!("alt=\"{content}\"")), "{rendered}" ); } } #[test] fn unrepresentable_tables_expand_without_losing_or_duplicating_content() { for row in [ "onetwo", "onetwo", "onetwo", "onetwo", "onetwoextra", ] { let actual = markdown(&format!( "{row}
AB
" )); let extra = if row.contains("extra") { "\n\nextra" } else { "" }; assert_eq!(actual, format!("A\n\nB\n\none\n\ntwo{extra}")); } for content in [ "
  code\nnext\n
", "", "

Title

", "
quote
", "
nested
", ] { for cell in ["th", "td"] { let actual = markdown(&format!( "<{cell}>A<{cell}>B
one{content}
" )); assert_eq!( actual, format!("A\n\nB\n\none\n\n{}", markdown(content)), "{cell}: {content}" ); } } } #[test] fn table_blocks_work_inside_lists_and_blockquotes() { let table = "
AB
onetwo
"; for wrapper in [ format!("
{table}
"), format!(""), ] { let actual = markdown(&wrapper); let rendered = rendered_html(&actual); assert_eq!(rendered.matches("").count(), 1, "{actual}"); assert_eq!(rendered.matches("")); assert_eq!(markdown("
").count(), 2, "{actual}"); } } #[test] fn table_blocks_preserve_surrounding_inline_styles() { for (tag, open, close) in [ ("strong", "**", "**"), ("a href='/outer'", "[", "](/outer)"), ] { let end_tag = tag.split(' ').next().unwrap(); let actual = markdown(&format!( "<{tag}>before
cell
after" )); assert_eq!( actual, format!( "{open}before{close}\n\n| |\n| --- |\n| {open}cell{close} |\n\n{open}after{close}" ) ); } } #[test] fn multiple_heading_rows_fall_back_to_blocks() { assert_eq!( markdown( "
AB
onetwo
threefour
" ), "A\n\nB\n\none\n\ntwo\n\nthree\n\nfour" ); } #[test] fn footer_only_tables_keep_the_footer_as_data() { assert_eq!( markdown("
Footer
"), "| |\n| --- |\n| Footer |" ); } #[test] fn empty_headers_and_unit_spans_are_representable() { let actual = markdown( "
onetwo
", ); assert_eq!(actual, "| | |\n| --- | --- |\n| one | two |"); assert!(rendered_html(&actual).contains("
onetwo
"), ""); assert_eq!(markdown("
"), ""); } #[test] fn short_rows_do_not_expand_to_a_large_grid() { for cell in ["th", "td"] { let html = format!( "{}{}
", format!("<{cell}>Header").repeat(64), "cell".repeat(64) ); let actual = markdown(&html); assert!( actual.len() < html.len(), "short rows must not be padded to the header width" ); let rendered = rendered_html(&actual); assert_eq!(rendered.matches("cell").count(), 64); let data_rows = if cell == "th" { 64 } else { 65 }; assert_eq!(rendered.matches("").count(), data_rows * 64); } } #[test] fn table_cells_respect_the_depth_limit() { let dom = Tree::parse( "
Heading
visiblehidden
", ); let converter = Converter::new(Options { max_depth: 6, ..Options::default() }); assert_eq!( converter.convert(&dom, dom.root), "| Heading |\n| --- |\n| visible |" ); assert_eq!( convert(&dom, dom.root), "| Heading |\n| --- |\n| visible*hidden* |" ); } #[test] fn table_presentation_attributes_do_not_change_conversion() { for attributes in [ "", "border='0' width='100%'", "border='1'", "style='border:0'", ] { let html = format!( "
HomeHelp
" ); assert_eq!( markdown(&html), "| | |\n| --- | --- |\n| [Home](/) | [Help](/help) |" ); } } #[test] fn nested_tables_obey_the_conversion_depth_limit() { let dom = Tree::parse( "
visible
too deep
", ); for (max_depth, expected) in [(6, "| |\n| --- |\n| visible |"), (7, "visible")] { let converter = Converter::new(Options { max_depth, ..Options::default() }); assert_eq!(converter.convert(&dom, dom.root), expected); } assert_eq!( convert(&dom, dom.root), "visible\n\n| |\n| --- |\n| too deep |" ); }