use_markdown/
code_fence.rs1use crate::frontmatter::frontmatter_line_count;
2
3#[derive(Clone, Debug, Eq, PartialEq)]
5pub struct MarkdownCodeFence {
6 pub language: Option<String>,
8 pub content: String,
10 pub start_line: usize,
12 pub end_line: usize,
14}
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub(crate) struct FenceDelimiter {
18 pub character: char,
19 pub length: usize,
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub(crate) struct FenceOpening<'a> {
24 pub delimiter: FenceDelimiter,
25 pub info: &'a str,
26}
27
28pub fn extract_code_fences(markdown: &str) -> Vec<MarkdownCodeFence> {
30 let mut fences = Vec::new();
31 let frontmatter_lines = frontmatter_line_count(markdown);
32 let total_lines = markdown.lines().count();
33 let mut current: Option<(FenceDelimiter, Option<String>, usize, Vec<String>)> = None;
34
35 for (index, line) in markdown.lines().enumerate() {
36 if index < frontmatter_lines {
37 continue;
38 }
39
40 let line_number = index + 1;
41
42 if let Some((delimiter, _, _, _)) = current {
43 if is_closing_fence(line, delimiter) {
44 let (delimiter, language, start_line, content) = current
45 .take()
46 .expect("fence state should exist when closing");
47 let _ = delimiter;
48 fences.push(MarkdownCodeFence {
49 language,
50 content: content.join("\n"),
51 start_line,
52 end_line: line_number,
53 });
54 continue;
55 }
56
57 if let Some((_, _, _, content)) = current.as_mut() {
58 content.push(line.to_owned());
59 }
60 continue;
61 }
62
63 if let Some(opening) = parse_opening_fence(line) {
64 current = Some((
65 opening.delimiter,
66 language_from_info(opening.info),
67 line_number,
68 Vec::new(),
69 ));
70 }
71 }
72
73 if let Some((_, language, start_line, content)) = current {
74 fences.push(MarkdownCodeFence {
75 language,
76 content: content.join("\n"),
77 start_line,
78 end_line: total_lines.max(start_line),
79 });
80 }
81
82 fences
83}
84
85pub(crate) fn parse_opening_fence(line: &str) -> Option<FenceOpening<'_>> {
86 let candidate = crate::strip_up_to_three_leading_spaces(line);
87 let marker = candidate.chars().next()?;
88 if !matches!(marker, '`' | '~') {
89 return None;
90 }
91
92 let count = candidate
93 .chars()
94 .take_while(|character| *character == marker)
95 .count();
96 if count < 3 {
97 return None;
98 }
99
100 let info_start = count * marker.len_utf8();
101 Some(FenceOpening {
102 delimiter: FenceDelimiter {
103 character: marker,
104 length: count,
105 },
106 info: candidate[info_start..].trim(),
107 })
108}
109
110pub(crate) fn is_closing_fence(line: &str, delimiter: FenceDelimiter) -> bool {
111 let candidate = crate::strip_up_to_three_leading_spaces(line);
112 let count = candidate
113 .chars()
114 .take_while(|character| *character == delimiter.character)
115 .count();
116
117 if count < delimiter.length {
118 return false;
119 }
120
121 candidate[count * delimiter.character.len_utf8()..]
122 .trim()
123 .is_empty()
124}
125
126fn language_from_info(info: &str) -> Option<String> {
127 info.split_whitespace().next().map(ToOwned::to_owned)
128}