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