cdx_core/presentation/
notes.rs1use serde::{Deserialize, Serialize};
4
5use super::Style;
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct FootnotesConfig {
11 #[serde(default, skip_serializing_if = "Option::is_none")]
13 pub numbering: Option<String>,
14
15 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub position: Option<FootnotePosition>,
18
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub separator: Option<FootnoteSeparator>,
22
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub style: Option<Style>,
26}
27
28impl FootnotesConfig {
29 #[must_use]
31 pub fn new() -> Self {
32 Self {
33 numbering: None,
34 position: None,
35 separator: None,
36 style: None,
37 }
38 }
39
40 #[must_use]
42 pub fn with_numbering(mut self, numbering: impl Into<String>) -> Self {
43 self.numbering = Some(numbering.into());
44 self
45 }
46
47 #[must_use]
49 pub const fn with_position(mut self, position: FootnotePosition) -> Self {
50 self.position = Some(position);
51 self
52 }
53
54 #[must_use]
56 pub fn with_separator(mut self, separator: FootnoteSeparator) -> Self {
57 self.separator = Some(separator);
58 self
59 }
60}
61
62impl Default for FootnotesConfig {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub enum FootnotePosition {
72 PageBottom,
74 SectionEnd,
76 DocumentEnd,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct FootnoteSeparator {
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub width: Option<String>,
87
88 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub style: Option<String>,
91
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub margin: Option<String>,
95}
96
97impl FootnoteSeparator {
98 #[must_use]
100 pub fn new() -> Self {
101 Self {
102 width: None,
103 style: None,
104 margin: None,
105 }
106 }
107
108 #[must_use]
110 pub fn with_width(mut self, width: impl Into<String>) -> Self {
111 self.width = Some(width.into());
112 self
113 }
114
115 #[must_use]
117 pub fn with_style(mut self, style: impl Into<String>) -> Self {
118 self.style = Some(style.into());
119 self
120 }
121}
122
123impl Default for FootnoteSeparator {
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct EndnotesConfig {
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub title: Option<String>,
136
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub numbering: Option<String>,
140
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub per_chapter: Option<bool>,
144}
145
146impl EndnotesConfig {
147 #[must_use]
149 pub fn new() -> Self {
150 Self {
151 title: None,
152 numbering: None,
153 per_chapter: None,
154 }
155 }
156
157 #[must_use]
159 pub fn with_title(mut self, title: impl Into<String>) -> Self {
160 self.title = Some(title.into());
161 self
162 }
163
164 #[must_use]
166 pub fn with_numbering(mut self, numbering: impl Into<String>) -> Self {
167 self.numbering = Some(numbering.into());
168 self
169 }
170
171 #[must_use]
173 pub const fn per_chapter(mut self) -> Self {
174 self.per_chapter = Some(true);
175 self
176 }
177}
178
179impl Default for EndnotesConfig {
180 fn default() -> Self {
181 Self::new()
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_footnotes_config_serde() {
191 let config = FootnotesConfig::new()
192 .with_numbering("lower-roman")
193 .with_position(FootnotePosition::PageBottom)
194 .with_separator(
195 FootnoteSeparator::new()
196 .with_width("33%")
197 .with_style("solid"),
198 );
199
200 let json = serde_json::to_string_pretty(&config).unwrap();
201 assert!(json.contains("\"numbering\": \"lower-roman\""));
202 assert!(json.contains("\"position\": \"pageBottom\""));
203
204 let parsed: FootnotesConfig = serde_json::from_str(&json).unwrap();
205 assert_eq!(parsed, config);
206 }
207
208 #[test]
209 fn test_footnote_position_serde() {
210 assert_eq!(
211 serde_json::to_string(&FootnotePosition::PageBottom).unwrap(),
212 "\"pageBottom\""
213 );
214 assert_eq!(
215 serde_json::to_string(&FootnotePosition::SectionEnd).unwrap(),
216 "\"sectionEnd\""
217 );
218 assert_eq!(
219 serde_json::to_string(&FootnotePosition::DocumentEnd).unwrap(),
220 "\"documentEnd\""
221 );
222 }
223
224 #[test]
225 fn test_endnotes_config_serde() {
226 let config = EndnotesConfig::new()
227 .with_title("Notes")
228 .with_numbering("decimal")
229 .per_chapter();
230
231 let json = serde_json::to_string_pretty(&config).unwrap();
232 assert!(json.contains("\"title\": \"Notes\""));
233 assert!(json.contains("\"perChapter\": true"));
234
235 let parsed: EndnotesConfig = serde_json::from_str(&json).unwrap();
236 assert_eq!(parsed, config);
237 }
238
239 #[test]
240 fn test_footnote_separator_serde() {
241 let sep = FootnoteSeparator::new()
242 .with_width("50%")
243 .with_style("dashed");
244 let json = serde_json::to_string(&sep).unwrap();
245 assert!(json.contains("\"width\":\"50%\""));
246
247 let parsed: FootnoteSeparator = serde_json::from_str(&json).unwrap();
248 assert_eq!(parsed, sep);
249 }
250
251 #[test]
252 fn test_footnotes_defaults() {
253 let json = "{}";
254 let config: FootnotesConfig = serde_json::from_str(json).unwrap();
255 assert!(config.numbering.is_none());
256 assert!(config.position.is_none());
257 assert!(config.separator.is_none());
258 assert!(config.style.is_none());
259 }
260
261 #[test]
262 fn test_endnotes_defaults() {
263 let json = "{}";
264 let config: EndnotesConfig = serde_json::from_str(json).unwrap();
265 assert!(config.title.is_none());
266 assert!(config.numbering.is_none());
267 assert!(config.per_chapter.is_none());
268 }
269}