Skip to main content

mdcast_api/
doc.rs

1//! The document model: what to render (`DocMeta`) and what to render it into
2//! (`Target`).
3
4use std::collections::BTreeMap;
5
6use serde::{Deserialize, Serialize};
7
8/// Output target. One backend per variant.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "kebab-case")]
11pub enum Target {
12    Docx,
13    Odt,
14    Pdf,
15    PdfPresentation,
16    Pptx,
17    HtmlReveal,
18}
19
20impl Target {
21    pub fn as_str(&self) -> &'static str {
22        match self {
23            Target::Docx => "docx",
24            Target::Odt => "odt",
25            Target::Pdf => "pdf",
26            Target::PdfPresentation => "pdf-presentation",
27            Target::Pptx => "pptx",
28            Target::HtmlReveal => "html-reveal",
29        }
30    }
31
32    /// File extension (without the leading dot) for a suggested filename.
33    pub fn extension(&self) -> &'static str {
34        match self {
35            Target::Docx => "docx",
36            Target::Odt => "odt",
37            Target::Pdf | Target::PdfPresentation => "pdf",
38            Target::Pptx => "pptx",
39            Target::HtmlReveal => "html",
40        }
41    }
42}
43
44/// Document-level metadata extracted from frontmatter / config.
45#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
46pub struct DocMeta {
47    pub title: Option<String>,
48    pub author: Option<String>,
49    pub date: Option<String>,
50    #[serde(default)]
51    pub extra: BTreeMap<String, String>,
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn target_serializes_as_its_kebab_case_str() {
60        for target in [
61            Target::Docx,
62            Target::Odt,
63            Target::Pdf,
64            Target::PdfPresentation,
65            Target::Pptx,
66            Target::HtmlReveal,
67        ] {
68            let json = serde_json::to_string(&target).unwrap();
69            assert_eq!(json, format!("\"{}\"", target.as_str()));
70            assert_eq!(serde_json::from_str::<Target>(&json).unwrap(), target);
71        }
72    }
73
74    #[test]
75    fn target_extension_collapses_both_pdf_targets() {
76        assert_eq!(Target::Pdf.extension(), "pdf");
77        assert_eq!(Target::PdfPresentation.extension(), "pdf");
78        assert_eq!(Target::HtmlReveal.extension(), "html");
79    }
80
81    #[test]
82    fn doc_meta_round_trips_through_json() {
83        let meta = DocMeta {
84            title: Some("Q3 Review".to_string()),
85            author: Some("Martin".to_string()),
86            date: None,
87            extra: BTreeMap::from([("subtitle".to_string(), "Board copy".to_string())]),
88        };
89
90        let json = serde_json::to_string(&meta).unwrap();
91        assert_eq!(serde_json::from_str::<DocMeta>(&json).unwrap(), meta);
92    }
93
94    #[test]
95    fn doc_meta_defaults_extra_when_absent() {
96        let meta: DocMeta = serde_json::from_str(r#"{"title":"T","author":null,"date":null}"#)
97            .expect("extra is optional");
98        assert!(meta.extra.is_empty());
99    }
100}