text_view: Fix list item to merge inline paragraphs, and fix paragraphs margin. (#1036)

## Before

<img width="1712" alt="image"
src="https://github.com/user-attachments/assets/579385e6-a206-45e4-80a1-e1e2cdd6b164"
/>

## After

<img width="1712" alt="image"
src="https://github.com/user-attachments/assets/2f551d22-5662-4c3c-a0e5-5ce9f26e1e66"
/>
This commit is contained in:
Jason Lee 2025-07-03 18:02:08 +08:00 committed by GitHub
parent 9eb7e10a1d
commit 4075447c63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 91 additions and 57 deletions

View file

@ -201,6 +201,25 @@ impl Paragraph {
Self::Image { .. } => 1, 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)] #[derive(Debug, Clone, PartialEq)]
@ -262,15 +281,10 @@ pub enum Node {
html: bool, html: bool,
}, },
Divider, Divider,
Ignore,
Unknown, Unknown,
} }
impl Node { impl Node {
pub(super) fn is_ignore(&self) -> bool {
matches!(self, Self::Ignore)
}
pub(super) fn is_list_item(&self) -> bool { pub(super) fn is_list_item(&self) -> bool {
matches!(self, Self::ListItem { .. }) matches!(self, Self::ListItem { .. })
} }
@ -283,11 +297,7 @@ impl Node {
pub(super) fn compact(&self) -> Node { pub(super) fn compact(&self) -> Node {
match self { match self {
Self::Root { children } => { Self::Root { children } => {
let children = children let children = children.iter().map(|c| c.compact()).collect::<Vec<_>>();
.iter()
.map(|c| c.compact())
.filter(|c| !c.is_ignore())
.collect::<Vec<_>>();
if children.len() == 1 { if children.len() == 1 {
children.first().unwrap().compact() children.first().unwrap().compact()
} else { } else {
@ -310,7 +320,13 @@ impl RenderOnce for Paragraph {
for text_node in children.into_iter() { for text_node in children.into_iter() {
let text_len = text_node.text.len(); 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![]; let mut node_highlights = vec![];
for (range, style) in text_node.marks { for (range, style) in text_node.marks {
@ -421,6 +437,7 @@ impl Node {
ordered: state.ordered, ordered: state.ordered,
todo: checked.is_some(), todo: checked.is_some(),
}), }),
false,
true, true,
text_view_style, text_view_style,
window, window,
@ -485,6 +502,7 @@ impl Node {
todo: checked.is_some(), todo: checked.is_some(),
}), }),
true, true,
true,
text_view_style, text_view_style,
window, window,
cx, cx,
@ -607,6 +625,7 @@ impl Node {
pub(crate) fn render( pub(crate) fn render(
self, self,
list_state: Option<ListState>, list_state: Option<ListState>,
is_root: bool,
is_last_child: bool, is_last_child: bool,
style: &TextViewStyle, style: &TextViewStyle,
window: &mut Window, window: &mut Window,
@ -624,8 +643,8 @@ impl Node {
.children({ .children({
let children_len = children.len(); let children_len = children.len();
children.into_iter().enumerate().map(move |(index, c)| { children.into_iter().enumerate().map(move |(index, c)| {
let is_last_child = index == children_len - 1; let is_last_child = is_root && index == children_len - 1;
c.render(None, is_last_child, style, window, cx) c.render(None, false, is_last_child, style, window, cx)
}) })
}) })
.into_any_element(), .into_any_element(),
@ -699,10 +718,9 @@ impl Node {
.mb(mb) .mb(mb)
.into_any_element(), .into_any_element(),
Node::Break { .. } => div().into_any_element(), Node::Break { .. } => div().into_any_element(),
Node::Ignore => div().into_any_element(),
_ => { _ => {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
eprintln!("Unknown implementation: {:?}", self); tracing::warn!("unknown implementation: {:?}", self);
} }
div().into_any_element() div().into_any_element()
@ -869,7 +887,6 @@ impl Node {
} }
} }
Node::Divider => "---".to_string(), Node::Divider => "---".to_string(),
Node::Ignore => "".to_string(),
Node::Unknown => "".to_string(), Node::Unknown => "".to_string(),
} }
.trim() .trim()

View file

@ -75,7 +75,8 @@ pub(super) fn parse_html(source: &str) -> Result<element::Node, SharedString> {
let mut paragraph = Paragraph::default(); let mut paragraph = Paragraph::default();
// NOTE: The outer paragraph is not used. // 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(); let node = node.compact();
Ok(node) Ok(node)
@ -173,7 +174,7 @@ impl Element for HtmlElement {
let mut el = div() let mut el = div()
.map(|this| match root { .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( Err(err) => this.child(
v_flex() v_flex()
.gap_1() .gap_1()
@ -492,7 +493,7 @@ fn parse_paragraph(
local_name!("img") => { local_name!("img") => {
let Some(src) = attr_value(attrs, local_name!("src")) else { let Some(src) = attr_value(attrs, local_name!("src")) else {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
eprintln!("[html] Image node missing src attribute"); tracing::warn!("Image node missing src attribute");
} }
return (text, marks); return (text, marks);
}; };
@ -509,7 +510,6 @@ fn parse_paragraph(
title: title.map(Into::into), title: title.map(Into::into),
}); });
} }
_ => { _ => {
// All unknown tags to as text // All unknown tags to as text
let mut child_paragraph = Paragraph::default(); let mut child_paragraph = Paragraph::default();
@ -539,7 +539,7 @@ fn parse_paragraph(
(text, marks) (text, marks)
} }
fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node { fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Node> {
match node.data { match node.data {
NodeData::Text { ref contents } => { NodeData::Text { ref contents } => {
let text = contents.borrow().to_string(); let text = contents.borrow().to_string();
@ -547,14 +547,14 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
paragraph.push_str(&text); paragraph.push_str(&text);
} }
element::Node::Ignore None
} }
NodeData::Element { NodeData::Element {
ref name, ref name,
ref attrs, ref attrs,
.. ..
} => match name.local { } => 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!("h1")
| local_name!("h2") | local_name!("h2")
| local_name!("h3") | local_name!("h3")
@ -587,9 +587,9 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
if children.len() > 0 { if children.len() > 0 {
children.push(heading); children.push(heading);
element::Node::Root { children } Some(element::Node::Root { children })
} else { } else {
heading Some(heading)
} }
} }
local_name!("img") => { local_name!("img") => {
@ -601,9 +601,9 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
let Some(src) = attr_value(attrs, local_name!("src")) else { let Some(src) = attr_value(attrs, local_name!("src")) else {
if cfg!(debug_assertions) { 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")); let alt = attr_value(&attrs, local_name!("alt"));
@ -623,9 +623,9 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
if children.len() > 0 { if children.len() > 0 {
children.push(element::Node::Paragraph(image)); children.push(element::Node::Paragraph(image));
element::Node::Root { children } Some(element::Node::Root { children })
} else { } else {
element::Node::Paragraph(image) Some(element::Node::Paragraph(image))
} }
} }
local_name!("ul") | local_name!("ol") => { local_name!("ul") | local_name!("ol") => {
@ -641,7 +641,9 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
for child in node.children.borrow().iter() { for child in node.children.borrow().iter() {
let mut child_paragraph = Paragraph::default(); 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 { let list = element::Node::List {
@ -650,19 +652,30 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
}; };
if children.len() > 0 { if children.len() > 0 {
children.push(list); children.push(list);
element::Node::Root { children } Some(element::Node::Root { children })
} else { } else {
list Some(list)
} }
} }
local_name!("li") => { local_name!("li") => {
let mut children = vec![]; let mut children = vec![];
for child in node.children.borrow().iter() { for child in node.children.borrow().iter() {
let mut child_paragraph = Paragraph::default(); 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 { if child_paragraph.text_len() > 0 {
children.push(element::Node::Paragraph(child_paragraph.clone())); // If last child is paragraph, merge child
child_paragraph.clear(); 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<Node>, paragraph: &mut Paragraph) -> element::Node {
paragraph.clear(); paragraph.clear();
} }
element::Node::ListItem { Some(element::Node::ListItem {
children, children,
spread: false, spread: false,
checked: None, checked: None,
} })
} }
local_name!("table") => { local_name!("table") => {
let mut children = vec![]; let mut children = vec![];
@ -704,9 +717,9 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
let table = element::Node::Table(table); let table = element::Node::Table(table);
if children.len() > 0 { if children.len() > 0 {
children.push(table); children.push(table);
element::Node::Root { children } Some(element::Node::Root { children })
} else { } else {
table Some(table)
} }
} }
local_name!("blockquote") => { local_name!("blockquote") => {
@ -725,9 +738,9 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
} }
children.push(element::Node::Blockquote(blockquote)); 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()) { if BLOCK_ELEMENTS.contains(&name.local.trim()) {
let mut children: Vec<element::Node> = vec![]; let mut children: Vec<element::Node> = vec![];
@ -744,7 +757,9 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
// Inner of the block element -- The "Inner text of block element" // Inner of the block element -- The "Inner text of block element"
for child in node.children.borrow().iter() { 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() { // if !paragraph.is_empty() {
@ -753,9 +768,9 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
// } // }
if children.is_empty() { if children.is_empty() {
element::Node::Ignore None
} else { } else {
element::Node::Root { children } Some(element::Node::Root { children })
} }
} else { } else {
// Others to as Inline // Others to as Inline
@ -764,9 +779,9 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
if paragraph.is_image() { if paragraph.is_image() {
let image = paragraph.clone(); let image = paragraph.clone();
paragraph.clear(); paragraph.clear();
element::Node::Paragraph(image) Some(element::Node::Paragraph(image))
} else { } else {
element::Node::Ignore None
} }
} }
} }
@ -774,7 +789,9 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
NodeData::Document => { NodeData::Document => {
let mut children = vec![]; let mut children = vec![];
for child in node.children.borrow().iter() { 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() { if !paragraph.is_empty() {
@ -782,11 +799,11 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
paragraph.clear(); paragraph.clear();
} }
element::Node::Root { children } Some(element::Node::Root { children })
} }
NodeData::Doctype { .. } => element::Node::Ignore, NodeData::Doctype { .. }
NodeData::Comment { .. } => element::Node::Ignore, | NodeData::Comment { .. }
NodeData::ProcessingInstruction { .. } => element::Node::Ignore, | NodeData::ProcessingInstruction { .. } => None,
} }
} }

View file

@ -124,7 +124,7 @@ impl Element for MarkdownElement {
let mut el = div() let mut el = div()
.map(|this| match root { .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( Err(err) => this.child(
v_flex() v_flex()
.gap_1() .gap_1()
@ -341,13 +341,13 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
}); });
} else { } else {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
eprintln!("[markdown] unsupported inline html tag: {:#?}", el); tracing::warn!("unsupported inline html tag: {:#?}", el);
} }
} }
} }
Err(err) => { Err(err) => {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
eprintln!("[markdown] error parsing html: {:#?}", err); tracing::warn!("failed parsing html: {:#?}", err);
} }
text.push_str(&val.value); text.push_str(&val.value);
@ -355,7 +355,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
}, },
_ => { _ => {
if cfg!(debug_assertions) { 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, Ok(el) => el,
Err(err) => { Err(err) => {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
eprintln!("[markdown] error parsing html: {:#?}", err); tracing::warn!("error parsing html: {:#?}", err);
} }
element::Node::Paragraph(val.value.into()) 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) { if cfg!(debug_assertions) {
eprintln!("[markdown] unsupported node: {:#?}", value); tracing::warn!("unsupported node: {:#?}", value);
} }
element::Node::Unknown element::Node::Unknown
} }