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, Copy, PartialEq, Eq, Serialize, Deserialize)]
263pub enum MarkdownSourceRole {
264 Heading,
265 Link,
266 Quote,
267 Code,
268 Strong,
269 Emphasis,
270 Strikethrough,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
274pub struct MarkdownSourceStyle {
275 pub source: SourceRange,
276 pub role: MarkdownSourceRole,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
281pub struct MarkdownDocument {
282 source_path: Option<String>,
283 title: Option<String>,
284 source: Arc<str>,
285 blocks: Vec<MarkdownBlock>,
286 outline: Vec<MarkdownHeading>,
287 targets: Vec<MarkdownTarget>,
288 source_styles: Vec<MarkdownSourceStyle>,
289}
290
291impl MarkdownDocument {
292 #[must_use]
294 pub fn new(source: impl Into<String>) -> Self {
295 Self::parse(source)
296 }
297
298 #[must_use]
300 pub fn empty() -> Self {
301 Self::parse("")
302 }
303
304 #[must_use]
306 pub fn parse(source: impl Into<String>) -> Self {
307 Self::parse_with_metadata(None, None, source)
308 }
309
310 #[must_use]
312 pub fn from_source(source: impl Into<String>) -> Self {
313 Self::parse(source)
314 }
315
316 #[must_use]
318 pub fn parse_with_metadata(
319 source_path: Option<String>,
320 title: Option<String>,
321 source: impl Into<String>,
322 ) -> Self {
323 let source: Arc<str> = Arc::from(source.into());
324 let line_starts = LineIndex::new(&source);
325 let events = Parser::new_ext(&source, parser_options())
326 .into_offset_iter()
327 .map(|(event, range)| (event.into_static(), range))
328 .collect::<Vec<_>>();
329 let roots = event_tree(&events);
330 let mut source_styles = Vec::new();
331 collect_source_styles(&roots, &line_starts, &mut source_styles);
332 let mut blocks = roots
333 .iter()
334 .filter_map(|node| parse_block(node, 0, &source, &line_starts))
335 .collect::<Vec<_>>();
336 let mut targets = Vec::new();
337 let mut outline = Vec::new();
338 assign_targets(&mut blocks, &mut targets, &mut outline);
339 Self {
340 source_path,
341 title,
342 source,
343 blocks,
344 outline,
345 targets,
346 source_styles,
347 }
348 }
349
350 #[must_use]
351 pub fn source_styles(&self) -> &[MarkdownSourceStyle] {
352 &self.source_styles
353 }
354
355 #[must_use]
357 pub fn source(&self) -> &str {
358 &self.source
359 }
360
361 #[must_use]
363 pub fn source_path(&self) -> Option<&str> {
364 self.source_path.as_deref()
365 }
366
367 #[must_use]
369 pub fn title(&self) -> Option<&str> {
370 self.title.as_deref()
371 }
372
373 #[must_use]
375 pub fn blocks(&self) -> &[MarkdownBlock] {
376 &self.blocks
377 }
378
379 #[must_use]
381 pub fn outline(&self) -> &[MarkdownHeading] {
382 &self.outline
383 }
384
385 #[must_use]
387 pub fn targets(&self) -> &[MarkdownTarget] {
388 &self.targets
389 }
390
391 #[must_use]
393 pub fn target(&self, id: MarkdownTargetId) -> Option<&MarkdownTarget> {
394 self.targets.get(id.0)
395 }
396
397 #[must_use]
398 pub fn code_blocks(&self) -> Vec<&MarkdownCodeBlock> {
399 fn collect<'a>(blocks: &'a [MarkdownBlock], output: &mut Vec<&'a MarkdownCodeBlock>) {
400 for block in blocks {
401 match &block.kind {
402 MarkdownBlockKind::CodeBlock(code) => output.push(code),
403 MarkdownBlockKind::List { items, .. } => {
404 for item in items {
405 collect(&item.blocks, output);
406 }
407 }
408 MarkdownBlockKind::BlockQuote { blocks } => collect(blocks, output),
409 _ => {}
410 }
411 }
412 }
413 let mut output = Vec::new();
414 collect(&self.blocks, &mut output);
415 output
416 }
417}
418
419#[derive(Debug, Clone)]
420struct Node {
421 event: Option<Event<'static>>,
422 range: Range<usize>,
423 children: Vec<Node>,
424}
425
426fn parser_options() -> Options {
427 Options::ENABLE_TABLES | Options::ENABLE_TASKLISTS | Options::ENABLE_STRIKETHROUGH
428}
429
430fn event_tree(events: &[(Event<'static>, Range<usize>)]) -> Vec<Node> {
431 let mut stack = vec![Node {
432 event: None,
433 range: 0..0,
434 children: Vec::new(),
435 }];
436 for (event, range) in events {
437 match event {
438 Event::Start(_) => stack.push(Node {
439 event: Some(event.clone()),
440 range: range.clone(),
441 children: Vec::new(),
442 }),
443 Event::End(_) => {
444 if stack.len() > 1
445 && let Some(mut node) = stack.pop()
446 {
447 node.range.end = cmp::max(node.range.end, range.end);
448 if let Some(parent) = stack.last_mut() {
449 parent.children.push(node);
450 }
451 }
452 }
453 _ => {
454 if let Some(parent) = stack.last_mut() {
455 parent.children.push(Node {
456 event: Some(event.clone()),
457 range: range.clone(),
458 children: Vec::new(),
459 });
460 }
461 }
462 }
463 }
464 while stack.len() > 1 {
467 if let Some(node) = stack.pop()
468 && let Some(parent) = stack.last_mut()
469 {
470 parent.children.push(node);
471 }
472 }
473 stack.pop().map_or_else(Vec::new, |root| root.children)
474}
475
476fn collect_source_styles(nodes: &[Node], index: &LineIndex, output: &mut Vec<MarkdownSourceStyle>) {
477 for node in nodes {
478 let role = match node.event.as_ref() {
479 Some(Event::Start(Tag::Heading { .. })) => Some(MarkdownSourceRole::Heading),
480 Some(Event::Start(Tag::Link { .. })) => Some(MarkdownSourceRole::Link),
481 Some(Event::Start(Tag::BlockQuote(_))) => Some(MarkdownSourceRole::Quote),
482 Some(Event::Start(Tag::Strong)) => Some(MarkdownSourceRole::Strong),
483 Some(Event::Start(Tag::Emphasis)) => Some(MarkdownSourceRole::Emphasis),
484 Some(Event::Start(Tag::Strikethrough)) => Some(MarkdownSourceRole::Strikethrough),
485 Some(Event::Code(_)) => Some(MarkdownSourceRole::Code),
486 _ => None,
487 };
488 if let Some(role) = role {
489 output.push(MarkdownSourceStyle {
490 source: source_range(node.range.clone(), index),
491 role,
492 });
493 }
494 collect_source_styles(&node.children, index, output);
495 }
496}
497
498fn source_range(range: Range<usize>, index: &LineIndex) -> SourceRange {
499 SourceRange {
500 lines: index.range(range.clone()),
501 bytes: range,
502 }
503}
504
505fn parse_block(
506 node: &Node,
507 depth: usize,
508 source: &str,
509 line_index: &LineIndex,
510) -> Option<MarkdownBlock> {
511 let event = node.event.as_ref()?;
512 let kind = match event {
513 Event::Start(Tag::Heading { level, .. }) => MarkdownBlockKind::Heading {
514 level: heading_level(*level),
515 content: inline_children(&node.children),
516 },
517 Event::Start(Tag::Paragraph) => MarkdownBlockKind::Paragraph {
518 content: inline_children(&node.children),
519 },
520 Event::Start(Tag::BlockQuote(_)) => MarkdownBlockKind::BlockQuote {
521 blocks: node
522 .children
523 .iter()
524 .filter_map(|child| parse_block(child, depth, source, line_index))
525 .collect(),
526 },
527 Event::Start(Tag::List(start)) => MarkdownBlockKind::List {
528 ordered: start.is_some(),
529 start: *start,
530 items: node
531 .children
532 .iter()
533 .filter_map(|child| parse_item(child, depth, source, line_index))
534 .collect(),
535 },
536 Event::Start(Tag::CodeBlock(kind)) => {
537 MarkdownBlockKind::CodeBlock(parse_code_block(node, kind, source, line_index))
538 }
539 Event::Start(Tag::Table(alignments)) => {
540 MarkdownBlockKind::Table(parse_table(node, alignments, line_index))
541 }
542 Event::Rule => MarkdownBlockKind::Rule,
543 Event::Start(Tag::HtmlBlock) => MarkdownBlockKind::HtmlFallback {
544 content: html_fallback(&node.children),
545 },
546 _ => return None,
547 };
548 Some(MarkdownBlock {
549 source: source_range(node.range.clone(), line_index),
550 kind,
551 target_id: None,
552 })
553}
554
555fn parse_item(
556 node: &Node,
557 depth: usize,
558 source: &str,
559 line_index: &LineIndex,
560) -> Option<MarkdownListItem> {
561 if !matches!(node.event, Some(Event::Start(Tag::Item))) {
562 return None;
563 }
564 let mut content = Vec::new();
565 let mut blocks = Vec::new();
566 let mut took_paragraph = false;
567 for child in &node.children {
568 if !took_paragraph && matches!(child.event, Some(Event::Start(Tag::Paragraph))) {
569 content = inline_children(&child.children);
570 took_paragraph = true;
571 } else if let Some(inline) = inline_node(child) {
572 content.push(inline);
575 } else if let Some(block) = parse_block(child, depth + 1, source, line_index) {
576 blocks.push(block);
577 }
578 }
579 Some(MarkdownListItem {
580 depth,
581 source: source_range(node.range.clone(), line_index),
582 content,
583 blocks,
584 target_id: None,
585 })
586}
587
588fn parse_code_block(
589 node: &Node,
590 kind: &CodeBlockKind<'static>,
591 source: &str,
592 line_index: &LineIndex,
593) -> MarkdownCodeBlock {
594 let source_range_value = source_range(node.range.clone(), line_index);
595 let text = node
596 .children
597 .iter()
598 .filter_map(|child| match child.event.as_ref()? {
599 Event::Text(text) => Some(text.as_ref()),
600 _ => None,
601 })
602 .collect::<String>();
603 let (info, language) = match kind {
604 CodeBlockKind::Indented => (None, None),
605 CodeBlockKind::Fenced(info) => {
606 let info = info.to_string();
607 let language = info.split_whitespace().next().map(str::to_owned);
608 (Some(info), language)
609 }
610 };
611 let content_bytes = code_content_range(node.range.clone(), kind, source);
612 let fenced = matches!(kind, CodeBlockKind::Fenced(_));
613 let code = if fenced {
614 source
615 .get(content_bytes.clone())
616 .unwrap_or(text.as_str())
617 .to_owned()
618 } else {
619 text.strip_suffix('\n').unwrap_or(&text).to_owned()
620 };
621 let first_source_line = line_index.line_for_offset(node.range.start);
622 let lines = code.is_empty().then(Vec::new).unwrap_or_else(|| {
623 code.split('\n')
624 .enumerate()
625 .map(|(index, line)| {
626 let line_offset = code_line_offset(&content_bytes, &code, index);
627 let source_line = if fenced {
628 line_index.line_for_offset(line_offset)
629 } else {
630 first_source_line + index
631 };
632 let line_source = if fenced {
633 let end = line_offset + line.len();
634 source_range(line_offset..end, line_index)
635 } else {
636 line_index.source_line_range(source_line)
637 };
638 MarkdownCodeLine {
639 index,
640 source: line_source,
641 source_line: Some(source_line),
642 text: line.strip_suffix('\r').unwrap_or(line).to_owned(),
643 target_id: None,
644 }
645 })
646 .collect()
647 });
648 MarkdownCodeBlock {
649 language,
650 info,
651 source: source_range_value,
652 content: source_range(content_bytes, line_index),
653 lines,
654 target_id: None,
655 }
656}
657
658fn code_line_offset(content: &Range<usize>, code: &str, index: usize) -> usize {
659 content.start
660 + code
661 .split_inclusive('\n')
662 .take(index)
663 .map(str::len)
664 .sum::<usize>()
665}
666
667fn code_content_range(
668 block: Range<usize>,
669 kind: &CodeBlockKind<'static>,
670 source: &str,
671) -> Range<usize> {
672 if !matches!(kind, CodeBlockKind::Fenced(_)) {
673 return block;
674 }
675 let start = block.start.min(source.len());
676 let end = block.end.min(source.len());
677 let opening_end = source[start..end]
678 .find('\n')
679 .map_or(end, |offset| start + offset + 1);
680 if opening_end >= end {
681 return opening_end..opening_end;
682 }
683 let opening_line = source[start..opening_end].trim_end_matches(['\r', '\n']);
684 let trimmed = opening_line.trim_start();
685 let marker = trimmed.chars().next().unwrap_or('`');
686 let marker_len = trimmed
687 .chars()
688 .take_while(|character| *character == marker)
689 .count();
690 let mut closing_start = None;
691 let mut cursor = opening_end;
692 while cursor < end {
693 let line_end = source[cursor..end]
694 .find('\n')
695 .map_or(end, |offset| cursor + offset + 1);
696 let line = source[cursor..line_end].trim_end_matches(['\r', '\n']);
697 let candidate = line.trim_start();
698 let count = candidate
699 .chars()
700 .take_while(|character| *character == marker)
701 .count();
702 if count >= marker_len && marker_len > 0 && candidate[count..].trim().is_empty() {
703 closing_start = Some(cursor);
704 break;
705 }
706 cursor = line_end;
707 }
708 let mut content_end = closing_start.unwrap_or(end);
709 if closing_start.is_some() && source[..content_end].ends_with('\n') {
710 content_end -= 1;
711 if content_end > opening_end && source.as_bytes()[content_end - 1] == b'\r' {
712 content_end -= 1;
713 }
714 }
715 opening_end.min(content_end)..content_end
716}
717
718fn parse_table(node: &Node, alignments: &[Alignment], line_index: &LineIndex) -> MarkdownTable {
719 let rows = node
720 .children
721 .iter()
722 .filter_map(|section| {
723 let header = matches!(section.event, Some(Event::Start(Tag::TableHead)));
724 if !matches!(section.event, Some(Event::Start(Tag::TableHead)))
725 && !matches!(section.event, Some(Event::Start(Tag::TableRow)))
726 {
727 return None;
728 }
729 table_row(section, header, line_index)
730 })
731 .collect();
732 MarkdownTable {
733 alignments: alignments.iter().copied().map(table_alignment).collect(),
734 rows,
735 }
736}
737
738fn table_row(node: &Node, header: bool, line_index: &LineIndex) -> Option<MarkdownTableRow> {
739 if !matches!(
740 node.event,
741 Some(Event::Start(Tag::TableRow | Tag::TableHead))
742 ) {
743 return None;
744 }
745 let cells = node
746 .children
747 .iter()
748 .filter_map(|cell| {
749 if !matches!(cell.event, Some(Event::Start(Tag::TableCell))) {
750 return None;
751 }
752 Some(MarkdownTableCell {
753 source: source_range(cell.range.clone(), line_index),
754 content: inline_children(&cell.children),
755 })
756 })
757 .collect();
758 Some(MarkdownTableRow {
759 header,
760 source: source_range(node.range.clone(), line_index),
761 cells,
762 target_id: None,
763 })
764}
765
766fn table_alignment(alignment: Alignment) -> MarkdownTableAlignment {
767 match alignment {
768 Alignment::None => MarkdownTableAlignment::None,
769 Alignment::Left => MarkdownTableAlignment::Left,
770 Alignment::Center => MarkdownTableAlignment::Center,
771 Alignment::Right => MarkdownTableAlignment::Right,
772 }
773}
774
775fn inline_children(children: &[Node]) -> Vec<MarkdownInline> {
776 children.iter().filter_map(inline_node).collect()
777}
778
779fn inline_node(node: &Node) -> Option<MarkdownInline> {
780 let event = node.event.as_ref()?;
781 Some(match event {
782 Event::Text(text) => MarkdownInline::Text(text.to_string()),
783 Event::Code(text) => MarkdownInline::Code(text.to_string()),
784 Event::Start(Tag::Strong) => MarkdownInline::Strong(inline_children(&node.children)),
785 Event::Start(Tag::Emphasis) => MarkdownInline::Emphasis(inline_children(&node.children)),
786 Event::Start(Tag::Strikethrough) => {
787 MarkdownInline::Strikethrough(inline_children(&node.children))
788 }
789 Event::Start(Tag::Link {
790 dest_url, title, ..
791 }) => MarkdownInline::Link {
792 destination: dest_url.to_string(),
793 title: (!title.is_empty()).then(|| title.to_string()),
794 content: inline_children(&node.children),
795 },
796 Event::Start(Tag::Image { .. }) => {
797 MarkdownInline::ImageAlt(rendered_text(&inline_children(&node.children)))
798 }
799 Event::SoftBreak => MarkdownInline::SoftBreak,
800 Event::HardBreak => MarkdownInline::HardBreak,
801 Event::InlineHtml(html) | Event::Html(html) => MarkdownInline::Text(html.to_string()),
802 Event::TaskListMarker(checked) => MarkdownInline::Text(if *checked {
803 "☑".to_owned()
804 } else {
805 "☐".to_owned()
806 }),
807 Event::InlineMath(text) | Event::DisplayMath(text) | Event::FootnoteReference(text) => {
810 MarkdownInline::Text(text.to_string())
811 }
812 Event::Start(_) | Event::End(_) | Event::Rule => return None,
813 })
814}
815
816fn html_fallback(children: &[Node]) -> Vec<MarkdownInline> {
817 children
818 .iter()
819 .filter_map(|child| match child.event.as_ref()? {
820 Event::Html(text) | Event::InlineHtml(text) => {
821 Some(MarkdownInline::Text(text.to_string()))
822 }
823 _ => None,
824 })
825 .collect()
826}
827
828fn assign_targets(
829 blocks: &mut [MarkdownBlock],
830 targets: &mut Vec<MarkdownTarget>,
831 outline: &mut Vec<MarkdownHeading>,
832) {
833 for block in blocks {
834 let mut block_target = None;
835 match &mut block.kind {
836 MarkdownBlockKind::Heading { level, content } => {
837 let title = rendered_text(content);
838 let id = push_target(
839 MarkdownTargetKind::Heading,
840 block.source.clone(),
841 title.clone(),
842 targets,
843 );
844 block_target = Some(id);
845 outline.push(MarkdownHeading {
846 level: *level,
847 title,
848 source: block.source.clone(),
849 target_id: id,
850 });
851 }
852 MarkdownBlockKind::Paragraph { content } => {
853 block_target = Some(push_target(
854 MarkdownTargetKind::Paragraph,
855 block.source.clone(),
856 rendered_text(content),
857 targets,
858 ));
859 }
860 MarkdownBlockKind::List { items, .. } => {
861 for item in items {
862 let id = MarkdownTargetId(targets.len());
863 item.target_id = Some(id);
864 targets.push(MarkdownTarget {
865 id,
866 kind: MarkdownTargetKind::ListItem,
867 source: item.source.clone(),
868 display_label: rendered_text(&item.content),
869 });
870 assign_nested_list_targets(&mut item.blocks, targets);
871 }
872 }
873 MarkdownBlockKind::BlockQuote { blocks: children } => {
874 block_target = Some(push_target(
875 MarkdownTargetKind::BlockQuote,
876 block.source.clone(),
877 block_text(children),
878 targets,
879 ));
880 }
881 MarkdownBlockKind::CodeBlock(code) => {
882 let id = MarkdownTargetId(targets.len());
883 code.target_id = Some(id);
884 targets.push(MarkdownTarget {
885 id,
886 kind: MarkdownTargetKind::CodeBlock,
887 source: code.source.clone(),
888 display_label: "Code block".to_owned(),
889 });
890 block_target = Some(id);
891 for line in &mut code.lines {
892 let id = MarkdownTargetId(targets.len());
893 line.target_id = Some(id);
894 targets.push(MarkdownTarget {
895 id,
896 kind: MarkdownTargetKind::CodeLine,
897 source: line.source.clone(),
898 display_label: format!("Code line {}", line.index + 1),
899 });
900 }
901 }
902 MarkdownBlockKind::Table(table) => {
903 for row in &mut table.rows {
904 let id = MarkdownTargetId(targets.len());
905 row.target_id = Some(id);
906 targets.push(MarkdownTarget {
907 id,
908 kind: MarkdownTargetKind::TableRow,
909 source: row.source.clone(),
910 display_label: row_text(row),
911 });
912 }
913 }
914 MarkdownBlockKind::HtmlFallback { .. } | MarkdownBlockKind::Rule => {}
915 }
916 block.target_id = block_target;
917 }
918}
919
920fn assign_nested_list_targets(blocks: &mut [MarkdownBlock], targets: &mut Vec<MarkdownTarget>) {
921 for block in blocks {
922 if let MarkdownBlockKind::List { items, .. } = &mut block.kind {
923 for item in items {
924 let id = MarkdownTargetId(targets.len());
925 item.target_id = Some(id);
926 targets.push(MarkdownTarget {
927 id,
928 kind: MarkdownTargetKind::ListItem,
929 source: item.source.clone(),
930 display_label: rendered_text(&item.content),
931 });
932 assign_nested_list_targets(&mut item.blocks, targets);
933 }
934 }
935 }
936}
937
938fn push_target(
939 kind: MarkdownTargetKind,
940 source: SourceRange,
941 display_label: String,
942 targets: &mut Vec<MarkdownTarget>,
943) -> MarkdownTargetId {
944 let id = MarkdownTargetId(targets.len());
945 targets.push(MarkdownTarget {
946 id,
947 kind,
948 source,
949 display_label,
950 });
951 id
952}
953
954fn block_text(blocks: &[MarkdownBlock]) -> String {
955 blocks
956 .iter()
957 .map(|block| match &block.kind {
958 MarkdownBlockKind::Heading { content, .. }
959 | MarkdownBlockKind::Paragraph { content }
960 | MarkdownBlockKind::HtmlFallback { content } => rendered_text(content),
961 MarkdownBlockKind::List { items, .. } => items
962 .iter()
963 .map(|item| rendered_text(&item.content))
964 .collect::<Vec<_>>()
965 .join(" "),
966 MarkdownBlockKind::BlockQuote { blocks } => block_text(blocks),
967 MarkdownBlockKind::CodeBlock(code) => code
968 .lines
969 .iter()
970 .map(|line| line.text.as_str())
971 .collect::<Vec<_>>()
972 .join("\n"),
973 MarkdownBlockKind::Table(table) => table
974 .rows
975 .iter()
976 .map(row_text)
977 .collect::<Vec<_>>()
978 .join(" "),
979 MarkdownBlockKind::Rule => String::new(),
980 })
981 .collect::<Vec<_>>()
982 .join(" ")
983}
984
985fn row_text(row: &MarkdownTableRow) -> String {
986 row.cells
987 .iter()
988 .map(|cell| rendered_text(&cell.content))
989 .collect::<Vec<_>>()
990 .join(" | ")
991}
992
993fn heading_level(level: HeadingLevel) -> u8 {
994 match level {
995 HeadingLevel::H1 => 1,
996 HeadingLevel::H2 => 2,
997 HeadingLevel::H3 => 3,
998 HeadingLevel::H4 => 4,
999 HeadingLevel::H5 => 5,
1000 HeadingLevel::H6 => 6,
1001 }
1002}
1003
1004#[derive(Debug, Clone)]
1005struct LineIndex {
1006 starts: Vec<usize>,
1007 content_ends: Vec<usize>,
1008 source_len: usize,
1009}
1010
1011impl LineIndex {
1012 fn new(source: &str) -> Self {
1013 let mut starts = vec![0];
1014 let mut content_ends = Vec::new();
1015 for (index, byte) in source.bytes().enumerate() {
1016 if byte == b'\n' {
1017 content_ends.push(index.saturating_sub(usize::from(
1018 index > 0 && source.as_bytes()[index - 1] == b'\r',
1019 )));
1020 starts.push(index + 1);
1021 }
1022 }
1023 content_ends.push(source.len());
1024 Self {
1025 starts,
1026 content_ends,
1027 source_len: source.len(),
1028 }
1029 }
1030
1031 fn line_for_offset(&self, offset: usize) -> usize {
1032 let offset = offset.min(*self.starts.last().unwrap_or(&0));
1033 self.starts.partition_point(|start| *start <= offset)
1034 }
1035
1036 fn range(&self, range: Range<usize>) -> MarkdownLineRange {
1037 let start = self.line_for_offset(range.start);
1038 let end_offset = range.end.saturating_sub(1).max(range.start);
1039 let end = self.line_for_offset(end_offset).max(start);
1040 MarkdownLineRange { start, end }
1041 }
1042
1043 fn source_line_range(&self, line: usize) -> SourceRange {
1044 let start = self
1045 .starts
1046 .get(line.saturating_sub(1))
1047 .copied()
1048 .unwrap_or(0);
1049 let end = self
1050 .content_ends
1051 .get(line.saturating_sub(1))
1052 .copied()
1053 .unwrap_or(self.source_len)
1054 .max(start);
1055 SourceRange {
1056 bytes: start..end,
1057 lines: MarkdownLineRange {
1058 start: line,
1059 end: line,
1060 },
1061 }
1062 }
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067 use super::*;
1068
1069 #[test]
1070 fn parses_targets_in_document_order() {
1071 let document = MarkdownDocument::parse("# Title\n\nText\n\n```rust\nlet x = 1;\n```\n");
1072 assert_eq!(document.targets().len(), 4);
1073 assert_eq!(document.targets()[0].kind, MarkdownTargetKind::Heading);
1074 assert_eq!(document.targets()[1].kind, MarkdownTargetKind::Paragraph);
1075 assert_eq!(document.targets()[2].kind, MarkdownTargetKind::CodeBlock);
1076 assert_eq!(document.targets()[3].kind, MarkdownTargetKind::CodeLine);
1077 assert_eq!(document.outline()[0].title, "Title");
1078 }
1079
1080 #[test]
1081 fn preserves_source_and_maps_crlf_lines() {
1082 let source = "# Héading\r\n\r\n```\r\none\r\ntwo\r\n```\r\n";
1083 let document = MarkdownDocument::parse(source);
1084 assert_eq!(document.source(), source);
1085 let MarkdownBlockKind::CodeBlock(code) = &document.blocks()[1].kind else {
1086 panic!("expected code block")
1087 };
1088 assert_eq!(
1089 code.lines
1090 .iter()
1091 .map(|line| line.source_line)
1092 .collect::<Vec<_>>(),
1093 vec![Some(4), Some(5)]
1094 );
1095 assert_eq!(code.lines[0].text, "one");
1096 }
1097
1098 #[test]
1099 fn parses_nested_inline_nodes_and_table_rows() {
1100 let document = MarkdownDocument::parse(
1101 "**bold _em_** and ~~gone~~ [link](url) \n\n| A | B |\n| --- | --- |\n| 1 | 2 |\n",
1102 );
1103 assert!(matches!(
1104 document.blocks()[0].kind,
1105 MarkdownBlockKind::Paragraph { .. }
1106 ));
1107 let MarkdownBlockKind::Table(table) = &document.blocks()[1].kind else {
1108 panic!("expected table")
1109 };
1110 assert_eq!(table.rows.len(), 2);
1111 assert_eq!(
1112 document
1113 .targets()
1114 .iter()
1115 .filter(|target| target.kind == MarkdownTargetKind::TableRow)
1116 .count(),
1117 2
1118 );
1119 }
1120
1121 #[test]
1122 fn longer_outer_fence_does_not_close_on_shorter_run() {
1123 let document = MarkdownDocument::parse("````\n```\nstill code\n````\n");
1124 let MarkdownBlockKind::CodeBlock(code) = &document.blocks()[0].kind else {
1125 panic!("expected code block")
1126 };
1127 assert_eq!(code.lines.len(), 2);
1128 assert_eq!(code.lines[0].text, "```");
1129 }
1130
1131 #[test]
1132 fn blockquote_is_the_only_target_for_its_contents() {
1133 let document = MarkdownDocument::parse("> # quoted\n>\n> text\n");
1134 assert_eq!(document.targets().len(), 1);
1135 assert_eq!(document.targets()[0].kind, MarkdownTargetKind::BlockQuote);
1136 }
1137
1138 #[test]
1139 fn nested_list_items_are_targets_without_overlapping_paragraph_targets() {
1140 let document = MarkdownDocument::parse("1. outer\n - inner\n - second\n2. last\n");
1141 let kinds = document
1142 .targets()
1143 .iter()
1144 .map(|target| target.kind)
1145 .collect::<Vec<_>>();
1146 assert_eq!(
1147 kinds,
1148 vec![
1149 MarkdownTargetKind::ListItem,
1150 MarkdownTargetKind::ListItem,
1151 MarkdownTargetKind::ListItem,
1152 MarkdownTargetKind::ListItem,
1153 ]
1154 );
1155 let MarkdownBlockKind::List { items, .. } = &document.blocks()[0].kind else {
1156 panic!("expected list")
1157 };
1158 assert_eq!(items[0].depth, 0);
1159 assert_eq!(items[0].blocks.len(), 1);
1160 }
1161
1162 #[test]
1163 fn indented_code_strips_parser_indentation() {
1164 let document = MarkdownDocument::parse(" first\n second\n");
1165 let MarkdownBlockKind::CodeBlock(code) = &document.blocks()[0].kind else {
1166 panic!("expected code block")
1167 };
1168 assert_eq!(
1169 code.lines
1170 .iter()
1171 .map(|line| line.text.as_str())
1172 .collect::<Vec<_>>(),
1173 ["first", "second"]
1174 );
1175 assert_eq!(code.lines[0].source_line, Some(1));
1176 assert_eq!(code.lines[1].source.bytes, 10..20);
1177 }
1178
1179 #[test]
1180 fn indented_code_preserves_blank_content_lines() {
1181 let document = MarkdownDocument::parse(" one\n\n two\n");
1182 let MarkdownBlockKind::CodeBlock(code) = &document.blocks()[0].kind else {
1183 panic!("expected code block")
1184 };
1185 assert_eq!(
1186 code.lines
1187 .iter()
1188 .map(|line| line.text.as_str())
1189 .collect::<Vec<_>>(),
1190 ["one", "", "two"]
1191 );
1192 assert_eq!(
1193 code.lines
1194 .iter()
1195 .map(|line| line.source_line)
1196 .collect::<Vec<_>>(),
1197 [Some(1), Some(2), Some(3)]
1198 );
1199 }
1200
1201 #[test]
1202 fn raw_html_is_plain_fallback_without_a_target() {
1203 let document = MarkdownDocument::parse("<div>not executed</div>\n");
1204 assert!(matches!(
1205 document.blocks()[0].kind,
1206 MarkdownBlockKind::HtmlFallback { .. }
1207 ));
1208 assert!(document.targets().is_empty());
1209 }
1210
1211 #[test]
1212 fn empty_and_malformed_documents_are_safe() {
1213 assert!(MarkdownDocument::parse("").targets().is_empty());
1214 let document = MarkdownDocument::parse("# unclosed **emphasis\n\n```rust\ncode");
1215 assert!(!document.blocks().is_empty());
1216 }
1217}