//! Module for converting HTML markup to blocks with Pango Markup formatted content.
//!
//! The functions in this module turn the HTML input to a list of block elements (see
//! [`HtmlBlock`]) rather then an output string.  This can be useful if the blocks are rendered
//! separately, for example if you have some text followed by a code block. The former may be
//! rendered using a different widget than the latter.
//!
//! The main function of this module is [`markup_html`].
//! It yields HTML blocks for the following tags:
//! * `<h1>`, `<h2>`, `<h3>`, `<h4>`, `<h5>`, `<h6>` => [`HtmlBlock::Heading`]
//! * `<pre>` => [`HtmlBlock::Code`]
//! * `<ul>` => [`HtmlBlock::UList`]
//! * `<ol>` => [`HtmlBlock::OList`]
//! * `<blockquote>` => [`HtmlBlock::Quote`]
//!
//! It handles the following tags as text, yielding an [`HtmlBlock::Text`] with the contents:
//! * `<a>` => `<a>` with only the `href` attribute set
//! * `<b>` and `<strong>` => `<b>`
//! * `<body>` and `<mx-reply>` => removed
//! * `<br>` => `\n`
//! * `<code>` => `<tt>`
//! * `<del>` and `<s>` => `<s>`
//! * `<em>` and `<i>` => `<i>`
//! * `<span>` and `<font>` => `<span>`
//! * `<p>` => contents followed by `\n`
//! * `<u>` => `<u>`
//!
//! Finally, it removes all other tags and replaces URIs by HTML link tags. and merges
//! subsequent text HTML blocks into a single one.
//!
//! # Examples
//!
//! Converting HTML markup with a heading and two paragraphs:
//!
//! ```rust
//! # use html2pango::block::*;
//! let blks =
//!     markup_html("<h1>Heading</h1><p>This is paragraph 1.</p><p>Paragraph 2</p>").unwrap();
//! assert_eq!(blks[0], HtmlBlock::Heading(1, String::from("Heading")));
//! assert_eq!(blks[1], HtmlBlock::Text(String::from("This is paragraph 1.\n\nParagraph 2")));
//! ```
//!
//! Converting HTML markup with an unordered list and some formatting:
//!
//! ```rust
//! # use html2pango::block::*;
//! let blks =
//!     markup_html("<ul><li>This is <b>bold</b></li><li><code>Some code</code></ul>").unwrap();
//! assert_eq!(
//!     blks[0],
//!     HtmlBlock::UList(vec![
//!         String::from("This is <b>bold</b>"),
//!         String::from("<tt>Some code</tt>"),
//!     ])
//! );
//! ```
//!
//! Converting HTML markup links and URIs that are converted into links:
//!
//! ```rust
//! # use html2pango::block::*;
//! let blks = markup_html("a <a href=\"https://gnome.org\" rel=\"nofollow\">link</a> and \
//!                         another: https://gnome.org").unwrap();
//! assert_eq!(
//!     blks[0],
//!     HtmlBlock::Text(
//!         String::from(
//!             "a <a href=\"https://gnome.org\" title=\"https://gnome.org\">link</a> and \
//!              another: <a href=\"https://gnome.org\">https://gnome.org</a>"
//!         )
//!     )
//! );
//! ```
//!
//! For more examples, see [`markup_html`].

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;

/// Supported HTML block elements.
///
/// Each block element contains a textual representation of the contained inline HTML elements in
/// [Pango Markup]. The exception is [`HtmlBlock::Quote`], which contains the list of block
/// elements that are contained in the quote.
///
/// [Pango Markup]: https://docs.gtk.org/Pango/pango_markup.html
#[derive(Debug, Clone, PartialEq)]
pub enum HtmlBlock {
    /// Just some text.
    Text(String),
    /// Heading with its level and text.
    Heading(u32, String),
    /// Unordered list of text.
    UList(Vec<String>),
    /// Ordered list of text.
    OList(Vec<String>),
    /// Code block with its contained (preformatted) text.
    Code(String),
    /// (Block)quote with its contained block elements.
    Quote(Rc<Vec<HtmlBlock>>),
    /// Vertical separator (i.e. horizontal rule)
    Separator,
}

/// Converts HTML body markup into a list of HTML blocks while ignoring some tags.
///
/// The provided tags are ignored while parsing as if they are not there, i.e. the parser just
/// descends into them and leaves them out of the result.
///
/// For more details on what is actually converted, see the [module documentation](crate::block).
///
/// # Errors
///
/// Returns an error if reading from the strings fails or if parsing somehow fails.
///
/// # Examples
///
/// ```rust
/// # use html2pango::block::*;
/// let blks =
///   markup_html_ignore_tags("<reply>this is <em>nice</em></reply>", &["body"]).unwrap();
/// assert_eq!(blks, vec![HtmlBlock::Text(String::from("this is <i>nice</i>"))]);
/// ```
///
/// For more examples about the markup conversion, see [`markup_html`].
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();
    // TODO: When writing `<i>...</i>` in markdown, the first children is of type
    // Comment { contents: "raw HTML omitted" }
    // This should be handled more gracefully
    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)))
}

/// Converts HTML body markup into a list of HTML blocks.
///
/// Calls [`markup_html_ignore_tags`] to do the actual work instructing it to ignore the `<body>`
/// and `mx-reply` (for Matrix) tags.
///
/// For more details on what is actually converted, see the [module documentation](crate::block).
///
/// # Errors
///
/// Returns an error if reading from the string fails or if parsing fails.
///
/// # Examples
///
/// Some HTML markup of a heading and an unordered list:
///
/// ```rust
/// # use html2pango::block::*;
/// let blks =
///     markup_html("<body><h1>Heading 1</h1><ul><li>First</li><li>Second</li><body>").unwrap();
/// assert_eq!(blks[0], HtmlBlock::Heading(1, String::from("Heading 1")));
/// assert_eq!(blks[1], HtmlBlock::UList(vec![String::from("First"), String::from("Second")]));
/// ```
///
/// Pango Markup is applied, paragraphs are handled but merged:
///
/// ```rust
/// # use html2pango::block::*;
/// let blks =
///     markup_html("<body><p>This is <em>nice</em>!</p><p>Second paragraph.</p></body>").unwrap();
/// assert_eq!(
///     blks[0],
///     HtmlBlock::Text(String::from("This is <i>nice</i>!\n\nSecond paragraph."))
/// );
/// ```
///
/// URIs are replaced by links:
///
/// ```rust
/// # use html2pango::block::*;
/// let blks =
///     markup_html("<body>Go to https://gnome.org</body>").unwrap();
/// assert_eq!(
///     blks[0],
///     HtmlBlock::Text(String::from("Go to <a href=\"https://gnome.org\">https://gnome.org</a>"))
/// );
/// ```
///
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"];

    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>")),
        _ => 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)) => {
                        // We don't want tt tags inside codeblocks which
                        // already have <tt>.
                        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(ref 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(); // Drop the following space
                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("&amp;", "&")
        .replace("&lt;", "<")
        .replace("&gt;", ">")
        .replace("&quot;", "\"")
}

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 {
                // tags to ignore
                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 &lt;a href=&quot;http://gnome.org&quot;&gt;GNOME&lt;/a&gt;, <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 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>&amp;</code> was not scaped as <code>&amp;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>&amp;</tt> was not scaped as <tt>&amp;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 >&nbsp;Fractal-next&nbsp;</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");
        }
    }
}
