use html5ever::{local_name, namespace_url, ns, LocalName, QualName}; use kuchiki::{iter::NodeEdge, traits::*, NodeData}; use super::{MessageComponent, MessageContent}; pub fn convert_matrix(message: &str) -> MessageContent { let dom = kuchiki::parse_fragment( QualName::new(None, ns!(html), LocalName::from("div")), vec![], ) .one(message); let mut parents = vec![]; let mut components = vec![]; for edge in dom.traverse() { match edge { NodeEdge::Start(node) => { if let NodeData::Element(element) = node.data() { if element.name.ns == ns!(html) { match element.name.local { local_name!("strong") | local_name!("b") | local_name!("em") | local_name!("i") | local_name!("s") | local_name!("u") | local_name!("a") | local_name!("blockquote") => { parents.push(components); components = vec![]; } local_name!("span") => { let attrs = element.attributes.borrow(); if attrs.get("data-mx-spoiler").is_some() { parents.push(components); components = vec![]; } } _ => {} } } } } NodeEdge::End(node) => match node.data() { NodeData::Text(text) => { components.push(MessageComponent::Plain(text.borrow().clone())); } NodeData::Element(element) => { macro_rules! construct_component { ($f:expr) => {{ let component_type = $f; if let Some(mut parent_components) = parents.pop() { parent_components.push((component_type)(components)); components = parent_components; } }}; } if element.name.ns == ns!(html) { match element.name.local { local_name!("strong") | local_name!("b") => { construct_component!(MessageComponent::Bold) } local_name!("em") | local_name!("i") => { construct_component!(MessageComponent::Italic) } local_name!("s") => { construct_component!(MessageComponent::Strikethrough) } local_name!("u") => { construct_component!(MessageComponent::Underline) } local_name!("a") => { if let Some(mut parent_components) = parents.pop() { let attrs = element.attributes.borrow(); if let Some(href) = attrs.get(local_name!("href")) { parent_components.push(MessageComponent::Link { target: href.to_string(), text: components, }); } else { parent_components.append(&mut components); } components = parent_components; } } local_name!("br") => { components.push(MessageComponent::HardBreak); } local_name!("blockquote") => { construct_component!(MessageComponent::BlockQuote) } local_name!("span") => { let attrs = element.attributes.borrow(); if let Some(spoiler_reason) = attrs.get("data-mx-spoiler") { construct_component!(|inner| MessageComponent::Spoiler { reason: (!spoiler_reason.is_empty()) .then(|| spoiler_reason.to_string()), content: inner, }) } } _ => {} } } } _ => {} }, }; } components } pub fn format_matrix(message_content: &[MessageComponent]) -> String { message_content .iter() .map(|component| match component { MessageComponent::Plain(text) => html_escape::encode_text(text).to_string(), MessageComponent::Link { target, text } => format!( r#"{}"#, html_escape::encode_quoted_attribute(target), format_matrix(text) ), MessageComponent::Italic(inner) => format!("{}", format_matrix(inner)), MessageComponent::Bold(inner) => format!("{}", format_matrix(inner)), MessageComponent::Strikethrough(inner) => { format!("{}", format_matrix(inner)) } MessageComponent::Underline(inner) => format!("{}", format_matrix(inner)), MessageComponent::Code(code) => { format!("{}", html_escape::encode_text(code)) } MessageComponent::CodeBlock { lang, source } => { format!( r#"
{}
"#, lang.as_ref() .map(|lang| format!( r#" class="language-{}""#, html_escape::encode_quoted_attribute(lang) )) .unwrap_or_else(|| "".to_string()), source, ) } MessageComponent::Spoiler { reason, content } => format!( "{}", reason .as_ref() .map(|reason| format!(r#"="{}""#, html_escape::encode_quoted_attribute(reason))) .unwrap_or_else(|| "".to_string()), format_matrix(content) ), MessageComponent::HardBreak => "
".to_string(), MessageComponent::BlockQuote(inner) => { format!("
{}
", format_matrix(inner)) } }) .collect() } #[test] fn simple_matrix_parsing() { use MessageComponent::*; let html = r#"hello! <> example"#; assert_eq!( convert_matrix(html), vec![ Bold(vec![ Plain("hello! ".to_string(),), Italic(vec![Plain("<>".to_string())]), ]), Plain(" ".to_string()), Link { target: "https://example.com/".to_string(), text: vec![Plain("example".to_string())] }, ] ) } #[test] fn spoiler_parsing() { use MessageComponent::*; let html = r#"the whole island is populated by lesbians"#; assert_eq!( convert_matrix(html), vec![Spoiler { reason: None, content: vec![ Plain("the ".to_string()), Italic(vec![Plain("whole".to_string())]), Plain(" island is populated by lesbians".to_string()) ] }] ); }