Skip to main content

snapper_fmt/parser/
mod.rs

1pub mod latex;
2pub mod markdown;
3pub mod org;
4#[cfg(feature = "pandoc")]
5pub mod pandoc;
6pub mod plaintext;
7pub mod rst;
8
9/// A region of text classified by a format parser.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum Region {
12    /// Prose text that should be reflowed with semantic line breaks.
13    Prose(String),
14    /// Structural content that must pass through unchanged.
15    Structure(String),
16    /// Blank line(s) preserved as paragraph separators.
17    BlankLines(String),
18    /// A fenced code block. `header` and `footer` carry the fence lines
19    /// (with their trailing newline) verbatim. `body` is the raw block
20    /// contents between the fences; the reflow stage may rewrite comments
21    /// inside `body` per the `[code]` configuration. `lang` is `None`
22    /// when the parser could not infer a language identifier.
23    Code {
24        lang: Option<String>,
25        header: String,
26        body: String,
27        footer: String,
28    },
29}
30
31/// Trait for format-specific parsers that classify text into regions.
32pub trait FormatParser {
33    fn parse(&self, input: &str) -> Vec<Region>;
34}
35
36/// Create the appropriate parser for a given format.
37pub fn parser_for_format(format: crate::format::Format) -> Box<dyn FormatParser> {
38    use crate::format::Format;
39    match format {
40        Format::Org => Box::new(org::OrgParser),
41        Format::Latex => Box::new(latex::LatexParser),
42        Format::Markdown => Box::new(markdown::MarkdownParser),
43        Format::Rst => Box::new(rst::RstParser),
44        Format::Plaintext => Box::new(plaintext::PlaintextParser),
45    }
46}
47
48/// Flush accumulated prose into the region list, clearing the buffer.
49pub fn flush_prose(prose: &mut String, regions: &mut Vec<Region>) {
50    if !prose.is_empty() {
51        regions.push(Region::Prose(prose.clone()));
52        prose.clear();
53    }
54}
55
56/// Check if a line contains a snapper pragma.
57/// Returns Some(false) for "snapper:off", Some(true) for "snapper:on", None otherwise.
58pub fn check_pragma(line: &str) -> Option<bool> {
59    let trimmed = line.trim();
60    // Strip format-specific comment markers
61    let content = trimmed
62        .strip_prefix("# ") // Org comment
63        .or_else(|| trimmed.strip_prefix("% ")) // LaTeX comment
64        .or_else(|| {
65            // HTML/Markdown comment
66            trimmed
67                .strip_prefix("<!-- ")
68                .and_then(|s| s.strip_suffix(" -->"))
69        })
70        .unwrap_or(trimmed); // Plaintext: bare pragma
71    let content = content.trim();
72    if content == "snapper:off" {
73        Some(false)
74    } else if content == "snapper:on" {
75        Some(true)
76    } else {
77        None
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn pragma_org_comment() {
87        assert_eq!(check_pragma("# snapper:off"), Some(false));
88        assert_eq!(check_pragma("# snapper:on"), Some(true));
89    }
90
91    #[test]
92    fn pragma_latex_comment() {
93        assert_eq!(check_pragma("% snapper:off"), Some(false));
94        assert_eq!(check_pragma("% snapper:on"), Some(true));
95    }
96
97    #[test]
98    fn pragma_html_comment() {
99        assert_eq!(check_pragma("<!-- snapper:off -->"), Some(false));
100        assert_eq!(check_pragma("<!-- snapper:on -->"), Some(true));
101    }
102
103    #[test]
104    fn pragma_bare() {
105        assert_eq!(check_pragma("snapper:off"), Some(false));
106        assert_eq!(check_pragma("snapper:on"), Some(true));
107    }
108
109    #[test]
110    fn pragma_none() {
111        assert_eq!(check_pragma("regular text"), None);
112        assert_eq!(check_pragma("# a comment"), None);
113        assert_eq!(check_pragma(""), None);
114    }
115}