Skip to main content

documents/
format.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Which grammar a document is written in, read from its name
7//! (`crates/documents/Spec.lean` D6).
8
9use serde::{Deserialize, Serialize};
10
11/// The grammar a document's blocks are read by.
12///
13/// Decided by the file name, never by the bytes: a window into a
14/// Markdown file looks exactly like a plain file, and a Markdown file
15/// with no heading is still Markdown.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
19pub enum Format {
20    /// Blank lines separate blocks, and a code block written between
21    /// two delimiter lines holds the blank lines inside it.
22    Markdown,
23    /// Every line is a block, and nothing in a line is structure.
24    Plain,
25}
26
27/// The extensions read as Markdown, compared without regard to case.
28const MARKDOWN_EXTENSIONS: [&str; 2] = ["md", "markdown"];
29
30impl Format {
31    /// The format of a file called `name`; a path's last segment decides.
32    pub fn of_name(name: &str) -> Format {
33        let last = name.rsplit(['/', '\\']).next().unwrap_or(name);
34        let markdown = last.rsplit_once('.').is_some_and(|(stem, extension)| {
35            !stem.is_empty()
36                && MARKDOWN_EXTENSIONS
37                    .iter()
38                    .any(|known| known.eq_ignore_ascii_case(extension))
39        });
40        if markdown {
41            Format::Markdown
42        } else {
43            Format::Plain
44        }
45    }
46}
47
48#[cfg(test)]
49#[allow(
50    clippy::unwrap_used,
51    clippy::expect_used,
52    clippy::panic,
53    clippy::indexing_slicing,
54    reason = "test code"
55)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn the_extension_of_the_last_segment_decides() {
61        assert_eq!(Format::of_name("lab/Roadmap.md"), Format::Markdown);
62        assert_eq!(Format::of_name("lab/NOTES.Markdown"), Format::Markdown);
63        assert_eq!(Format::of_name("lab/notes.txt"), Format::Plain);
64        assert_eq!(Format::of_name("lab.md/notes"), Format::Plain);
65        assert_eq!(Format::of_name(".md"), Format::Plain);
66    }
67}