Skip to main content

typst_batch/codegen/
builder.rs

1//! Typst dict/array builder utilities.
2//!
3//! Helpers for constructing Typst collection literals.
4
5use typst::foundations::{IntoValue, Repr};
6
7/// Builder for Typst dictionary literals.
8///
9/// # Example
10///
11/// ```ignore
12/// use typst_batch::codegen::{DictBuilder, array};
13///
14/// let code = DictBuilder::new()
15///     .field("url", "/posts/")
16///     .field_opt("date", Some("2024-01-15"))
17///     .field_raw("tags", array(["rust", "typst"]))
18///     .build();
19/// ```
20#[derive(Default)]
21pub struct DictBuilder {
22    fields: Vec<String>,
23}
24
25impl DictBuilder {
26    /// Create an empty builder.
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    /// Add a field (value converted via `IntoValue`).
32    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    /// Add an optional field (None outputs `none`).
46    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    /// Add a field with raw Typst code (no conversion).
60    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    /// Add an optional raw field.
71    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    /// Build the Typst dictionary literal.
85    pub fn build(self) -> String {
86        format!("({})", self.fields.join(", "))
87    }
88}
89
90
91
92/// Build a Typst dictionary from entries.
93pub 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
106/// Build a Typst dictionary from raw code entries.
107pub 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
120/// Build a Typst dictionary with optional values.
121pub 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
140/// Build a Typst array from items.
141pub 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
153/// Build a Typst array from raw code items.
154pub 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
163/// Format items as Typst array literal.
164///
165/// Handles edge cases:
166/// - Empty: `()`
167/// - Single: `(item,)` (trailing comma required)
168/// - Multiple: `(a, b, c)`
169pub 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}