use std::cell::RefCell;
use std::rc::Rc;
use super::html_escape;
use super::markup_links;
use html5ever::tendril::TendrilSink;
use html5ever::tree_builder::Attribute;
use html5ever::tree_builder::TreeBuilderOpts;
use html5ever::{parse_document, ParseOpts};
use markup5ever_rcdom::Node;
use markup5ever_rcdom::NodeData;
use markup5ever_rcdom::RcDom;
#[derive(Debug, Clone, PartialEq)]
pub enum HtmlBlock {
Text(String),
Heading(u32, String),
UList(Vec<String>),
OList(Vec<String>),
Code(String),
Quote(Rc<Vec<HtmlBlock>>),
Separator,
}
pub fn markup_html_ignore_tags(s: &str, tags: &[&str]) -> Result<Vec<HtmlBlock>, anyhow::Error> {
let opts = ParseOpts {
tree_builder: TreeBuilderOpts {
drop_doctype: true,
..Default::default()
},
..Default::default()
};
let dom = parse_document(RcDom::default(), opts)
.from_utf8()
.read_from(&mut s.as_bytes())?;
let document = &dom.document;
let mut html = document.children.borrow().clone();
html.retain(|x| !matches!(x.data, NodeData::Comment { .. }));
if let Some(h) = &html.get(0) {
if let Some(node) = &h.children.borrow().get(1) {
let markup = convert_node(node, tags);
return Ok(trim_text_blocks(markup));
}
}
Err(anyhow::anyhow!(format!("Could not parse {:?}", html)))
}
pub fn markup_html(s: &str) -> Result<Vec<HtmlBlock>, anyhow::Error> {
let tags = vec!["body", "mx-reply"];
markup_html_ignore_tags(s, &tags)
}
fn to_pango(t: &str) -> Option<(&'static str, &'static str)> {
let allowed = [
"u", "del", "s", "em", "i", "strong", "b", "code", "a", "br", "sub", "sup",
];
if !allowed.contains(&t) {
return None;
}
match t {
"a" => Some(("<a>", "</a>")),
"br" => Some(("", "\n")),
"em" | "i" => Some(("<i>", "</i>")),
"strong" | "b" => Some(("<b>", "</b>")),
"del" | "s" => Some(("<s>", "</s>")),
"u" => Some(("<u>", "</u>")),
"code" => Some(("<tt>", "</tt>")),
"sub" => Some(("<sub>", "</sub>")),
"sup" => Some(("<sup>", "</sup>")),
_ => None,
}
}
fn parse_link(node: &Node, attrs: &RefCell<Vec<Attribute>>) -> String {
let mut link = "".to_string();
for attr in attrs.borrow().iter() {
let s = attr.name.local.to_string();
if &s[..] == "href" {
link = attr.value.to_string();
}
}
format!(
"<a href=\"{0}\" title=\"{0}\">{1}</a>",
html_escape(&link),
get_text_content(node)
)
}
fn get_text_content(node: &Node) -> String {
let text = node
.children
.borrow()
.iter()
.map(|node| match node.data {
NodeData::Text { contents: ref c } => {
markup_links(&replace_html_whitespace(&html_escape(&c.borrow())))
}
NodeData::Element {
name: ref n,
attrs: ref a,
..
} => {
let inside = get_text_content(node);
if &n.local == "a" {
return parse_link(node, a);
}
match to_pango(&n.local) {
Some((t1, t2)) => format!("{}{}{}", t1, inside, t2),
None => inside,
}
}
_ => get_text_content(node),
})
.collect::<Vec<String>>()
.concat();
collapse_spaces(&text)
}
fn get_plain_text_content(node: &Node) -> String {
node.children
.borrow()
.iter()
.map(|node| match node.data {
NodeData::Text { contents: ref c } => c.borrow().to_string(),
NodeData::Element {
name: ref n,
attrs: ref _a,
..
} => {
let inside = get_plain_text_content(node);
match to_pango(&n.local) {
Some((t1, t2)) => {
if t1 != "<tt>" {
format!("{}{}{}", t1, inside, t2)
} else {
inside
}
}
None => inside,
}
}
_ => get_plain_text_content(node),
})
.collect::<Vec<String>>()
.concat()
}
fn get_li_elements(node: &Node) -> Vec<String> {
node.children
.borrow()
.iter()
.filter_map(|node| match node.data {
NodeData::Element { name: ref n, .. } if &n.local == "li" => {
Some(get_text_content(node))
}
_ => None,
})
.collect::<Vec<String>>()
}
fn join_text_blocks(mut blocks: Vec<HtmlBlock>) -> Vec<HtmlBlock> {
use HtmlBlock::Text;
blocks.drain(..).fold(Vec::<HtmlBlock>::new(), |mut v, b| {
let last = v.last();
if let (Text(c), Some(Text(a))) = (&b, last) {
let t = join_collapse_spaces(a, c);
v.pop();
v.push(Text(t));
} else {
v.push(b);
}
v
})
}
fn join_collapse_spaces(s: &str, extra: &str) -> String {
if s.ends_with(' ') && (extra.starts_with('\n') || extra.starts_with(' ')) {
s.trim_end().to_string() + extra
} else if s.ends_with('\n') && extra.starts_with(' ') {
s.to_string() + extra.trim_start()
} else {
s.to_string() + extra
}
}
fn collapse_spaces(s: &str) -> String {
let mut collapsed_s = String::with_capacity(s.len());
let mut iter = s.chars().peekable();
while let Some(c) = iter.next() {
match (c, iter.peek()) {
(' ', Some('\n')) | (' ', Some(' ')) => continue,
('\n', Some(' ')) => {
iter.next(); while iter.peek() == Some(&' ') {
iter.next();
}
}
_ => {}
}
collapsed_s.push(c);
}
collapsed_s
}
fn replace_html_whitespace(s: &str) -> String {
let mut result = String::new();
let mut last = 0;
for (index, matched) in s.match_indices(|c: char| !c.is_ascii_whitespace()) {
if last != index {
result.push(' ');
}
result.push_str(matched);
last = index + matched.len();
}
if last < s.len() {
result.push(' ');
}
result
}
fn html_unescape(s: &str) -> String {
s.to_string()
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
}
fn trim_text_blocks(mut blocks: Vec<HtmlBlock>) -> Vec<HtmlBlock> {
blocks.drain(..).fold(vec![], |mut v, b| {
if let HtmlBlock::Text(c) = &b {
if !c.trim().is_empty() {
v.push(HtmlBlock::Text(c.trim().to_string()));
}
} else {
v.push(b);
}
v
})
}
fn convert_node(node: &Node, tags_to_ignore: &[&str]) -> Vec<HtmlBlock> {
let mut output = vec![];
match node.data {
NodeData::Text { contents: ref c } => {
let s = markup_links(&replace_html_whitespace(&html_escape(&c.borrow())));
output.push(HtmlBlock::Text(s));
}
NodeData::Element {
name: ref n,
attrs: ref a,
..
} => {
match &n.local as &str {
tag if tags_to_ignore.contains(&tag) => {
for child in node.children.borrow().iter() {
for block in convert_node(child, tags_to_ignore) {
output.push(block);
}
}
}
h if ["h1", "h2", "h3", "h4", "h5", "h6"].contains(&h) => {
let n: u32 = h[1..].parse().unwrap_or(6);
let text = get_text_content(node);
output.push(HtmlBlock::Heading(n, text));
}
"a" => {
let link = parse_link(node, a);
output.push(HtmlBlock::Text(link));
}
"pre" => {
let text = get_plain_text_content(node);
output.push(HtmlBlock::Code(html_unescape(text.trim())));
}
"ul" => {
let elements = get_li_elements(node);
output.push(HtmlBlock::UList(elements));
}
"ol" => {
let elements = get_li_elements(node);
output.push(HtmlBlock::OList(elements));
}
"blockquote" => {
let mut content = vec![];
for child in node.children.borrow().iter() {
for block in convert_node(child, tags_to_ignore) {
content.push(block);
}
}
content = trim_text_blocks(join_text_blocks(content));
output.push(HtmlBlock::Quote(Rc::new(content)));
}
"p" => {
let content = &get_text_content(node);
output.push(HtmlBlock::Text(format!("\n{}\n", content.trim())));
}
"hr" => {
output.push(HtmlBlock::Separator);
}
"br" => {
output.push(HtmlBlock::Text("\n".to_string()));
}
"font" | "span" => {
let attrs = a.borrow();
let span_attrs = attrs
.iter()
.flat_map(|attr| match &*attr.name.local {
"color" | "data-mx-color" => Some(("foreground", &*attr.value)),
"data-mx-bg-color" => Some(("background", &*attr.value)),
_ => None,
})
.collect::<Vec<(&str, &str)>>();
let content = get_text_content(node);
let mut span = String::new();
crate::format_span(&mut span, content, span_attrs);
output.push(HtmlBlock::Text(span));
}
tag => {
let content = get_text_content(node);
let block = match to_pango(tag) {
Some((t1, t2)) => format!("{}{}{}", t1, content, t2),
None => content,
};
output.push(HtmlBlock::Text(block));
}
};
}
_ => {}
}
join_text_blocks(output)
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_html_blocks() {
let text = "<h1>heading 1 <em>italic</em></h1>\n<h2>heading 2</h2>\n<p>Some text with <em>markup</em> and <strong>other</strong> and more text and some <code>inline code</code>, that's all. And maybe some links http://google.es or <a href=\"http://gnome.org\">GNOME</a>.</p>\n<pre><code>Block text\n</code></pre>\n<ul>\n<li>This is a list</li>\n<li>second element</li>\n</ul>\n<ol>\n<li>another list</li>\n<li>that's all</li>\n</ol>\n<hr/>";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 7);
assert_eq!(
blocks[0],
HtmlBlock::Heading(1, "heading 1 <i>italic</i>".to_string())
);
assert_eq!(blocks[1], HtmlBlock::Heading(2, "heading 2".to_string()));
assert!(matches!(&blocks[2], HtmlBlock::Text(..)));
assert!(matches!(&blocks[3], HtmlBlock::Code(..)));
assert!(matches!(&blocks[4], HtmlBlock::UList(..)));
assert!(matches!(&blocks[5], HtmlBlock::OList(..)));
assert!(matches!(&blocks[6], HtmlBlock::Separator));
}
#[test]
fn test_html_blocks_quote() {
let text = "
<blockquote>
<h1>heading 1 <em>bold</em></h1>
<h2>heading 2</h2>
<p>Some text with <em>markup</em> and <strong>other</strong> and more things ~~strike~~ and more text and some <code>inline code</code>, that's all. And maybe some links http://google.es or <a href="http://gnome.org">GNOME</a>, <a href=\"http://gnome.org\">GNOME</a>.</p>
<pre><code>`Block text
`
</code></pre>
<ul>
<li>This is a list</li>
<li>second element</li>
</ul>
</blockquote>
<p>quote :D</p>
";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 2);
if let HtmlBlock::Quote(blks) = &blocks[0] {
assert_eq!(blks.len(), 5);
}
}
#[test]
fn test_separator() {
let text = "aaa\n<hr/>foobar<hr/>baz<hr></hr>";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 6);
let expected_blocks = [
HtmlBlock::Text(String::from("aaa")),
HtmlBlock::Separator,
HtmlBlock::Text(String::from("foobar")),
HtmlBlock::Separator,
HtmlBlock::Text(String::from("baz")),
HtmlBlock::Separator,
];
for (block, expected) in blocks.iter().zip(expected_blocks.iter()) {
assert_eq!(block, expected);
}
}
#[test]
fn test_mxreply() {
let text = "<mx-reply><blockquote><a href=\"https://matrix.to/#/!hwiGbsdSTZIwSRfybq:matrix.org/$1553513281991555ZdMuB:matrix.org\">In reply to</a> <a href=\"https://matrix.to/#/@afranke:matrix.org\">@afranke:matrix.org</a><br><a href=\"https://matrix.to/#/@gergely:polonkai.eu\">Gergely Polonkai</a>: we have https://gitlab.gnome.org/GNOME/fractal/issues/467 and https://gitlab.gnome.org/GNOME/fractal/issues/347 open, does your issue fit any of these two?</blockquote></mx-reply><p>#467 <em>might</em> be it, let me test a bit<p>";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 2);
if let HtmlBlock::Text(t) = &blocks[0] {
assert_eq!(t, "<a href=\"https://matrix.to/#/!hwiGbsdSTZIwSRfybq:matrix.org/$1553513281991555ZdMuB:matrix.org\">In reply to</a> <a href=\"https://matrix.to/#/@afranke:matrix.org\">@afranke:matrix.org</a><a href=\"https://matrix.to/#/@gergely:polonkai.eu\">Gergely Polonkai</a>: we have <a href=\"https://gitlab.gnome.org/GNOME/fractal/issues/467\">https://gitlab.gnome.org/GNOME/fractal/issues/467</a> and <a href=\"https://gitlab.gnome.org/GNOME/fractal/issues/347\">https://gitlab.gnome.org/GNOME/fractal/issues/347</a> open, does your issue fit any of these two?\n#467 <i>might</i> be it, let me test a bit");
}
}
#[test]
fn test_html_lists() {
let text = "
<ul>
<li>item 1</li>
<li>item 2</li>
</ul>
";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 1);
if let HtmlBlock::UList(t) = &blocks[0] {
assert_eq!(t.len(), 2);
assert_eq!(t[0], "item 1");
assert_eq!(t[1], "item 2");
}
}
#[test]
fn test_html_paragraphs() {
let text = "
<p>text</p>
<p><i>text2</i></p>
<p><b>text3</b></p>
<ul><li>li<li/></ul>
<p>text4</p>
<p>text5</p>
";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert!(!blocks.is_empty());
assert_eq!(blocks.len(), 3);
if let HtmlBlock::Text(t) = &blocks[0] {
assert_eq!(t, "text\n\n<i>text2</i>\n\n<b>text3</b>");
}
if let HtmlBlock::Text(t) = &blocks[2] {
assert_eq!(t, "text4\n\ntext5");
}
}
#[test]
fn newline_in_blockquote() {
let text = "<blockquote>html was a mistake<br />Indeed</blockquote>";
let matrix_text = "<blockquote>\n<p>a<br />\nb</p>\n<p>c</p>\nd</blockquote>\n";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 1);
if let HtmlBlock::Quote(blk) = &blocks[0] {
assert_eq!(blk.len(), 1);
if let HtmlBlock::Text(h) = &blk[0] {
assert_eq!(h, "html was a mistake\nIndeed");
}
}
let blocks = markup_html(matrix_text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 1);
if let HtmlBlock::Quote(blk) = &blocks[0] {
assert_eq!(blk.len(), 1);
if let HtmlBlock::Text(h) = &blk[0] {
assert_eq!(h, "a\nb\n\nc\nd");
}
}
}
#[test]
fn html_blocks_quote_multiple() {
let text = "<blockquote>\n<p>Some</p>\n</blockquote>\n<p>text</p>\n<blockquote>\n<p>No</p>\n</blockquote>\n<p><strong>u</strong></p>\n";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 4);
if let HtmlBlock::Quote(blk) = &blocks[0] {
assert_eq!(blk.len(), 1);
if let HtmlBlock::Text(h) = &blk[0] {
assert_eq!(h, "Some");
}
}
if let HtmlBlock::Text(t) = &blocks[1] {
assert_eq!(t, "text");
}
if let HtmlBlock::Quote(blk) = &blocks[2] {
assert_eq!(blk.len(), 1);
if let HtmlBlock::Text(h) = &blk[0] {
assert_eq!(h, "No");
}
}
if let HtmlBlock::Text(t) = &blocks[3] {
assert_eq!(t, "<b>u</b>");
}
}
#[test]
fn html_url_and_text() {
let text = "<a href=\"https://gnome.org\">GNOME</a>text";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 1);
if let HtmlBlock::Text(h) = &blocks[0] {
assert_eq!(
h,
"<a href=\"https://gnome.org\" title=\"https://gnome.org\">GNOME</a>text"
);
}
}
#[test]
fn html_font_span() {
let text = "GitLab labels: <span data-mx-color=\"#000000\"\n \
data-mx-bg-color=\"#ccc063\"\n \
>1. Enhancement</span>\n and \
<font data-mx-color=\"#ffffff\"\n \
data-mx-bg-color=\"#8574cc\"\n \
>4. Newcomers</span>";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 1);
if let HtmlBlock::Text(h) = &blocks[0] {
assert_eq!(h, "GitLab labels: <span foreground=\"#000000\" background=\"#ccc063\">1. Enhancement</span> and <span foreground=\"#ffffff\" background=\"#8574cc\">4. Newcomers</span>");
}
}
#[test]
fn html_non_block_tags() {
let text =
"How about some <u>underlined</u> <del>deleted</del> or <s>strikethrough</s> text? \
Maybe <em>emphasized</em>, <i>italic</i>, <strong>strong</strong> or <b>bold</b> text? \
Possibly a newline<br>with some more <code>code</code>? \
Finally some <sub>sub</sub> or <sup>super</sup> par lines!";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 1);
let HtmlBlock::Text(h) = &blocks[0] else {
panic!("No text block found")
};
assert_eq!(
h,
"How about some <u>underlined</u> <s>deleted</s> or <s>strikethrough</s> text? \
Maybe <i>emphasized</i>, <i>italic</i>, <b>strong</b> or <b>bold</b> text? \
Possibly a newline\nwith some more <tt>code</tt>? \
Finally some <sub>sub</sub> or <sup>super</sup> par lines!"
);
}
#[test]
fn codeblock_empty_whitespace() {
let text = "
<pre><code>
Block text `a` <i>i</i>
space?
</code></pre>
";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 1);
if let HtmlBlock::Code(t) = &blocks[0] {
assert_eq!(t, "Block text `a` <i>i</i>\n\nspace?")
}
}
#[test]
fn escape_amp() {
let text = "text: <code>&</code> was not scaped as <code>&amp;</code>";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 1);
if let HtmlBlock::Text(t) = &blocks[0] {
assert_eq!(
t,
"text: <tt>&</tt> was not scaped as <tt>&amp;</tt>"
);
}
}
#[test]
fn dont_escape_codeblocks() {
let text = "
<pre><code>
&
</code></pre>
<code>&<code>
";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 2);
if let HtmlBlock::Code(t) = &blocks[0] {
assert_eq!(t, "&")
}
if let HtmlBlock::Code(t) = &blocks[1] {
assert_eq!(t, "&")
}
}
#[test]
fn newlines_in_text() {
let text = "
1
2
<span> 3</span>
<p> 4
5</p>
<p>6 </p>
";
let text_matrix = "<p>a<br />\nb</p>\n<p>c</p>\nd";
let text_matrix_gitlab = "<strong>[<a href=\"https://gitlab.gnome.org/GNOME/fractal\">GNOME/fractal</a>]</strong> <a href=\"https://gitlab.gnome.org/user\">user</a>\n added <span title=\"This label is for the rewrite of Fractal, currently under the working name of fractal-next\"\n > Fractal-next </span>\n to\n\n <a href=\"https://gitlab.gnome.org/GNOME/fractal/-/issues/194\" >issue #194</a>: Replies support";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 1);
if let HtmlBlock::Text(t) = &blocks[0] {
assert_eq!(t, "1 2 3\n4 5\n\n6")
}
let blocks = markup_html(text_matrix);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 1);
if let HtmlBlock::Text(t) = &blocks[0] {
assert_eq!(t, "a\nb\n\nc\nd")
}
let blocks = markup_html(text_matrix_gitlab);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 1);
if let HtmlBlock::Text(t) = &blocks[0] {
assert_eq!(t, "<b>[<a href=\"https://gitlab.gnome.org/GNOME/fractal\" title=\"https://gitlab.gnome.org/GNOME/fractal\">GNOME/fractal</a>]</b> <a href=\"https://gitlab.gnome.org/user\" title=\"https://gitlab.gnome.org/user\">user</a> added \u{a0}Fractal-next\u{a0} to <a href=\"https://gitlab.gnome.org/GNOME/fractal/-/issues/194\" title=\"https://gitlab.gnome.org/GNOME/fractal/-/issues/194\">issue #194</a>: Replies support");
}
let text_h1 = "<h1>foo \n bar</h1>";
let blocks = markup_html(text_h1).unwrap();
if let HtmlBlock::Heading(1, t) = &blocks[0] {
assert_eq!(t, "foo bar");
}
let text_ul = "<ul><li>first \n element</li><li>second<br/> element</li></ul>";
let blocks = markup_html(text_ul).unwrap();
if let HtmlBlock::UList(ts) = &blocks[0] {
assert_eq!(ts[0], "first element");
assert_eq!(ts[1], "second\nelement");
}
let text_ol = "<ol><li>first \n element</li><li>second<br/> element</li></ol>";
let blocks = markup_html(text_ol).unwrap();
if let HtmlBlock::OList(ts) = &blocks[0] {
assert_eq!(ts[0], "first element");
assert_eq!(ts[1], "second\nelement");
}
let text_bq = "<blockquote><p>Foo:\n <p><ul><li>First \n item</li></ul></blockquote>";
let blocks = markup_html(text_bq).unwrap();
if let HtmlBlock::Quote(contents) = &blocks[0] {
assert_eq!(contents[0], HtmlBlock::Text(String::from("Foo:")));
assert_eq!(
contents[1],
HtmlBlock::UList(vec![String::from("First item")])
);
}
}
#[test]
fn links_inside_code() {
let text = "
<pre><code>https://gitlab.gnome.org/World/Fractal/</code></pre>\n
";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 1);
if let HtmlBlock::Code(t) = &blocks[0] {
assert_eq!(t, "https://gitlab.gnome.org/World/Fractal/");
}
}
#[test]
fn ci_links() {
let text = "
[<a href='https://gitlab.gnome.org/World/Fractal'>World/Fractal</a>]
";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 1);
if let HtmlBlock::Text(s) = &blocks[0] {
assert_eq!(s, "[<a href=\"https://gitlab.gnome.org/World/Fractal\" title=\"https://gitlab.gnome.org/World/Fractal\">World/Fractal</a>]");
}
}
#[test]
fn newline_after_quotes() {
let text = "<mx-reply><blockquote><a href=\"https://matrix.org\">In reply to</a> <a href=\"https://matrix.org\">@okias:matrix.org</a><br>Text</blockquote></mx-reply>F";
let target = "<a href=\"https://matrix.org\" title=\"https://matrix.org\">In reply to</a> <a href=\"https://matrix.org\" title=\"https://matrix.org\">@okias:matrix.org</a>\nText";
let blocks = markup_html(text);
assert!(blocks.is_ok());
let blocks = blocks.unwrap();
assert_eq!(blocks.len(), 2);
if let HtmlBlock::Quote(blk) = &blocks[0] {
assert_eq!(blk.len(), 1);
if let HtmlBlock::Text(h) = &blk[0] {
assert_eq!(h, target);
}
}
if let HtmlBlock::Text(t) = &blocks[1] {
assert_eq!(t, "F");
}
}
}