1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct TypographyConfig {
9 #[serde(default, skip_serializing_if = "Option::is_none")]
11 pub line_numbering: Option<LineNumbering>,
12
13 #[serde(default, skip_serializing_if = "Option::is_none")]
15 pub baseline_grid: Option<BaselineGrid>,
16
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub hyphenation: Option<HyphenationConfig>,
20}
21
22impl TypographyConfig {
23 #[must_use]
25 pub fn new() -> Self {
26 Self {
27 line_numbering: None,
28 baseline_grid: None,
29 hyphenation: None,
30 }
31 }
32
33 #[must_use]
35 pub fn with_line_numbering(mut self, ln: LineNumbering) -> Self {
36 self.line_numbering = Some(ln);
37 self
38 }
39
40 #[must_use]
42 pub fn with_baseline_grid(mut self, bg: BaselineGrid) -> Self {
43 self.baseline_grid = Some(bg);
44 self
45 }
46
47 #[must_use]
49 pub fn with_hyphenation(mut self, h: HyphenationConfig) -> Self {
50 self.hyphenation = Some(h);
51 self
52 }
53}
54
55impl Default for TypographyConfig {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct LineNumbering {
65 pub enabled: bool,
67
68 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub start: Option<u32>,
71
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub interval: Option<u32>,
75
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub side: Option<LineNumberingSide>,
79
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub restart: Option<LineNumberingRestart>,
83}
84
85impl LineNumbering {
86 #[must_use]
88 pub fn enabled() -> Self {
89 Self {
90 enabled: true,
91 start: None,
92 interval: None,
93 side: None,
94 restart: None,
95 }
96 }
97
98 #[must_use]
100 pub const fn with_start(mut self, start: u32) -> Self {
101 self.start = Some(start);
102 self
103 }
104
105 #[must_use]
107 pub const fn with_interval(mut self, interval: u32) -> Self {
108 self.interval = Some(interval);
109 self
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "lowercase")]
116pub enum LineNumberingSide {
117 Left,
119 Right,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(rename_all = "lowercase")]
126pub enum LineNumberingRestart {
127 Page,
129 Section,
131 #[serde(rename = "none")]
133 NoRestart,
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct BaselineGrid {
140 pub enabled: bool,
142
143 pub line_height: String,
145
146 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub offset: Option<String>,
149}
150
151impl BaselineGrid {
152 #[must_use]
154 pub fn new(line_height: impl Into<String>) -> Self {
155 Self {
156 enabled: true,
157 line_height: line_height.into(),
158 offset: None,
159 }
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct HyphenationConfig {
167 pub enabled: bool,
169
170 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub language: Option<String>,
173
174 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub min_word_length: Option<u32>,
177
178 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub min_before: Option<u32>,
181
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub min_after: Option<u32>,
185
186 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub max_consecutive: Option<u32>,
189}
190
191impl HyphenationConfig {
192 #[must_use]
194 pub fn enabled() -> Self {
195 Self {
196 enabled: true,
197 language: None,
198 min_word_length: None,
199 min_before: None,
200 min_after: None,
201 max_consecutive: None,
202 }
203 }
204
205 #[must_use]
207 pub fn with_language(mut self, lang: impl Into<String>) -> Self {
208 self.language = Some(lang.into());
209 self
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_typography_config_serde() {
219 let config = TypographyConfig::new()
220 .with_line_numbering(LineNumbering::enabled().with_interval(5))
221 .with_hyphenation(HyphenationConfig::enabled().with_language("en-US"));
222
223 let json = serde_json::to_string_pretty(&config).unwrap();
224 let parsed: TypographyConfig = serde_json::from_str(&json).unwrap();
225 assert_eq!(parsed, config);
226 }
227
228 #[test]
229 fn test_line_numbering_serde() {
230 let ln = LineNumbering::enabled().with_start(10).with_interval(5);
231 let json = serde_json::to_string(&ln).unwrap();
232 assert!(json.contains("\"start\":10"));
233 assert!(json.contains("\"interval\":5"));
234
235 let parsed: LineNumbering = serde_json::from_str(&json).unwrap();
236 assert_eq!(parsed, ln);
237 }
238
239 #[test]
240 fn test_line_numbering_side_serde() {
241 let side = LineNumberingSide::Left;
242 let json = serde_json::to_string(&side).unwrap();
243 assert_eq!(json, "\"left\"");
244
245 let right: LineNumberingSide = serde_json::from_str("\"right\"").unwrap();
246 assert_eq!(right, LineNumberingSide::Right);
247 }
248
249 #[test]
250 fn test_line_numbering_restart_serde() {
251 let restart = LineNumberingRestart::NoRestart;
252 let json = serde_json::to_string(&restart).unwrap();
253 assert_eq!(json, "\"none\"");
254
255 let page: LineNumberingRestart = serde_json::from_str("\"page\"").unwrap();
256 assert_eq!(page, LineNumberingRestart::Page);
257 }
258
259 #[test]
260 fn test_baseline_grid_serde() {
261 let grid = BaselineGrid::new("14pt");
262 let json = serde_json::to_string(&grid).unwrap();
263 assert!(json.contains("\"lineHeight\":\"14pt\""));
264
265 let parsed: BaselineGrid = serde_json::from_str(&json).unwrap();
266 assert_eq!(parsed, grid);
267 }
268
269 #[test]
270 fn test_hyphenation_serde() {
271 let hyph = HyphenationConfig::enabled().with_language("de");
272 let json = serde_json::to_string(&hyph).unwrap();
273 assert!(json.contains("\"language\":\"de\""));
274
275 let parsed: HyphenationConfig = serde_json::from_str(&json).unwrap();
276 assert_eq!(parsed, hyph);
277 }
278
279 #[test]
280 fn test_typography_defaults() {
281 let json = "{}";
282 let config: TypographyConfig = serde_json::from_str(json).unwrap();
283 assert!(config.line_numbering.is_none());
284 assert!(config.baseline_grid.is_none());
285 assert!(config.hyphenation.is_none());
286 }
287}