1use crate::{
2 markdown_elements::*,
3 markdown_minifier::{Minifier, MinifierOptions},
4};
5use async_recursion::async_recursion;
6use collections::FxHashMap;
7use gpui::{DefiniteLength, FontWeight, px, relative};
8use html5ever::{ParseOpts, local_name, parse_document, tendril::TendrilSink};
9use language::LanguageRegistry;
10use markdown::parser::PARSE_OPTIONS;
11use markup5ever_rcdom::RcDom;
12use pulldown_cmark::{Alignment, Event, Parser, Tag, TagEnd};
13use stacksafe::stacksafe;
14use std::{
15 cell::RefCell, collections::HashMap, mem, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec,
16};
17use ui::SharedString;
18
19pub async fn parse_markdown(
20 markdown_input: &str,
21 file_location_directory: Option<PathBuf>,
22 language_registry: Option<Arc<LanguageRegistry>>,
23) -> ParsedMarkdown {
24 let parser = Parser::new_ext(markdown_input, PARSE_OPTIONS);
25 let parser = MarkdownParser::new(
26 parser.into_offset_iter().collect(),
27 file_location_directory,
28 language_registry,
29 );
30 let renderer = parser.parse_document().await;
31 ParsedMarkdown {
32 children: renderer.parsed,
33 }
34}
35
36fn cleanup_html(source: &str) -> Vec<u8> {
37 let mut writer = std::io::Cursor::new(Vec::new());
38 let mut reader = std::io::Cursor::new(source);
39 let mut minify = Minifier::new(
40 &mut writer,
41 MinifierOptions {
42 omit_doctype: true,
43 collapse_whitespace: true,
44 ..Default::default()
45 },
46 );
47 if let Ok(()) = minify.minify(&mut reader) {
48 writer.into_inner()
49 } else {
50 source.bytes().collect()
51 }
52}
53
54struct MarkdownParser<'a> {
55 tokens: Vec<(Event<'a>, Range<usize>)>,
56 /// The current index in the tokens array
57 cursor: usize,
58 /// The blocks that we have successfully parsed so far
59 parsed: Vec<ParsedMarkdownElement>,
60 file_location_directory: Option<PathBuf>,
61 language_registry: Option<Arc<LanguageRegistry>>,
62}
63
64#[derive(Debug)]
65struct ParseHtmlNodeContext {
66 list_item_depth: u16,
67}
68
69impl Default for ParseHtmlNodeContext {
70 fn default() -> Self {
71 Self { list_item_depth: 1 }
72 }
73}
74
75struct MarkdownListItem {
76 content: Vec<ParsedMarkdownElement>,
77 item_type: ParsedMarkdownListItemType,
78}
79
80impl Default for MarkdownListItem {
81 fn default() -> Self {
82 Self {
83 content: Vec::new(),
84 item_type: ParsedMarkdownListItemType::Unordered,
85 }
86 }
87}
88
89impl<'a> MarkdownParser<'a> {
90 fn new(
91 tokens: Vec<(Event<'a>, Range<usize>)>,
92 file_location_directory: Option<PathBuf>,
93 language_registry: Option<Arc<LanguageRegistry>>,
94 ) -> Self {
95 Self {
96 tokens,
97 file_location_directory,
98 language_registry,
99 cursor: 0,
100 parsed: vec![],
101 }
102 }
103
104 fn eof(&self) -> bool {
105 if self.tokens.is_empty() {
106 return true;
107 }
108 self.cursor >= self.tokens.len() - 1
109 }
110
111 fn peek(&self, steps: usize) -> Option<&(Event<'_>, Range<usize>)> {
112 if self.eof() || (steps + self.cursor) >= self.tokens.len() {
113 return self.tokens.last();
114 }
115 self.tokens.get(self.cursor + steps)
116 }
117
118 fn previous(&self) -> Option<&(Event<'_>, Range<usize>)> {
119 if self.cursor == 0 || self.cursor > self.tokens.len() {
120 return None;
121 }
122 self.tokens.get(self.cursor - 1)
123 }
124
125 fn current(&self) -> Option<&(Event<'_>, Range<usize>)> {
126 self.peek(0)
127 }
128
129 fn current_event(&self) -> Option<&Event<'_>> {
130 self.current().map(|(event, _)| event)
131 }
132
133 fn is_text_like(event: &Event) -> bool {
134 match event {
135 Event::Text(_)
136 // Represent an inline code block
137 | Event::Code(_)
138 | Event::Html(_)
139 | Event::InlineHtml(_)
140 | Event::FootnoteReference(_)
141 | Event::Start(Tag::Link { .. })
142 | Event::Start(Tag::Emphasis)
143 | Event::Start(Tag::Strong)
144 | Event::Start(Tag::Strikethrough)
145 | Event::Start(Tag::Image { .. }) => {
146 true
147 }
148 _ => false,
149 }
150 }
151
152 async fn parse_document(mut self) -> Self {
153 while !self.eof() {
154 if let Some(block) = self.parse_block().await {
155 self.parsed.extend(block);
156 } else {
157 self.cursor += 1;
158 }
159 }
160 self
161 }
162
163 #[async_recursion]
164 async fn parse_block(&mut self) -> Option<Vec<ParsedMarkdownElement>> {
165 let (current, source_range) = self.current().unwrap();
166 let source_range = source_range.clone();
167 match current {
168 Event::Start(tag) => match tag {
169 Tag::Paragraph => {
170 self.cursor += 1;
171 let text = self.parse_text(false, Some(source_range));
172 Some(vec![ParsedMarkdownElement::Paragraph(text)])
173 }
174 Tag::Heading { level, .. } => {
175 let level = *level;
176 self.cursor += 1;
177 let heading = self.parse_heading(level);
178 Some(vec![ParsedMarkdownElement::Heading(heading)])
179 }
180 Tag::Table(alignment) => {
181 let alignment = alignment.clone();
182 self.cursor += 1;
183 let table = self.parse_table(alignment);
184 Some(vec![ParsedMarkdownElement::Table(table)])
185 }
186 Tag::List(order) => {
187 let order = *order;
188 self.cursor += 1;
189 let list = self.parse_list(order).await;
190 Some(list)
191 }
192 Tag::BlockQuote(_kind) => {
193 self.cursor += 1;
194 let block_quote = self.parse_block_quote().await;
195 Some(vec![ParsedMarkdownElement::BlockQuote(block_quote)])
196 }
197 Tag::CodeBlock(kind) => {
198 let (language, scale) = match kind {
199 pulldown_cmark::CodeBlockKind::Indented => (None, None),
200 pulldown_cmark::CodeBlockKind::Fenced(language) => {
201 if language.is_empty() {
202 (None, None)
203 } else {
204 let parts: Vec<&str> = language.split_whitespace().collect();
205 let lang = parts.first().map(|s| s.to_string());
206 let scale = parts.get(1).and_then(|s| s.parse::<u32>().ok());
207 (lang, scale)
208 }
209 }
210 };
211
212 self.cursor += 1;
213
214 if language.as_deref() == Some("mermaid") {
215 let mermaid_diagram = self.parse_mermaid_diagram(scale).await?;
216 Some(vec![ParsedMarkdownElement::MermaidDiagram(mermaid_diagram)])
217 } else {
218 let code_block = self.parse_code_block(language).await?;
219 Some(vec![ParsedMarkdownElement::CodeBlock(code_block)])
220 }
221 }
222 Tag::HtmlBlock => {
223 self.cursor += 1;
224
225 Some(self.parse_html_block().await)
226 }
227 _ => None,
228 },
229 Event::Rule => {
230 self.cursor += 1;
231 Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)])
232 }
233 _ => None,
234 }
235 }
236
237 fn parse_text(
238 &mut self,
239 should_complete_on_soft_break: bool,
240 source_range: Option<Range<usize>>,
241 ) -> MarkdownParagraph {
242 let source_range = source_range.unwrap_or_else(|| {
243 self.current()
244 .map(|(_, range)| range.clone())
245 .unwrap_or_default()
246 });
247
248 let mut markdown_text_like = Vec::new();
249 let mut text = String::new();
250 let mut bold_depth = 0;
251 let mut italic_depth = 0;
252 let mut strikethrough_depth = 0;
253 let mut link: Option<Link> = None;
254 let mut image: Option<Image> = None;
255 let mut regions: Vec<(Range<usize>, ParsedRegion)> = vec![];
256 let mut highlights: Vec<(Range<usize>, MarkdownHighlight)> = vec![];
257 let mut link_urls: Vec<String> = vec![];
258 let mut link_ranges: Vec<Range<usize>> = vec![];
259
260 loop {
261 if self.eof() {
262 break;
263 }
264
265 let (current, _) = self.current().unwrap();
266 let prev_len = text.len();
267 match current {
268 Event::SoftBreak => {
269 if should_complete_on_soft_break {
270 break;
271 }
272 text.push(' ');
273 }
274
275 Event::HardBreak => {
276 text.push('\n');
277 }
278
279 // We want to ignore any inline HTML tags in the text but keep
280 // the text between them
281 Event::InlineHtml(_) => {}
282
283 Event::Text(t) => {
284 text.push_str(t.as_ref());
285 let mut style = MarkdownHighlightStyle::default();
286
287 if bold_depth > 0 {
288 style.weight = FontWeight::BOLD;
289 }
290
291 if italic_depth > 0 {
292 style.italic = true;
293 }
294
295 if strikethrough_depth > 0 {
296 style.strikethrough = true;
297 }
298
299 let last_run_len = if let Some(link) = link.clone() {
300 regions.push((
301 prev_len..text.len(),
302 ParsedRegion {
303 code: false,
304 link: Some(link),
305 },
306 ));
307 style.link = true;
308 prev_len
309 } else {
310 // Manually scan for links
311 let mut finder = linkify::LinkFinder::new();
312 finder.kinds(&[linkify::LinkKind::Url]);
313 let mut last_link_len = prev_len;
314 for link in finder.links(t) {
315 let start = prev_len + link.start();
316 let end = prev_len + link.end();
317 let range = start..end;
318 link_ranges.push(range.clone());
319 link_urls.push(link.as_str().to_string());
320
321 // If there is a style before we match a link, we have to add this to the highlighted ranges
322 if style != MarkdownHighlightStyle::default() && last_link_len < start {
323 highlights.push((
324 last_link_len..start,
325 MarkdownHighlight::Style(style.clone()),
326 ));
327 }
328
329 highlights.push((
330 range.clone(),
331 MarkdownHighlight::Style(MarkdownHighlightStyle {
332 underline: true,
333 ..style
334 }),
335 ));
336
337 regions.push((
338 range.clone(),
339 ParsedRegion {
340 code: false,
341 link: Some(Link::Web {
342 url: link.as_str().to_string(),
343 }),
344 },
345 ));
346 last_link_len = end;
347 }
348 last_link_len
349 };
350
351 if style != MarkdownHighlightStyle::default() && last_run_len < text.len() {
352 let mut new_highlight = true;
353 if let Some((last_range, last_style)) = highlights.last_mut()
354 && last_range.end == last_run_len
355 && last_style == &MarkdownHighlight::Style(style.clone())
356 {
357 last_range.end = text.len();
358 new_highlight = false;
359 }
360 if new_highlight {
361 highlights.push((
362 last_run_len..text.len(),
363 MarkdownHighlight::Style(style.clone()),
364 ));
365 }
366 }
367 }
368 Event::Code(t) => {
369 text.push_str(t.as_ref());
370 let range = prev_len..text.len();
371
372 if link.is_some() {
373 highlights.push((
374 range.clone(),
375 MarkdownHighlight::Style(MarkdownHighlightStyle {
376 link: true,
377 ..Default::default()
378 }),
379 ));
380 }
381 regions.push((
382 range,
383 ParsedRegion {
384 code: true,
385 link: link.clone(),
386 },
387 ));
388 }
389 Event::Start(tag) => match tag {
390 Tag::Emphasis => italic_depth += 1,
391 Tag::Strong => bold_depth += 1,
392 Tag::Strikethrough => strikethrough_depth += 1,
393 Tag::Link { dest_url, .. } => {
394 link = Link::identify(
395 self.file_location_directory.clone(),
396 dest_url.to_string(),
397 );
398 }
399 Tag::Image { dest_url, .. } => {
400 if !text.is_empty() {
401 let parsed_regions = MarkdownParagraphChunk::Text(ParsedMarkdownText {
402 source_range: source_range.clone(),
403 contents: mem::take(&mut text).into(),
404 highlights: mem::take(&mut highlights),
405 regions: mem::take(&mut regions),
406 });
407 markdown_text_like.push(parsed_regions);
408 }
409 image = Image::identify(
410 dest_url.to_string(),
411 source_range.clone(),
412 self.file_location_directory.clone(),
413 );
414 }
415 _ => {
416 break;
417 }
418 },
419
420 Event::End(tag) => match tag {
421 TagEnd::Emphasis => italic_depth -= 1,
422 TagEnd::Strong => bold_depth -= 1,
423 TagEnd::Strikethrough => strikethrough_depth -= 1,
424 TagEnd::Link => {
425 link = None;
426 }
427 TagEnd::Image => {
428 if let Some(mut image) = image.take() {
429 if !text.is_empty() {
430 image.set_alt_text(std::mem::take(&mut text).into());
431 mem::take(&mut highlights);
432 mem::take(&mut regions);
433 }
434 markdown_text_like.push(MarkdownParagraphChunk::Image(image));
435 }
436 }
437 TagEnd::Paragraph => {
438 self.cursor += 1;
439 break;
440 }
441 _ => {
442 break;
443 }
444 },
445 _ => {
446 break;
447 }
448 }
449
450 self.cursor += 1;
451 }
452 if !text.is_empty() {
453 markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText {
454 source_range,
455 contents: text.into(),
456 highlights,
457 regions,
458 }));
459 }
460 markdown_text_like
461 }
462
463 fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading {
464 let (_event, source_range) = self.previous().unwrap();
465 let source_range = source_range.clone();
466 let text = self.parse_text(true, None);
467
468 // Advance past the heading end tag
469 self.cursor += 1;
470
471 ParsedMarkdownHeading {
472 source_range,
473 level: match level {
474 pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1,
475 pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2,
476 pulldown_cmark::HeadingLevel::H3 => HeadingLevel::H3,
477 pulldown_cmark::HeadingLevel::H4 => HeadingLevel::H4,
478 pulldown_cmark::HeadingLevel::H5 => HeadingLevel::H5,
479 pulldown_cmark::HeadingLevel::H6 => HeadingLevel::H6,
480 },
481 contents: text,
482 }
483 }
484
485 fn parse_table(&mut self, alignment: Vec<Alignment>) -> ParsedMarkdownTable {
486 let (_event, source_range) = self.previous().unwrap();
487 let source_range = source_range.clone();
488 let mut header = vec![];
489 let mut body = vec![];
490 let mut row_columns = vec![];
491 let mut in_header = true;
492 let column_alignments = alignment
493 .iter()
494 .map(Self::convert_alignment)
495 .collect::<Vec<_>>();
496
497 loop {
498 if self.eof() {
499 break;
500 }
501
502 let (current, source_range) = self.current().unwrap();
503 let source_range = source_range.clone();
504 match current {
505 Event::Start(Tag::TableHead)
506 | Event::Start(Tag::TableRow)
507 | Event::End(TagEnd::TableCell) => {
508 self.cursor += 1;
509 }
510 Event::Start(Tag::TableCell) => {
511 self.cursor += 1;
512 let cell_contents = self.parse_text(false, Some(source_range));
513 row_columns.push(ParsedMarkdownTableColumn {
514 col_span: 1,
515 row_span: 1,
516 is_header: in_header,
517 children: cell_contents,
518 alignment: column_alignments
519 .get(row_columns.len())
520 .copied()
521 .unwrap_or_default(),
522 });
523 }
524 Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => {
525 self.cursor += 1;
526 let columns = std::mem::take(&mut row_columns);
527 if in_header {
528 header.push(ParsedMarkdownTableRow { columns: columns });
529 in_header = false;
530 } else {
531 body.push(ParsedMarkdownTableRow::with_columns(columns));
532 }
533 }
534 Event::End(TagEnd::Table) => {
535 self.cursor += 1;
536 break;
537 }
538 _ => {
539 break;
540 }
541 }
542 }
543
544 ParsedMarkdownTable {
545 source_range,
546 header,
547 body,
548 caption: None,
549 }
550 }
551
552 fn convert_alignment(alignment: &Alignment) -> ParsedMarkdownTableAlignment {
553 match alignment {
554 Alignment::None => ParsedMarkdownTableAlignment::None,
555 Alignment::Left => ParsedMarkdownTableAlignment::Left,
556 Alignment::Center => ParsedMarkdownTableAlignment::Center,
557 Alignment::Right => ParsedMarkdownTableAlignment::Right,
558 }
559 }
560
561 async fn parse_list(&mut self, order: Option<u64>) -> Vec<ParsedMarkdownElement> {
562 let (_, list_source_range) = self.previous().unwrap();
563
564 let mut items = Vec::new();
565 let mut items_stack = vec![MarkdownListItem::default()];
566 let mut depth = 1;
567 let mut order = order;
568 let mut order_stack = Vec::new();
569
570 let mut insertion_indices = FxHashMap::default();
571 let mut source_ranges = FxHashMap::default();
572 let mut start_item_range = list_source_range.clone();
573
574 while !self.eof() {
575 let (current, source_range) = self.current().unwrap();
576 match current {
577 Event::Start(Tag::List(new_order)) => {
578 if items_stack.last().is_some() && !insertion_indices.contains_key(&depth) {
579 insertion_indices.insert(depth, items.len());
580 }
581
582 // We will use the start of the nested list as the end for the current item's range,
583 // because we don't care about the hierarchy of list items
584 if let collections::hash_map::Entry::Vacant(e) = source_ranges.entry(depth) {
585 e.insert(start_item_range.start..source_range.start);
586 }
587
588 order_stack.push(order);
589 order = *new_order;
590 self.cursor += 1;
591 depth += 1;
592 }
593 Event::End(TagEnd::List(_)) => {
594 order = order_stack.pop().flatten();
595 self.cursor += 1;
596 depth -= 1;
597
598 if depth == 0 {
599 break;
600 }
601 }
602 Event::Start(Tag::Item) => {
603 start_item_range = source_range.clone();
604
605 self.cursor += 1;
606 items_stack.push(MarkdownListItem::default());
607
608 let mut task_list = None;
609 // Check for task list marker (`- [ ]` or `- [x]`)
610 if let Some(event) = self.current_event() {
611 // If there is a linebreak in between two list items the task list marker will actually be the first element of the paragraph
612 if event == &Event::Start(Tag::Paragraph) {
613 self.cursor += 1;
614 }
615
616 if let Some((Event::TaskListMarker(checked), range)) = self.current() {
617 task_list = Some((*checked, range.clone()));
618 self.cursor += 1;
619 }
620 }
621
622 if let Some((event, range)) = self.current() {
623 // This is a plain list item.
624 // For example `- some text` or `1. [Docs](./docs.md)`
625 if MarkdownParser::is_text_like(event) {
626 let text = self.parse_text(false, Some(range.clone()));
627 let block = ParsedMarkdownElement::Paragraph(text);
628 if let Some(content) = items_stack.last_mut() {
629 let item_type = if let Some((checked, range)) = task_list {
630 ParsedMarkdownListItemType::Task(checked, range)
631 } else if let Some(order) = order {
632 ParsedMarkdownListItemType::Ordered(order)
633 } else {
634 ParsedMarkdownListItemType::Unordered
635 };
636 content.item_type = item_type;
637 content.content.push(block);
638 }
639 } else {
640 let block = self.parse_block().await;
641 if let Some(block) = block
642 && let Some(list_item) = items_stack.last_mut()
643 {
644 list_item.content.extend(block);
645 }
646 }
647 }
648
649 // If there is a linebreak in between two list items the task list marker will actually be the first element of the paragraph
650 if self.current_event() == Some(&Event::End(TagEnd::Paragraph)) {
651 self.cursor += 1;
652 }
653 }
654 Event::End(TagEnd::Item) => {
655 self.cursor += 1;
656
657 if let Some(current) = order {
658 order = Some(current + 1);
659 }
660
661 if let Some(list_item) = items_stack.pop() {
662 let source_range = source_ranges
663 .remove(&depth)
664 .unwrap_or(start_item_range.clone());
665
666 // We need to remove the last character of the source range, because it includes the newline character
667 let source_range = source_range.start..source_range.end - 1;
668 let item = ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
669 source_range,
670 content: list_item.content,
671 depth,
672 item_type: list_item.item_type,
673 nested: false,
674 });
675
676 if let Some(index) = insertion_indices.get(&depth) {
677 items.insert(*index, item);
678 insertion_indices.remove(&depth);
679 } else {
680 items.push(item);
681 }
682 }
683 }
684 _ => {
685 if depth == 0 {
686 break;
687 }
688 // This can only happen if a list item starts with more then one paragraph,
689 // or the list item contains blocks that should be rendered after the nested list items
690 let block = self.parse_block().await;
691 if let Some(block) = block {
692 if let Some(list_item) = items_stack.last_mut() {
693 // If we did not insert any nested items yet (in this case insertion index is set), we can append the block to the current list item
694 if !insertion_indices.contains_key(&depth) {
695 list_item.content.extend(block);
696 continue;
697 }
698 }
699
700 // Otherwise we need to insert the block after all the nested items
701 // that have been parsed so far
702 items.extend(block);
703 } else {
704 self.cursor += 1;
705 }
706 }
707 }
708 }
709
710 items
711 }
712
713 #[async_recursion]
714 async fn parse_block_quote(&mut self) -> ParsedMarkdownBlockQuote {
715 let (_event, source_range) = self.previous().unwrap();
716 let source_range = source_range.clone();
717 let mut nested_depth = 1;
718
719 let mut children: Vec<ParsedMarkdownElement> = vec![];
720
721 while !self.eof() {
722 let block = self.parse_block().await;
723
724 if let Some(block) = block {
725 children.extend(block);
726 } else {
727 break;
728 }
729
730 if self.eof() {
731 break;
732 }
733
734 let (current, _source_range) = self.current().unwrap();
735 match current {
736 // This is a nested block quote.
737 // Record that we're in a nested block quote and continue parsing.
738 // We don't need to advance the cursor since the next
739 // call to `parse_block` will handle it.
740 Event::Start(Tag::BlockQuote(_kind)) => {
741 nested_depth += 1;
742 }
743 Event::End(TagEnd::BlockQuote(_kind)) => {
744 nested_depth -= 1;
745 if nested_depth == 0 {
746 self.cursor += 1;
747 break;
748 }
749 }
750 _ => {}
751 };
752 }
753
754 ParsedMarkdownBlockQuote {
755 source_range,
756 children,
757 }
758 }
759
760 async fn parse_code_block(
761 &mut self,
762 language: Option<String>,
763 ) -> Option<ParsedMarkdownCodeBlock> {
764 let Some((_event, source_range)) = self.previous() else {
765 return None;
766 };
767
768 let source_range = source_range.clone();
769 let mut code = String::new();
770
771 while !self.eof() {
772 let Some((current, _source_range)) = self.current() else {
773 break;
774 };
775
776 match current {
777 Event::Text(text) => {
778 code.push_str(text);
779 self.cursor += 1;
780 }
781 Event::End(TagEnd::CodeBlock) => {
782 self.cursor += 1;
783 break;
784 }
785 _ => {
786 break;
787 }
788 }
789 }
790
791 code = code.strip_suffix('\n').unwrap_or(&code).to_string();
792
793 let highlights = if let Some(language) = &language {
794 if let Some(registry) = &self.language_registry {
795 let rope: language::Rope = code.as_str().into();
796 registry
797 .language_for_name_or_extension(language)
798 .await
799 .map(|l| l.highlight_text(&rope, 0..code.len()))
800 .ok()
801 } else {
802 None
803 }
804 } else {
805 None
806 };
807
808 Some(ParsedMarkdownCodeBlock {
809 source_range,
810 contents: code.into(),
811 language,
812 highlights,
813 })
814 }
815
816 async fn parse_mermaid_diagram(
817 &mut self,
818 scale: Option<u32>,
819 ) -> Option<ParsedMarkdownMermaidDiagram> {
820 let Some((_event, source_range)) = self.previous() else {
821 return None;
822 };
823
824 let source_range = source_range.clone();
825 let mut code = String::new();
826
827 while !self.eof() {
828 let Some((current, _source_range)) = self.current() else {
829 break;
830 };
831
832 match current {
833 Event::Text(text) => {
834 code.push_str(text);
835 self.cursor += 1;
836 }
837 Event::End(TagEnd::CodeBlock) => {
838 self.cursor += 1;
839 break;
840 }
841 _ => {
842 break;
843 }
844 }
845 }
846
847 code = code.strip_suffix('\n').unwrap_or(&code).to_string();
848
849 let scale = scale.unwrap_or(100).clamp(10, 500);
850
851 Some(ParsedMarkdownMermaidDiagram {
852 source_range,
853 contents: ParsedMarkdownMermaidDiagramContents {
854 contents: code.into(),
855 scale,
856 },
857 })
858 }
859
860 async fn parse_html_block(&mut self) -> Vec<ParsedMarkdownElement> {
861 let mut elements = Vec::new();
862 let Some((_event, _source_range)) = self.previous() else {
863 return elements;
864 };
865
866 let mut html_source_range_start = None;
867 let mut html_source_range_end = None;
868 let mut html_buffer = String::new();
869
870 while !self.eof() {
871 let Some((current, source_range)) = self.current() else {
872 break;
873 };
874 let source_range = source_range.clone();
875 match current {
876 Event::Html(html) => {
877 html_source_range_start.get_or_insert(source_range.start);
878 html_source_range_end = Some(source_range.end);
879 html_buffer.push_str(html);
880 self.cursor += 1;
881 }
882 Event::End(TagEnd::CodeBlock) => {
883 self.cursor += 1;
884 break;
885 }
886 _ => {
887 break;
888 }
889 }
890 }
891
892 let bytes = cleanup_html(&html_buffer);
893
894 let mut cursor = std::io::Cursor::new(bytes);
895 if let Ok(dom) = parse_document(RcDom::default(), ParseOpts::default())
896 .from_utf8()
897 .read_from(&mut cursor)
898 && let Some((start, end)) = html_source_range_start.zip(html_source_range_end)
899 {
900 self.parse_html_node(
901 start..end,
902 &dom.document,
903 &mut elements,
904 &ParseHtmlNodeContext::default(),
905 );
906 }
907
908 elements
909 }
910
911 #[stacksafe]
912 fn parse_html_node(
913 &self,
914 source_range: Range<usize>,
915 node: &Rc<markup5ever_rcdom::Node>,
916 elements: &mut Vec<ParsedMarkdownElement>,
917 context: &ParseHtmlNodeContext,
918 ) {
919 match &node.data {
920 markup5ever_rcdom::NodeData::Document => {
921 self.consume_children(source_range, node, elements, context);
922 }
923 markup5ever_rcdom::NodeData::Text { contents } => {
924 elements.push(ParsedMarkdownElement::Paragraph(vec![
925 MarkdownParagraphChunk::Text(ParsedMarkdownText {
926 source_range,
927 regions: Vec::default(),
928 highlights: Vec::default(),
929 contents: contents.borrow().to_string().into(),
930 }),
931 ]));
932 }
933 markup5ever_rcdom::NodeData::Comment { .. } => {}
934 markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
935 let mut styles = if let Some(styles) = Self::markdown_style_from_html_styles(
936 Self::extract_styles_from_attributes(attrs),
937 ) {
938 vec![MarkdownHighlight::Style(styles)]
939 } else {
940 Vec::default()
941 };
942
943 if local_name!("img") == name.local {
944 if let Some(image) = self.extract_image(source_range, attrs) {
945 elements.push(ParsedMarkdownElement::Image(image));
946 }
947 } else if local_name!("p") == name.local {
948 let mut paragraph = MarkdownParagraph::new();
949 self.parse_paragraph(
950 source_range,
951 node,
952 &mut paragraph,
953 &mut styles,
954 &mut Vec::new(),
955 );
956
957 if !paragraph.is_empty() {
958 elements.push(ParsedMarkdownElement::Paragraph(paragraph));
959 }
960 } else if matches!(
961 name.local,
962 local_name!("h1")
963 | local_name!("h2")
964 | local_name!("h3")
965 | local_name!("h4")
966 | local_name!("h5")
967 | local_name!("h6")
968 ) {
969 let mut paragraph = MarkdownParagraph::new();
970 self.consume_paragraph(
971 source_range.clone(),
972 node,
973 &mut paragraph,
974 &mut styles,
975 &mut Vec::new(),
976 );
977
978 if !paragraph.is_empty() {
979 elements.push(ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
980 source_range,
981 level: match name.local {
982 local_name!("h1") => HeadingLevel::H1,
983 local_name!("h2") => HeadingLevel::H2,
984 local_name!("h3") => HeadingLevel::H3,
985 local_name!("h4") => HeadingLevel::H4,
986 local_name!("h5") => HeadingLevel::H5,
987 local_name!("h6") => HeadingLevel::H6,
988 _ => unreachable!(),
989 },
990 contents: paragraph,
991 }));
992 }
993 } else if local_name!("ul") == name.local || local_name!("ol") == name.local {
994 if let Some(list_items) = self.extract_html_list(
995 node,
996 local_name!("ol") == name.local,
997 context.list_item_depth,
998 source_range,
999 ) {
1000 elements.extend(list_items);
1001 }
1002 } else if local_name!("blockquote") == name.local {
1003 if let Some(blockquote) = self.extract_html_blockquote(node, source_range) {
1004 elements.push(ParsedMarkdownElement::BlockQuote(blockquote));
1005 }
1006 } else if local_name!("table") == name.local {
1007 if let Some(table) = self.extract_html_table(node, source_range) {
1008 elements.push(ParsedMarkdownElement::Table(table));
1009 }
1010 } else {
1011 self.consume_children(source_range, node, elements, context);
1012 }
1013 }
1014 _ => {}
1015 }
1016 }
1017
1018 #[stacksafe]
1019 fn parse_paragraph(
1020 &self,
1021 source_range: Range<usize>,
1022 node: &Rc<markup5ever_rcdom::Node>,
1023 paragraph: &mut MarkdownParagraph,
1024 highlights: &mut Vec<MarkdownHighlight>,
1025 regions: &mut Vec<(Range<usize>, ParsedRegion)>,
1026 ) {
1027 fn items_with_range<T>(
1028 range: Range<usize>,
1029 items: impl IntoIterator<Item = T>,
1030 ) -> Vec<(Range<usize>, T)> {
1031 items
1032 .into_iter()
1033 .map(|item| (range.clone(), item))
1034 .collect()
1035 }
1036
1037 match &node.data {
1038 markup5ever_rcdom::NodeData::Text { contents } => {
1039 // append the text to the last chunk, so we can have a hacky version
1040 // of inline text with highlighting
1041 if let Some(text) = paragraph.iter_mut().last().and_then(|p| match p {
1042 MarkdownParagraphChunk::Text(text) => Some(text),
1043 _ => None,
1044 }) {
1045 let mut new_text = text.contents.to_string();
1046 new_text.push_str(&contents.borrow());
1047
1048 text.highlights.extend(items_with_range(
1049 text.contents.len()..new_text.len(),
1050 std::mem::take(highlights),
1051 ));
1052 text.regions.extend(items_with_range(
1053 text.contents.len()..new_text.len(),
1054 std::mem::take(regions)
1055 .into_iter()
1056 .map(|(_, region)| region),
1057 ));
1058 text.contents = SharedString::from(new_text);
1059 } else {
1060 let contents = contents.borrow().to_string();
1061 paragraph.push(MarkdownParagraphChunk::Text(ParsedMarkdownText {
1062 source_range,
1063 highlights: items_with_range(0..contents.len(), std::mem::take(highlights)),
1064 regions: items_with_range(
1065 0..contents.len(),
1066 std::mem::take(regions)
1067 .into_iter()
1068 .map(|(_, region)| region),
1069 ),
1070 contents: contents.into(),
1071 }));
1072 }
1073 }
1074 markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
1075 if local_name!("img") == name.local {
1076 if let Some(image) = self.extract_image(source_range, attrs) {
1077 paragraph.push(MarkdownParagraphChunk::Image(image));
1078 }
1079 } else if local_name!("b") == name.local || local_name!("strong") == name.local {
1080 highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle {
1081 weight: FontWeight::BOLD,
1082 ..Default::default()
1083 }));
1084
1085 self.consume_paragraph(source_range, node, paragraph, highlights, regions);
1086 } else if local_name!("i") == name.local {
1087 highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle {
1088 italic: true,
1089 ..Default::default()
1090 }));
1091
1092 self.consume_paragraph(source_range, node, paragraph, highlights, regions);
1093 } else if local_name!("em") == name.local {
1094 highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle {
1095 oblique: true,
1096 ..Default::default()
1097 }));
1098
1099 self.consume_paragraph(source_range, node, paragraph, highlights, regions);
1100 } else if local_name!("del") == name.local {
1101 highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle {
1102 strikethrough: true,
1103 ..Default::default()
1104 }));
1105
1106 self.consume_paragraph(source_range, node, paragraph, highlights, regions);
1107 } else if local_name!("ins") == name.local {
1108 highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle {
1109 underline: true,
1110 ..Default::default()
1111 }));
1112
1113 self.consume_paragraph(source_range, node, paragraph, highlights, regions);
1114 } else if local_name!("a") == name.local {
1115 if let Some(url) = Self::attr_value(attrs, local_name!("href"))
1116 && let Some(link) =
1117 Link::identify(self.file_location_directory.clone(), url)
1118 {
1119 highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle {
1120 link: true,
1121 ..Default::default()
1122 }));
1123
1124 regions.push((
1125 source_range.clone(),
1126 ParsedRegion {
1127 code: false,
1128 link: Some(link),
1129 },
1130 ));
1131 }
1132
1133 self.consume_paragraph(source_range, node, paragraph, highlights, regions);
1134 } else {
1135 self.consume_paragraph(source_range, node, paragraph, highlights, regions);
1136 }
1137 }
1138 _ => {}
1139 }
1140 }
1141
1142 fn consume_paragraph(
1143 &self,
1144 source_range: Range<usize>,
1145 node: &Rc<markup5ever_rcdom::Node>,
1146 paragraph: &mut MarkdownParagraph,
1147 highlights: &mut Vec<MarkdownHighlight>,
1148 regions: &mut Vec<(Range<usize>, ParsedRegion)>,
1149 ) {
1150 for node in node.children.borrow().iter() {
1151 self.parse_paragraph(source_range.clone(), node, paragraph, highlights, regions);
1152 }
1153 }
1154
1155 fn parse_table_row(
1156 &self,
1157 source_range: Range<usize>,
1158 node: &Rc<markup5ever_rcdom::Node>,
1159 ) -> Option<ParsedMarkdownTableRow> {
1160 let mut columns = Vec::new();
1161
1162 match &node.data {
1163 markup5ever_rcdom::NodeData::Element { name, .. } => {
1164 if local_name!("tr") != name.local {
1165 return None;
1166 }
1167
1168 for node in node.children.borrow().iter() {
1169 if let Some(column) = self.parse_table_column(source_range.clone(), node) {
1170 columns.push(column);
1171 }
1172 }
1173 }
1174 _ => {}
1175 }
1176
1177 if columns.is_empty() {
1178 None
1179 } else {
1180 Some(ParsedMarkdownTableRow { columns })
1181 }
1182 }
1183
1184 fn parse_table_column(
1185 &self,
1186 source_range: Range<usize>,
1187 node: &Rc<markup5ever_rcdom::Node>,
1188 ) -> Option<ParsedMarkdownTableColumn> {
1189 match &node.data {
1190 markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
1191 if !matches!(name.local, local_name!("th") | local_name!("td")) {
1192 return None;
1193 }
1194
1195 let mut children = MarkdownParagraph::new();
1196 self.consume_paragraph(
1197 source_range,
1198 node,
1199 &mut children,
1200 &mut Vec::new(),
1201 &mut Vec::new(),
1202 );
1203
1204 let is_header = matches!(name.local, local_name!("th"));
1205
1206 Some(ParsedMarkdownTableColumn {
1207 col_span: std::cmp::max(
1208 Self::attr_value(attrs, local_name!("colspan"))
1209 .and_then(|span| span.parse().ok())
1210 .unwrap_or(1),
1211 1,
1212 ),
1213 row_span: std::cmp::max(
1214 Self::attr_value(attrs, local_name!("rowspan"))
1215 .and_then(|span| span.parse().ok())
1216 .unwrap_or(1),
1217 1,
1218 ),
1219 is_header,
1220 children,
1221 alignment: Self::attr_value(attrs, local_name!("align"))
1222 .and_then(|align| match align.as_str() {
1223 "left" => Some(ParsedMarkdownTableAlignment::Left),
1224 "center" => Some(ParsedMarkdownTableAlignment::Center),
1225 "right" => Some(ParsedMarkdownTableAlignment::Right),
1226 _ => None,
1227 })
1228 .unwrap_or_else(|| {
1229 if is_header {
1230 ParsedMarkdownTableAlignment::Center
1231 } else {
1232 ParsedMarkdownTableAlignment::default()
1233 }
1234 }),
1235 })
1236 }
1237 _ => None,
1238 }
1239 }
1240
1241 fn consume_children(
1242 &self,
1243 source_range: Range<usize>,
1244 node: &Rc<markup5ever_rcdom::Node>,
1245 elements: &mut Vec<ParsedMarkdownElement>,
1246 context: &ParseHtmlNodeContext,
1247 ) {
1248 for node in node.children.borrow().iter() {
1249 self.parse_html_node(source_range.clone(), node, elements, context);
1250 }
1251 }
1252
1253 fn attr_value(
1254 attrs: &RefCell<Vec<html5ever::Attribute>>,
1255 name: html5ever::LocalName,
1256 ) -> Option<String> {
1257 attrs.borrow().iter().find_map(|attr| {
1258 if attr.name.local == name {
1259 Some(attr.value.to_string())
1260 } else {
1261 None
1262 }
1263 })
1264 }
1265
1266 fn markdown_style_from_html_styles(
1267 styles: HashMap<String, String>,
1268 ) -> Option<MarkdownHighlightStyle> {
1269 let mut markdown_style = MarkdownHighlightStyle::default();
1270
1271 if let Some(text_decoration) = styles.get("text-decoration") {
1272 match text_decoration.to_lowercase().as_str() {
1273 "underline" => {
1274 markdown_style.underline = true;
1275 }
1276 "line-through" => {
1277 markdown_style.strikethrough = true;
1278 }
1279 _ => {}
1280 }
1281 }
1282
1283 if let Some(font_style) = styles.get("font-style") {
1284 match font_style.to_lowercase().as_str() {
1285 "italic" => {
1286 markdown_style.italic = true;
1287 }
1288 "oblique" => {
1289 markdown_style.oblique = true;
1290 }
1291 _ => {}
1292 }
1293 }
1294
1295 if let Some(font_weight) = styles.get("font-weight") {
1296 match font_weight.to_lowercase().as_str() {
1297 "bold" => {
1298 markdown_style.weight = FontWeight::BOLD;
1299 }
1300 "lighter" => {
1301 markdown_style.weight = FontWeight::THIN;
1302 }
1303 _ => {
1304 if let Some(weight) = font_weight.parse::<f32>().ok() {
1305 markdown_style.weight = FontWeight(weight);
1306 }
1307 }
1308 }
1309 }
1310
1311 if markdown_style != MarkdownHighlightStyle::default() {
1312 Some(markdown_style)
1313 } else {
1314 None
1315 }
1316 }
1317
1318 fn extract_styles_from_attributes(
1319 attrs: &RefCell<Vec<html5ever::Attribute>>,
1320 ) -> HashMap<String, String> {
1321 let mut styles = HashMap::new();
1322
1323 if let Some(style) = Self::attr_value(attrs, local_name!("style")) {
1324 for decl in style.split(';') {
1325 let mut parts = decl.splitn(2, ':');
1326 if let Some((key, value)) = parts.next().zip(parts.next()) {
1327 styles.insert(
1328 key.trim().to_lowercase().to_string(),
1329 value.trim().to_string(),
1330 );
1331 }
1332 }
1333 }
1334
1335 styles
1336 }
1337
1338 fn extract_image(
1339 &self,
1340 source_range: Range<usize>,
1341 attrs: &RefCell<Vec<html5ever::Attribute>>,
1342 ) -> Option<Image> {
1343 let src = Self::attr_value(attrs, local_name!("src"))?;
1344
1345 let mut image = Image::identify(src, source_range, self.file_location_directory.clone())?;
1346
1347 if let Some(alt) = Self::attr_value(attrs, local_name!("alt")) {
1348 image.set_alt_text(alt.into());
1349 }
1350
1351 let styles = Self::extract_styles_from_attributes(attrs);
1352
1353 if let Some(width) = Self::attr_value(attrs, local_name!("width"))
1354 .or_else(|| styles.get("width").cloned())
1355 .and_then(|width| Self::parse_html_element_dimension(&width))
1356 {
1357 image.set_width(width);
1358 }
1359
1360 if let Some(height) = Self::attr_value(attrs, local_name!("height"))
1361 .or_else(|| styles.get("height").cloned())
1362 .and_then(|height| Self::parse_html_element_dimension(&height))
1363 {
1364 image.set_height(height);
1365 }
1366
1367 Some(image)
1368 }
1369
1370 fn extract_html_list(
1371 &self,
1372 node: &Rc<markup5ever_rcdom::Node>,
1373 ordered: bool,
1374 depth: u16,
1375 source_range: Range<usize>,
1376 ) -> Option<Vec<ParsedMarkdownElement>> {
1377 let mut list_items = Vec::with_capacity(node.children.borrow().len());
1378
1379 for (index, node) in node.children.borrow().iter().enumerate() {
1380 match &node.data {
1381 markup5ever_rcdom::NodeData::Element { name, .. } => {
1382 if local_name!("li") != name.local {
1383 continue;
1384 }
1385
1386 let mut content = Vec::new();
1387 self.consume_children(
1388 source_range.clone(),
1389 node,
1390 &mut content,
1391 &ParseHtmlNodeContext {
1392 list_item_depth: depth + 1,
1393 },
1394 );
1395
1396 if !content.is_empty() {
1397 list_items.push(ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
1398 depth,
1399 source_range: source_range.clone(),
1400 item_type: if ordered {
1401 ParsedMarkdownListItemType::Ordered(index as u64 + 1)
1402 } else {
1403 ParsedMarkdownListItemType::Unordered
1404 },
1405 content,
1406 nested: true,
1407 }));
1408 }
1409 }
1410 _ => {}
1411 }
1412 }
1413
1414 if list_items.is_empty() {
1415 None
1416 } else {
1417 Some(list_items)
1418 }
1419 }
1420
1421 fn parse_html_element_dimension(value: &str) -> Option<DefiniteLength> {
1422 if value.ends_with("%") {
1423 value
1424 .trim_end_matches("%")
1425 .parse::<f32>()
1426 .ok()
1427 .map(|value| relative(value / 100.))
1428 } else {
1429 value
1430 .trim_end_matches("px")
1431 .parse()
1432 .ok()
1433 .map(|value| px(value).into())
1434 }
1435 }
1436
1437 fn extract_html_blockquote(
1438 &self,
1439 node: &Rc<markup5ever_rcdom::Node>,
1440 source_range: Range<usize>,
1441 ) -> Option<ParsedMarkdownBlockQuote> {
1442 let mut children = Vec::new();
1443 self.consume_children(
1444 source_range.clone(),
1445 node,
1446 &mut children,
1447 &ParseHtmlNodeContext::default(),
1448 );
1449
1450 if children.is_empty() {
1451 None
1452 } else {
1453 Some(ParsedMarkdownBlockQuote {
1454 children,
1455 source_range,
1456 })
1457 }
1458 }
1459
1460 fn extract_html_table(
1461 &self,
1462 node: &Rc<markup5ever_rcdom::Node>,
1463 source_range: Range<usize>,
1464 ) -> Option<ParsedMarkdownTable> {
1465 let mut header_rows = Vec::new();
1466 let mut body_rows = Vec::new();
1467 let mut caption = None;
1468
1469 // node should be a thead, tbody or caption element
1470 for node in node.children.borrow().iter() {
1471 match &node.data {
1472 markup5ever_rcdom::NodeData::Element { name, .. } => {
1473 if local_name!("caption") == name.local {
1474 let mut paragraph = MarkdownParagraph::new();
1475 self.parse_paragraph(
1476 source_range.clone(),
1477 node,
1478 &mut paragraph,
1479 &mut Vec::new(),
1480 &mut Vec::new(),
1481 );
1482 caption = Some(paragraph);
1483 }
1484 if local_name!("thead") == name.local {
1485 // node should be a tr element
1486 for node in node.children.borrow().iter() {
1487 if let Some(row) = self.parse_table_row(source_range.clone(), node) {
1488 header_rows.push(row);
1489 }
1490 }
1491 } else if local_name!("tbody") == name.local {
1492 // node should be a tr element
1493 for node in node.children.borrow().iter() {
1494 if let Some(row) = self.parse_table_row(source_range.clone(), node) {
1495 body_rows.push(row);
1496 }
1497 }
1498 }
1499 }
1500 _ => {}
1501 }
1502 }
1503
1504 if !header_rows.is_empty() || !body_rows.is_empty() {
1505 Some(ParsedMarkdownTable {
1506 source_range,
1507 body: body_rows,
1508 header: header_rows,
1509 caption,
1510 })
1511 } else {
1512 None
1513 }
1514 }
1515}
1516
1517#[cfg(test)]
1518mod tests {
1519 use super::*;
1520 use ParsedMarkdownListItemType::*;
1521 use core::panic;
1522 use gpui::{AbsoluteLength, BackgroundExecutor, DefiniteLength};
1523 use language::{HighlightId, LanguageRegistry};
1524 use pretty_assertions::assert_eq;
1525
1526 async fn parse(input: &str) -> ParsedMarkdown {
1527 parse_markdown(input, None, None).await
1528 }
1529
1530 #[gpui::test]
1531 async fn test_headings() {
1532 let parsed = parse("# Heading one\n## Heading two\n### Heading three").await;
1533
1534 assert_eq!(
1535 parsed.children,
1536 vec![
1537 h1(text("Heading one", 2..13), 0..14),
1538 h2(text("Heading two", 17..28), 14..29),
1539 h3(text("Heading three", 33..46), 29..46),
1540 ]
1541 );
1542 }
1543
1544 #[gpui::test]
1545 async fn test_newlines_dont_new_paragraphs() {
1546 let parsed = parse("Some text **that is bolded**\n and *italicized*").await;
1547
1548 assert_eq!(
1549 parsed.children,
1550 vec![p("Some text that is bolded and italicized", 0..46)]
1551 );
1552 }
1553
1554 #[gpui::test]
1555 async fn test_heading_with_paragraph() {
1556 let parsed = parse("# Zed\nThe editor").await;
1557
1558 assert_eq!(
1559 parsed.children,
1560 vec![h1(text("Zed", 2..5), 0..6), p("The editor", 6..16),]
1561 );
1562 }
1563
1564 #[gpui::test]
1565 async fn test_double_newlines_do_new_paragraphs() {
1566 let parsed = parse("Some text **that is bolded**\n\n and *italicized*").await;
1567
1568 assert_eq!(
1569 parsed.children,
1570 vec![
1571 p("Some text that is bolded", 0..29),
1572 p("and italicized", 31..47),
1573 ]
1574 );
1575 }
1576
1577 #[gpui::test]
1578 async fn test_bold_italic_text() {
1579 let parsed = parse("Some text **that is bolded** and *italicized*").await;
1580
1581 assert_eq!(
1582 parsed.children,
1583 vec![p("Some text that is bolded and italicized", 0..45)]
1584 );
1585 }
1586
1587 #[gpui::test]
1588 async fn test_nested_bold_strikethrough_text() {
1589 let parsed = parse("Some **bo~~strikethrough~~ld** text").await;
1590
1591 assert_eq!(parsed.children.len(), 1);
1592 assert_eq!(
1593 parsed.children[0],
1594 ParsedMarkdownElement::Paragraph(vec![MarkdownParagraphChunk::Text(
1595 ParsedMarkdownText {
1596 source_range: 0..35,
1597 contents: "Some bostrikethroughld text".into(),
1598 highlights: Vec::new(),
1599 regions: Vec::new(),
1600 }
1601 )])
1602 );
1603
1604 let new_text = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1605 text
1606 } else {
1607 panic!("Expected a paragraph");
1608 };
1609
1610 let paragraph = if let MarkdownParagraphChunk::Text(text) = &new_text[0] {
1611 text
1612 } else {
1613 panic!("Expected a text");
1614 };
1615
1616 assert_eq!(
1617 paragraph.highlights,
1618 vec![
1619 (
1620 5..7,
1621 MarkdownHighlight::Style(MarkdownHighlightStyle {
1622 weight: FontWeight::BOLD,
1623 ..Default::default()
1624 }),
1625 ),
1626 (
1627 7..20,
1628 MarkdownHighlight::Style(MarkdownHighlightStyle {
1629 weight: FontWeight::BOLD,
1630 strikethrough: true,
1631 ..Default::default()
1632 }),
1633 ),
1634 (
1635 20..22,
1636 MarkdownHighlight::Style(MarkdownHighlightStyle {
1637 weight: FontWeight::BOLD,
1638 ..Default::default()
1639 }),
1640 ),
1641 ]
1642 );
1643 }
1644
1645 #[gpui::test]
1646 async fn test_html_inline_style_elements() {
1647 let parsed =
1648 parse("<p>Some text <strong>strong text</strong> more text <b>bold text</b> more text <i>italic text</i> more text <em>emphasized text</em> more text <del>deleted text</del> more text <ins>inserted text</ins></p>").await;
1649
1650 assert_eq!(1, parsed.children.len());
1651 let chunks = if let ParsedMarkdownElement::Paragraph(chunks) = &parsed.children[0] {
1652 chunks
1653 } else {
1654 panic!("Expected a paragraph");
1655 };
1656
1657 assert_eq!(1, chunks.len());
1658 let text = if let MarkdownParagraphChunk::Text(text) = &chunks[0] {
1659 text
1660 } else {
1661 panic!("Expected a paragraph");
1662 };
1663
1664 assert_eq!(0..205, text.source_range);
1665 assert_eq!(
1666 "Some text strong text more text bold text more text italic text more text emphasized text more text deleted text more text inserted text",
1667 text.contents.as_str(),
1668 );
1669 assert_eq!(
1670 vec![
1671 (
1672 10..21,
1673 MarkdownHighlight::Style(MarkdownHighlightStyle {
1674 weight: FontWeight(700.0),
1675 ..Default::default()
1676 },),
1677 ),
1678 (
1679 32..41,
1680 MarkdownHighlight::Style(MarkdownHighlightStyle {
1681 weight: FontWeight(700.0),
1682 ..Default::default()
1683 },),
1684 ),
1685 (
1686 52..63,
1687 MarkdownHighlight::Style(MarkdownHighlightStyle {
1688 italic: true,
1689 weight: FontWeight(400.0),
1690 ..Default::default()
1691 },),
1692 ),
1693 (
1694 74..89,
1695 MarkdownHighlight::Style(MarkdownHighlightStyle {
1696 weight: FontWeight(400.0),
1697 oblique: true,
1698 ..Default::default()
1699 },),
1700 ),
1701 (
1702 100..112,
1703 MarkdownHighlight::Style(MarkdownHighlightStyle {
1704 strikethrough: true,
1705 weight: FontWeight(400.0),
1706 ..Default::default()
1707 },),
1708 ),
1709 (
1710 123..136,
1711 MarkdownHighlight::Style(MarkdownHighlightStyle {
1712 underline: true,
1713 weight: FontWeight(400.0,),
1714 ..Default::default()
1715 },),
1716 ),
1717 ],
1718 text.highlights
1719 );
1720 }
1721
1722 #[gpui::test]
1723 async fn test_html_href_element() {
1724 let parsed =
1725 parse("<p>Some text <a href=\"https://example.com\">link</a> more text</p>").await;
1726
1727 assert_eq!(1, parsed.children.len());
1728 let chunks = if let ParsedMarkdownElement::Paragraph(chunks) = &parsed.children[0] {
1729 chunks
1730 } else {
1731 panic!("Expected a paragraph");
1732 };
1733
1734 assert_eq!(1, chunks.len());
1735 let text = if let MarkdownParagraphChunk::Text(text) = &chunks[0] {
1736 text
1737 } else {
1738 panic!("Expected a paragraph");
1739 };
1740
1741 assert_eq!(0..65, text.source_range);
1742 assert_eq!("Some text link more text", text.contents.as_str(),);
1743 assert_eq!(
1744 vec![(
1745 10..14,
1746 MarkdownHighlight::Style(MarkdownHighlightStyle {
1747 link: true,
1748 ..Default::default()
1749 },),
1750 )],
1751 text.highlights
1752 );
1753 assert_eq!(
1754 vec![(
1755 10..14,
1756 ParsedRegion {
1757 code: false,
1758 link: Some(Link::Web {
1759 url: "https://example.com".into()
1760 })
1761 }
1762 )],
1763 text.regions
1764 )
1765 }
1766
1767 #[gpui::test]
1768 async fn test_text_with_inline_html() {
1769 let parsed = parse("This is a paragraph with an inline HTML <sometag>tag</sometag>.").await;
1770
1771 assert_eq!(
1772 parsed.children,
1773 vec![p("This is a paragraph with an inline HTML tag.", 0..63),],
1774 );
1775 }
1776
1777 #[gpui::test]
1778 async fn test_raw_links_detection() {
1779 let parsed = parse("Checkout this https://zed.dev link").await;
1780
1781 assert_eq!(
1782 parsed.children,
1783 vec![p("Checkout this https://zed.dev link", 0..34)]
1784 );
1785 }
1786
1787 #[gpui::test]
1788 async fn test_empty_image() {
1789 let parsed = parse("![]()").await;
1790
1791 let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1792 text
1793 } else {
1794 panic!("Expected a paragraph");
1795 };
1796 assert_eq!(paragraph.len(), 0);
1797 }
1798
1799 #[gpui::test]
1800 async fn test_image_links_detection() {
1801 let parsed = parse("").await;
1802
1803 let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1804 text
1805 } else {
1806 panic!("Expected a paragraph");
1807 };
1808 assert_eq!(
1809 paragraph[0],
1810 MarkdownParagraphChunk::Image(Image {
1811 source_range: 0..111,
1812 link: Link::Web {
1813 url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
1814 },
1815 alt_text: Some("test".into()),
1816 height: None,
1817 width: None,
1818 },)
1819 );
1820 }
1821
1822 #[gpui::test]
1823 async fn test_image_alt_text() {
1824 let parsed = parse("[](https://zed.dev)\n ").await;
1825
1826 let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1827 text
1828 } else {
1829 panic!("Expected a paragraph");
1830 };
1831 assert_eq!(
1832 paragraph[0],
1833 MarkdownParagraphChunk::Image(Image {
1834 source_range: 0..142,
1835 link: Link::Web {
1836 url: "https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json".to_string(),
1837 },
1838 alt_text: Some("Zed".into()),
1839 height: None,
1840 width: None,
1841 },)
1842 );
1843 }
1844
1845 #[gpui::test]
1846 async fn test_image_without_alt_text() {
1847 let parsed = parse("").await;
1848
1849 let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1850 text
1851 } else {
1852 panic!("Expected a paragraph");
1853 };
1854 assert_eq!(
1855 paragraph[0],
1856 MarkdownParagraphChunk::Image(Image {
1857 source_range: 0..31,
1858 link: Link::Web {
1859 url: "http://example.com/foo.png".to_string(),
1860 },
1861 alt_text: None,
1862 height: None,
1863 width: None,
1864 },)
1865 );
1866 }
1867
1868 #[gpui::test]
1869 async fn test_image_with_alt_text_containing_formatting() {
1870 let parsed = parse("").await;
1871
1872 let ParsedMarkdownElement::Paragraph(chunks) = &parsed.children[0] else {
1873 panic!("Expected a paragraph");
1874 };
1875 assert_eq!(
1876 chunks,
1877 &[MarkdownParagraphChunk::Image(Image {
1878 source_range: 0..44,
1879 link: Link::Web {
1880 url: "http://example.com/foo.png".to_string(),
1881 },
1882 alt_text: Some("foo bar baz".into()),
1883 height: None,
1884 width: None,
1885 }),],
1886 );
1887 }
1888
1889 #[gpui::test]
1890 async fn test_images_with_text_in_between() {
1891 let parsed = parse(
1892 "\nLorem Ipsum\n",
1893 )
1894 .await;
1895
1896 let chunks = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1897 text
1898 } else {
1899 panic!("Expected a paragraph");
1900 };
1901 assert_eq!(
1902 chunks,
1903 &vec![
1904 MarkdownParagraphChunk::Image(Image {
1905 source_range: 0..81,
1906 link: Link::Web {
1907 url: "http://example.com/foo.png".to_string(),
1908 },
1909 alt_text: Some("foo".into()),
1910 height: None,
1911 width: None,
1912 }),
1913 MarkdownParagraphChunk::Text(ParsedMarkdownText {
1914 source_range: 0..81,
1915 contents: " Lorem Ipsum ".into(),
1916 highlights: Vec::new(),
1917 regions: Vec::new(),
1918 }),
1919 MarkdownParagraphChunk::Image(Image {
1920 source_range: 0..81,
1921 link: Link::Web {
1922 url: "http://example.com/bar.png".to_string(),
1923 },
1924 alt_text: Some("bar".into()),
1925 height: None,
1926 width: None,
1927 })
1928 ]
1929 );
1930 }
1931
1932 #[test]
1933 fn test_parse_html_element_dimension() {
1934 // Test percentage values
1935 assert_eq!(
1936 MarkdownParser::parse_html_element_dimension("50%"),
1937 Some(DefiniteLength::Fraction(0.5))
1938 );
1939 assert_eq!(
1940 MarkdownParser::parse_html_element_dimension("100%"),
1941 Some(DefiniteLength::Fraction(1.0))
1942 );
1943 assert_eq!(
1944 MarkdownParser::parse_html_element_dimension("25%"),
1945 Some(DefiniteLength::Fraction(0.25))
1946 );
1947 assert_eq!(
1948 MarkdownParser::parse_html_element_dimension("0%"),
1949 Some(DefiniteLength::Fraction(0.0))
1950 );
1951
1952 // Test pixel values
1953 assert_eq!(
1954 MarkdownParser::parse_html_element_dimension("100px"),
1955 Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0))))
1956 );
1957 assert_eq!(
1958 MarkdownParser::parse_html_element_dimension("50px"),
1959 Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(50.0))))
1960 );
1961 assert_eq!(
1962 MarkdownParser::parse_html_element_dimension("0px"),
1963 Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(0.0))))
1964 );
1965
1966 // Test values without units (should be treated as pixels)
1967 assert_eq!(
1968 MarkdownParser::parse_html_element_dimension("100"),
1969 Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0))))
1970 );
1971 assert_eq!(
1972 MarkdownParser::parse_html_element_dimension("42"),
1973 Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))))
1974 );
1975
1976 // Test invalid values
1977 assert_eq!(
1978 MarkdownParser::parse_html_element_dimension("invalid"),
1979 None
1980 );
1981 assert_eq!(MarkdownParser::parse_html_element_dimension("px"), None);
1982 assert_eq!(MarkdownParser::parse_html_element_dimension("%"), None);
1983 assert_eq!(MarkdownParser::parse_html_element_dimension(""), None);
1984 assert_eq!(MarkdownParser::parse_html_element_dimension("abc%"), None);
1985 assert_eq!(MarkdownParser::parse_html_element_dimension("abcpx"), None);
1986
1987 // Test decimal values
1988 assert_eq!(
1989 MarkdownParser::parse_html_element_dimension("50.5%"),
1990 Some(DefiniteLength::Fraction(0.505))
1991 );
1992 assert_eq!(
1993 MarkdownParser::parse_html_element_dimension("100.25px"),
1994 Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.25))))
1995 );
1996 assert_eq!(
1997 MarkdownParser::parse_html_element_dimension("42.0"),
1998 Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))))
1999 );
2000 }
2001
2002 #[gpui::test]
2003 async fn test_html_unordered_list() {
2004 let parsed = parse(
2005 "<ul>
2006 <li>Item 1</li>
2007 <li>Item 2</li>
2008 </ul>",
2009 )
2010 .await;
2011
2012 assert_eq!(
2013 ParsedMarkdown {
2014 children: vec![
2015 nested_list_item(
2016 0..82,
2017 1,
2018 ParsedMarkdownListItemType::Unordered,
2019 vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..82))]
2020 ),
2021 nested_list_item(
2022 0..82,
2023 1,
2024 ParsedMarkdownListItemType::Unordered,
2025 vec![ParsedMarkdownElement::Paragraph(text("Item 2", 0..82))]
2026 ),
2027 ]
2028 },
2029 parsed
2030 );
2031 }
2032
2033 #[gpui::test]
2034 async fn test_html_ordered_list() {
2035 let parsed = parse(
2036 "<ol>
2037 <li>Item 1</li>
2038 <li>Item 2</li>
2039 </ol>",
2040 )
2041 .await;
2042
2043 assert_eq!(
2044 ParsedMarkdown {
2045 children: vec![
2046 nested_list_item(
2047 0..82,
2048 1,
2049 ParsedMarkdownListItemType::Ordered(1),
2050 vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..82))]
2051 ),
2052 nested_list_item(
2053 0..82,
2054 1,
2055 ParsedMarkdownListItemType::Ordered(2),
2056 vec![ParsedMarkdownElement::Paragraph(text("Item 2", 0..82))]
2057 ),
2058 ]
2059 },
2060 parsed
2061 );
2062 }
2063
2064 #[gpui::test]
2065 async fn test_html_nested_ordered_list() {
2066 let parsed = parse(
2067 "<ol>
2068 <li>Item 1</li>
2069 <li>Item 2
2070 <ol>
2071 <li>Sub-Item 1</li>
2072 <li>Sub-Item 2</li>
2073 </ol>
2074 </li>
2075 </ol>",
2076 )
2077 .await;
2078
2079 assert_eq!(
2080 ParsedMarkdown {
2081 children: vec![
2082 nested_list_item(
2083 0..216,
2084 1,
2085 ParsedMarkdownListItemType::Ordered(1),
2086 vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..216))]
2087 ),
2088 nested_list_item(
2089 0..216,
2090 1,
2091 ParsedMarkdownListItemType::Ordered(2),
2092 vec![
2093 ParsedMarkdownElement::Paragraph(text("Item 2", 0..216)),
2094 nested_list_item(
2095 0..216,
2096 2,
2097 ParsedMarkdownListItemType::Ordered(1),
2098 vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 1", 0..216))]
2099 ),
2100 nested_list_item(
2101 0..216,
2102 2,
2103 ParsedMarkdownListItemType::Ordered(2),
2104 vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 2", 0..216))]
2105 ),
2106 ]
2107 ),
2108 ]
2109 },
2110 parsed
2111 );
2112 }
2113
2114 #[gpui::test]
2115 async fn test_html_nested_unordered_list() {
2116 let parsed = parse(
2117 "<ul>
2118 <li>Item 1</li>
2119 <li>Item 2
2120 <ul>
2121 <li>Sub-Item 1</li>
2122 <li>Sub-Item 2</li>
2123 </ul>
2124 </li>
2125 </ul>",
2126 )
2127 .await;
2128
2129 assert_eq!(
2130 ParsedMarkdown {
2131 children: vec![
2132 nested_list_item(
2133 0..216,
2134 1,
2135 ParsedMarkdownListItemType::Unordered,
2136 vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..216))]
2137 ),
2138 nested_list_item(
2139 0..216,
2140 1,
2141 ParsedMarkdownListItemType::Unordered,
2142 vec![
2143 ParsedMarkdownElement::Paragraph(text("Item 2", 0..216)),
2144 nested_list_item(
2145 0..216,
2146 2,
2147 ParsedMarkdownListItemType::Unordered,
2148 vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 1", 0..216))]
2149 ),
2150 nested_list_item(
2151 0..216,
2152 2,
2153 ParsedMarkdownListItemType::Unordered,
2154 vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 2", 0..216))]
2155 ),
2156 ]
2157 ),
2158 ]
2159 },
2160 parsed
2161 );
2162 }
2163
2164 #[gpui::test]
2165 async fn test_inline_html_image_tag() {
2166 let parsed =
2167 parse("<p>Some text<img src=\"http://example.com/foo.png\" /> some more text</p>")
2168 .await;
2169
2170 assert_eq!(
2171 ParsedMarkdown {
2172 children: vec![ParsedMarkdownElement::Paragraph(vec![
2173 MarkdownParagraphChunk::Text(ParsedMarkdownText {
2174 source_range: 0..71,
2175 contents: "Some text".into(),
2176 highlights: Default::default(),
2177 regions: Default::default()
2178 }),
2179 MarkdownParagraphChunk::Image(Image {
2180 source_range: 0..71,
2181 link: Link::Web {
2182 url: "http://example.com/foo.png".to_string(),
2183 },
2184 alt_text: None,
2185 height: None,
2186 width: None,
2187 }),
2188 MarkdownParagraphChunk::Text(ParsedMarkdownText {
2189 source_range: 0..71,
2190 contents: " some more text".into(),
2191 highlights: Default::default(),
2192 regions: Default::default()
2193 }),
2194 ])]
2195 },
2196 parsed
2197 );
2198 }
2199
2200 #[gpui::test]
2201 async fn test_html_block_quote() {
2202 let parsed = parse(
2203 "<blockquote>
2204 <p>some description</p>
2205 </blockquote>",
2206 )
2207 .await;
2208
2209 assert_eq!(
2210 ParsedMarkdown {
2211 children: vec![block_quote(
2212 vec![ParsedMarkdownElement::Paragraph(text(
2213 "some description",
2214 0..78
2215 ))],
2216 0..78,
2217 )]
2218 },
2219 parsed
2220 );
2221 }
2222
2223 #[gpui::test]
2224 async fn test_html_nested_block_quote() {
2225 let parsed = parse(
2226 "<blockquote>
2227 <p>some description</p>
2228 <blockquote>
2229 <p>second description</p>
2230 </blockquote>
2231 </blockquote>",
2232 )
2233 .await;
2234
2235 assert_eq!(
2236 ParsedMarkdown {
2237 children: vec![block_quote(
2238 vec![
2239 ParsedMarkdownElement::Paragraph(text("some description", 0..179)),
2240 block_quote(
2241 vec![ParsedMarkdownElement::Paragraph(text(
2242 "second description",
2243 0..179
2244 ))],
2245 0..179,
2246 )
2247 ],
2248 0..179,
2249 )]
2250 },
2251 parsed
2252 );
2253 }
2254
2255 #[gpui::test]
2256 async fn test_html_table() {
2257 let parsed = parse(
2258 "<table>
2259 <thead>
2260 <tr>
2261 <th>Id</th>
2262 <th>Name</th>
2263 </tr>
2264 </thead>
2265 <tbody>
2266 <tr>
2267 <td>1</td>
2268 <td>Chris</td>
2269 </tr>
2270 <tr>
2271 <td>2</td>
2272 <td>Dennis</td>
2273 </tr>
2274 </tbody>
2275 </table>",
2276 )
2277 .await;
2278
2279 assert_eq!(
2280 ParsedMarkdown {
2281 children: vec![ParsedMarkdownElement::Table(table(
2282 0..366,
2283 None,
2284 vec![row(vec![
2285 column(
2286 1,
2287 1,
2288 true,
2289 text("Id", 0..366),
2290 ParsedMarkdownTableAlignment::Center
2291 ),
2292 column(
2293 1,
2294 1,
2295 true,
2296 text("Name ", 0..366),
2297 ParsedMarkdownTableAlignment::Center
2298 )
2299 ])],
2300 vec![
2301 row(vec![
2302 column(
2303 1,
2304 1,
2305 false,
2306 text("1", 0..366),
2307 ParsedMarkdownTableAlignment::None
2308 ),
2309 column(
2310 1,
2311 1,
2312 false,
2313 text("Chris", 0..366),
2314 ParsedMarkdownTableAlignment::None
2315 )
2316 ]),
2317 row(vec![
2318 column(
2319 1,
2320 1,
2321 false,
2322 text("2", 0..366),
2323 ParsedMarkdownTableAlignment::None
2324 ),
2325 column(
2326 1,
2327 1,
2328 false,
2329 text("Dennis", 0..366),
2330 ParsedMarkdownTableAlignment::None
2331 )
2332 ]),
2333 ],
2334 ))],
2335 },
2336 parsed
2337 );
2338 }
2339
2340 #[gpui::test]
2341 async fn test_html_table_with_caption() {
2342 let parsed = parse(
2343 "<table>
2344 <caption>My Table</caption>
2345 <tbody>
2346 <tr>
2347 <td>1</td>
2348 <td>Chris</td>
2349 </tr>
2350 <tr>
2351 <td>2</td>
2352 <td>Dennis</td>
2353 </tr>
2354 </tbody>
2355 </table>",
2356 )
2357 .await;
2358
2359 assert_eq!(
2360 ParsedMarkdown {
2361 children: vec![ParsedMarkdownElement::Table(table(
2362 0..280,
2363 Some(vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2364 source_range: 0..280,
2365 contents: "My Table".into(),
2366 highlights: Default::default(),
2367 regions: Default::default()
2368 })]),
2369 vec![],
2370 vec![
2371 row(vec![
2372 column(
2373 1,
2374 1,
2375 false,
2376 text("1", 0..280),
2377 ParsedMarkdownTableAlignment::None
2378 ),
2379 column(
2380 1,
2381 1,
2382 false,
2383 text("Chris", 0..280),
2384 ParsedMarkdownTableAlignment::None
2385 )
2386 ]),
2387 row(vec![
2388 column(
2389 1,
2390 1,
2391 false,
2392 text("2", 0..280),
2393 ParsedMarkdownTableAlignment::None
2394 ),
2395 column(
2396 1,
2397 1,
2398 false,
2399 text("Dennis", 0..280),
2400 ParsedMarkdownTableAlignment::None
2401 )
2402 ]),
2403 ],
2404 ))],
2405 },
2406 parsed
2407 );
2408 }
2409
2410 #[gpui::test]
2411 async fn test_html_table_without_headings() {
2412 let parsed = parse(
2413 "<table>
2414 <tbody>
2415 <tr>
2416 <td>1</td>
2417 <td>Chris</td>
2418 </tr>
2419 <tr>
2420 <td>2</td>
2421 <td>Dennis</td>
2422 </tr>
2423 </tbody>
2424 </table>",
2425 )
2426 .await;
2427
2428 assert_eq!(
2429 ParsedMarkdown {
2430 children: vec![ParsedMarkdownElement::Table(table(
2431 0..240,
2432 None,
2433 vec![],
2434 vec![
2435 row(vec![
2436 column(
2437 1,
2438 1,
2439 false,
2440 text("1", 0..240),
2441 ParsedMarkdownTableAlignment::None
2442 ),
2443 column(
2444 1,
2445 1,
2446 false,
2447 text("Chris", 0..240),
2448 ParsedMarkdownTableAlignment::None
2449 )
2450 ]),
2451 row(vec![
2452 column(
2453 1,
2454 1,
2455 false,
2456 text("2", 0..240),
2457 ParsedMarkdownTableAlignment::None
2458 ),
2459 column(
2460 1,
2461 1,
2462 false,
2463 text("Dennis", 0..240),
2464 ParsedMarkdownTableAlignment::None
2465 )
2466 ]),
2467 ],
2468 ))],
2469 },
2470 parsed
2471 );
2472 }
2473
2474 #[gpui::test]
2475 async fn test_html_table_without_body() {
2476 let parsed = parse(
2477 "<table>
2478 <thead>
2479 <tr>
2480 <th>Id</th>
2481 <th>Name</th>
2482 </tr>
2483 </thead>
2484 </table>",
2485 )
2486 .await;
2487
2488 assert_eq!(
2489 ParsedMarkdown {
2490 children: vec![ParsedMarkdownElement::Table(table(
2491 0..150,
2492 None,
2493 vec![row(vec![
2494 column(
2495 1,
2496 1,
2497 true,
2498 text("Id", 0..150),
2499 ParsedMarkdownTableAlignment::Center
2500 ),
2501 column(
2502 1,
2503 1,
2504 true,
2505 text("Name", 0..150),
2506 ParsedMarkdownTableAlignment::Center
2507 )
2508 ])],
2509 vec![],
2510 ))],
2511 },
2512 parsed
2513 );
2514 }
2515
2516 #[gpui::test]
2517 async fn test_html_heading_tags() {
2518 let parsed = parse("<h1>Heading</h1><h2>Heading</h2><h3>Heading</h3><h4>Heading</h4><h5>Heading</h5><h6>Heading</h6>").await;
2519
2520 assert_eq!(
2521 ParsedMarkdown {
2522 children: vec![
2523 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2524 level: HeadingLevel::H1,
2525 source_range: 0..96,
2526 contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2527 source_range: 0..96,
2528 contents: "Heading".into(),
2529 highlights: Vec::default(),
2530 regions: Vec::default()
2531 })],
2532 }),
2533 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2534 level: HeadingLevel::H2,
2535 source_range: 0..96,
2536 contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2537 source_range: 0..96,
2538 contents: "Heading".into(),
2539 highlights: Vec::default(),
2540 regions: Vec::default()
2541 })],
2542 }),
2543 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2544 level: HeadingLevel::H3,
2545 source_range: 0..96,
2546 contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2547 source_range: 0..96,
2548 contents: "Heading".into(),
2549 highlights: Vec::default(),
2550 regions: Vec::default()
2551 })],
2552 }),
2553 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2554 level: HeadingLevel::H4,
2555 source_range: 0..96,
2556 contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2557 source_range: 0..96,
2558 contents: "Heading".into(),
2559 highlights: Vec::default(),
2560 regions: Vec::default()
2561 })],
2562 }),
2563 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2564 level: HeadingLevel::H5,
2565 source_range: 0..96,
2566 contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2567 source_range: 0..96,
2568 contents: "Heading".into(),
2569 highlights: Vec::default(),
2570 regions: Vec::default()
2571 })],
2572 }),
2573 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2574 level: HeadingLevel::H6,
2575 source_range: 0..96,
2576 contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2577 source_range: 0..96,
2578 contents: "Heading".into(),
2579 highlights: Vec::default(),
2580 regions: Vec::default()
2581 })],
2582 }),
2583 ],
2584 },
2585 parsed
2586 );
2587 }
2588
2589 #[gpui::test]
2590 async fn test_html_image_tag() {
2591 let parsed = parse("<img src=\"http://example.com/foo.png\" />").await;
2592
2593 assert_eq!(
2594 ParsedMarkdown {
2595 children: vec![ParsedMarkdownElement::Image(Image {
2596 source_range: 0..40,
2597 link: Link::Web {
2598 url: "http://example.com/foo.png".to_string(),
2599 },
2600 alt_text: None,
2601 height: None,
2602 width: None,
2603 })]
2604 },
2605 parsed
2606 );
2607 }
2608
2609 #[gpui::test]
2610 async fn test_html_image_tag_with_alt_text() {
2611 let parsed = parse("<img src=\"http://example.com/foo.png\" alt=\"Foo\" />").await;
2612
2613 assert_eq!(
2614 ParsedMarkdown {
2615 children: vec![ParsedMarkdownElement::Image(Image {
2616 source_range: 0..50,
2617 link: Link::Web {
2618 url: "http://example.com/foo.png".to_string(),
2619 },
2620 alt_text: Some("Foo".into()),
2621 height: None,
2622 width: None,
2623 })]
2624 },
2625 parsed
2626 );
2627 }
2628
2629 #[gpui::test]
2630 async fn test_html_image_tag_with_height_and_width() {
2631 let parsed =
2632 parse("<img src=\"http://example.com/foo.png\" height=\"100\" width=\"200\" />").await;
2633
2634 assert_eq!(
2635 ParsedMarkdown {
2636 children: vec![ParsedMarkdownElement::Image(Image {
2637 source_range: 0..65,
2638 link: Link::Web {
2639 url: "http://example.com/foo.png".to_string(),
2640 },
2641 alt_text: None,
2642 height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))),
2643 width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))),
2644 })]
2645 },
2646 parsed
2647 );
2648 }
2649
2650 #[gpui::test]
2651 async fn test_html_image_style_tag_with_height_and_width() {
2652 let parsed = parse(
2653 "<img src=\"http://example.com/foo.png\" style=\"height:100px; width:200px;\" />",
2654 )
2655 .await;
2656
2657 assert_eq!(
2658 ParsedMarkdown {
2659 children: vec![ParsedMarkdownElement::Image(Image {
2660 source_range: 0..75,
2661 link: Link::Web {
2662 url: "http://example.com/foo.png".to_string(),
2663 },
2664 alt_text: None,
2665 height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))),
2666 width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))),
2667 })]
2668 },
2669 parsed
2670 );
2671 }
2672
2673 #[gpui::test]
2674 async fn test_header_only_table() {
2675 let markdown = "\
2676| Header 1 | Header 2 |
2677|----------|----------|
2678
2679Some other content
2680";
2681
2682 let expected_table = table(
2683 0..48,
2684 None,
2685 vec![row(vec![
2686 column(
2687 1,
2688 1,
2689 true,
2690 text("Header 1", 1..11),
2691 ParsedMarkdownTableAlignment::None,
2692 ),
2693 column(
2694 1,
2695 1,
2696 true,
2697 text("Header 2", 12..22),
2698 ParsedMarkdownTableAlignment::None,
2699 ),
2700 ])],
2701 vec![],
2702 );
2703
2704 assert_eq!(
2705 parse(markdown).await.children[0],
2706 ParsedMarkdownElement::Table(expected_table)
2707 );
2708 }
2709
2710 #[gpui::test]
2711 async fn test_basic_table() {
2712 let markdown = "\
2713| Header 1 | Header 2 |
2714|----------|----------|
2715| Cell 1 | Cell 2 |
2716| Cell 3 | Cell 4 |";
2717
2718 let expected_table = table(
2719 0..95,
2720 None,
2721 vec![row(vec![
2722 column(
2723 1,
2724 1,
2725 true,
2726 text("Header 1", 1..11),
2727 ParsedMarkdownTableAlignment::None,
2728 ),
2729 column(
2730 1,
2731 1,
2732 true,
2733 text("Header 2", 12..22),
2734 ParsedMarkdownTableAlignment::None,
2735 ),
2736 ])],
2737 vec![
2738 row(vec![
2739 column(
2740 1,
2741 1,
2742 false,
2743 text("Cell 1", 49..59),
2744 ParsedMarkdownTableAlignment::None,
2745 ),
2746 column(
2747 1,
2748 1,
2749 false,
2750 text("Cell 2", 60..70),
2751 ParsedMarkdownTableAlignment::None,
2752 ),
2753 ]),
2754 row(vec![
2755 column(
2756 1,
2757 1,
2758 false,
2759 text("Cell 3", 73..83),
2760 ParsedMarkdownTableAlignment::None,
2761 ),
2762 column(
2763 1,
2764 1,
2765 false,
2766 text("Cell 4", 84..94),
2767 ParsedMarkdownTableAlignment::None,
2768 ),
2769 ]),
2770 ],
2771 );
2772
2773 assert_eq!(
2774 parse(markdown).await.children[0],
2775 ParsedMarkdownElement::Table(expected_table)
2776 );
2777 }
2778
2779 #[gpui::test]
2780 async fn test_table_with_checkboxes() {
2781 let markdown = "\
2782| Done | Task |
2783|------|---------|
2784| [x] | Fix bug |
2785| [ ] | Add feature |";
2786
2787 let parsed = parse(markdown).await;
2788 let table = match &parsed.children[0] {
2789 ParsedMarkdownElement::Table(table) => table,
2790 other => panic!("Expected table, got: {:?}", other),
2791 };
2792
2793 let first_cell = &table.body[0].columns[0];
2794 let first_cell_text = match &first_cell.children[0] {
2795 MarkdownParagraphChunk::Text(t) => t.contents.to_string(),
2796 other => panic!("Expected text chunk, got: {:?}", other),
2797 };
2798 assert_eq!(first_cell_text.trim(), "[x]");
2799
2800 let second_cell = &table.body[1].columns[0];
2801 let second_cell_text = match &second_cell.children[0] {
2802 MarkdownParagraphChunk::Text(t) => t.contents.to_string(),
2803 other => panic!("Expected text chunk, got: {:?}", other),
2804 };
2805 assert_eq!(second_cell_text.trim(), "[ ]");
2806 }
2807
2808 #[gpui::test]
2809 async fn test_list_basic() {
2810 let parsed = parse(
2811 "\
2812* Item 1
2813* Item 2
2814* Item 3
2815",
2816 )
2817 .await;
2818
2819 assert_eq!(
2820 parsed.children,
2821 vec![
2822 list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]),
2823 list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]),
2824 list_item(18..26, 1, Unordered, vec![p("Item 3", 20..26)]),
2825 ],
2826 );
2827 }
2828
2829 #[gpui::test]
2830 async fn test_list_with_tasks() {
2831 let parsed = parse(
2832 "\
2833- [ ] TODO
2834- [x] Checked
2835",
2836 )
2837 .await;
2838
2839 assert_eq!(
2840 parsed.children,
2841 vec![
2842 list_item(0..10, 1, Task(false, 2..5), vec![p("TODO", 6..10)]),
2843 list_item(11..24, 1, Task(true, 13..16), vec![p("Checked", 17..24)]),
2844 ],
2845 );
2846 }
2847
2848 #[gpui::test]
2849 async fn test_list_with_indented_task() {
2850 let parsed = parse(
2851 "\
2852- [ ] TODO
2853 - [x] Checked
2854 - Unordered
2855 1. Number 1
2856 1. Number 2
28571. Number A
2858",
2859 )
2860 .await;
2861
2862 assert_eq!(
2863 parsed.children,
2864 vec![
2865 list_item(0..12, 1, Task(false, 2..5), vec![p("TODO", 6..10)]),
2866 list_item(13..26, 2, Task(true, 15..18), vec![p("Checked", 19..26)]),
2867 list_item(29..40, 2, Unordered, vec![p("Unordered", 31..40)]),
2868 list_item(43..54, 2, Ordered(1), vec![p("Number 1", 46..54)]),
2869 list_item(57..68, 2, Ordered(2), vec![p("Number 2", 60..68)]),
2870 list_item(69..80, 1, Ordered(1), vec![p("Number A", 72..80)]),
2871 ],
2872 );
2873 }
2874
2875 #[gpui::test]
2876 async fn test_list_with_linebreak_is_handled_correctly() {
2877 let parsed = parse(
2878 "\
2879- [ ] Task 1
2880
2881- [x] Task 2
2882",
2883 )
2884 .await;
2885
2886 assert_eq!(
2887 parsed.children,
2888 vec![
2889 list_item(0..13, 1, Task(false, 2..5), vec![p("Task 1", 6..12)]),
2890 list_item(14..26, 1, Task(true, 16..19), vec![p("Task 2", 20..26)]),
2891 ],
2892 );
2893 }
2894
2895 #[gpui::test]
2896 async fn test_list_nested() {
2897 let parsed = parse(
2898 "\
2899* Item 1
2900* Item 2
2901* Item 3
2902
29031. Hello
29041. Two
2905 1. Three
29062. Four
29073. Five
2908
2909* First
2910 1. Hello
2911 1. Goodbyte
2912 - Inner
2913 - Inner
2914 2. Goodbyte
2915 - Next item empty
2916 -
2917* Last
2918",
2919 )
2920 .await;
2921
2922 assert_eq!(
2923 parsed.children,
2924 vec![
2925 list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]),
2926 list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]),
2927 list_item(18..27, 1, Unordered, vec![p("Item 3", 20..26)]),
2928 list_item(28..36, 1, Ordered(1), vec![p("Hello", 31..36)]),
2929 list_item(37..46, 1, Ordered(2), vec![p("Two", 40..43),]),
2930 list_item(47..55, 2, Ordered(1), vec![p("Three", 50..55)]),
2931 list_item(56..63, 1, Ordered(3), vec![p("Four", 59..63)]),
2932 list_item(64..72, 1, Ordered(4), vec![p("Five", 67..71)]),
2933 list_item(73..82, 1, Unordered, vec![p("First", 75..80)]),
2934 list_item(83..96, 2, Ordered(1), vec![p("Hello", 86..91)]),
2935 list_item(97..116, 3, Ordered(1), vec![p("Goodbyte", 100..108)]),
2936 list_item(117..124, 4, Unordered, vec![p("Inner", 119..124)]),
2937 list_item(133..140, 4, Unordered, vec![p("Inner", 135..140)]),
2938 list_item(143..159, 2, Ordered(2), vec![p("Goodbyte", 146..154)]),
2939 list_item(160..180, 3, Unordered, vec![p("Next item empty", 165..180)]),
2940 list_item(186..190, 3, Unordered, vec![]),
2941 list_item(191..197, 1, Unordered, vec![p("Last", 193..197)]),
2942 ]
2943 );
2944 }
2945
2946 #[gpui::test]
2947 async fn test_list_with_nested_content() {
2948 let parsed = parse(
2949 "\
2950* This is a list item with two paragraphs.
2951
2952 This is the second paragraph in the list item.
2953",
2954 )
2955 .await;
2956
2957 assert_eq!(
2958 parsed.children,
2959 vec![list_item(
2960 0..96,
2961 1,
2962 Unordered,
2963 vec![
2964 p("This is a list item with two paragraphs.", 4..44),
2965 p("This is the second paragraph in the list item.", 50..97)
2966 ],
2967 ),],
2968 );
2969 }
2970
2971 #[gpui::test]
2972 async fn test_list_item_with_inline_html() {
2973 let parsed = parse(
2974 "\
2975* This is a list item with an inline HTML <sometag>tag</sometag>.
2976",
2977 )
2978 .await;
2979
2980 assert_eq!(
2981 parsed.children,
2982 vec![list_item(
2983 0..67,
2984 1,
2985 Unordered,
2986 vec![p("This is a list item with an inline HTML tag.", 4..44),],
2987 ),],
2988 );
2989 }
2990
2991 #[gpui::test]
2992 async fn test_nested_list_with_paragraph_inside() {
2993 let parsed = parse(
2994 "\
29951. a
2996 1. b
2997 1. c
2998
2999 text
3000
3001 1. d
3002",
3003 )
3004 .await;
3005
3006 assert_eq!(
3007 parsed.children,
3008 vec![
3009 list_item(0..7, 1, Ordered(1), vec![p("a", 3..4)],),
3010 list_item(8..20, 2, Ordered(1), vec![p("b", 12..13),],),
3011 list_item(21..27, 3, Ordered(1), vec![p("c", 25..26),],),
3012 p("text", 32..37),
3013 list_item(41..46, 2, Ordered(1), vec![p("d", 45..46),],),
3014 ],
3015 );
3016 }
3017
3018 #[gpui::test]
3019 async fn test_list_with_leading_text() {
3020 let parsed = parse(
3021 "\
3022* `code`
3023* **bold**
3024* [link](https://example.com)
3025",
3026 )
3027 .await;
3028
3029 assert_eq!(
3030 parsed.children,
3031 vec![
3032 list_item(0..8, 1, Unordered, vec![p("code", 2..8)]),
3033 list_item(9..19, 1, Unordered, vec![p("bold", 11..19)]),
3034 list_item(20..49, 1, Unordered, vec![p("link", 22..49)],),
3035 ],
3036 );
3037 }
3038
3039 #[gpui::test]
3040 async fn test_simple_block_quote() {
3041 let parsed = parse("> Simple block quote with **styled text**").await;
3042
3043 assert_eq!(
3044 parsed.children,
3045 vec![block_quote(
3046 vec![p("Simple block quote with styled text", 2..41)],
3047 0..41
3048 )]
3049 );
3050 }
3051
3052 #[gpui::test]
3053 async fn test_simple_block_quote_with_multiple_lines() {
3054 let parsed = parse(
3055 "\
3056> # Heading
3057> More
3058> text
3059>
3060> More text
3061",
3062 )
3063 .await;
3064
3065 assert_eq!(
3066 parsed.children,
3067 vec![block_quote(
3068 vec![
3069 h1(text("Heading", 4..11), 2..12),
3070 p("More text", 14..26),
3071 p("More text", 30..40)
3072 ],
3073 0..40
3074 )]
3075 );
3076 }
3077
3078 #[gpui::test]
3079 async fn test_nested_block_quote() {
3080 let parsed = parse(
3081 "\
3082> A
3083>
3084> > # B
3085>
3086> C
3087
3088More text
3089",
3090 )
3091 .await;
3092
3093 assert_eq!(
3094 parsed.children,
3095 vec![
3096 block_quote(
3097 vec![
3098 p("A", 2..4),
3099 block_quote(vec![h1(text("B", 12..13), 10..14)], 8..14),
3100 p("C", 18..20)
3101 ],
3102 0..20
3103 ),
3104 p("More text", 21..31)
3105 ]
3106 );
3107 }
3108
3109 #[gpui::test]
3110 async fn test_dollar_signs_are_plain_text() {
3111 // Dollar signs should be preserved as plain text, not treated as math delimiters.
3112 // Regression test for https://github.com/zed-industries/zed/issues/50170
3113 let parsed = parse("$100$ per unit").await;
3114 assert_eq!(parsed.children, vec![p("$100$ per unit", 0..14)]);
3115 }
3116
3117 #[gpui::test]
3118 async fn test_dollar_signs_in_list_items() {
3119 let parsed = parse("- $18,000 budget\n- $20,000 budget\n").await;
3120 assert_eq!(
3121 parsed.children,
3122 vec![
3123 list_item(0..16, 1, Unordered, vec![p("$18,000 budget", 2..16)]),
3124 list_item(17..33, 1, Unordered, vec![p("$20,000 budget", 19..33)]),
3125 ]
3126 );
3127 }
3128
3129 #[gpui::test]
3130 async fn test_code_block() {
3131 let parsed = parse(
3132 "\
3133```
3134fn main() {
3135 return 0;
3136}
3137```
3138",
3139 )
3140 .await;
3141
3142 assert_eq!(
3143 parsed.children,
3144 vec![code_block(
3145 None,
3146 "fn main() {\n return 0;\n}",
3147 0..35,
3148 None
3149 )]
3150 );
3151 }
3152
3153 #[gpui::test]
3154 async fn test_code_block_with_language(executor: BackgroundExecutor) {
3155 let language_registry = Arc::new(LanguageRegistry::test(executor.clone()));
3156 language_registry.add(language::rust_lang());
3157
3158 let parsed = parse_markdown(
3159 "\
3160```rust
3161fn main() {
3162 return 0;
3163}
3164```
3165",
3166 None,
3167 Some(language_registry),
3168 )
3169 .await;
3170
3171 assert_eq!(
3172 parsed.children,
3173 vec![code_block(
3174 Some("rust".to_string()),
3175 "fn main() {\n return 0;\n}",
3176 0..39,
3177 Some(vec![])
3178 )]
3179 );
3180 }
3181
3182 fn h1(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
3183 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
3184 source_range,
3185 level: HeadingLevel::H1,
3186 contents,
3187 })
3188 }
3189
3190 fn h2(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
3191 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
3192 source_range,
3193 level: HeadingLevel::H2,
3194 contents,
3195 })
3196 }
3197
3198 fn h3(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
3199 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
3200 source_range,
3201 level: HeadingLevel::H3,
3202 contents,
3203 })
3204 }
3205
3206 fn p(contents: &str, source_range: Range<usize>) -> ParsedMarkdownElement {
3207 ParsedMarkdownElement::Paragraph(text(contents, source_range))
3208 }
3209
3210 fn text(contents: &str, source_range: Range<usize>) -> MarkdownParagraph {
3211 vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
3212 highlights: Vec::new(),
3213 regions: Vec::new(),
3214 source_range,
3215 contents: contents.to_string().into(),
3216 })]
3217 }
3218
3219 fn block_quote(
3220 children: Vec<ParsedMarkdownElement>,
3221 source_range: Range<usize>,
3222 ) -> ParsedMarkdownElement {
3223 ParsedMarkdownElement::BlockQuote(ParsedMarkdownBlockQuote {
3224 source_range,
3225 children,
3226 })
3227 }
3228
3229 fn code_block(
3230 language: Option<String>,
3231 code: &str,
3232 source_range: Range<usize>,
3233 highlights: Option<Vec<(Range<usize>, HighlightId)>>,
3234 ) -> ParsedMarkdownElement {
3235 ParsedMarkdownElement::CodeBlock(ParsedMarkdownCodeBlock {
3236 source_range,
3237 language,
3238 contents: code.to_string().into(),
3239 highlights,
3240 })
3241 }
3242
3243 fn list_item(
3244 source_range: Range<usize>,
3245 depth: u16,
3246 item_type: ParsedMarkdownListItemType,
3247 content: Vec<ParsedMarkdownElement>,
3248 ) -> ParsedMarkdownElement {
3249 ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
3250 source_range,
3251 item_type,
3252 depth,
3253 content,
3254 nested: false,
3255 })
3256 }
3257
3258 fn nested_list_item(
3259 source_range: Range<usize>,
3260 depth: u16,
3261 item_type: ParsedMarkdownListItemType,
3262 content: Vec<ParsedMarkdownElement>,
3263 ) -> ParsedMarkdownElement {
3264 ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
3265 source_range,
3266 item_type,
3267 depth,
3268 content,
3269 nested: true,
3270 })
3271 }
3272
3273 fn table(
3274 source_range: Range<usize>,
3275 caption: Option<MarkdownParagraph>,
3276 header: Vec<ParsedMarkdownTableRow>,
3277 body: Vec<ParsedMarkdownTableRow>,
3278 ) -> ParsedMarkdownTable {
3279 ParsedMarkdownTable {
3280 source_range,
3281 header,
3282 body,
3283 caption,
3284 }
3285 }
3286
3287 fn row(columns: Vec<ParsedMarkdownTableColumn>) -> ParsedMarkdownTableRow {
3288 ParsedMarkdownTableRow { columns }
3289 }
3290
3291 fn column(
3292 col_span: usize,
3293 row_span: usize,
3294 is_header: bool,
3295 children: MarkdownParagraph,
3296 alignment: ParsedMarkdownTableAlignment,
3297 ) -> ParsedMarkdownTableColumn {
3298 ParsedMarkdownTableColumn {
3299 col_span,
3300 row_span,
3301 is_header,
3302 children,
3303 alignment,
3304 }
3305 }
3306
3307 impl PartialEq for ParsedMarkdownTable {
3308 fn eq(&self, other: &Self) -> bool {
3309 self.source_range == other.source_range
3310 && self.header == other.header
3311 && self.body == other.body
3312 }
3313 }
3314
3315 impl PartialEq for ParsedMarkdownText {
3316 fn eq(&self, other: &Self) -> bool {
3317 self.source_range == other.source_range && self.contents == other.contents
3318 }
3319 }
3320}