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