text_view: Fix nested Blockquote render in Markdown and HTML. (#1053)

<img width="986" height="271" alt="image"
src="https://github.com/user-attachments/assets/1de04629-2bdc-48f2-ae69-45c33482aeff"
/>

- Also fixed short language name in code block, e.g.: `rs` -> `rust`.
This commit is contained in:
Jason Lee 2025-07-14 14:30:40 +08:00 committed by GitHub
parent 3b759cd448
commit 82b87d92ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 103 additions and 99 deletions

View file

@ -16,8 +16,19 @@
<p>This is a text before blockquote.</p>
<blockquote>
This is before paragraph in blockquote.
<p>This is a second blockquote paragraph.</p>
<p>This is after paragraph in blockquote.</p>
<p>This is first blockquote paragraph.</p>
<blockquote>
<p>This is second level</p>
<blockquote>
<p>This is third level</p>
</blockquote>
<ul>
<li>List item in a blockquote.</li>
<li>Second list item</li>
</ul>
</blockquote>
This is after paragraph in blockquote.
</blockquote>
<style type="text/css">
.highlight {

View file

@ -42,6 +42,11 @@ And this is next blockquote
> First level
>
> > Second level
> > Third level
>
> ```rs
> const FOO: &str = "bar";
> ```
## Code block

View file

@ -8,13 +8,13 @@ use std::{
};
use super::LanguageConfig;
use crate::{highlighter::languages, ThemeMode};
use crate::{
highlighter::{languages, Language},
ThemeMode,
};
pub(super) fn init(cx: &mut App) {
let mut register = LanguageRegistry::new();
for language in languages::Language::all() {
register.register(language.name(), &language.config());
}
let register = LanguageRegistry::new();
cx.set_global(register);
}
@ -422,12 +422,19 @@ impl LanguageRegistry {
cx.global_mut::<LanguageRegistry>()
}
/// Create a new language registry with default languages and themes.
pub fn new() -> Self {
Self {
let mut registry = Self {
languages: HashMap::new(),
light_theme: Arc::new(HighlightTheme::default_light()),
dark_theme: Arc::new(HighlightTheme::default_dark()),
};
for language in languages::Language::all() {
registry.register(language.name(), &language.config());
}
registry
}
pub fn register(&mut self, lang: &str, config: &LanguageConfig) {
@ -455,7 +462,8 @@ impl LanguageRegistry {
/// Returns the language configuration for the given language name.
pub fn language(&self, name: &str) -> Option<&LanguageConfig> {
self.languages.get(name)
let language = Language::from_str(name);
self.languages.get(language.name())
}
}
@ -475,4 +483,15 @@ mod tests {
assert_eq!(syntax.style("keyword.repeat"), Some(rgb(0x0433ff).into()));
assert_eq!(syntax.style("foo"), None);
}
#[test]
fn test_registry() {
use super::LanguageRegistry;
let registry = LanguageRegistry::new();
assert!(registry.language("rust").is_some());
assert!(registry.language("rs").is_some());
assert!(registry.language("javascript").is_some());
assert!(registry.language("js").is_some());
}
}

View file

@ -263,7 +263,9 @@ pub enum Node {
level: u8,
children: Paragraph,
},
Blockquote(Paragraph),
Blockquote {
children: Vec<Node>,
},
List {
/// Only contains ListItem, others will be ignored
children: Vec<Node>,
@ -670,14 +672,20 @@ impl Node {
.child(children)
.into_any_element()
}
Node::Blockquote(children) => div()
Node::Blockquote { children } => div()
.w_full()
.mb(mb)
.text_color(cx.theme().muted_foreground)
.border_l_3()
.border_color(cx.theme().secondary_active)
.px_4()
.child(children)
.children({
let children_len = children.len();
children.into_iter().enumerate().map(move |(index, c)| {
let is_last_child = is_root && index == children_len - 1;
c.render(None, false, is_last_child, style, window, cx)
})
})
.into_any_element(),
Node::List { children, ordered } => v_flex()
.mb(mb)
@ -789,8 +797,13 @@ impl Node {
let hashes = "#".repeat(*level as usize);
format!("{} {}", hashes, children.to_markdown())
}
Node::Blockquote(paragraph) => {
let content = paragraph.to_markdown();
Node::Blockquote { children } => {
let content = children
.iter()
.map(|child| child.to_markdown())
.collect::<Vec<_>>()
.join("\n\n");
content
.lines()
.map(|line| format!("> {}", line))

View file

@ -562,10 +562,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Nod
| local_name!("h5")
| local_name!("h6") => {
let mut children = vec![];
if !paragraph.is_empty() {
children.push(element::Node::Paragraph(paragraph.clone()));
paragraph.clear();
}
consume_paragraph(&mut children, paragraph);
let level = name
.local
@ -594,10 +591,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Nod
}
local_name!("img") => {
let mut children = vec![];
if !paragraph.is_empty() {
children.push(element::Node::Paragraph(paragraph.clone()));
paragraph.clear();
}
consume_paragraph(&mut children, paragraph);
let Some(src) = attr_value(attrs, local_name!("src")) else {
if cfg!(debug_assertions) {
@ -629,36 +623,13 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Nod
}
}
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();
if let Some(child_node) = parse_node(child, &mut child_paragraph) {
list_children.push(child_node);
}
}
let list = element::Node::List {
children: list_children,
ordered,
};
if children.len() > 0 {
children.push(list);
Some(element::Node::Root { children })
} else {
Some(list)
}
let children = consume_children_nodes(node, paragraph);
Some(element::Node::List { children, ordered })
}
local_name!("li") => {
let mut children = vec![];
consume_paragraph(&mut children, paragraph);
for child in node.children.borrow().iter() {
let mut child_paragraph = Paragraph::default();
@ -679,10 +650,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Nod
}
}
if !paragraph.is_empty() {
children.push(element::Node::Paragraph(paragraph.clone()));
paragraph.clear();
}
consume_paragraph(&mut children, paragraph);
Some(element::Node::ListItem {
children,
@ -692,10 +660,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Nod
}
local_name!("table") => {
let mut children = vec![];
if !paragraph.is_empty() {
children.push(element::Node::Paragraph(paragraph.clone()));
paragraph.clear();
}
consume_paragraph(&mut children, paragraph);
let mut table = Table::default();
for child in node.children.borrow().iter() {
@ -713,6 +678,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Nod
}
}
}
consume_paragraph(&mut children, paragraph);
let table = element::Node::Table(table);
if children.len() > 0 {
@ -723,22 +689,8 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Nod
}
}
local_name!("blockquote") => {
let mut children = vec![];
if !paragraph.is_empty() {
children.push(element::Node::Paragraph(paragraph.clone()));
paragraph.clear();
}
let mut blockquote = Paragraph::default();
for (i, child) in node.children.borrow().iter().enumerate() {
if i > 0 {
blockquote.push_str("\n");
}
parse_paragraph(&mut blockquote, child);
}
children.push(element::Node::Blockquote(blockquote));
Some(element::Node::Root { children: children })
let children = consume_children_nodes(node, paragraph);
Some(element::Node::Blockquote { children })
}
local_name!("style") | local_name!("script") => None,
_ => {
@ -750,10 +702,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Nod
// Hello <p>Inner text of block element</p> World
// Insert before text as a node -- The "Hello"
if !paragraph.is_empty() {
children.push(element::Node::Paragraph(paragraph.clone()));
paragraph.clear();
}
consume_paragraph(&mut children, paragraph);
// Inner of the block element -- The "Inner text of block element"
for child in node.children.borrow().iter() {
@ -761,11 +710,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Nod
children.push(child_node);
}
}
// if !paragraph.is_empty() {
// children.push(element::Node::Paragraph(paragraph.clone()));
// paragraph.clear();
// }
consume_paragraph(&mut children, paragraph);
if children.is_empty() {
None
@ -787,18 +732,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Nod
}
},
NodeData::Document => {
let mut children = vec![];
for child in node.children.borrow().iter() {
if let Some(child_node) = parse_node(child, paragraph) {
children.push(child_node);
}
}
if !paragraph.is_empty() {
children.push(element::Node::Paragraph(paragraph.clone()));
paragraph.clear();
}
let children = consume_children_nodes(node, paragraph);
Some(element::Node::Root { children })
}
NodeData::Doctype { .. }
@ -807,6 +741,28 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Nod
}
}
fn consume_children_nodes(node: &Node, paragraph: &mut Paragraph) -> Vec<element::Node> {
let mut children = vec![];
consume_paragraph(&mut children, paragraph);
for child in node.children.borrow().iter() {
if let Some(child_node) = parse_node(child, paragraph) {
children.push(child_node);
}
consume_paragraph(&mut children, paragraph);
}
children
}
fn consume_paragraph(children: &mut Vec<element::Node>, paragraph: &mut Paragraph) {
if paragraph.is_empty() {
return;
}
children.push(element::Node::Paragraph(paragraph.clone()));
paragraph.clear();
}
#[cfg(test)]
mod tests {
use gpui::{px, relative};

View file

@ -382,12 +382,12 @@ fn ast_to_node(value: mdast::Node, style: &TextViewStyle, cx: &mut App) -> eleme
element::Node::Paragraph(paragraph)
}
Node::Blockquote(val) => {
let mut paragraph = Paragraph::default();
val.children.iter().for_each(|c| {
parse_paragraph(&mut paragraph, c);
});
element::Node::Blockquote(paragraph)
let children = val
.children
.into_iter()
.map(|c| ast_to_node(c, style, cx))
.collect();
element::Node::Blockquote { children }
}
Node::List(list) => {
let children = list