diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index 560e468439efce..b7ec68951e3711 100644 --- a/crates/markdown_preview/src/markdown_elements.rs +++ b/crates/markdown_preview/src/markdown_elements.rs @@ -155,7 +155,7 @@ pub struct ParsedMarkdownText { /// Where the text is located in the source Markdown document. pub source_range: Range, /// The text content stripped of any formatting symbols. - pub contents: String, + pub contents: SharedString, /// The list of highlights contained in the Markdown document. pub highlights: Vec<(Range, MarkdownHighlight)>, /// The regions of the various ranges in the Markdown document. diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 1b116c50d9820d..d47db4dfe44b34 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -353,7 +353,7 @@ impl<'a> MarkdownParser<'a> { if !text.is_empty() { let parsed_regions = MarkdownParagraphChunk::Text(ParsedMarkdownText { source_range: source_range.clone(), - contents: text.clone(), + contents: text.into(), highlights: highlights.clone(), region_ranges: region_ranges.clone(), regions: regions.clone(), @@ -408,7 +408,7 @@ impl<'a> MarkdownParser<'a> { if !text.is_empty() { markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText { source_range, - contents: text, + contents: text.into(), highlights, regions, region_ranges, @@ -808,15 +808,14 @@ impl<'a> MarkdownParser<'a> { markup5ever_rcdom::NodeData::Document => { self.consume_children(source_range, node, elements); } - markup5ever_rcdom::NodeData::Doctype { .. } => {} markup5ever_rcdom::NodeData::Text { contents } => { elements.push(ParsedMarkdownElement::Paragraph(vec![ MarkdownParagraphChunk::Text(ParsedMarkdownText { source_range, - contents: contents.borrow().to_string(), - highlights: Vec::default(), - region_ranges: Vec::default(), regions: Vec::default(), + region_ranges: Vec::default(), + highlights: Vec::default(), + contents: contents.borrow().to_string().into(), }), ])); } @@ -826,11 +825,64 @@ impl<'a> MarkdownParser<'a> { if let Some(image) = self.extract_image(source_range, attrs) { elements.push(ParsedMarkdownElement::Image(image)); } + } else if local_name!("p") == name.local { + self.parse_paragraph( + source_range, + node, + &mut MarkdownParagraph::new(), + elements, + ); } else { self.consume_children(source_range, node, elements); } } - markup5ever_rcdom::NodeData::ProcessingInstruction { .. } => {} + _ => {} + } + } + + fn parse_paragraph( + &self, + source_range: Range, + node: &Rc, + paragraph: &mut MarkdownParagraph, + elements: &mut Vec, + ) { + match &node.data { + markup5ever_rcdom::NodeData::Text { contents } => { + paragraph.push(MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range, + regions: Vec::default(), + region_ranges: Vec::default(), + highlights: Vec::default(), + contents: contents.borrow().to_string().into(), + })); + } + markup5ever_rcdom::NodeData::Element { name, attrs, .. } => { + if local_name!("img") == name.local { + if let Some(image) = self.extract_image(source_range, attrs) { + paragraph.push(MarkdownParagraphChunk::Image(image)); + } + } else { + self.consume_paragraph(source_range, node, paragraph, elements); + + if !paragraph.is_empty() { + elements.push(ParsedMarkdownElement::Paragraph(std::mem::take(paragraph))); + } + } + } + _ => {} + } + } + + fn consume_paragraph( + &self, + source_range: Range, + node: &Rc, + paragraph: &mut MarkdownParagraph, + elements: &mut Vec, + ) { + for node in node.children.borrow().iter() { + self.parse_paragraph(source_range.clone(), node, paragraph, elements); } } @@ -895,14 +947,14 @@ impl<'a> MarkdownParser<'a> { if let Some(width) = Self::attr_value(attrs, local_name!("width")) .or_else(|| styles.get("width").cloned()) - .and_then(|width| Self::parse_length(&width)) + .and_then(|width| Self::parse_html_element_dimension(&width)) { image.set_width(width); } if let Some(height) = Self::attr_value(attrs, local_name!("height")) .or_else(|| styles.get("height").cloned()) - .and_then(|height| Self::parse_length(&height)) + .and_then(|height| Self::parse_html_element_dimension(&height)) { image.set_height(height); } @@ -910,8 +962,7 @@ impl<'a> MarkdownParser<'a> { Some(image) } - /// Parses the width/height attribute value of an html element (e.g. img element) - fn parse_length(value: &str) -> Option { + fn parse_html_element_dimension(value: &str) -> Option { if value.ends_with("%") { value .trim_end_matches("%") @@ -1010,7 +1061,7 @@ mod tests { ParsedMarkdownElement::Paragraph(vec![MarkdownParagraphChunk::Text( ParsedMarkdownText { source_range: 0..35, - contents: "Some bostrikethroughld text".to_string(), + contents: "Some bostrikethroughld text".into(), highlights: Vec::new(), region_ranges: Vec::new(), regions: Vec::new(), @@ -1184,7 +1235,7 @@ mod tests { }), MarkdownParagraphChunk::Text(ParsedMarkdownText { source_range: 0..81, - contents: " Lorem Ipsum ".to_string(), + contents: " Lorem Ipsum ".into(), highlights: Vec::new(), region_ranges: Vec::new(), regions: Vec::new(), @@ -1203,90 +1254,130 @@ mod tests { } #[test] - fn test_parse_length() { + fn test_parse_html_element_dimension() { // Test percentage values assert_eq!( - MarkdownParser::parse_length("50%"), + MarkdownParser::parse_html_element_dimension("50%"), Some(DefiniteLength::Fraction(0.5)) ); assert_eq!( - MarkdownParser::parse_length("100%"), + MarkdownParser::parse_html_element_dimension("100%"), Some(DefiniteLength::Fraction(1.0)) ); assert_eq!( - MarkdownParser::parse_length("25%"), + MarkdownParser::parse_html_element_dimension("25%"), Some(DefiniteLength::Fraction(0.25)) ); assert_eq!( - MarkdownParser::parse_length("0%"), + MarkdownParser::parse_html_element_dimension("0%"), Some(DefiniteLength::Fraction(0.0)) ); // Test pixel values assert_eq!( - MarkdownParser::parse_length("100px"), + MarkdownParser::parse_html_element_dimension("100px"), Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0)))) ); assert_eq!( - MarkdownParser::parse_length("50px"), + MarkdownParser::parse_html_element_dimension("50px"), Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(50.0)))) ); assert_eq!( - MarkdownParser::parse_length("0px"), + MarkdownParser::parse_html_element_dimension("0px"), Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(0.0)))) ); // Test values without units (should be treated as pixels) assert_eq!( - MarkdownParser::parse_length("100"), + MarkdownParser::parse_html_element_dimension("100"), Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0)))) ); assert_eq!( - MarkdownParser::parse_length("42"), + MarkdownParser::parse_html_element_dimension("42"), Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0)))) ); // Test invalid values - assert_eq!(MarkdownParser::parse_length("invalid"), None); - assert_eq!(MarkdownParser::parse_length("px"), None); - assert_eq!(MarkdownParser::parse_length("%"), None); - assert_eq!(MarkdownParser::parse_length(""), None); - assert_eq!(MarkdownParser::parse_length("abc%"), None); - assert_eq!(MarkdownParser::parse_length("abcpx"), None); + assert_eq!( + MarkdownParser::parse_html_element_dimension("invalid"), + None + ); + assert_eq!(MarkdownParser::parse_html_element_dimension("px"), None); + assert_eq!(MarkdownParser::parse_html_element_dimension("%"), None); + assert_eq!(MarkdownParser::parse_html_element_dimension(""), None); + assert_eq!(MarkdownParser::parse_html_element_dimension("abc%"), None); + assert_eq!(MarkdownParser::parse_html_element_dimension("abcpx"), None); // Test decimal values assert_eq!( - MarkdownParser::parse_length("50.5%"), + MarkdownParser::parse_html_element_dimension("50.5%"), Some(DefiniteLength::Fraction(0.505)) ); assert_eq!( - MarkdownParser::parse_length("100.25px"), + MarkdownParser::parse_html_element_dimension("100.25px"), Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.25)))) ); assert_eq!( - MarkdownParser::parse_length("42.0"), + MarkdownParser::parse_html_element_dimension("42.0"), Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0)))) ); } + #[gpui::test] + async fn test_inline_html_image_tag() { + let parsed = + parse("

Some text some more text

") + .await; + + assert_eq!( + ParsedMarkdown { + children: vec![ParsedMarkdownElement::Paragraph(vec![ + MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: 0..71, + contents: "Some text".into(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default() + }), + MarkdownParagraphChunk::Image(Image { + source_range: 0..71, + link: Link::Web { + url: "http://example.com/foo.png".to_string(), + }, + alt_text: None, + height: None, + width: None, + }), + MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: 0..71, + contents: " some more text".into(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default() + }), + ])] + }, + parsed + ); + } + #[gpui::test] async fn test_html_image_tag() { let parsed = parse("").await; - let ParsedMarkdownElement::Image(image) = &parsed.children[0] else { - panic!("Expected a image element"); - }; assert_eq!( - image.clone(), - Image { - source_range: 0..40, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: None, - height: None, - width: None, + ParsedMarkdown { + children: vec![ParsedMarkdownElement::Image(Image { + source_range: 0..40, + link: Link::Web { + url: "http://example.com/foo.png".to_string(), + }, + alt_text: None, + height: None, + width: None, + })] }, + parsed ); } @@ -1294,20 +1385,19 @@ mod tests { async fn test_html_image_tag_with_alt_text() { let parsed = parse("\"Foo\"").await; - let ParsedMarkdownElement::Image(image) = &parsed.children[0] else { - panic!("Expected a image element"); - }; assert_eq!( - image.clone(), - Image { - source_range: 0..50, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: Some("Foo".into()), - height: None, - width: None, + ParsedMarkdown { + children: vec![ParsedMarkdownElement::Image(Image { + source_range: 0..50, + link: Link::Web { + url: "http://example.com/foo.png".to_string(), + }, + alt_text: Some("Foo".into()), + height: None, + width: None, + })] }, + parsed ); } @@ -1316,20 +1406,19 @@ mod tests { let parsed = parse("").await; - let ParsedMarkdownElement::Image(image) = &parsed.children[0] else { - panic!("Expected a image element"); - }; assert_eq!( - image.clone(), - Image { - source_range: 0..65, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: None, - height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))), - width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))), + ParsedMarkdown { + children: vec![ParsedMarkdownElement::Image(Image { + source_range: 0..65, + link: Link::Web { + url: "http://example.com/foo.png".to_string(), + }, + alt_text: None, + height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))), + width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))), + })] }, + parsed ); } @@ -1340,20 +1429,19 @@ mod tests { ) .await; - let ParsedMarkdownElement::Image(image) = &parsed.children[0] else { - panic!("Expected a image element"); - }; assert_eq!( - image.clone(), - Image { - source_range: 0..75, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: None, - height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))), - width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))), + ParsedMarkdown { + children: vec![ParsedMarkdownElement::Image(Image { + source_range: 0..75, + link: Link::Web { + url: "http://example.com/foo.png".to_string(), + }, + alt_text: None, + height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))), + width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))), + })] }, + parsed ); } @@ -1804,7 +1892,7 @@ fn main() { region_ranges: Vec::new(), regions: Vec::new(), source_range, - contents: contents.to_string(), + contents: contents.to_string().into(), })] } diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index b07b4686a4eaeb..1e679b42cbf8a0 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -624,15 +624,13 @@ fn render_markdown_code_block( } fn render_markdown_paragraph(parsed: &MarkdownParagraph, cx: &mut RenderContext) -> AnyElement { - cx.with_common_p(div()) + cx.with_common_p(h_flex().flex_wrap()) .children(render_markdown_text(parsed, cx)) - .flex() - .flex_col() .into_any_element() } fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) -> Vec { - let mut any_element = vec![]; + let mut any_element = Vec::with_capacity(parsed_new.len()); // these values are cloned in-order satisfy borrow checker let syntax_theme = cx.syntax_theme.clone(); let workspace_clone = cx.workspace.clone();