Skip to main content

mant_core/markdown/
container.rs

1//! Extracts `ManT`'s optional document-owned tldr preface.
2//!
3//! The directive is deliberately structural rather than ordinary Markdown:
4//! it must be the first non-empty construct, and its contents use the
5//! tldr-pages dialect. The returned document text masks the complete directive
6//! while preserving byte offsets and line numbers for source diagnostics.
7
8use 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    // split_inclusive yields every byte, including a final line without a
100    // trailing newline, so no trailing-segment fixup is needed here.
101    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}