1use pulldown_cmark::{Alignment, CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
2use serde::{Deserialize, Serialize};
3use std::{cmp, ops::Range, sync::Arc};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub struct MarkdownLineRange {
8 pub start: usize,
9 pub end: usize,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct SourceRange {
15 pub bytes: Range<usize>,
16 pub lines: MarkdownLineRange,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
21pub struct MarkdownTargetId(usize);
22
23impl MarkdownTargetId {
24 #[must_use]
26 pub const fn index(self) -> usize {
27 self.0
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub enum MarkdownTargetKind {
34 Heading,
35 Paragraph,
36 ListItem,
37 BlockQuote,
38 TableRow,
39 CodeBlock,
40 CodeLine,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub enum MarkdownInline {
46 Text(String),
47 Code(String),
48 Strong(Vec<MarkdownInline>),
49 Emphasis(Vec<MarkdownInline>),
50 Strikethrough(Vec<MarkdownInline>),
51 Link {
52 destination: String,
53 title: Option<String>,
54 content: Vec<MarkdownInline>,
55 },
56 SoftBreak,
57 HardBreak,
58 ImageAlt(String),
60}
61
62impl MarkdownInline {
63 fn visible_text(&self, output: &mut String) {
64 match self {
65 Self::Text(text) | Self::Code(text) | Self::ImageAlt(text) => output.push_str(text),
66 Self::Strong(children) | Self::Emphasis(children) | Self::Strikethrough(children) => {
67 for child in children {
68 child.visible_text(output);
69 }
70 }
71 Self::Link { content, .. } => {
72 for child in content {
73 child.visible_text(output);
74 }
75 }
76 Self::SoftBreak | Self::HardBreak => output.push('\n'),
77 }
78 }
79}
80
81#[must_use]
83pub fn parse_markdown(source: impl Into<String>) -> MarkdownDocument {
84 MarkdownDocument::parse(source)
85}
86
87#[must_use]
89pub fn rendered_text(inlines: &[MarkdownInline]) -> String {
90 let mut text = String::new();
91 for inline in inlines {
92 inline.visible_text(&mut text);
93 }
94 text
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct MarkdownBlock {
100 pub source: SourceRange,
101 pub kind: MarkdownBlockKind,
102 pub target_id: Option<MarkdownTargetId>,
103}
104
105impl MarkdownBlock {
106 #[must_use]
108 pub const fn target_id(&self) -> Option<MarkdownTargetId> {
109 self.target_id
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115pub enum MarkdownBlockKind {
116 Heading {
117 level: u8,
118 content: Vec<MarkdownInline>,
119 },
120 Paragraph {
121 content: Vec<MarkdownInline>,
122 },
123 List {
124 ordered: bool,
125 start: Option<u64>,
126 items: Vec<MarkdownListItem>,
127 },
128 BlockQuote {
129 blocks: Vec<MarkdownBlock>,
130 },
131 CodeBlock(MarkdownCodeBlock),
132 Table(MarkdownTable),
133 HtmlFallback {
135 content: Vec<MarkdownInline>,
136 },
137 Rule,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142pub struct MarkdownListItem {
143 pub depth: usize,
144 pub source: SourceRange,
145 pub content: Vec<MarkdownInline>,
146 pub blocks: Vec<MarkdownBlock>,
147 pub target_id: Option<MarkdownTargetId>,
148}
149
150impl MarkdownListItem {
151 #[must_use]
153 pub const fn target_id(&self) -> Option<MarkdownTargetId> {
154 self.target_id
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160pub struct MarkdownCodeBlock {
161 pub language: Option<String>,
162 pub info: Option<String>,
164 pub source: SourceRange,
165 pub content: SourceRange,
166 pub lines: Vec<MarkdownCodeLine>,
167 pub target_id: Option<MarkdownTargetId>,
168}
169
170impl MarkdownCodeBlock {
171 #[must_use]
173 pub const fn target_id(&self) -> Option<MarkdownTargetId> {
174 self.target_id
175 }
176
177 #[must_use]
178 pub fn highlight_hint(&self) -> &str {
179 self.info
180 .as_deref()
181 .or(self.language.as_deref())
182 .unwrap_or_default()
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188pub struct MarkdownCodeLine {
189 pub index: usize,
190 pub source: SourceRange,
191 pub source_line: Option<usize>,
192 pub text: String,
193 pub target_id: Option<MarkdownTargetId>,
194}
195
196impl MarkdownCodeLine {
197 #[must_use]
199 pub const fn target_id(&self) -> Option<MarkdownTargetId> {
200 self.target_id
201 }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
206pub struct MarkdownTable {
207 pub alignments: Vec<MarkdownTableAlignment>,
208 pub rows: Vec<MarkdownTableRow>,
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
213pub enum MarkdownTableAlignment {
214 None,
215 Left,
216 Center,
217 Right,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
222pub struct MarkdownTableRow {
223 pub header: bool,
224 pub source: SourceRange,
225 pub cells: Vec<MarkdownTableCell>,
226 pub target_id: Option<MarkdownTargetId>,
227}
228
229impl MarkdownTableRow {
230 #[must_use]
232 pub const fn target_id(&self) -> Option<MarkdownTargetId> {
233 self.target_id
234 }
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239pub struct MarkdownTableCell {
240 pub source: SourceRange,
241 pub content: Vec<MarkdownInline>,
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246pub struct MarkdownHeading {
247 pub level: u8,
248 pub title: String,
249 pub source: SourceRange,
250 pub target_id: MarkdownTargetId,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255pub struct MarkdownTarget {
256 pub id: MarkdownTargetId,
257 pub kind: MarkdownTargetKind,
258 pub source: SourceRange,
259 pub display_label: String,
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
264pub struct MarkdownDocument {
265 source_path: Option<String>,
266 title: Option<String>,
267 source: Arc<str>,
268 blocks: Vec<MarkdownBlock>,
269 outline: Vec<MarkdownHeading>,
270 targets: Vec<MarkdownTarget>,
271}
272
273impl MarkdownDocument {
274 #[must_use]
276 pub fn new(source: impl Into<String>) -> Self {
277 Self::parse(source)
278 }
279
280 #[must_use]
282 pub fn empty() -> Self {
283 Self::parse("")
284 }
285
286 #[must_use]
288 pub fn parse(source: impl Into<String>) -> Self {
289 Self::parse_with_metadata(None, None, source)
290 }
291
292 #[must_use]
294 pub fn from_source(source: impl Into<String>) -> Self {
295 Self::parse(source)
296 }
297
298 #[must_use]
300 pub fn parse_with_metadata(
301 source_path: Option<String>,
302 title: Option<String>,
303 source: impl Into<String>,
304 ) -> Self {
305 let source: Arc<str> = Arc::from(source.into());
306 let line_starts = LineIndex::new(&source);
307 let events = Parser::new_ext(&source, parser_options())
308 .into_offset_iter()
309 .map(|(event, range)| (event.into_static(), range))
310 .collect::<Vec<_>>();
311 let roots = event_tree(&events);
312 let mut blocks = roots
313 .iter()
314 .filter_map(|node| parse_block(node, 0, &source, &line_starts))
315 .collect::<Vec<_>>();
316 let mut targets = Vec::new();
317 let mut outline = Vec::new();
318 assign_targets(&mut blocks, &mut targets, &mut outline);
319 Self {
320 source_path,
321 title,
322 source,
323 blocks,
324 outline,
325 targets,
326 }
327 }
328
329 #[must_use]
331 pub fn source(&self) -> &str {
332 &self.source
333 }
334
335 #[must_use]
337 pub fn source_path(&self) -> Option<&str> {
338 self.source_path.as_deref()
339 }
340
341 #[must_use]
343 pub fn title(&self) -> Option<&str> {
344 self.title.as_deref()
345 }
346
347 #[must_use]
349 pub fn blocks(&self) -> &[MarkdownBlock] {
350 &self.blocks
351 }
352
353 #[must_use]
355 pub fn outline(&self) -> &[MarkdownHeading] {
356 &self.outline
357 }
358
359 #[must_use]
361 pub fn targets(&self) -> &[MarkdownTarget] {
362 &self.targets
363 }
364
365 #[must_use]
367 pub fn target(&self, id: MarkdownTargetId) -> Option<&MarkdownTarget> {
368 self.targets.get(id.0)
369 }
370}
371
372#[derive(Debug, Clone)]
373struct Node {
374 event: Option<Event<'static>>,
375 range: Range<usize>,
376 children: Vec<Node>,
377}
378
379fn parser_options() -> Options {
380 Options::ENABLE_TABLES | Options::ENABLE_TASKLISTS | Options::ENABLE_STRIKETHROUGH
381}
382
383fn event_tree(events: &[(Event<'static>, Range<usize>)]) -> Vec<Node> {
384 let mut stack = vec![Node {
385 event: None,
386 range: 0..0,
387 children: Vec::new(),
388 }];
389 for (event, range) in events {
390 match event {
391 Event::Start(_) => stack.push(Node {
392 event: Some(event.clone()),
393 range: range.clone(),
394 children: Vec::new(),
395 }),
396 Event::End(_) => {
397 if stack.len() > 1
398 && let Some(mut node) = stack.pop()
399 {
400 node.range.end = cmp::max(node.range.end, range.end);
401 if let Some(parent) = stack.last_mut() {
402 parent.children.push(node);
403 }
404 }
405 }
406 _ => {
407 if let Some(parent) = stack.last_mut() {
408 parent.children.push(Node {
409 event: Some(event.clone()),
410 range: range.clone(),
411 children: Vec::new(),
412 });
413 }
414 }
415 }
416 }
417 while stack.len() > 1 {
420 if let Some(node) = stack.pop()
421 && let Some(parent) = stack.last_mut()
422 {
423 parent.children.push(node);
424 }
425 }
426 stack.pop().map_or_else(Vec::new, |root| root.children)
427}
428
429fn source_range(range: Range<usize>, index: &LineIndex) -> SourceRange {
430 SourceRange {
431 lines: index.range(range.clone()),
432 bytes: range,
433 }
434}
435
436fn parse_block(
437 node: &Node,
438 depth: usize,
439 source: &str,
440 line_index: &LineIndex,
441) -> Option<MarkdownBlock> {
442 let event = node.event.as_ref()?;
443 let kind = match event {
444 Event::Start(Tag::Heading { level, .. }) => MarkdownBlockKind::Heading {
445 level: heading_level(*level),
446 content: inline_children(&node.children),
447 },
448 Event::Start(Tag::Paragraph) => MarkdownBlockKind::Paragraph {
449 content: inline_children(&node.children),
450 },
451 Event::Start(Tag::BlockQuote(_)) => MarkdownBlockKind::BlockQuote {
452 blocks: node
453 .children
454 .iter()
455 .filter_map(|child| parse_block(child, depth, source, line_index))
456 .collect(),
457 },
458 Event::Start(Tag::List(start)) => MarkdownBlockKind::List {
459 ordered: start.is_some(),
460 start: *start,
461 items: node
462 .children
463 .iter()
464 .filter_map(|child| parse_item(child, depth, source, line_index))
465 .collect(),
466 },
467 Event::Start(Tag::CodeBlock(kind)) => {
468 MarkdownBlockKind::CodeBlock(parse_code_block(node, kind, source, line_index))
469 }
470 Event::Start(Tag::Table(alignments)) => {
471 MarkdownBlockKind::Table(parse_table(node, alignments, line_index))
472 }
473 Event::Rule => MarkdownBlockKind::Rule,
474 Event::Start(Tag::HtmlBlock) => MarkdownBlockKind::HtmlFallback {
475 content: html_fallback(&node.children),
476 },
477 _ => return None,
478 };
479 Some(MarkdownBlock {
480 source: source_range(node.range.clone(), line_index),
481 kind,
482 target_id: None,
483 })
484}
485
486fn parse_item(
487 node: &Node,
488 depth: usize,
489 source: &str,
490 line_index: &LineIndex,
491) -> Option<MarkdownListItem> {
492 if !matches!(node.event, Some(Event::Start(Tag::Item))) {
493 return None;
494 }
495 let mut content = Vec::new();
496 let mut blocks = Vec::new();
497 let mut took_paragraph = false;
498 for child in &node.children {
499 if !took_paragraph && matches!(child.event, Some(Event::Start(Tag::Paragraph))) {
500 content = inline_children(&child.children);
501 took_paragraph = true;
502 } else if let Some(inline) = inline_node(child) {
503 content.push(inline);
506 } else if let Some(block) = parse_block(child, depth + 1, source, line_index) {
507 blocks.push(block);
508 }
509 }
510 Some(MarkdownListItem {
511 depth,
512 source: source_range(node.range.clone(), line_index),
513 content,
514 blocks,
515 target_id: None,
516 })
517}
518
519fn parse_code_block(
520 node: &Node,
521 kind: &CodeBlockKind<'static>,
522 source: &str,
523 line_index: &LineIndex,
524) -> MarkdownCodeBlock {
525 let source_range_value = source_range(node.range.clone(), line_index);
526 let text = node
527 .children
528 .iter()
529 .filter_map(|child| match child.event.as_ref()? {
530 Event::Text(text) => Some(text.as_ref()),
531 _ => None,
532 })
533 .collect::<String>();
534 let (info, language) = match kind {
535 CodeBlockKind::Indented => (None, None),
536 CodeBlockKind::Fenced(info) => {
537 let info = info.to_string();
538 let language = info.split_whitespace().next().map(str::to_owned);
539 (Some(info), language)
540 }
541 };
542 let content_bytes = code_content_range(node.range.clone(), kind, source);
543 let fenced = matches!(kind, CodeBlockKind::Fenced(_));
544 let code = if fenced {
545 source
546 .get(content_bytes.clone())
547 .unwrap_or(text.as_str())
548 .to_owned()
549 } else {
550 text.strip_suffix('\n').unwrap_or(&text).to_owned()
551 };
552 let first_source_line = line_index.line_for_offset(node.range.start);
553 let lines = code.is_empty().then(Vec::new).unwrap_or_else(|| {
554 code.split('\n')
555 .enumerate()
556 .map(|(index, line)| {
557 let line_offset = code_line_offset(&content_bytes, &code, index);
558 let source_line = if fenced {
559 line_index.line_for_offset(line_offset)
560 } else {
561 first_source_line + index
562 };
563 let line_source = if fenced {
564 let end = line_offset + line.len();
565 source_range(line_offset..end, line_index)
566 } else {
567 line_index.source_line_range(source_line)
568 };
569 MarkdownCodeLine {
570 index,
571 source: line_source,
572 source_line: Some(source_line),
573 text: line.strip_suffix('\r').unwrap_or(line).to_owned(),
574 target_id: None,
575 }
576 })
577 .collect()
578 });
579 MarkdownCodeBlock {
580 language,
581 info,
582 source: source_range_value,
583 content: source_range(content_bytes, line_index),
584 lines,
585 target_id: None,
586 }
587}
588
589fn code_line_offset(content: &Range<usize>, code: &str, index: usize) -> usize {
590 content.start
591 + code
592 .split_inclusive('\n')
593 .take(index)
594 .map(str::len)
595 .sum::<usize>()
596}
597
598fn code_content_range(
599 block: Range<usize>,
600 kind: &CodeBlockKind<'static>,
601 source: &str,
602) -> Range<usize> {
603 if !matches!(kind, CodeBlockKind::Fenced(_)) {
604 return block;
605 }
606 let start = block.start.min(source.len());
607 let end = block.end.min(source.len());
608 let opening_end = source[start..end]
609 .find('\n')
610 .map_or(end, |offset| start + offset + 1);
611 if opening_end >= end {
612 return opening_end..opening_end;
613 }
614 let opening_line = source[start..opening_end].trim_end_matches(['\r', '\n']);
615 let trimmed = opening_line.trim_start();
616 let marker = trimmed.chars().next().unwrap_or('`');
617 let marker_len = trimmed
618 .chars()
619 .take_while(|character| *character == marker)
620 .count();
621 let mut closing_start = None;
622 let mut cursor = opening_end;
623 while cursor < end {
624 let line_end = source[cursor..end]
625 .find('\n')
626 .map_or(end, |offset| cursor + offset + 1);
627 let line = source[cursor..line_end].trim_end_matches(['\r', '\n']);
628 let candidate = line.trim_start();
629 let count = candidate
630 .chars()
631 .take_while(|character| *character == marker)
632 .count();
633 if count >= marker_len && marker_len > 0 && candidate[count..].trim().is_empty() {
634 closing_start = Some(cursor);
635 break;
636 }
637 cursor = line_end;
638 }
639 let mut content_end = closing_start.unwrap_or(end);
640 if closing_start.is_some() && source[..content_end].ends_with('\n') {
641 content_end -= 1;
642 if content_end > opening_end && source.as_bytes()[content_end - 1] == b'\r' {
643 content_end -= 1;
644 }
645 }
646 opening_end.min(content_end)..content_end
647}
648
649fn parse_table(node: &Node, alignments: &[Alignment], line_index: &LineIndex) -> MarkdownTable {
650 let rows = node
651 .children
652 .iter()
653 .filter_map(|section| {
654 let header = matches!(section.event, Some(Event::Start(Tag::TableHead)));
655 if !matches!(section.event, Some(Event::Start(Tag::TableHead)))
656 && !matches!(section.event, Some(Event::Start(Tag::TableRow)))
657 {
658 return None;
659 }
660 table_row(section, header, line_index)
661 })
662 .collect();
663 MarkdownTable {
664 alignments: alignments.iter().copied().map(table_alignment).collect(),
665 rows,
666 }
667}
668
669fn table_row(node: &Node, header: bool, line_index: &LineIndex) -> Option<MarkdownTableRow> {
670 if !matches!(
671 node.event,
672 Some(Event::Start(Tag::TableRow | Tag::TableHead))
673 ) {
674 return None;
675 }
676 let cells = node
677 .children
678 .iter()
679 .filter_map(|cell| {
680 if !matches!(cell.event, Some(Event::Start(Tag::TableCell))) {
681 return None;
682 }
683 Some(MarkdownTableCell {
684 source: source_range(cell.range.clone(), line_index),
685 content: inline_children(&cell.children),
686 })
687 })
688 .collect();
689 Some(MarkdownTableRow {
690 header,
691 source: source_range(node.range.clone(), line_index),
692 cells,
693 target_id: None,
694 })
695}
696
697fn table_alignment(alignment: Alignment) -> MarkdownTableAlignment {
698 match alignment {
699 Alignment::None => MarkdownTableAlignment::None,
700 Alignment::Left => MarkdownTableAlignment::Left,
701 Alignment::Center => MarkdownTableAlignment::Center,
702 Alignment::Right => MarkdownTableAlignment::Right,
703 }
704}
705
706fn inline_children(children: &[Node]) -> Vec<MarkdownInline> {
707 children.iter().filter_map(inline_node).collect()
708}
709
710fn inline_node(node: &Node) -> Option<MarkdownInline> {
711 let event = node.event.as_ref()?;
712 Some(match event {
713 Event::Text(text) => MarkdownInline::Text(text.to_string()),
714 Event::Code(text) => MarkdownInline::Code(text.to_string()),
715 Event::Start(Tag::Strong) => MarkdownInline::Strong(inline_children(&node.children)),
716 Event::Start(Tag::Emphasis) => MarkdownInline::Emphasis(inline_children(&node.children)),
717 Event::Start(Tag::Strikethrough) => {
718 MarkdownInline::Strikethrough(inline_children(&node.children))
719 }
720 Event::Start(Tag::Link {
721 dest_url, title, ..
722 }) => MarkdownInline::Link {
723 destination: dest_url.to_string(),
724 title: (!title.is_empty()).then(|| title.to_string()),
725 content: inline_children(&node.children),
726 },
727 Event::Start(Tag::Image { .. }) => {
728 MarkdownInline::ImageAlt(rendered_text(&inline_children(&node.children)))
729 }
730 Event::SoftBreak => MarkdownInline::SoftBreak,
731 Event::HardBreak => MarkdownInline::HardBreak,
732 Event::InlineHtml(html) | Event::Html(html) => MarkdownInline::Text(html.to_string()),
733 Event::TaskListMarker(checked) => MarkdownInline::Text(if *checked {
734 "☑".to_owned()
735 } else {
736 "☐".to_owned()
737 }),
738 Event::InlineMath(text) | Event::DisplayMath(text) | Event::FootnoteReference(text) => {
741 MarkdownInline::Text(text.to_string())
742 }
743 Event::Start(_) | Event::End(_) | Event::Rule => return None,
744 })
745}
746
747fn html_fallback(children: &[Node]) -> Vec<MarkdownInline> {
748 children
749 .iter()
750 .filter_map(|child| match child.event.as_ref()? {
751 Event::Html(text) | Event::InlineHtml(text) => {
752 Some(MarkdownInline::Text(text.to_string()))
753 }
754 _ => None,
755 })
756 .collect()
757}
758
759fn assign_targets(
760 blocks: &mut [MarkdownBlock],
761 targets: &mut Vec<MarkdownTarget>,
762 outline: &mut Vec<MarkdownHeading>,
763) {
764 for block in blocks {
765 let mut block_target = None;
766 match &mut block.kind {
767 MarkdownBlockKind::Heading { level, content } => {
768 let title = rendered_text(content);
769 let id = push_target(
770 MarkdownTargetKind::Heading,
771 block.source.clone(),
772 title.clone(),
773 targets,
774 );
775 block_target = Some(id);
776 outline.push(MarkdownHeading {
777 level: *level,
778 title,
779 source: block.source.clone(),
780 target_id: id,
781 });
782 }
783 MarkdownBlockKind::Paragraph { content } => {
784 block_target = Some(push_target(
785 MarkdownTargetKind::Paragraph,
786 block.source.clone(),
787 rendered_text(content),
788 targets,
789 ));
790 }
791 MarkdownBlockKind::List { items, .. } => {
792 for item in items {
793 let id = MarkdownTargetId(targets.len());
794 item.target_id = Some(id);
795 targets.push(MarkdownTarget {
796 id,
797 kind: MarkdownTargetKind::ListItem,
798 source: item.source.clone(),
799 display_label: rendered_text(&item.content),
800 });
801 assign_nested_list_targets(&mut item.blocks, targets);
802 }
803 }
804 MarkdownBlockKind::BlockQuote { blocks: children } => {
805 block_target = Some(push_target(
806 MarkdownTargetKind::BlockQuote,
807 block.source.clone(),
808 block_text(children),
809 targets,
810 ));
811 }
812 MarkdownBlockKind::CodeBlock(code) => {
813 let id = MarkdownTargetId(targets.len());
814 code.target_id = Some(id);
815 targets.push(MarkdownTarget {
816 id,
817 kind: MarkdownTargetKind::CodeBlock,
818 source: code.source.clone(),
819 display_label: "Code block".to_owned(),
820 });
821 block_target = Some(id);
822 for line in &mut code.lines {
823 let id = MarkdownTargetId(targets.len());
824 line.target_id = Some(id);
825 targets.push(MarkdownTarget {
826 id,
827 kind: MarkdownTargetKind::CodeLine,
828 source: line.source.clone(),
829 display_label: format!("Code line {}", line.index + 1),
830 });
831 }
832 }
833 MarkdownBlockKind::Table(table) => {
834 for row in &mut table.rows {
835 let id = MarkdownTargetId(targets.len());
836 row.target_id = Some(id);
837 targets.push(MarkdownTarget {
838 id,
839 kind: MarkdownTargetKind::TableRow,
840 source: row.source.clone(),
841 display_label: row_text(row),
842 });
843 }
844 }
845 MarkdownBlockKind::HtmlFallback { .. } | MarkdownBlockKind::Rule => {}
846 }
847 block.target_id = block_target;
848 }
849}
850
851fn assign_nested_list_targets(blocks: &mut [MarkdownBlock], targets: &mut Vec<MarkdownTarget>) {
852 for block in blocks {
853 if let MarkdownBlockKind::List { items, .. } = &mut block.kind {
854 for item in items {
855 let id = MarkdownTargetId(targets.len());
856 item.target_id = Some(id);
857 targets.push(MarkdownTarget {
858 id,
859 kind: MarkdownTargetKind::ListItem,
860 source: item.source.clone(),
861 display_label: rendered_text(&item.content),
862 });
863 assign_nested_list_targets(&mut item.blocks, targets);
864 }
865 }
866 }
867}
868
869fn push_target(
870 kind: MarkdownTargetKind,
871 source: SourceRange,
872 display_label: String,
873 targets: &mut Vec<MarkdownTarget>,
874) -> MarkdownTargetId {
875 let id = MarkdownTargetId(targets.len());
876 targets.push(MarkdownTarget {
877 id,
878 kind,
879 source,
880 display_label,
881 });
882 id
883}
884
885fn block_text(blocks: &[MarkdownBlock]) -> String {
886 blocks
887 .iter()
888 .map(|block| match &block.kind {
889 MarkdownBlockKind::Heading { content, .. }
890 | MarkdownBlockKind::Paragraph { content }
891 | MarkdownBlockKind::HtmlFallback { content } => rendered_text(content),
892 MarkdownBlockKind::List { items, .. } => items
893 .iter()
894 .map(|item| rendered_text(&item.content))
895 .collect::<Vec<_>>()
896 .join(" "),
897 MarkdownBlockKind::BlockQuote { blocks } => block_text(blocks),
898 MarkdownBlockKind::CodeBlock(code) => code
899 .lines
900 .iter()
901 .map(|line| line.text.as_str())
902 .collect::<Vec<_>>()
903 .join("\n"),
904 MarkdownBlockKind::Table(table) => table
905 .rows
906 .iter()
907 .map(row_text)
908 .collect::<Vec<_>>()
909 .join(" "),
910 MarkdownBlockKind::Rule => String::new(),
911 })
912 .collect::<Vec<_>>()
913 .join(" ")
914}
915
916fn row_text(row: &MarkdownTableRow) -> String {
917 row.cells
918 .iter()
919 .map(|cell| rendered_text(&cell.content))
920 .collect::<Vec<_>>()
921 .join(" | ")
922}
923
924fn heading_level(level: HeadingLevel) -> u8 {
925 match level {
926 HeadingLevel::H1 => 1,
927 HeadingLevel::H2 => 2,
928 HeadingLevel::H3 => 3,
929 HeadingLevel::H4 => 4,
930 HeadingLevel::H5 => 5,
931 HeadingLevel::H6 => 6,
932 }
933}
934
935#[derive(Debug, Clone)]
936struct LineIndex {
937 starts: Vec<usize>,
938 content_ends: Vec<usize>,
939 source_len: usize,
940}
941
942impl LineIndex {
943 fn new(source: &str) -> Self {
944 let mut starts = vec![0];
945 let mut content_ends = Vec::new();
946 for (index, byte) in source.bytes().enumerate() {
947 if byte == b'\n' {
948 content_ends.push(index.saturating_sub(usize::from(
949 index > 0 && source.as_bytes()[index - 1] == b'\r',
950 )));
951 starts.push(index + 1);
952 }
953 }
954 content_ends.push(source.len());
955 Self {
956 starts,
957 content_ends,
958 source_len: source.len(),
959 }
960 }
961
962 fn line_for_offset(&self, offset: usize) -> usize {
963 let offset = offset.min(*self.starts.last().unwrap_or(&0));
964 self.starts.partition_point(|start| *start <= offset)
965 }
966
967 fn range(&self, range: Range<usize>) -> MarkdownLineRange {
968 let start = self.line_for_offset(range.start);
969 let end_offset = range.end.saturating_sub(1).max(range.start);
970 let end = self.line_for_offset(end_offset).max(start);
971 MarkdownLineRange { start, end }
972 }
973
974 fn source_line_range(&self, line: usize) -> SourceRange {
975 let start = self
976 .starts
977 .get(line.saturating_sub(1))
978 .copied()
979 .unwrap_or(0);
980 let end = self
981 .content_ends
982 .get(line.saturating_sub(1))
983 .copied()
984 .unwrap_or(self.source_len)
985 .max(start);
986 SourceRange {
987 bytes: start..end,
988 lines: MarkdownLineRange {
989 start: line,
990 end: line,
991 },
992 }
993 }
994}
995
996#[cfg(test)]
997mod tests {
998 use super::*;
999
1000 #[test]
1001 fn parses_targets_in_document_order() {
1002 let document = MarkdownDocument::parse("# Title\n\nText\n\n```rust\nlet x = 1;\n```\n");
1003 assert_eq!(document.targets().len(), 4);
1004 assert_eq!(document.targets()[0].kind, MarkdownTargetKind::Heading);
1005 assert_eq!(document.targets()[1].kind, MarkdownTargetKind::Paragraph);
1006 assert_eq!(document.targets()[2].kind, MarkdownTargetKind::CodeBlock);
1007 assert_eq!(document.targets()[3].kind, MarkdownTargetKind::CodeLine);
1008 assert_eq!(document.outline()[0].title, "Title");
1009 }
1010
1011 #[test]
1012 fn preserves_source_and_maps_crlf_lines() {
1013 let source = "# Héading\r\n\r\n```\r\none\r\ntwo\r\n```\r\n";
1014 let document = MarkdownDocument::parse(source);
1015 assert_eq!(document.source(), source);
1016 let MarkdownBlockKind::CodeBlock(code) = &document.blocks()[1].kind else {
1017 panic!("expected code block")
1018 };
1019 assert_eq!(
1020 code.lines
1021 .iter()
1022 .map(|line| line.source_line)
1023 .collect::<Vec<_>>(),
1024 vec![Some(4), Some(5)]
1025 );
1026 assert_eq!(code.lines[0].text, "one");
1027 }
1028
1029 #[test]
1030 fn parses_nested_inline_nodes_and_table_rows() {
1031 let document = MarkdownDocument::parse(
1032 "**bold _em_** and ~~gone~~ [link](url) \n\n| A | B |\n| --- | --- |\n| 1 | 2 |\n",
1033 );
1034 assert!(matches!(
1035 document.blocks()[0].kind,
1036 MarkdownBlockKind::Paragraph { .. }
1037 ));
1038 let MarkdownBlockKind::Table(table) = &document.blocks()[1].kind else {
1039 panic!("expected table")
1040 };
1041 assert_eq!(table.rows.len(), 2);
1042 assert_eq!(
1043 document
1044 .targets()
1045 .iter()
1046 .filter(|target| target.kind == MarkdownTargetKind::TableRow)
1047 .count(),
1048 2
1049 );
1050 }
1051
1052 #[test]
1053 fn longer_outer_fence_does_not_close_on_shorter_run() {
1054 let document = MarkdownDocument::parse("````\n```\nstill code\n````\n");
1055 let MarkdownBlockKind::CodeBlock(code) = &document.blocks()[0].kind else {
1056 panic!("expected code block")
1057 };
1058 assert_eq!(code.lines.len(), 2);
1059 assert_eq!(code.lines[0].text, "```");
1060 }
1061
1062 #[test]
1063 fn blockquote_is_the_only_target_for_its_contents() {
1064 let document = MarkdownDocument::parse("> # quoted\n>\n> text\n");
1065 assert_eq!(document.targets().len(), 1);
1066 assert_eq!(document.targets()[0].kind, MarkdownTargetKind::BlockQuote);
1067 }
1068
1069 #[test]
1070 fn nested_list_items_are_targets_without_overlapping_paragraph_targets() {
1071 let document = MarkdownDocument::parse("1. outer\n - inner\n - second\n2. last\n");
1072 let kinds = document
1073 .targets()
1074 .iter()
1075 .map(|target| target.kind)
1076 .collect::<Vec<_>>();
1077 assert_eq!(
1078 kinds,
1079 vec![
1080 MarkdownTargetKind::ListItem,
1081 MarkdownTargetKind::ListItem,
1082 MarkdownTargetKind::ListItem,
1083 MarkdownTargetKind::ListItem,
1084 ]
1085 );
1086 let MarkdownBlockKind::List { items, .. } = &document.blocks()[0].kind else {
1087 panic!("expected list")
1088 };
1089 assert_eq!(items[0].depth, 0);
1090 assert_eq!(items[0].blocks.len(), 1);
1091 }
1092
1093 #[test]
1094 fn indented_code_strips_parser_indentation() {
1095 let document = MarkdownDocument::parse(" first\n second\n");
1096 let MarkdownBlockKind::CodeBlock(code) = &document.blocks()[0].kind else {
1097 panic!("expected code block")
1098 };
1099 assert_eq!(
1100 code.lines
1101 .iter()
1102 .map(|line| line.text.as_str())
1103 .collect::<Vec<_>>(),
1104 ["first", "second"]
1105 );
1106 assert_eq!(code.lines[0].source_line, Some(1));
1107 assert_eq!(code.lines[1].source.bytes, 10..20);
1108 }
1109
1110 #[test]
1111 fn indented_code_preserves_blank_content_lines() {
1112 let document = MarkdownDocument::parse(" one\n\n two\n");
1113 let MarkdownBlockKind::CodeBlock(code) = &document.blocks()[0].kind else {
1114 panic!("expected code block")
1115 };
1116 assert_eq!(
1117 code.lines
1118 .iter()
1119 .map(|line| line.text.as_str())
1120 .collect::<Vec<_>>(),
1121 ["one", "", "two"]
1122 );
1123 assert_eq!(
1124 code.lines
1125 .iter()
1126 .map(|line| line.source_line)
1127 .collect::<Vec<_>>(),
1128 [Some(1), Some(2), Some(3)]
1129 );
1130 }
1131
1132 #[test]
1133 fn raw_html_is_plain_fallback_without_a_target() {
1134 let document = MarkdownDocument::parse("<div>not executed</div>\n");
1135 assert!(matches!(
1136 document.blocks()[0].kind,
1137 MarkdownBlockKind::HtmlFallback { .. }
1138 ));
1139 assert!(document.targets().is_empty());
1140 }
1141
1142 #[test]
1143 fn empty_and_malformed_documents_are_safe() {
1144 assert!(MarkdownDocument::parse("").targets().is_empty());
1145 let document = MarkdownDocument::parse("# unclosed **emphasis\n\n```rust\ncode");
1146 assert!(!document.blocks().is_empty());
1147 }
1148}