use super::*; // Simple transformer to make testing easier.fn test_parse_html(source: &str) -> Vec<FormattedTextLine> { parse_html(source).unwrap().lines.into()} #[test]fn test_parse_plain_text() { assert_eq!( test_parse_html("<meta charset='utf-8'>Some"), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("Some"), ])] ); assert_eq!( test_parse_html("<meta charset='utf-8'><p>Some</p><p>tests</p>"), vec![ FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("Some"),]), FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("tests"),]) ] ); // Example from GDocs. assert_eq!( test_parse_html( "<meta charset='utf-8'><meta charset=\"utf-8\"><b style=\"font-weight:normal;\" id=\"docs-internal-guid-27b0e865-7fff-b40d-5b19-8e9e7ccf7c8c\">\ <p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;\ font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">Some plain</span></p><p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\">\ <span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;\ white-space:pre-wrap;\">text</span></p></b>" ), vec![ FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("Some plain"),]), FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("text"),]) ] );} #[test]fn test_parse_text_styles() { assert_eq!( test_parse_html( "<meta charset='utf-8'>So<span style=\"font-weight:600\" data-token-index=\"1\" class=\"notion-enable-hover\">me</span>" ), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("So"), // font-weight:600 preserves as Semibold rather than collapsing to Bold. FormattedTextFragment::weighted("me", Some(CustomWeight::Semibold)), ])] ); assert_eq!( test_parse_html( "<meta charset='utf-8'><p>So<strong>me</strong></p><p><em><strong>tes</strong></em>ts</p>" ), vec![ FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("So"), FormattedTextFragment::bold("me") ]), FormattedTextLine::Line(vec![ FormattedTextFragment::bold_italic("tes"), FormattedTextFragment::plain_text("ts") ]) ] ); assert_eq!( test_parse_html( "<meta charset='utf-8'><meta charset=\"utf-8\"><b style=\"font-weight:normal;\" id=\"docs-internal-guid-a96b449f-7fff-d755-78a4-efcebc867940\">\ <p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;\ font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">So</span>\ <span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">\ me</span><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">\ p</span><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">\ la</span><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">in\ </span></p><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">t\ </span><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">ext</span></b>" ), vec![ FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("So"), FormattedTextFragment::italic("me"), FormattedTextFragment::bold_italic("p"), FormattedTextFragment::bold("la"), FormattedTextFragment::plain_text("in") ]), FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("t"), FormattedTextFragment::italic("ext") ]) ] ); assert_eq!( test_parse_html( "<meta charset='utf-8'><meta charset=\"utf-8\"><b style=\"font-weight:normal;\" id=\"docs-internal-guid-27b0e865-7fff-b40d-5b19-8e9e7ccf7c8c\">\ <p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;\ font-variant:normal;text-decoration:underline;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">This is underlined</span></p><p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\">\ <span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;\ white-space:pre-wrap;\"> text</span></p></b>" ), vec![ FormattedTextLine::Line(vec![FormattedTextFragment::underline("This is underlined")]), FormattedTextLine::Line(vec![FormattedTextFragment::plain_text(" text")]), ] ); assert_eq!( test_parse_html( "<meta charset='utf-8'>abc <a href=\"https://google.com/\" style=\"cursor:pointer;color:inherit;word-wrap:break-word;text-decoration:inherit\" class=\"notion-link-token notion-focusable-token notion-enable-hover\" rel=\"noopener noreferrer\"\ data-token-index=\"1\" tabindex=\"0\"><span style=\"border-bottom:0.05em solid;border-color:rgba(55,53,47,0.4);opacity:0.7\" class=\"link-annotation-unknown-block-id--760432549\">NewLinadafekene</span></a> ghi def" ), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("abc "), FormattedTextFragment::hyperlink("NewLinadafekene", "https://google.com/"), FormattedTextFragment::plain_text(" ghi def") ]),] ); assert_eq!( test_parse_html( r#"<meta charset='utf-8'>the <span style="font-family:"SFMono-Regular", Menlo, Consolas, "PT Mono", "Liberation Mono", Courier, monospace;line-height:normal;background:rgba(135,131,120,.15);color:#EB5757;border-radius:3px;font-size:85%;padding:0.2em 0.4em" data-token-index="1" spellcheck="false" class="notion-enable-hover"><ul></span> and <span style="font-family:"SFMono-Regular", Menlo, Consolas, "PT Mono", "Liberation Mono", Courier, monospace;line-height:normal;background:rgba(135,131,120,.15);color:#EB5757;border-radius:3px;font-size:85%;padding:0.2em 0.4em" data-token-index="3" spellcheck="false" class="notion-enable-hover"><li></span>"# ), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("the "), FormattedTextFragment::inline_code("<ul>"), FormattedTextFragment::plain_text(" and "), FormattedTextFragment::inline_code("<li>"), ]),] ); assert_eq!( test_parse_html( r#"<meta charset='utf-8'><span style="text-decoration:line-through" data-token-index="0" class="notion-enable-hover">Strike</span><span style="font-weight:600" data-token-index="1" class="notion-enable-hover">Bold</span>"# ), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::strikethrough("Strike"), // font-weight:600 preserves as Semibold rather than collapsing to Bold. FormattedTextFragment::weighted("Bold", Some(CustomWeight::Semibold)), ]),] )} #[test]fn test_parse_numeric_font_weights_preserve_custom_weight() { // Regression test for #14429: numeric CSS font weights on pasted HTML must round-trip to the // matching `CustomWeight`, not collapse to bold-or-plain. Prior behavior mapped every // weight > 400 to Bold and everything else to no weight, dropping Light/Medium/Black/etc. let cases = [ ("100", Some(CustomWeight::Thin)), ("200", Some(CustomWeight::ExtraLight)), ("300", Some(CustomWeight::Light)), ("400", None), ("500", Some(CustomWeight::Medium)), ("600", Some(CustomWeight::Semibold)), ("700", Some(CustomWeight::Bold)), ("800", Some(CustomWeight::ExtraBold)), ("900", Some(CustomWeight::Black)), ]; for (css_weight, expected) in cases { let html = format!("<meta charset='utf-8'>So<span style=\"font-weight:{css_weight}\">me</span>"); assert_eq!( test_parse_html(&html), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("So"), FormattedTextFragment::weighted("me", expected), ])], "font-weight:{css_weight} should map to {expected:?}" ); }} #[test]fn test_parse_keyword_font_weights() { // `bold`/`bolder` keywords stay Bold; `normal`/`lighter` carry no custom weight. assert_eq!( test_parse_html("<meta charset='utf-8'>So<span style=\"font-weight:bold\">me</span>"), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("So"), FormattedTextFragment::bold("me"), ])] ); assert_eq!( test_parse_html("<meta charset='utf-8'>So<span style=\"font-weight:normal\">me</span>"), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("So"), FormattedTextFragment::plain_text("me"), ])] );} #[test]fn test_block() { assert_eq!( test_parse_html( "<meta charset='utf-8'><pre><code class=\"language-jsx\">git checkout -b branch</code></pre>" ), vec![FormattedTextLine::CodeBlock(CodeBlockText { lang: "jsx".to_string(), code: "git checkout -b branch".to_string() })] ); assert_eq!( test_parse_html( "<meta charset='utf-8'><p>tests</p><pre><code class=\"language-jsx\">git checkout -b branch</code></pre><p>More</p>" ), vec![ FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("tests"),]), FormattedTextLine::CodeBlock(CodeBlockText { lang: "jsx".to_string(), code: "git checkout -b branch".to_string() }), FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("More"),]) ] );} #[test]fn test_transform_non_breaking_spaces() { let expected_text = vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text( "Open the Docker desktop app. This is necessary to create the symbolic links that will make the", ), FormattedTextFragment::plain_text(" "), FormattedTextFragment::inline_code("docker"), FormattedTextFragment::plain_text(" "), FormattedTextFragment::plain_text("CLI available."), ])]; let chromium_html = r#"Open the Docker desktop app. This is necessary to create the symbolic links that will make the<span> </span><code>docker</code><span> </span>CLI available."#; assert_eq!(test_parse_html(chromium_html), expected_text); let safari_html = r#"Open the Docker desktop app. This is necessary to create the symbolic links that will make the<span class="Apple-converted-space"> </span><code>docker</code><span class="Apple-converted-space"> </span>CLI available."#; assert_eq!(test_parse_html(safari_html), expected_text);} // TODO: remove/update this test when we eventually support these HTML element types!#[test]fn test_unsupported_html_types() { assert_eq!( test_parse_html( "<meta charset='utf-8'><p>Test test</p><blockquote><p>Block quotes with <strong>bold text</strong></p></blockquote>" ), vec![ FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("Test test"),]), FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("Block quotes with "), FormattedTextFragment::bold("bold text") ]) ] ); assert_eq!( test_parse_html( "<meta charset='utf-8'><table><thead><tr><th>Text 1</th><th>Text 2</th></tr></thead><tbody><tr><td>Test</td><td>Test</td></tr></tbody></table>" ), vec![ FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("Text 1"), FormattedTextFragment::plain_text("Text 2") ]), FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("Test"), FormattedTextFragment::plain_text("Test") ]) ] );} #[test]fn test_sub_lists() { assert_eq!( test_parse_html( "<meta charset='utf-8'><ul><li>def<ul><li>abc<ul><li>sub-list</li></ul></li><li>abc</li></ul></li></ul>" ), vec![ FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("def")] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("abc")] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 2, text: vec![FormattedTextFragment::plain_text("sub-list")] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("abc")] }) ] ); assert_eq!( test_parse_html( "<meta charset='utf-8'><ul><li>d<strong>ef</strong><ul><li><em>abc</em><ul><li>sub-list</li></ul></li><li>abc</li></ul></li></ul><p>normal text</p>" ), vec![ FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 0, text: vec![ FormattedTextFragment::plain_text("d"), FormattedTextFragment::bold("ef") ] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::italic("abc")] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 2, text: vec![FormattedTextFragment::plain_text("sub-list")] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("abc")] }), FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("normal text")]) ] ); assert_eq!( test_parse_html( "<meta charset='utf-8'><ul><li>abc<ul><li>def</li></ul></li></ul><ol><li>abc<ol><li>def</li></ol></li></ol>" ), vec![ FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("abc")] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("def")] }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("abc".to_string())] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("def".to_string())] } }), ] ); assert_eq!( test_parse_html("<meta charset='utf-8'><ul><li>abc<ol><li>def</li></ol></li></ul>"), vec![ FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("abc".to_string())] }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("def")] } }), ] );} #[test]fn test_formatted_sub_lists() { assert_eq!( test_parse_html( "<meta charset='utf-8'><ul>\n<li>By default, the client goes to staging\n<ul>\n<li>To target localhost, build the client with</li>\n</ul>\n</li>\n</ul>\n" ), vec![ FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text( "By default, the client goes to staging" )] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text( "To target localhost, build the client with" )] }), ] );} #[test]fn test_ordered_lists() { let html = r#"<meta charset='utf-8'><ol start="3"> <li>First</li> <li>Second<ol> <li>A</li> <li>B</li> </ol> </li> <li>Third</li> <li>Fourth<ol start="invalid"> <li>G</li> <li>H</li> </ol> </li></ol> "#; assert_eq!( test_parse_html(html), vec![ FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: Some(3), indented_text: FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("First")] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("Second")] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("A")] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("B")] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("Third")] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("Fourth")] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("G")] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("H")] } }), ] );} #[test]fn test_horizontal_rules() { assert_eq!( test_parse_html("<meta charset='utf-8'><ul>\n<li>bcf</li>\n</ul>\n<hr>\n<p>abc</p>"), vec![ FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("bcf")] }), FormattedTextLine::HorizontalRule, FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("abc")]) ] );} #[test]Lines 1–500 of 620. Load more
use super::*; // Simple transformer to make testing easier. fn test_parse_html(source: &str) -> Vec<FormattedTextLine> { parse_html(source).unwrap().lines.into() } #[test] fn test_parse_plain_text() { assert_eq!( test_parse_html("<meta charset='utf-8'>Some"), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("Some"), ])] ); assert_eq!( test_parse_html("<meta charset='utf-8'><p>Some</p><p>tests</p>"), vec![ FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("Some"),]), FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("tests"),]) ] ); // Example from GDocs. assert_eq!( test_parse_html( "<meta charset='utf-8'><meta charset=\"utf-8\"><b style=\"font-weight:normal;\" id=\"docs-internal-guid-27b0e865-7fff-b40d-5b19-8e9e7ccf7c8c\">\ <p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;\ font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">Some plain</span></p><p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\">\ <span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;\ white-space:pre-wrap;\">text</span></p></b>" ), vec![ FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("Some plain"),]), FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("text"),]) ] ); } #[test] fn test_parse_text_styles() { assert_eq!( test_parse_html( "<meta charset='utf-8'>So<span style=\"font-weight:600\" data-token-index=\"1\" class=\"notion-enable-hover\">me</span>" ), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("So"), // font-weight:600 preserves as Semibold rather than collapsing to Bold. FormattedTextFragment::weighted("me", Some(CustomWeight::Semibold)), ])] ); assert_eq!( test_parse_html( "<meta charset='utf-8'><p>So<strong>me</strong></p><p><em><strong>tes</strong></em>ts</p>" ), vec![ FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("So"), FormattedTextFragment::bold("me") ]), FormattedTextLine::Line(vec![ FormattedTextFragment::bold_italic("tes"), FormattedTextFragment::plain_text("ts") ]) ] ); assert_eq!( test_parse_html( "<meta charset='utf-8'><meta charset=\"utf-8\"><b style=\"font-weight:normal;\" id=\"docs-internal-guid-a96b449f-7fff-d755-78a4-efcebc867940\">\ <p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;\ font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">So</span>\ <span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">\ me</span><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">\ p</span><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">\ la</span><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">in\ </span></p><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">t\ </span><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:italic;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">ext</span></b>" ), vec![ FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("So"), FormattedTextFragment::italic("me"), FormattedTextFragment::bold_italic("p"), FormattedTextFragment::bold("la"), FormattedTextFragment::plain_text("in") ]), FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("t"), FormattedTextFragment::italic("ext") ]) ] ); assert_eq!( test_parse_html( "<meta charset='utf-8'><meta charset=\"utf-8\"><b style=\"font-weight:normal;\" id=\"docs-internal-guid-27b0e865-7fff-b40d-5b19-8e9e7ccf7c8c\">\ <p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\"><span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;\ font-variant:normal;text-decoration:underline;vertical-align:baseline;white-space:pre;white-space:pre-wrap;\">This is underlined</span></p><p dir=\"ltr\" style=\"line-height:1.38;margin-top:0pt;margin-bottom:0pt;\">\ <span style=\"font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;\ white-space:pre-wrap;\"> text</span></p></b>" ), vec![ FormattedTextLine::Line(vec![FormattedTextFragment::underline("This is underlined")]), FormattedTextLine::Line(vec![FormattedTextFragment::plain_text(" text")]), ] ); assert_eq!( test_parse_html( "<meta charset='utf-8'>abc <a href=\"https://google.com/\" style=\"cursor:pointer;color:inherit;word-wrap:break-word;text-decoration:inherit\" class=\"notion-link-token notion-focusable-token notion-enable-hover\" rel=\"noopener noreferrer\"\ data-token-index=\"1\" tabindex=\"0\"><span style=\"border-bottom:0.05em solid;border-color:rgba(55,53,47,0.4);opacity:0.7\" class=\"link-annotation-unknown-block-id--760432549\">NewLinadafekene</span></a> ghi def" ), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("abc "), FormattedTextFragment::hyperlink("NewLinadafekene", "https://google.com/"), FormattedTextFragment::plain_text(" ghi def") ]),] ); assert_eq!( test_parse_html( r#"<meta charset='utf-8'>the <span style="font-family:"SFMono-Regular", Menlo, Consolas, "PT Mono", "Liberation Mono", Courier, monospace;line-height:normal;background:rgba(135,131,120,.15);color:#EB5757;border-radius:3px;font-size:85%;padding:0.2em 0.4em" data-token-index="1" spellcheck="false" class="notion-enable-hover"><ul></span> and <span style="font-family:"SFMono-Regular", Menlo, Consolas, "PT Mono", "Liberation Mono", Courier, monospace;line-height:normal;background:rgba(135,131,120,.15);color:#EB5757;border-radius:3px;font-size:85%;padding:0.2em 0.4em" data-token-index="3" spellcheck="false" class="notion-enable-hover"><li></span>"# ), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("the "), FormattedTextFragment::inline_code("<ul>"), FormattedTextFragment::plain_text(" and "), FormattedTextFragment::inline_code("<li>"), ]),] ); assert_eq!( test_parse_html( r#"<meta charset='utf-8'><span style="text-decoration:line-through" data-token-index="0" class="notion-enable-hover">Strike</span><span style="font-weight:600" data-token-index="1" class="notion-enable-hover">Bold</span>"# ), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::strikethrough("Strike"), // font-weight:600 preserves as Semibold rather than collapsing to Bold. FormattedTextFragment::weighted("Bold", Some(CustomWeight::Semibold)), ]),] ) } #[test] fn test_parse_numeric_font_weights_preserve_custom_weight() { // Regression test for #14429: numeric CSS font weights on pasted HTML must round-trip to the // matching `CustomWeight`, not collapse to bold-or-plain. Prior behavior mapped every // weight > 400 to Bold and everything else to no weight, dropping Light/Medium/Black/etc. let cases = [ ("100", Some(CustomWeight::Thin)), ("200", Some(CustomWeight::ExtraLight)), ("300", Some(CustomWeight::Light)), ("400", None), ("500", Some(CustomWeight::Medium)), ("600", Some(CustomWeight::Semibold)), ("700", Some(CustomWeight::Bold)), ("800", Some(CustomWeight::ExtraBold)), ("900", Some(CustomWeight::Black)), ]; for (css_weight, expected) in cases { let html = format!("<meta charset='utf-8'>So<span style=\"font-weight:{css_weight}\">me</span>"); assert_eq!( test_parse_html(&html), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("So"), FormattedTextFragment::weighted("me", expected), ])], "font-weight:{css_weight} should map to {expected:?}" ); } } #[test] fn test_parse_keyword_font_weights() { // `bold`/`bolder` keywords stay Bold; `normal`/`lighter` carry no custom weight. assert_eq!( test_parse_html("<meta charset='utf-8'>So<span style=\"font-weight:bold\">me</span>"), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("So"), FormattedTextFragment::bold("me"), ])] ); assert_eq!( test_parse_html("<meta charset='utf-8'>So<span style=\"font-weight:normal\">me</span>"), vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("So"), FormattedTextFragment::plain_text("me"), ])] ); } #[test] fn test_block() { assert_eq!( test_parse_html( "<meta charset='utf-8'><pre><code class=\"language-jsx\">git checkout -b branch</code></pre>" ), vec![FormattedTextLine::CodeBlock(CodeBlockText { lang: "jsx".to_string(), code: "git checkout -b branch".to_string() })] ); assert_eq!( test_parse_html( "<meta charset='utf-8'><p>tests</p><pre><code class=\"language-jsx\">git checkout -b branch</code></pre><p>More</p>" ), vec![ FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("tests"),]), FormattedTextLine::CodeBlock(CodeBlockText { lang: "jsx".to_string(), code: "git checkout -b branch".to_string() }), FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("More"),]) ] ); } #[test] fn test_transform_non_breaking_spaces() { let expected_text = vec![FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text( "Open the Docker desktop app. This is necessary to create the symbolic links that will make the", ), FormattedTextFragment::plain_text(" "), FormattedTextFragment::inline_code("docker"), FormattedTextFragment::plain_text(" "), FormattedTextFragment::plain_text("CLI available."), ])]; let chromium_html = r#"Open the Docker desktop app. This is necessary to create the symbolic links that will make the<span> </span><code>docker</code><span> </span>CLI available."#; assert_eq!(test_parse_html(chromium_html), expected_text); let safari_html = r#"Open the Docker desktop app. This is necessary to create the symbolic links that will make the<span class="Apple-converted-space"> </span><code>docker</code><span class="Apple-converted-space"> </span>CLI available."#; assert_eq!(test_parse_html(safari_html), expected_text); } // TODO: remove/update this test when we eventually support these HTML element types! #[test] fn test_unsupported_html_types() { assert_eq!( test_parse_html( "<meta charset='utf-8'><p>Test test</p><blockquote><p>Block quotes with <strong>bold text</strong></p></blockquote>" ), vec![ FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("Test test"),]), FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("Block quotes with "), FormattedTextFragment::bold("bold text") ]) ] ); assert_eq!( test_parse_html( "<meta charset='utf-8'><table><thead><tr><th>Text 1</th><th>Text 2</th></tr></thead><tbody><tr><td>Test</td><td>Test</td></tr></tbody></table>" ), vec![ FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("Text 1"), FormattedTextFragment::plain_text("Text 2") ]), FormattedTextLine::Line(vec![ FormattedTextFragment::plain_text("Test"), FormattedTextFragment::plain_text("Test") ]) ] ); } #[test] fn test_sub_lists() { assert_eq!( test_parse_html( "<meta charset='utf-8'><ul><li>def<ul><li>abc<ul><li>sub-list</li></ul></li><li>abc</li></ul></li></ul>" ), vec![ FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("def")] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("abc")] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 2, text: vec![FormattedTextFragment::plain_text("sub-list")] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("abc")] }) ] ); assert_eq!( test_parse_html( "<meta charset='utf-8'><ul><li>d<strong>ef</strong><ul><li><em>abc</em><ul><li>sub-list</li></ul></li><li>abc</li></ul></li></ul><p>normal text</p>" ), vec![ FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 0, text: vec![ FormattedTextFragment::plain_text("d"), FormattedTextFragment::bold("ef") ] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::italic("abc")] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 2, text: vec![FormattedTextFragment::plain_text("sub-list")] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("abc")] }), FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("normal text")]) ] ); assert_eq!( test_parse_html( "<meta charset='utf-8'><ul><li>abc<ul><li>def</li></ul></li></ul><ol><li>abc<ol><li>def</li></ol></li></ol>" ), vec![ FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("abc")] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("def")] }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("abc".to_string())] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("def".to_string())] } }), ] ); assert_eq!( test_parse_html("<meta charset='utf-8'><ul><li>abc<ol><li>def</li></ol></li></ul>"), vec![ FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("abc".to_string())] }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("def")] } }), ] ); } #[test] fn test_formatted_sub_lists() { assert_eq!( test_parse_html( "<meta charset='utf-8'><ul>\n<li>By default, the client goes to staging\n<ul>\n<li>To target localhost, build the client with</li>\n</ul>\n</li>\n</ul>\n" ), vec![ FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text( "By default, the client goes to staging" )] }), FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text( "To target localhost, build the client with" )] }), ] ); } #[test] fn test_ordered_lists() { let html = r#" <meta charset='utf-8'> <ol start="3"> <li>First</li> <li>Second<ol> <li>A</li> <li>B</li> </ol> </li> <li>Third</li> <li>Fourth<ol start="invalid"> <li>G</li> <li>H</li> </ol> </li> </ol> "#; assert_eq!( test_parse_html(html), vec![ FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: Some(3), indented_text: FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("First")] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("Second")] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("A")] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("B")] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("Third")] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("Fourth")] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("G")] } }), FormattedTextLine::OrderedList(OrderedFormattedIndentTextInline { number: None, indented_text: FormattedIndentTextInline { indent_level: 1, text: vec![FormattedTextFragment::plain_text("H")] } }), ] ); } #[test] fn test_horizontal_rules() { assert_eq!( test_parse_html("<meta charset='utf-8'><ul>\n<li>bcf</li>\n</ul>\n<hr>\n<p>abc</p>"), vec![ FormattedTextLine::UnorderedList(FormattedIndentTextInline { indent_level: 0, text: vec![FormattedTextFragment::plain_text("bcf")] }), FormattedTextLine::HorizontalRule, FormattedTextLine::Line(vec![FormattedTextFragment::plain_text("abc")]) ] ); } #[test]