klirr_core/models/
layout.rs1use crate::prelude::*;
2use derive_more::FromStr;
3
4const TYPST_LAYOUT_AIOO: &str = include_str!("../../layouts/aioo.typ");
6
7const TYPST_LAYOUT_TEST: &str = include_str!("../../layouts/test.typ");
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Default, FromStr, EnumIter)]
12pub enum Layout {
13 #[default]
20 Aioo,
21
22 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 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 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 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 fn used_font_weights_in_typst_file(layout: &Layout) -> HashSet<FontWeight> {
100 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 let default_layout: Layout = "Unknown".parse().unwrap_or_default();
154 assert_eq!(default_layout, Layout::Aioo);
155 }
156
157 #[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}