Skip to main content

snapper_fmt/
format.rs

1//! Document format detection and representation.
2//!
3//! The [`Format`] enum identifies the markup language of a document,
4//! enabling format-specific parsing in the pipeline. Format is detected
5//! from file extensions or can be specified explicitly via CLI flags.
6
7use std::path::Path;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum Format {
11    Org,
12    Latex,
13    Markdown,
14    Rst,
15    Plaintext,
16}
17
18impl Format {
19    /// Known prose extensions. `None` means "not a document snapper should
20    /// touch" (`.rs`, `.py`, no extension). `.txt` is plaintext; unknown
21    /// extensions are not silently treated as prose.
22    pub fn recognized_from_extension(ext: &str) -> Option<Self> {
23        match ext {
24            "org" => Some(Format::Org),
25            "tex" | "latex" | "ltx" | "sty" | "cls" => Some(Format::Latex),
26            "md" | "markdown" | "mkd" | "mdx" => Some(Format::Markdown),
27            "rst" | "rest" => Some(Format::Rst),
28            "txt" | "text" => Some(Format::Plaintext),
29            _ => None,
30        }
31    }
32
33    pub fn recognized_from_path(path: &Path) -> Option<Self> {
34        path.extension()
35            .and_then(|e| e.to_str())
36            .and_then(Self::recognized_from_extension)
37    }
38
39    /// Detect format from file extension, defaulting to Plaintext.
40    ///
41    /// Prefer [`recognized_from_path`] at CLI boundaries so `.rs` is not
42    /// formatted as prose. This fallback stays for stdin and explicit
43    /// `--format plaintext`.
44    pub fn from_path(path: &Path) -> Self {
45        Self::recognized_from_path(path).unwrap_or(Format::Plaintext)
46    }
47
48    /// Detect format from a bare file extension string (without the dot).
49    pub fn from_extension(ext: &str) -> Self {
50        Self::recognized_from_extension(ext).unwrap_or(Format::Plaintext)
51    }
52
53    pub fn config_key(self) -> &'static str {
54        match self {
55            Format::Org => "org",
56            Format::Latex => "latex",
57            Format::Markdown => "markdown",
58            Format::Rst => "rst",
59            Format::Plaintext => "plaintext",
60        }
61    }
62
63    #[cfg(feature = "cli")]
64    pub fn from_arg(arg: crate::cli::FormatArg) -> Self {
65        match arg {
66            crate::cli::FormatArg::Org => Format::Org,
67            crate::cli::FormatArg::Latex => Format::Latex,
68            crate::cli::FormatArg::Markdown => Format::Markdown,
69            crate::cli::FormatArg::Rst => Format::Rst,
70            crate::cli::FormatArg::Plaintext => Format::Plaintext,
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use std::path::Path;
79
80    #[test]
81    fn known_extensions_are_recognized() {
82        assert_eq!(
83            Format::recognized_from_path(Path::new("paper.org")),
84            Some(Format::Org)
85        );
86        assert_eq!(
87            Format::recognized_from_path(Path::new("paper.tex")),
88            Some(Format::Latex)
89        );
90        assert_eq!(
91            Format::recognized_from_path(Path::new("notes.md")),
92            Some(Format::Markdown)
93        );
94        assert_eq!(
95            Format::recognized_from_path(Path::new("index.rst")),
96            Some(Format::Rst)
97        );
98        assert_eq!(
99            Format::recognized_from_path(Path::new("notes.txt")),
100            Some(Format::Plaintext)
101        );
102    }
103
104    #[test]
105    fn source_extensions_are_not_prose() {
106        assert_eq!(Format::recognized_from_path(Path::new("main.rs")), None);
107        assert_eq!(Format::recognized_from_path(Path::new("app.py")), None);
108        assert_eq!(Format::recognized_from_path(Path::new("README")), None);
109    }
110}