From 4075447c63ef9ece3d6dc29841ef88e3d7e09bf3 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 3 Jul 2025 18:02:08 +0800 Subject: [PATCH] text_view: Fix list item to merge inline paragraphs, and fix paragraphs margin. (#1036) ## Before image ## After image --- crates/ui/src/text/element.rs | 49 ++++++++++++------- crates/ui/src/text/html.rs | 87 ++++++++++++++++++++-------------- crates/ui/src/text/markdown.rs | 12 ++--- 3 files changed, 91 insertions(+), 57 deletions(-) diff --git a/crates/ui/src/text/element.rs b/crates/ui/src/text/element.rs index 34ee11a4..82cefd9b 100644 --- a/crates/ui/src/text/element.rs +++ b/crates/ui/src/text/element.rs @@ -201,6 +201,25 @@ impl Paragraph { Self::Image { .. } => 1, } } + + /// Try to merge two paragraphs, if they are both text elements. + /// + /// - Returns `true` if other have merge into self. + /// - Returns `false` if not able to merge. + pub fn try_merge(&mut self, other: &Self) -> bool { + if let Self::Texts { children, .. } = self { + if let Self::Texts { + children: other_children, + .. + } = other + { + children.extend(other_children.clone()); + return true; + } + } + + false + } } #[derive(Debug, Clone, PartialEq)] @@ -262,15 +281,10 @@ pub enum Node { html: bool, }, Divider, - Ignore, Unknown, } impl Node { - pub(super) fn is_ignore(&self) -> bool { - matches!(self, Self::Ignore) - } - pub(super) fn is_list_item(&self) -> bool { matches!(self, Self::ListItem { .. }) } @@ -283,11 +297,7 @@ impl Node { pub(super) fn compact(&self) -> Node { match self { Self::Root { children } => { - let children = children - .iter() - .map(|c| c.compact()) - .filter(|c| !c.is_ignore()) - .collect::>(); + let children = children.iter().map(|c| c.compact()).collect::>(); if children.len() == 1 { children.first().unwrap().compact() } else { @@ -310,7 +320,13 @@ impl RenderOnce for Paragraph { for text_node in children.into_iter() { let text_len = text_node.text.len(); - text.push_str(&text_node.text); + let part = if text.len() == 0 { + // trim start for first text + text_node.text.trim_start() + } else { + text_node.text.as_str() + }; + text.push_str(&part); let mut node_highlights = vec![]; for (range, style) in text_node.marks { @@ -421,6 +437,7 @@ impl Node { ordered: state.ordered, todo: checked.is_some(), }), + false, true, text_view_style, window, @@ -485,6 +502,7 @@ impl Node { todo: checked.is_some(), }), true, + true, text_view_style, window, cx, @@ -607,6 +625,7 @@ impl Node { pub(crate) fn render( self, list_state: Option, + is_root: bool, is_last_child: bool, style: &TextViewStyle, window: &mut Window, @@ -624,8 +643,8 @@ impl Node { .children({ let children_len = children.len(); children.into_iter().enumerate().map(move |(index, c)| { - let is_last_child = index == children_len - 1; - c.render(None, is_last_child, style, window, cx) + let is_last_child = is_root && index == children_len - 1; + c.render(None, false, is_last_child, style, window, cx) }) }) .into_any_element(), @@ -699,10 +718,9 @@ impl Node { .mb(mb) .into_any_element(), Node::Break { .. } => div().into_any_element(), - Node::Ignore => div().into_any_element(), _ => { if cfg!(debug_assertions) { - eprintln!("Unknown implementation: {:?}", self); + tracing::warn!("unknown implementation: {:?}", self); } div().into_any_element() @@ -869,7 +887,6 @@ impl Node { } } Node::Divider => "---".to_string(), - Node::Ignore => "".to_string(), Node::Unknown => "".to_string(), } .trim() diff --git a/crates/ui/src/text/html.rs b/crates/ui/src/text/html.rs index 4f21f207..60d6381a 100644 --- a/crates/ui/src/text/html.rs +++ b/crates/ui/src/text/html.rs @@ -75,7 +75,8 @@ pub(super) fn parse_html(source: &str) -> Result { let mut paragraph = Paragraph::default(); // NOTE: The outer paragraph is not used. - let node: element::Node = parse_node(&dom.document, &mut paragraph); + let node: element::Node = + parse_node(&dom.document, &mut paragraph).unwrap_or(element::Node::Unknown); let node = node.compact(); Ok(node) @@ -173,7 +174,7 @@ impl Element for HtmlElement { let mut el = div() .map(|this| match root { - Ok(node) => this.child(node.render(None, true, &self.style, window, cx)), + Ok(node) => this.child(node.render(None, true, true, &self.style, window, cx)), Err(err) => this.child( v_flex() .gap_1() @@ -492,7 +493,7 @@ fn parse_paragraph( local_name!("img") => { let Some(src) = attr_value(attrs, local_name!("src")) else { if cfg!(debug_assertions) { - eprintln!("[html] Image node missing src attribute"); + tracing::warn!("Image node missing src attribute"); } return (text, marks); }; @@ -509,7 +510,6 @@ fn parse_paragraph( title: title.map(Into::into), }); } - _ => { // All unknown tags to as text let mut child_paragraph = Paragraph::default(); @@ -539,7 +539,7 @@ fn parse_paragraph( (text, marks) } -fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { +fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> Option { match node.data { NodeData::Text { ref contents } => { let text = contents.borrow().to_string(); @@ -547,14 +547,14 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { paragraph.push_str(&text); } - element::Node::Ignore + None } NodeData::Element { ref name, ref attrs, .. } => match name.local { - local_name!("br") => element::Node::Break { html: true }, + local_name!("br") => Some(element::Node::Break { html: true }), local_name!("h1") | local_name!("h2") | local_name!("h3") @@ -587,9 +587,9 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { if children.len() > 0 { children.push(heading); - element::Node::Root { children } + Some(element::Node::Root { children }) } else { - heading + Some(heading) } } local_name!("img") => { @@ -601,9 +601,9 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { let Some(src) = attr_value(attrs, local_name!("src")) else { if cfg!(debug_assertions) { - eprintln!("[html] Image node missing src attribute"); + tracing::warn!("image node missing src attribute"); } - return element::Node::Ignore; + return None; }; let alt = attr_value(&attrs, local_name!("alt")); @@ -623,9 +623,9 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { if children.len() > 0 { children.push(element::Node::Paragraph(image)); - element::Node::Root { children } + Some(element::Node::Root { children }) } else { - element::Node::Paragraph(image) + Some(element::Node::Paragraph(image)) } } local_name!("ul") | local_name!("ol") => { @@ -641,7 +641,9 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { for child in node.children.borrow().iter() { let mut child_paragraph = Paragraph::default(); - list_children.push(parse_node(child, &mut child_paragraph)); + if let Some(child_node) = parse_node(child, &mut child_paragraph) { + list_children.push(child_node); + } } let list = element::Node::List { @@ -650,19 +652,30 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { }; if children.len() > 0 { children.push(list); - element::Node::Root { children } + Some(element::Node::Root { children }) } else { - list + Some(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 let Some(child_node) = parse_node(child, &mut child_paragraph) { + children.push(child_node); + } if child_paragraph.text_len() > 0 { - children.push(element::Node::Paragraph(child_paragraph.clone())); - child_paragraph.clear(); + // If last child is paragraph, merge child + if let Some(last_child) = children.last_mut() { + if let element::Node::Paragraph(last_paragraph) = last_child { + if last_paragraph.try_merge(&child_paragraph) { + continue; + }; + } + } + + children.push(element::Node::Paragraph(child_paragraph)); } } @@ -671,11 +684,11 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { paragraph.clear(); } - element::Node::ListItem { + Some(element::Node::ListItem { children, spread: false, checked: None, - } + }) } local_name!("table") => { let mut children = vec![]; @@ -704,9 +717,9 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { let table = element::Node::Table(table); if children.len() > 0 { children.push(table); - element::Node::Root { children } + Some(element::Node::Root { children }) } else { - table + Some(table) } } local_name!("blockquote") => { @@ -725,9 +738,9 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { } children.push(element::Node::Blockquote(blockquote)); - element::Node::Root { children: children } + Some(element::Node::Root { children: children }) } - local_name!("style") | local_name!("script") => element::Node::Ignore, + local_name!("style") | local_name!("script") => None, _ => { if BLOCK_ELEMENTS.contains(&name.local.trim()) { let mut children: Vec = vec![]; @@ -744,7 +757,9 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { // 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 let Some(child_node) = parse_node(child, paragraph) { + children.push(child_node); + } } // if !paragraph.is_empty() { @@ -753,9 +768,9 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { // } if children.is_empty() { - element::Node::Ignore + None } else { - element::Node::Root { children } + Some(element::Node::Root { children }) } } else { // Others to as Inline @@ -764,9 +779,9 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { if paragraph.is_image() { let image = paragraph.clone(); paragraph.clear(); - element::Node::Paragraph(image) + Some(element::Node::Paragraph(image)) } else { - element::Node::Ignore + None } } } @@ -774,7 +789,9 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { NodeData::Document => { let mut children = vec![]; for child in node.children.borrow().iter() { - children.push(parse_node(child, paragraph)); + if let Some(child_node) = parse_node(child, paragraph) { + children.push(child_node); + } } if !paragraph.is_empty() { @@ -782,11 +799,11 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { paragraph.clear(); } - element::Node::Root { children } + Some(element::Node::Root { children }) } - NodeData::Doctype { .. } => element::Node::Ignore, - NodeData::Comment { .. } => element::Node::Ignore, - NodeData::ProcessingInstruction { .. } => element::Node::Ignore, + NodeData::Doctype { .. } + | NodeData::Comment { .. } + | NodeData::ProcessingInstruction { .. } => None, } } diff --git a/crates/ui/src/text/markdown.rs b/crates/ui/src/text/markdown.rs index 23a4b9ca..e8591d31 100644 --- a/crates/ui/src/text/markdown.rs +++ b/crates/ui/src/text/markdown.rs @@ -124,7 +124,7 @@ impl Element for MarkdownElement { let mut el = div() .map(|this| match root { - Ok(node) => this.child(node.render(None, true, &self.style, window, cx)), + Ok(node) => this.child(node.render(None, true, true, &self.style, window, cx)), Err(err) => this.child( v_flex() .gap_1() @@ -341,13 +341,13 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String { }); } else { if cfg!(debug_assertions) { - eprintln!("[markdown] unsupported inline html tag: {:#?}", el); + tracing::warn!("unsupported inline html tag: {:#?}", el); } } } Err(err) => { if cfg!(debug_assertions) { - eprintln!("[markdown] error parsing html: {:#?}", err); + tracing::warn!("failed parsing html: {:#?}", err); } text.push_str(&val.value); @@ -355,7 +355,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String { }, _ => { if cfg!(debug_assertions) { - eprintln!("[markdown] unsupported inline node: {:#?}", node); + tracing::warn!("unsupported inline node: {:#?}", node); } } } @@ -437,7 +437,7 @@ fn ast_to_node(value: mdast::Node, style: &TextViewStyle, cx: &mut App) -> eleme Ok(el) => el, Err(err) => { if cfg!(debug_assertions) { - eprintln!("[markdown] error parsing html: {:#?}", err); + tracing::warn!("error parsing html: {:#?}", err); } element::Node::Paragraph(val.value.into()) @@ -494,7 +494,7 @@ fn ast_to_node(value: mdast::Node, style: &TextViewStyle, cx: &mut App) -> eleme } _ => { if cfg!(debug_assertions) { - eprintln!("[markdown] unsupported node: {:#?}", value); + tracing::warn!("unsupported node: {:#?}", value); } element::Node::Unknown }