@@ -61,6 +61,17 @@ struct MarkdownParser<'a> {
language_registry: Option<Arc<LanguageRegistry>>,
}
+#[derive(Debug)]
+struct ParseHtmlNodeContext {
+ list_item_depth: u16,
+}
+
+impl Default for ParseHtmlNodeContext {
+ fn default() -> Self {
+ Self { list_item_depth: 1 }
+ }
+}
+
struct MarkdownListItem {
content: Vec<ParsedMarkdownElement>,
item_type: ParsedMarkdownListItemType,
@@ -646,6 +657,7 @@ impl<'a> MarkdownParser<'a> {
content: list_item.content,
depth,
item_type: list_item.item_type,
+ nested: false,
});
if let Some(index) = insertion_indices.get(&depth) {
@@ -828,7 +840,12 @@ impl<'a> MarkdownParser<'a> {
.read_from(&mut cursor)
&& let Some((start, end)) = html_source_range_start.zip(html_source_range_end)
{
- self.parse_html_node(start..end, &dom.document, &mut elements);
+ self.parse_html_node(
+ start..end,
+ &dom.document,
+ &mut elements,
+ &ParseHtmlNodeContext::default(),
+ );
}
elements
@@ -839,10 +856,11 @@ impl<'a> MarkdownParser<'a> {
source_range: Range<usize>,
node: &Rc<markup5ever_rcdom::Node>,
elements: &mut Vec<ParsedMarkdownElement>,
+ context: &ParseHtmlNodeContext,
) {
match &node.data {
markup5ever_rcdom::NodeData::Document => {
- self.consume_children(source_range, node, elements);
+ self.consume_children(source_range, node, elements, context);
}
markup5ever_rcdom::NodeData::Text { contents } => {
elements.push(ParsedMarkdownElement::Paragraph(vec![
@@ -895,6 +913,15 @@ impl<'a> MarkdownParser<'a> {
contents: paragraph,
}));
}
+ } else if local_name!("ul") == name.local || local_name!("ol") == name.local {
+ if let Some(list_items) = self.extract_html_list(
+ node,
+ local_name!("ol") == name.local,
+ context.list_item_depth,
+ source_range,
+ ) {
+ elements.extend(list_items);
+ }
} else if local_name!("blockquote") == name.local {
if let Some(blockquote) = self.extract_html_blockquote(node, source_range) {
elements.push(ParsedMarkdownElement::BlockQuote(blockquote));
@@ -904,7 +931,7 @@ impl<'a> MarkdownParser<'a> {
elements.push(ParsedMarkdownElement::Table(table));
}
} else {
- self.consume_children(source_range, node, elements);
+ self.consume_children(source_range, node, elements, context);
}
}
_ => {}
@@ -1036,9 +1063,10 @@ impl<'a> MarkdownParser<'a> {
source_range: Range<usize>,
node: &Rc<markup5ever_rcdom::Node>,
elements: &mut Vec<ParsedMarkdownElement>,
+ context: &ParseHtmlNodeContext,
) {
for node in node.children.borrow().iter() {
- self.parse_html_node(source_range.clone(), node, elements);
+ self.parse_html_node(source_range.clone(), node, elements, context);
}
}
@@ -1107,6 +1135,57 @@ impl<'a> MarkdownParser<'a> {
Some(image)
}
+ fn extract_html_list(
+ &self,
+ node: &Rc<markup5ever_rcdom::Node>,
+ ordered: bool,
+ depth: u16,
+ source_range: Range<usize>,
+ ) -> Option<Vec<ParsedMarkdownElement>> {
+ let mut list_items = Vec::with_capacity(node.children.borrow().len());
+
+ for (index, node) in node.children.borrow().iter().enumerate() {
+ match &node.data {
+ markup5ever_rcdom::NodeData::Element { name, .. } => {
+ if local_name!("li") != name.local {
+ continue;
+ }
+
+ let mut content = Vec::new();
+ self.consume_children(
+ source_range.clone(),
+ node,
+ &mut content,
+ &ParseHtmlNodeContext {
+ list_item_depth: depth + 1,
+ },
+ );
+
+ if !content.is_empty() {
+ list_items.push(ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
+ depth,
+ source_range: source_range.clone(),
+ item_type: if ordered {
+ ParsedMarkdownListItemType::Ordered(index as u64 + 1)
+ } else {
+ ParsedMarkdownListItemType::Unordered
+ },
+ content,
+ nested: true,
+ }));
+ }
+ }
+ _ => {}
+ }
+ }
+
+ if list_items.is_empty() {
+ None
+ } else {
+ Some(list_items)
+ }
+ }
+
fn parse_html_element_dimension(value: &str) -> Option<DefiniteLength> {
if value.ends_with("%") {
value
@@ -1129,7 +1208,12 @@ impl<'a> MarkdownParser<'a> {
source_range: Range<usize>,
) -> Option<ParsedMarkdownBlockQuote> {
let mut children = Vec::new();
- self.consume_children(source_range.clone(), node, &mut children);
+ self.consume_children(
+ source_range.clone(),
+ node,
+ &mut children,
+ &ParseHtmlNodeContext::default(),
+ );
if children.is_empty() {
None
@@ -1552,6 +1636,168 @@ mod tests {
);
}
+ #[gpui::test]
+ async fn test_html_unordered_list() {
+ let parsed = parse(
+ "<ul>
+ <li>Item 1</li>
+ <li>Item 2</li>
+ </ul>",
+ )
+ .await;
+
+ assert_eq!(
+ ParsedMarkdown {
+ children: vec![
+ nested_list_item(
+ 0..82,
+ 1,
+ ParsedMarkdownListItemType::Unordered,
+ vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..82))]
+ ),
+ nested_list_item(
+ 0..82,
+ 1,
+ ParsedMarkdownListItemType::Unordered,
+ vec![ParsedMarkdownElement::Paragraph(text("Item 2", 0..82))]
+ ),
+ ]
+ },
+ parsed
+ );
+ }
+
+ #[gpui::test]
+ async fn test_html_ordered_list() {
+ let parsed = parse(
+ "<ol>
+ <li>Item 1</li>
+ <li>Item 2</li>
+ </ol>",
+ )
+ .await;
+
+ assert_eq!(
+ ParsedMarkdown {
+ children: vec![
+ nested_list_item(
+ 0..82,
+ 1,
+ ParsedMarkdownListItemType::Ordered(1),
+ vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..82))]
+ ),
+ nested_list_item(
+ 0..82,
+ 1,
+ ParsedMarkdownListItemType::Ordered(2),
+ vec![ParsedMarkdownElement::Paragraph(text("Item 2", 0..82))]
+ ),
+ ]
+ },
+ parsed
+ );
+ }
+
+ #[gpui::test]
+ async fn test_html_nested_ordered_list() {
+ let parsed = parse(
+ "<ol>
+ <li>Item 1</li>
+ <li>Item 2
+ <ol>
+ <li>Sub-Item 1</li>
+ <li>Sub-Item 2</li>
+ </ol>
+ </li>
+ </ol>",
+ )
+ .await;
+
+ assert_eq!(
+ ParsedMarkdown {
+ children: vec![
+ nested_list_item(
+ 0..216,
+ 1,
+ ParsedMarkdownListItemType::Ordered(1),
+ vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..216))]
+ ),
+ nested_list_item(
+ 0..216,
+ 1,
+ ParsedMarkdownListItemType::Ordered(2),
+ vec![
+ ParsedMarkdownElement::Paragraph(text("Item 2", 0..216)),
+ nested_list_item(
+ 0..216,
+ 2,
+ ParsedMarkdownListItemType::Ordered(1),
+ vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 1", 0..216))]
+ ),
+ nested_list_item(
+ 0..216,
+ 2,
+ ParsedMarkdownListItemType::Ordered(2),
+ vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 2", 0..216))]
+ ),
+ ]
+ ),
+ ]
+ },
+ parsed
+ );
+ }
+
+ #[gpui::test]
+ async fn test_html_nested_unordered_list() {
+ let parsed = parse(
+ "<ul>
+ <li>Item 1</li>
+ <li>Item 2
+ <ul>
+ <li>Sub-Item 1</li>
+ <li>Sub-Item 2</li>
+ </ul>
+ </li>
+ </ul>",
+ )
+ .await;
+
+ assert_eq!(
+ ParsedMarkdown {
+ children: vec![
+ nested_list_item(
+ 0..216,
+ 1,
+ ParsedMarkdownListItemType::Unordered,
+ vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..216))]
+ ),
+ nested_list_item(
+ 0..216,
+ 1,
+ ParsedMarkdownListItemType::Unordered,
+ vec![
+ ParsedMarkdownElement::Paragraph(text("Item 2", 0..216)),
+ nested_list_item(
+ 0..216,
+ 2,
+ ParsedMarkdownListItemType::Unordered,
+ vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 1", 0..216))]
+ ),
+ nested_list_item(
+ 0..216,
+ 2,
+ ParsedMarkdownListItemType::Unordered,
+ vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 2", 0..216))]
+ ),
+ ]
+ ),
+ ]
+ },
+ parsed
+ );
+ }
+
#[gpui::test]
async fn test_inline_html_image_tag() {
let parsed =
@@ -1594,7 +1840,7 @@ mod tests {
async fn test_html_block_quote() {
let parsed = parse(
"<blockquote>
- <p>some description</p>
+ <p>some description</p>
</blockquote>",
)
.await;
@@ -1604,9 +1850,9 @@ mod tests {
children: vec![block_quote(
vec![ParsedMarkdownElement::Paragraph(text(
"some description",
- 0..76
+ 0..78
))],
- 0..76,
+ 0..78,
)]
},
parsed
@@ -1617,10 +1863,10 @@ mod tests {
async fn test_html_nested_block_quote() {
let parsed = parse(
"<blockquote>
- <p>some description</p>
- <blockquote>
+ <p>some description</p>
+ <blockquote>
<p>second description</p>
- </blockquote>
+ </blockquote>
</blockquote>",
)
.await;
@@ -1629,16 +1875,16 @@ mod tests {
ParsedMarkdown {
children: vec![block_quote(
vec![
- ParsedMarkdownElement::Paragraph(text("some description", 0..173)),
+ ParsedMarkdownElement::Paragraph(text("some description", 0..179)),
block_quote(
vec![ParsedMarkdownElement::Paragraph(text(
"second description",
- 0..173
+ 0..179
))],
- 0..173,
+ 0..179,
)
],
- 0..173,
+ 0..179,
)]
},
parsed
@@ -2542,6 +2788,22 @@ fn main() {
item_type,
depth,
content,
+ nested: false,
+ })
+ }
+
+ fn nested_list_item(
+ source_range: Range<usize>,
+ depth: u16,
+ item_type: ParsedMarkdownListItemType,
+ content: Vec<ParsedMarkdownElement>,
+ ) -> ParsedMarkdownElement {
+ ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
+ source_range,
+ item_type,
+ depth,
+ content,
+ nested: true,
})
}
@@ -6,10 +6,10 @@ use crate::markdown_elements::{
};
use fs::normalize_path;
use gpui::{
- AbsoluteLength, AnyElement, App, AppContext as _, ClipboardItem, Context, DefiniteLength, Div,
- Element, ElementId, Entity, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement,
- Keystroke, Modifiers, ParentElement, Render, Resource, SharedString, Styled, StyledText,
- TextStyle, WeakEntity, Window, div, img, rems,
+ AbsoluteLength, AnyElement, App, AppContext as _, ClipboardItem, Context, Div, Element,
+ ElementId, Entity, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke,
+ Modifiers, ParentElement, Render, Resource, SharedString, Styled, StyledText, TextStyle,
+ WeakEntity, Window, div, img, rems,
};
use settings::Settings;
use std::{
@@ -234,8 +234,6 @@ fn render_markdown_list_item(
) -> AnyElement {
use ParsedMarkdownListItemType::*;
- let padding = cx.scaled_rems((parsed.depth - 1) as f32);
-
let bullet = match &parsed.item_type {
Ordered(order) => format!("{}.", order).into_any_element(),
Unordered => "•".into_any_element(),
@@ -294,13 +292,16 @@ fn render_markdown_list_item(
.collect();
let item = h_flex()
- .pl(DefiniteLength::Absolute(AbsoluteLength::Rems(padding)))
+ .when(!parsed.nested, |this| {
+ this.pl(cx.scaled_rems(parsed.depth.saturating_sub(1) as f32))
+ })
+ .when(parsed.nested && parsed.depth > 1, |this| this.ml_neg_1p5())
.items_start()
.children(vec![
bullet,
v_flex()
.children(contents)
- .gap(cx.scaled_rems(1.0))
+ .when(!parsed.nested, |this| this.gap(cx.scaled_rems(1.0)))
.pr(cx.scaled_rems(1.0))
.w_full(),
]);