Skip to main content

termdoc_core/
format.rs

1//! Format identity and detection results.
2
3use std::borrow::Cow;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6pub enum FormatId {
7    PlainText,
8    Markdown,
9    Log,
10    SourceCode,
11    Json,
12    Yaml,
13    Toml,
14    Xml,
15    Csv,
16    Html,
17    Pdf,
18    Docx,
19    Odt,
20    Rtf,
21    Epub,
22    Xlsx,
23    Pptx,
24    /// A format contributed by a plugin, identified by its name.
25    External(&'static str),
26    /// Not interpretable as text: shown as a hex dump.
27    Binary,
28}
29
30impl FormatId {
31    pub fn name(self) -> &'static str {
32        match self {
33            FormatId::PlainText => "text",
34            FormatId::Markdown => "markdown",
35            FormatId::Log => "log",
36            FormatId::SourceCode => "code",
37            FormatId::Json => "json",
38            FormatId::Yaml => "yaml",
39            FormatId::Toml => "toml",
40            FormatId::Xml => "xml",
41            FormatId::Csv => "csv",
42            FormatId::Html => "html",
43            FormatId::Pdf => "pdf",
44            FormatId::Docx => "docx",
45            FormatId::Odt => "odt",
46            FormatId::Rtf => "rtf",
47            FormatId::Epub => "epub",
48            FormatId::Xlsx => "xlsx",
49            FormatId::Pptx => "pptx",
50            FormatId::External(n) => n,
51            FormatId::Binary => "binary",
52        }
53    }
54
55    /// Parses the value of `--from`.
56    pub fn parse(s: &str) -> Option<Self> {
57        let known = [
58            FormatId::PlainText,
59            FormatId::Markdown,
60            FormatId::Log,
61            FormatId::SourceCode,
62            FormatId::Json,
63            FormatId::Yaml,
64            FormatId::Toml,
65            FormatId::Xml,
66            FormatId::Csv,
67            FormatId::Html,
68            FormatId::Pdf,
69            FormatId::Docx,
70            FormatId::Odt,
71            FormatId::Rtf,
72            FormatId::Epub,
73            FormatId::Xlsx,
74            FormatId::Pptx,
75            FormatId::Binary,
76        ];
77        let lower = s.to_ascii_lowercase();
78        // Common aliases that people will type before the canonical name.
79        let lower = match lower.as_str() {
80            "md" => "markdown".to_string(),
81            "txt" => "text".to_string(),
82            "yml" => "yaml".to_string(),
83            "htm" => "html".to_string(),
84            "rs" | "py" | "js" | "source" => "code".to_string(),
85            other => other.to_string(),
86        };
87        known.into_iter().find(|f| f.name() == lower)
88    }
89
90    pub fn all_names() -> Vec<&'static str> {
91        vec![
92            "text", "markdown", "log", "code", "json", "yaml", "toml", "xml", "csv", "html", "pdf",
93            "docx", "odt", "rtf", "epub", "xlsx", "pptx", "binary",
94        ]
95    }
96}
97
98impl std::fmt::Display for FormatId {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        f.write_str(self.name())
101    }
102}
103
104/// The 0..=100 confidence with which a detector claims a format.
105///
106/// It exists so `--explain` can justify the choice, and so a plugin's detector cannot
107/// hijack detection on a weak hunch.
108pub type Confidence = u8;
109
110pub mod confidence {
111    use super::Confidence;
112    /// The user said so with `--from`. Nothing outranks it.
113    pub const EXPLICIT: Confidence = 100;
114    /// Unambiguous magic bytes, or a characteristic entry inside a ZIP.
115    pub const MAGIC: Confidence = 90;
116    /// A structural sniff that actually parsed the prefix.
117    pub const STRUCTURAL: Confidence = 70;
118    /// The filename extension.
119    pub const EXTENSION: Confidence = 50;
120    /// A weak heuristic (marker frequency and the like).
121    pub const HEURISTIC: Confidence = 30;
122    /// Last resort.
123    pub const FALLBACK: Confidence = 1;
124}
125
126#[derive(Clone, Debug, PartialEq, Eq)]
127pub struct Detection {
128    pub format: FormatId,
129    pub confidence: Confidence,
130    /// Why it was chosen. Printed by `--explain`; not decoration, but the only way to
131    /// debug a wrong guess without instrumenting the binary.
132    pub reason: Cow<'static, str>,
133    /// Name of the detected encoding, when one was determined.
134    pub encoding: Option<&'static str>,
135}
136
137impl Detection {
138    pub fn new(
139        format: FormatId,
140        confidence: Confidence,
141        reason: impl Into<Cow<'static, str>>,
142    ) -> Self {
143        Detection {
144            format,
145            confidence,
146            reason: reason.into(),
147            encoding: None,
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn parse_accepts_aliases() {
158        assert_eq!(FormatId::parse("md"), Some(FormatId::Markdown));
159        assert_eq!(FormatId::parse("MARKDOWN"), Some(FormatId::Markdown));
160        assert_eq!(FormatId::parse("yml"), Some(FormatId::Yaml));
161        assert_eq!(FormatId::parse("txt"), Some(FormatId::PlainText));
162        assert_eq!(FormatId::parse("nonexistent"), None);
163    }
164
165    #[test]
166    fn all_names_covers_every_parseable_format() {
167        for name in FormatId::all_names() {
168            assert!(
169                FormatId::parse(name).is_some(),
170                "'{name}' is advertised in --help but does not parse"
171            );
172        }
173    }
174}