mant_engine/markdown/
container.rs1use std::{borrow::Cow, error::Error, fmt, ops::Range};
10
11const OPENING_MARKER: &str = "<!-- mant:tldr:start -->";
12const CLOSING_MARKER: &str = "<!-- mant:tldr:end -->";
13
14#[derive(Debug)]
15pub(super) struct MarkdownParts<'a> {
16 pub(super) document: Cow<'a, str>,
17 pub(super) tldr: Option<&'a str>,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum TldrDirectiveError {
23 Unterminated,
25}
26
27impl fmt::Display for TldrDirectiveError {
28 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 Self::Unterminated => {
31 formatter.write_str(
32 "top-level <!-- mant:tldr:start --> marker is missing its <!-- mant:tldr:end --> marker",
33 )
34 }
35 }
36 }
37}
38
39impl Error for TldrDirectiveError {}
40
41pub(super) fn split_markdown(source: &str) -> Result<MarkdownParts<'_>, TldrDirectiveError> {
42 let lines = source_lines(source);
43 let Some(opening_index) = lines
44 .iter()
45 .position(|line| !line_text(source, line).trim().is_empty())
46 else {
47 return Ok(MarkdownParts {
48 document: Cow::Borrowed(source),
49 tldr: None,
50 });
51 };
52 let opening = &lines[opening_index];
53 if line_text(source, opening).trim() != OPENING_MARKER {
54 return Ok(MarkdownParts {
55 document: Cow::Borrowed(source),
56 tldr: None,
57 });
58 }
59
60 let closing = lines
61 .iter()
62 .skip(opening_index + 1)
63 .find(|line| line_text(source, line).trim() == CLOSING_MARKER)
64 .ok_or(TldrDirectiveError::Unterminated)?;
65 let tldr_start = opening.end;
66 let tldr_end = closing.start;
67 let masked_end = closing.end;
68
69 Ok(MarkdownParts {
70 document: Cow::Owned(mask_range(source, opening.start..masked_end)),
71 tldr: Some(&source[tldr_start..tldr_end]),
72 })
73}
74
75#[derive(Clone)]
76struct SourceLine {
77 start: usize,
78 content_end: usize,
79 end: usize,
80}
81
82fn source_lines(source: &str) -> Vec<SourceLine> {
83 if source.is_empty() {
84 return Vec::new();
85 }
86 let mut lines = Vec::new();
87 let mut start = 0;
88 for segment in source.split_inclusive('\n') {
89 let end = start + segment.len();
90 let content_end = if segment.ends_with("\r\n") {
91 end - 2
92 } else if segment.ends_with('\n') {
93 end - 1
94 } else {
95 end
96 };
97 lines.push(SourceLine {
98 start,
99 content_end,
100 end,
101 });
102 start = end;
103 }
104 lines
107}
108
109fn line_text<'a>(source: &'a str, line: &SourceLine) -> &'a str {
110 &source[line.start..line.content_end]
111}
112
113fn mask_range(source: &str, range: Range<usize>) -> String {
114 let mut masked = String::with_capacity(source.len());
115 masked.push_str(&source[..range.start]);
116 for character in source[range.clone()].chars() {
117 if matches!(character, '\n' | '\r') {
118 masked.push(character);
119 } else {
120 masked.extend(std::iter::repeat_n(' ', character.len_utf8()));
121 }
122 }
123 masked.push_str(&source[range.end..]);
124 masked
125}
126
127#[cfg(test)]
128mod tests {
129 use std::borrow::Cow;
130
131 use pulldown_cmark::{Event, Parser};
132
133 use super::{TldrDirectiveError, split_markdown};
134
135 #[test]
136 fn extracts_only_a_leading_directive_and_preserves_source_coordinates() {
137 let source = "\n<!-- mant:tldr:start -->\n# demo\n\n- Run:\n\n`demo`\n<!-- mant:tldr:end -->\n\n# Demo\n\nBody.\n";
138 let parts = split_markdown(source).expect("directive");
139
140 assert_eq!(parts.tldr, Some("# demo\n\n- Run:\n\n`demo`\n"));
141 assert_eq!(parts.document.len(), source.len());
142 assert_eq!(
143 parts.document.matches('\n').count(),
144 source.matches('\n').count()
145 );
146 assert_eq!(parts.document.find("# Demo"), source.find("# Demo"));
147 }
148
149 #[test]
150 fn leaves_later_directives_as_ordinary_markdown() {
151 let source = "# Demo\n\n<!-- mant:tldr:start -->\n# late\n<!-- mant:tldr:end -->\n";
152 let parts = split_markdown(source).expect("ordinary Markdown");
153
154 assert!(parts.tldr.is_none());
155 assert!(matches!(parts.document, Cow::Borrowed(_)));
156 }
157
158 #[test]
159 fn reports_an_unterminated_leading_directive() {
160 let error = split_markdown("<!-- mant:tldr:start -->\n# demo\n").expect_err("unterminated");
161 assert_eq!(error, TldrDirectiveError::Unterminated);
162 }
163
164 #[test]
165 fn does_not_accept_the_obsolete_fenced_container() {
166 let source = ":::tldr\n# demo\n:::\n\n# Demo\n";
167 let parts = split_markdown(source).expect("ordinary Markdown");
168
169 assert!(parts.tldr.is_none());
170 assert!(matches!(parts.document, Cow::Borrowed(_)));
171 }
172
173 #[test]
174 fn boundary_comments_are_not_visible_commonmark_text() {
175 let source = "<!-- mant:tldr:start -->\n# demo\n\n> Quick reference.\n<!-- mant:tldr:end -->\n\n# Demo\n";
176 let visible = Parser::new(source)
177 .filter_map(|event| match event {
178 Event::Text(text) => Some(text.into_string()),
179 _ => None,
180 })
181 .collect::<String>();
182
183 assert!(!visible.contains("mant:tldr"));
184 assert!(visible.contains("Quick reference."));
185 assert!(visible.contains("Demo"));
186 }
187}