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,
}
}
/// 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::<Vec<_>>();
let children = children.iter().map(|c| c.compact()).collect::<Vec<_>>();
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<ListState>,
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()

View file

@ -75,7 +75,8 @@ pub(super) fn parse_html(source: &str) -> Result<element::Node, SharedString> {
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<Node>, paragraph: &mut Paragraph) -> element::Node {
fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Node> {
match node.data {
NodeData::Text { ref contents } => {
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);
}
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<Node>, 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<Node>, 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<Node>, 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<Node>, 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<Node>, 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<Node>, 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<Node>, 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<Node>, 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<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"
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<Node>, 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<Node>, 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<Node>, 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<Node>, 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,
}
}

View file

@ -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
}