extern crate markup5ever_rcdom as rcdom; use std::cell::RefCell; use std::collections::HashMap; use std::ops::Range; use std::rc::Rc; use gpui::{ div, px, relative, Context, DefiniteLength, IntoElement, ParentElement as _, Render, SharedString, }; use html5ever::tendril::TendrilSink; use html5ever::{local_name, parse_document, LocalName, ParseOpts}; use markup5ever_rcdom::{Node, NodeData, RcDom}; use super::element::{ self, ImageNode, InlineTextStyle, LinkMark, Paragraph, Table, TableRow, TextNode, }; const BLOCK_ELEMENTS: [&str; 33] = [ "html", "body", "head", "address", "article", "aside", "blockquote", "details", "summary", "dialog", "div", "dl", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "main", "nav", "ol", "p", "pre", "section", "table", "ul", ]; pub(super) fn parse_html(source: &str) -> Result { let opts = ParseOpts { ..Default::default() }; let bytes = minify_html::minify(source.as_bytes(), &minify_html::Cfg::default()); let mut cursor = std::io::Cursor::new(bytes); // Ref // https://github.com/servo/html5ever/blob/main/rcdom/examples/print-rcdom.rs let dom = parse_document(RcDom::default(), opts) .from_utf8() .read_from(&mut cursor)?; let mut paragraph = Paragraph::default(); // NOTE: The outer paragraph is not used. let node: element::Node = parse_node(&dom.document, &mut paragraph); let node = node.compact(); Ok(node) } pub struct HtmlView { text: SharedString, parsed: bool, node: Option, } impl HtmlView { pub fn new(raw: impl Into) -> Self { Self { text: raw.into(), parsed: false, node: None, } } pub fn set_text(&mut self, raw: impl Into, cx: &mut Context) { self.text = raw.into(); self.parsed = false; self.node = None; cx.notify(); } fn parse_if_needed(&mut self) { if !self.parsed { self.node = parse_html(&self.text).ok(); self.parsed = true; } } } impl Render for HtmlView { fn render(&mut self, _: &mut gpui::Window, _: &mut Context<'_, Self>) -> impl IntoElement { self.parse_if_needed(); if let Some(node) = &self.node { div().child(node.clone()) } else { div() } } } fn attr_value(attrs: &RefCell>, name: LocalName) -> Option { attrs.borrow().iter().find_map(|attr| { if attr.name.local == name { Some(attr.value.to_string()) } else { None } }) } /// Get style properties to HashMap /// TODO: Use cssparser to parse style attribute. fn style_attrs(attrs: &RefCell>) -> HashMap { let mut styles = HashMap::new(); let Some(css_text) = attr_value(attrs, local_name!("style")) else { return styles; }; for decl in css_text.split(';') { for rule in decl.split(':') { let mut parts = rule.splitn(2, ':'); if let (Some(key), Some(value)) = (parts.next(), parts.next()) { styles.insert( key.trim().to_lowercase().to_string(), value.trim().to_string(), ); } } } styles } /// Parse length value from style attribute. /// /// When is percentage, it will be converted to relative length. /// Else, it will be converted to pixels. fn value_to_length(value: &str) -> Option { if value.ends_with("px") { value .trim_end_matches("px") .parse() .ok() .map(|v| px(v).into()) } else if value.ends_with("%") { value .trim_end_matches("%") .parse::() .ok() .map(|v| relative(v / 100.)) } else { value .trim_end_matches("px") .parse() .ok() .map(|v| px(v).into()) } } /// Get width, height from attributes or parse them from style attribute. fn attr_width_height( attrs: &RefCell>, ) -> (Option, Option) { let mut width = None; let mut height = None; if let Some(value) = attr_value(attrs, local_name!("width")) { width = value_to_length(&value); } if let Some(value) = attr_value(attrs, local_name!("height")) { height = value_to_length(&value); } if width.is_none() || height.is_none() { let styles = style_attrs(attrs); if width.is_none() { width = styles.get("width").and_then(|v| value_to_length(&v)); } if height.is_none() { height = styles.get("height").and_then(|v| value_to_length(&v)); } } (width, height) } fn parse_table_row(table: &mut Table, node: &Rc) { let mut row = TableRow::default(); let mut count = 0; for child in node.children.borrow().iter() { match child.data { NodeData::Element { ref name, ref attrs, .. } if name.local == local_name!("td") || name.local == local_name!("th") => { if child.children.borrow().is_empty() { continue; } count += 1; parse_table_cell(&mut row, child, attrs); } _ => {} } } if count > 0 { table.children.push(row); } } fn parse_table_cell( row: &mut element::TableRow, node: &Rc, attrs: &RefCell>, ) { let mut paragraph = Paragraph::default(); for child in node.children.borrow().iter() { parse_paragraph(&mut paragraph, child); } let width = attr_width_height(attrs).0; let table_cell = element::TableCell { children: paragraph, width, }; row.children.push(table_cell); } /// Trim text but leave at least one space. /// /// - Before: " \r\n Hello world \t " /// - After: " Hello world " fn trim_text(text: &str) -> String { let mut out = String::with_capacity(text.len()); for (i, c) in text.chars().enumerate() { if c.is_whitespace() { if i > 0 && out.ends_with(' ') { continue; } } out.push(c); } out } fn parse_paragraph( paragraph: &mut Paragraph, node: &Rc, ) -> (String, Vec<(Range, InlineTextStyle)>) { let mut text = String::new(); let mut marks = vec![]; /// Append new_text and new_marks to text and marks. fn merge_child_text( text: &mut String, marks: &mut Vec<(Range, InlineTextStyle)>, new_text: &str, new_marks: &[(Range, InlineTextStyle)], ) { let offset = text.len(); text.push_str(new_text); for (range, style) in new_marks { marks.push((range.start + offset..new_text.len() + offset, style.clone())); } } match &node.data { NodeData::Text { ref contents } => { let part = trim_text(&contents.borrow()); text.push_str(&part); paragraph.push_str(&text); } NodeData::Element { name, attrs, .. } => match name.local { local_name!("em") | local_name!("i") => { let mut child_paragraph = Paragraph::default(); for child in node.children.borrow().iter() { let (child_text, child_marks) = parse_paragraph(&mut child_paragraph, &child); merge_child_text(&mut text, &mut marks, &child_text, &child_marks); } marks.push(( 0..text.len(), InlineTextStyle { italic: true, ..Default::default() }, )); paragraph.push(element::TextNode { text: text.clone(), marks: marks.clone(), }); } local_name!("strong") | local_name!("b") => { let mut child_paragraph = Paragraph::default(); for child in node.children.borrow().iter() { let (child_text, child_marks) = parse_paragraph(&mut child_paragraph, &child); merge_child_text(&mut text, &mut marks, &child_text, &child_marks); } marks.push(( 0..text.len(), InlineTextStyle { bold: true, ..Default::default() }, )); paragraph.push(TextNode { text: text.clone(), marks: marks.clone(), }); } local_name!("del") | local_name!("s") => { let mut child_paragraph = Paragraph::default(); for child in node.children.borrow().iter() { let (child_text, child_marks) = parse_paragraph(&mut child_paragraph, &child); merge_child_text(&mut text, &mut marks, &child_text, &child_marks); } marks.push(( 0..text.len(), InlineTextStyle { strikethrough: true, ..Default::default() }, )); paragraph.push(TextNode { text: text.clone(), marks: marks.clone(), }); } local_name!("code") => { let mut child_paragraph = Paragraph::default(); for child in node.children.borrow().iter() { let (child_text, child_marks) = parse_paragraph(&mut child_paragraph, &child); merge_child_text(&mut text, &mut marks, &child_text, &child_marks); } marks.push(( 0..text.len(), InlineTextStyle { code: true, ..Default::default() }, )); paragraph.push(TextNode { text: text.clone(), marks: marks.clone(), }); } local_name!("a") => { let mut child_paragraph = Paragraph::default(); for child in node.children.borrow().iter() { let (child_text, child_marks) = parse_paragraph(&mut child_paragraph, &child); merge_child_text(&mut text, &mut marks, &child_text, &child_marks); } marks.push(( 0..text.len(), InlineTextStyle { link: Some(LinkMark { url: attr_value(&attrs, local_name!("href")).unwrap().into(), title: attr_value(&attrs, local_name!("title")).map(Into::into), }), ..Default::default() }, )); paragraph.push(TextNode { text: text.clone(), marks: marks.clone(), }); } local_name!("img") => { let Some(src) = attr_value(attrs, local_name!("src")) else { if cfg!(debug_assertions) { eprintln!("[html] Image node missing src attribute"); } return (text, marks); }; let alt = attr_value(attrs, local_name!("alt")); let title = attr_value(attrs, local_name!("title")); let (width, height) = attr_width_height(attrs); paragraph.set_image(ImageNode { url: src.into(), alt: alt.map(Into::into), width, height, title: title.map(Into::into), }); } _ => { // All unknown tags to as text let mut child_paragraph = Paragraph::default(); for child in node.children.borrow().iter() { let (child_text, child_marks) = parse_paragraph(&mut child_paragraph, &child); merge_child_text(&mut text, &mut marks, &child_text, &child_marks); } paragraph.push(element::TextNode { text: text.clone(), marks: marks.clone(), }); } }, _ => { let mut child_paragraph = Paragraph::default(); for child in node.children.borrow().iter() { let (child_text, child_marks) = parse_paragraph(&mut child_paragraph, &child); merge_child_text(&mut text, &mut marks, &child_text, &child_marks); } paragraph.push(element::TextNode { text: text.clone(), marks: marks.clone(), }); } } (text, marks) } fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { match node.data { NodeData::Text { ref contents } => { let text = contents.borrow().trim_start().to_string(); if text.len() > 0 { paragraph.push_str(&text); } element::Node::Ignore } NodeData::Element { ref name, ref attrs, .. } => match name.local { local_name!("br") => element::Node::Break, local_name!("h1") | local_name!("h2") | local_name!("h3") | local_name!("h4") | local_name!("h5") | local_name!("h6") => { let mut children = vec![]; if !paragraph.is_empty() { children.push(element::Node::Paragraph(paragraph.clone())); paragraph.clear(); } let level = name .local .chars() .last() .unwrap_or('6') .to_digit(10) .unwrap_or(6) as u8; let mut paragraph = Paragraph::default(); for child in node.children.borrow().iter() { parse_paragraph(&mut paragraph, child); } let heading = element::Node::Heading { level, children: paragraph, }; if children.len() > 0 { children.push(heading); element::Node::Root { children } } else { heading } } local_name!("img") => { let mut children = vec![]; if !paragraph.is_empty() { children.push(element::Node::Paragraph(paragraph.clone())); paragraph.clear(); } let Some(src) = attr_value(attrs, local_name!("src")) else { if cfg!(debug_assertions) { eprintln!("[html] Image node missing src attribute"); } return element::Node::Ignore; }; let alt = attr_value(&attrs, local_name!("alt")); let title = attr_value(&attrs, local_name!("title")); let (width, height) = attr_width_height(&attrs); let image = Paragraph::Image { span: None, image: ImageNode { url: src.into(), title: title.map(Into::into), alt: alt.map(Into::into), width, height, }, }; if children.len() > 0 { children.push(element::Node::Paragraph(image)); element::Node::Root { children } } else { element::Node::Paragraph(image) } } local_name!("ul") | local_name!("ol") => { let mut children = vec![]; if !paragraph.is_empty() { children.push(element::Node::Paragraph(paragraph.clone())); paragraph.clear(); } let ordered = name.local == local_name!("ol"); let mut list_children = vec![]; for child in node.children.borrow().iter() { let mut child_paragraph = Paragraph::default(); list_children.push(parse_node(child, &mut child_paragraph)); } let list = element::Node::List { children: list_children, ordered, }; if children.len() > 0 { children.push(list); element::Node::Root { children } } else { list } } local_name!("li") => { let mut children = vec![]; for child in node.children.borrow().iter() { let mut child_paragraph = Paragraph::default(); children.push(parse_node(child, &mut child_paragraph)); if child_paragraph.text_len() > 0 { children.push(element::Node::Paragraph(child_paragraph.clone())); child_paragraph.clear(); } } if !paragraph.is_empty() { children.push(element::Node::Paragraph(paragraph.clone())); paragraph.clear(); } element::Node::ListItem { children, spread: false, checked: None, } } local_name!("table") => { let mut children = vec![]; if !paragraph.is_empty() { children.push(element::Node::Paragraph(paragraph.clone())); paragraph.clear(); } let mut table = Table::default(); for child in node.children.borrow().iter() { match child.data { NodeData::Element { ref name, .. } if name.local == local_name!("tbody") || name.local == local_name!("thead") => { for sub_child in child.children.borrow().iter() { parse_table_row(&mut table, &sub_child); } } _ => { parse_table_row(&mut table, &child); } } } let table = element::Node::Table(table); if children.len() > 0 { children.push(table); element::Node::Root { children } } else { table } } _ => { if BLOCK_ELEMENTS.contains(&name.local.trim()) { let mut children: Vec = vec![]; // Case: // // Hello

Inner text of block element

World // Insert before text as a node -- The "Hello" if !paragraph.is_empty() { children.push(element::Node::Paragraph(paragraph.clone())); paragraph.clear(); } // Inner of the block element -- The "Inner text of block element" for child in node.children.borrow().iter() { children.push(parse_node(child, paragraph)); } // if !paragraph.is_empty() { // children.push(element::Node::Paragraph(paragraph.clone())); // paragraph.clear(); // } if children.is_empty() { element::Node::Ignore } else { element::Node::Root { children } } } else { // Others to as Inline parse_paragraph(paragraph, node); if paragraph.is_image() { let image = paragraph.clone(); paragraph.clear(); element::Node::Paragraph(image) } else { element::Node::Ignore } } } }, NodeData::Document => { let mut children = vec![]; for child in node.children.borrow().iter() { children.push(parse_node(child, paragraph)); } if !paragraph.is_empty() { children.push(element::Node::Paragraph(paragraph.clone())); paragraph.clear(); } element::Node::Root { children } } NodeData::Doctype { .. } => element::Node::Ignore, NodeData::Comment { .. } => element::Node::Ignore, NodeData::ProcessingInstruction { .. } => element::Node::Ignore, } } #[cfg(test)] mod tests { use gpui::{px, relative}; use crate::text::element::{Node, Paragraph}; use super::trim_text; #[test] fn test_trim_text() { assert_eq!(trim_text(" \n\tHello world \t\r "), " Hello world ",); } #[test] fn test_value_to_length() { assert_eq!(super::value_to_length("100px"), Some(px(100.).into())); assert_eq!(super::value_to_length("100%"), Some(relative(1.))); assert_eq!(super::value_to_length("56%"), Some(relative(0.56))); assert_eq!(super::value_to_length("240"), Some(px(240.).into())); } #[test] fn test_image() { let html = r#"Example"#; let node = super::parse_html(html).unwrap(); assert_eq!( node, Node::Paragraph(Paragraph::Image { span: None, image: super::ImageNode { url: "https://example.com/image.png".to_string().into(), alt: Some("Example".to_string().into()), width: Some(px(100.).into()), height: Some(px(200.).into()), title: Some("Example Image".to_string().into()) } }) ); let html = r#"Example"#; let node = super::parse_html(html).unwrap(); assert_eq!( node, Node::Paragraph(Paragraph::Image { span: None, image: super::ImageNode { url: "https://example.com/image.png".to_string().into(), alt: Some("Example".to_string().into()), width: Some(relative(0.8)), height: None, title: Some("Example Image".to_string().into()) } }) ); } }