Skip to main content

klirr_core/models/
layout.rs

1use crate::prelude::*;
2use derive_more::FromStr;
3
4/// The Typst layout "Aioo" as a string.
5const TYPST_LAYOUT_AIOO: &str = include_str!("../../layouts/aioo.typ");
6
7/// A layout used for testing only.
8const TYPST_LAYOUT_TEST: &str = include_str!("../../layouts/test.typ");
9
10/// Represents different Typst layouts used to render the invoice.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Default, FromStr, EnumIter)]
12pub enum Layout {
13    /// Originally created by [Andreas Lundblad][author], see his
14    /// [blog post][blog] presenting his [Latex Template][latex].
15    ///
16    /// [author]: https://aioo.be/
17    /// [blog]: https://aioo.be/2012/02/13/Fakturamall-Latex.html
18    /// [latex]: https://aioo.be/assets/blog/invoice-template/invoice.tex
19    #[default]
20    Aioo,
21
22    /// A Test layout to test if CMU font is installed.
23    Test,
24}
25
26impl ToTypst for Layout {}
27impl ToTypstFn for Layout {
28    fn to_typst_fn(&self) -> String {
29        match self {
30            Self::Aioo => TYPST_LAYOUT_AIOO.to_string(),
31            Self::Test => TYPST_LAYOUT_TEST.to_string(),
32        }
33    }
34}
35
36impl Layout {
37    pub fn required_fonts(&self) -> IndexSet<FontIdentifier> {
38        match self {
39            Self::Aioo => {
40                let mut fonts = IndexSet::new();
41                fonts.insert(FontIdentifier::ComputerModern(FontWeight::Regular));
42                fonts.insert(FontIdentifier::ComputerModern(FontWeight::Bold));
43                fonts
44            }
45            Self::Test => {
46                let mut fonts = IndexSet::new();
47                fonts.insert(FontIdentifier::ComputerModern(FontWeight::Regular));
48                fonts
49            }
50        }
51    }
52
53    /// Returns all available layouts as an iterator.
54    /// This can be used to iterate over all supported layouts.
55    /// # Examples
56    /// ```
57    /// use klirr_core::prelude::*;
58    /// for layout in Layout::all() {
59    ///     println!("Supported layout: {}", layout);
60    /// }
61    /// ```
62    pub fn all() -> impl Iterator<Item = Self> {
63        Self::iter()
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use std::collections::HashSet;
70
71    use super::*;
72    use test_log::test;
73
74    /// Returns the family names of the fonts used in the given layout.
75    fn used_fonts_in_typst_file(layout: &Layout) -> HashSet<String> {
76        let typst = layout.to_typst_fn();
77        let mut fonts = HashSet::new();
78        for line in typst.lines() {
79            // we will now for each line check for patterns:
80            // '    #set text(font: "CMU Serif", size: 12pt)'
81            // and extract `CMU Serif` as a String
82            if let Some(font) = line.split("font: ").nth(1) {
83                let font_name = font
84                    .split(',')
85                    .next()
86                    .unwrap_or("")
87                    .trim()
88                    .trim_matches('"')
89                    .to_string();
90                if !font_name.is_empty() {
91                    fonts.insert(font_name);
92                }
93            }
94        }
95        fonts
96    }
97
98    /// Returns
99    fn used_font_weights_in_typst_file(layout: &Layout) -> HashSet<FontWeight> {
100        // we will iterate over the lines in the Typst file and look for patterns like:
101        // #strong[#emph or #emph[#strong or #emph or #strong and return the FontWeight used. Regular wont
102        // be returned, as it is the default weight.
103        let typst = layout.to_typst_fn();
104        let mut weights = HashSet::new();
105        for line in typst.lines() {
106            if line.contains("#strong[#emph") || line.contains("#emph[#strong") {
107                weights.insert(FontWeight::BoldItalic);
108            } else if line.contains("#strong") {
109                weights.insert(FontWeight::Bold);
110            } else if line.contains("#emph") {
111                weights.insert(FontWeight::Italic);
112            }
113        }
114        weights
115    }
116
117    #[test]
118    fn test_no_layout_uses_italic_fonts() {
119        Layout::all().for_each(|layout| {
120            let used_weights = used_font_weights_in_typst_file(&layout);
121            assert!(
122                !used_weights.contains(&FontWeight::Italic),
123                "Layout {:?} uses italic fonts, not supported",
124                layout
125            );
126            assert!(
127                !used_weights.contains(&FontWeight::BoldItalic),
128                "Layout {:?} uses bold italic fonts, not supported",
129                layout
130            );
131        })
132    }
133
134    #[test]
135    fn all_layouts_define_render_function() {
136        for layout in Layout::all() {
137            let typst = layout.to_typst_fn();
138            assert!(
139                typst.contains("#let render_invoice(data, l18n) = {"),
140                "Layout {:?} does not define a render function in its Typst source: {}",
141                layout,
142                typst
143            );
144        }
145    }
146
147    #[test]
148    fn test_from_str() {
149        let layout: Layout = "Aioo".parse().unwrap();
150        assert_eq!(layout, Layout::Aioo);
151
152        // Test default value
153        let default_layout: Layout = "Unknown".parse().unwrap_or_default();
154        assert_eq!(default_layout, Layout::Aioo);
155    }
156
157    /// This tests helps us detect if we are writing a new layout using a font which
158    /// is not defined in the `required_fonts` method.
159    #[test]
160    fn test_required_fonts() {
161        Layout::all().for_each(|layout| {
162            let all_claimed_fonts = layout
163                .required_fonts()
164                .into_iter()
165                .map(|f| f.family_name().to_string())
166                .collect::<HashSet<String>>();
167            let all_identifier_fonts = used_fonts_in_typst_file(&layout);
168            assert_eq!(
169                all_claimed_fonts, all_identifier_fonts,
170                "Layout {:?} has mismatched fonts: claimed {:?}, found {:?}",
171                layout, all_claimed_fonts, all_identifier_fonts
172            );
173        })
174    }
175}