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