1use typst::foundations::{IntoValue, Repr};
6
7#[derive(Default)]
21pub struct DictBuilder {
22 fields: Vec<String>,
23}
24
25impl DictBuilder {
26 pub fn new() -> Self {
28 Self::default()
29 }
30
31 pub fn field<K, V>(mut self, key: K, value: V) -> Self
33 where
34 K: AsRef<str>,
35 V: IntoValue,
36 {
37 self.fields.push(format!(
38 "{}: {}",
39 key.as_ref(),
40 value.into_value().repr()
41 ));
42 self
43 }
44
45 pub fn field_opt<K, V>(mut self, key: K, value: Option<V>) -> Self
47 where
48 K: AsRef<str>,
49 V: IntoValue,
50 {
51 let repr = match value {
52 Some(v) => v.into_value().repr().to_string(),
53 None => "none".to_string(),
54 };
55 self.fields.push(format!("{}: {}", key.as_ref(), repr));
56 self
57 }
58
59 pub fn field_raw<K, V>(mut self, key: K, value: V) -> Self
61 where
62 K: AsRef<str>,
63 V: AsRef<str>,
64 {
65 self.fields
66 .push(format!("{}: {}", key.as_ref(), value.as_ref()));
67 self
68 }
69
70 pub fn field_raw_opt<K, V>(mut self, key: K, value: Option<V>) -> Self
72 where
73 K: AsRef<str>,
74 V: AsRef<str>,
75 {
76 let repr = match value {
77 Some(v) => v.as_ref().to_string(),
78 None => "none".to_string(),
79 };
80 self.fields.push(format!("{}: {}", key.as_ref(), repr));
81 self
82 }
83
84 pub fn build(self) -> String {
86 format!("({})", self.fields.join(", "))
87 }
88}
89
90
91
92pub fn dict<I, K, V>(entries: I) -> String
94where
95 I: IntoIterator<Item = (K, V)>,
96 K: AsRef<str>,
97 V: IntoValue,
98{
99 let items: Vec<_> = entries
100 .into_iter()
101 .map(|(k, v)| format!("{}: {}", k.as_ref(), v.into_value().repr()))
102 .collect();
103 format!("({})", items.join(", "))
104}
105
106pub fn dict_raw<I, K, V>(entries: I) -> String
108where
109 I: IntoIterator<Item = (K, V)>,
110 K: AsRef<str>,
111 V: AsRef<str>,
112{
113 let items: Vec<_> = entries
114 .into_iter()
115 .map(|(k, v)| format!("{}: {}", k.as_ref(), v.as_ref()))
116 .collect();
117 format!("({})", items.join(", "))
118}
119
120pub fn dict_sparse<I, K, V>(entries: I) -> String
122where
123 I: IntoIterator<Item = (K, Option<V>)>,
124 K: AsRef<str>,
125 V: IntoValue,
126{
127 let items: Vec<_> = entries
128 .into_iter()
129 .map(|(k, v)| {
130 let repr = match v {
131 Some(v) => v.into_value().repr().to_string(),
132 None => "none".to_string(),
133 };
134 format!("{}: {}", k.as_ref(), repr)
135 })
136 .collect();
137 format!("({})", items.join(", "))
138}
139
140pub fn array<I, V>(items: I) -> String
142where
143 I: IntoIterator<Item = V>,
144 V: IntoValue,
145{
146 let items: Vec<_> = items
147 .into_iter()
148 .map(|v| v.into_value().repr().to_string())
149 .collect();
150 format_array(items)
151}
152
153pub fn array_raw<I, S>(items: I) -> String
155where
156 I: IntoIterator<Item = S>,
157 S: AsRef<str>,
158{
159 let items: Vec<_> = items.into_iter().map(|s| s.as_ref().to_string()).collect();
160 format_array(items)
161}
162
163pub fn format_array(items: Vec<String>) -> String {
170 match items.len() {
171 0 => "()".to_string(),
172 1 => format!("({},)", items[0]),
173 _ => format!("({})", items.join(", ")),
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_dict() {
183 let code = dict([("url", "/posts/"), ("title", "Hello")]);
184 assert!(code.contains("url:"));
185 assert!(code.contains("title:"));
186 }
187
188 #[test]
189 fn test_dict_sparse() {
190 let code = dict_sparse([("url", Some("/posts/")), ("date", None::<&str>)]);
191 assert!(code.contains("url:"));
192 assert!(code.contains("date: none"));
193 }
194
195 #[test]
196 fn test_array_formatting() {
197 assert_eq!(format_array(vec![]), "()");
198 assert_eq!(format_array(vec!["a".into()]), "(a,)");
199 assert_eq!(format_array(vec!["a".into(), "b".into()]), "(a, b)");
200 }
201
202 #[test]
203 fn test_dict_builder() {
204 let code = DictBuilder::new()
205 .field("name", "test")
206 .field_opt("value", Some(42i64))
207 .field_opt("empty", None::<&str>)
208 .build();
209 assert!(code.contains("name:"));
210 assert!(code.contains("value:"));
211 assert!(code.contains("empty: none"));
212 }
213}