typst_batch/codegen/
inputs.rs1use std::path::Path;
4use std::sync::Arc;
5
6use serde_json::Value as JsonValue;
7use typst::comemo::Track;
8use typst::engine::{Engine, Route, Sink, Traced};
9use typst::foundations::{Array, Context, Dict, IntoValue, Value};
10use typst::introspection::Introspector;
11use typst::World;
12
13use super::{json_to_content, ConvertError};
14use crate::world::TypstWorld;
15
16pub struct Inputs {
20 pub(crate) dict: Dict,
21}
22
23impl Inputs {
24 pub fn empty() -> Self {
26 Self { dict: Dict::new() }
27 }
28
29 pub fn from_json(json: &JsonValue) -> Result<Self, ConvertError> {
54 match json {
55 JsonValue::Object(_) => {
56 let dict = json_to_simple_dict(json)?;
57 Ok(Self { dict })
58 }
59 _ => Err(ConvertError::Other("inputs must be a JSON object".into())),
60 }
61 }
62
63 pub fn from_json_with_content(json: &JsonValue, root: &Path) -> Result<Self, ConvertError> {
94 match json {
95 JsonValue::Object(_) => {
96 let converter = ContentConverter::new(root);
97 let dict = converter.convert_dict(json)?;
98 Ok(Self { dict })
99 }
100 _ => Err(ConvertError::Other("inputs must be a JSON object".into())),
101 }
102 }
103
104 pub fn merge(&mut self, other: Inputs) {
108 for (key, value) in other.dict {
109 self.dict.insert(key, value);
110 }
111 }
112
113 pub fn merge_json(&mut self, json: &JsonValue) -> Result<(), ConvertError> {
115 let other = Self::from_json(json)?;
116 self.merge(other);
117 Ok(())
118 }
119
120 pub fn into_dict(self) -> Dict {
122 self.dict
123 }
124}
125
126pub fn json_to_simple_value(json: &JsonValue) -> Result<Value, ConvertError> {
133 match json {
134 JsonValue::Null => Ok(Value::None),
135 JsonValue::Bool(b) => Ok(b.into_value()),
136 JsonValue::Number(n) => {
137 if let Some(i) = n.as_i64() {
138 Ok(i.into_value())
139 } else if let Some(f) = n.as_f64() {
140 Ok(f.into_value())
141 } else {
142 Err(ConvertError::Other(format!("unsupported number: {n}")))
143 }
144 }
145 JsonValue::String(s) => Ok(s.as_str().into_value()),
146 JsonValue::Array(arr) => {
147 let items: Result<Vec<_>, _> = arr.iter().map(json_to_simple_value).collect();
148 let array: typst::foundations::Array = items?.into_iter().collect();
149 Ok(array.into_value())
150 }
151 JsonValue::Object(_) => {
152 let dict = json_to_simple_dict(json)?;
153 Ok(dict.into_value())
154 }
155 }
156}
157
158fn json_to_simple_dict(json: &JsonValue) -> Result<Dict, ConvertError> {
160 let obj = json
161 .as_object()
162 .ok_or_else(|| ConvertError::Other("expected JSON object".into()))?;
163
164 let mut dict = Dict::new();
165 for (key, value) in obj {
166 dict.insert(key.as_str().into(), json_to_simple_value(value)?);
167 }
168 Ok(dict)
169}
170
171struct ContentConverter {
177 world: Arc<TypstWorld>,
178}
179
180impl ContentConverter {
181 fn new(root: &Path) -> Self {
182 let dummy_path = root.join("__content_converter_dummy.typ");
185 let world = TypstWorld::builder(&dummy_path, root)
186 .with_local_cache()
187 .no_fonts()
188 .build();
189
190 Self {
191 world: Arc::new(world),
192 }
193 }
194
195 fn convert_value(&self, json: &JsonValue) -> Result<Value, ConvertError> {
196 match json {
197 JsonValue::Null => Ok(Value::None),
198 JsonValue::Bool(b) => Ok(b.into_value()),
199 JsonValue::Number(n) => {
200 if let Some(i) = n.as_i64() {
201 Ok(i.into_value())
202 } else if let Some(f) = n.as_f64() {
203 Ok(f.into_value())
204 } else {
205 Err(ConvertError::Other(format!("unsupported number: {n}")))
206 }
207 }
208 JsonValue::String(s) => Ok(s.as_str().into_value()),
209 JsonValue::Array(arr) => {
210 let mut result = Array::new();
211 for item in arr {
212 result.push(self.convert_value(item)?);
213 }
214 Ok(result.into_value())
215 }
216 JsonValue::Object(obj) => {
217 if obj.contains_key("func") {
219 self.rebuild_content(json)
220 } else {
221 Ok(self.convert_dict(json)?.into_value())
222 }
223 }
224 }
225 }
226
227 fn convert_dict(&self, json: &JsonValue) -> Result<Dict, ConvertError> {
228 let obj = json
229 .as_object()
230 .ok_or_else(|| ConvertError::Other("expected JSON object".into()))?;
231
232 let mut dict = Dict::new();
233 for (key, value) in obj {
234 dict.insert(key.as_str().into(), self.convert_value(value)?);
235 }
236 Ok(dict)
237 }
238
239 fn rebuild_content(&self, json: &JsonValue) -> Result<Value, ConvertError> {
240 let introspector = Introspector::default();
241 let traced = Traced::default();
242 let mut sink = Sink::new();
243
244 let mut engine = Engine {
245 world: (&*self.world as &dyn World).track(),
246 introspector: introspector.track(),
247 traced: traced.track(),
248 sink: sink.track_mut(),
249 route: Route::default(),
250 routines: &typst::ROUTINES,
251 };
252
253 let library = self.world.library();
254 let context = Context::none();
255
256 let content = json_to_content(&mut engine, context.track(), library, json)?;
257 Ok(content.into_value())
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use serde_json::json;
265 use tempfile::TempDir;
266 use typst::foundations::Str;
267
268 #[test]
269 fn test_from_json_simple() {
270 let json = json!({
271 "title": "My Blog",
272 "count": 42,
273 "ratio": 3.14,
274 "draft": false
275 });
276 let inputs = Inputs::from_json(&json).unwrap();
277 assert_eq!(inputs.dict.len(), 4);
278 }
279
280 #[test]
281 fn test_from_json_nested() {
282 let json = json!({
283 "title": "My Blog",
284 "extra": {
285 "author": "Alice",
286 "twitter": "@alice"
287 }
288 });
289 let inputs = Inputs::from_json(&json).unwrap();
290 assert_eq!(inputs.dict.len(), 2);
291
292 let extra = inputs.dict.get(&Str::from("extra")).unwrap();
294 assert!(extra.clone().cast::<Dict>().is_ok());
295 }
296
297 #[test]
298 fn test_from_json_array() {
299 let json = json!({
300 "tags": ["rust", "typst", "blog"]
301 });
302 let inputs = Inputs::from_json(&json).unwrap();
303
304 let tags = inputs.dict.get(&Str::from("tags")).unwrap();
305 let arr = tags.clone().cast::<Array>().unwrap();
306 assert_eq!(arr.len(), 3);
307 }
308
309 #[test]
310 fn test_from_json_null() {
311 let json = json!({
312 "value": null
313 });
314 let inputs = Inputs::from_json(&json).unwrap();
315
316 let value = inputs.dict.get(&Str::from("value")).unwrap();
317 assert_eq!(*value, Value::None);
318 }
319
320 #[test]
321 fn test_from_json_not_object() {
322 let json = json!("not an object");
323 assert!(Inputs::from_json(&json).is_err());
324 }
325
326 #[test]
327 fn test_from_json_with_content_simple() {
328 let dir = TempDir::new().unwrap();
329
330 let json = json!({
332 "title": "My Blog",
333 "count": 42
334 });
335
336 let inputs = Inputs::from_json_with_content(&json, dir.path()).unwrap();
337 assert_eq!(inputs.dict.len(), 2);
338 }
339
340 #[test]
341 fn test_from_json_with_content_rebuilds_content() {
342 let dir = TempDir::new().unwrap();
343
344 let json = json!({
346 "pages": [
347 {
348 "url": "/post/1",
349 "title": "First Post",
350 "summary": {
351 "func": "text",
352 "text": "Hello world"
353 }
354 }
355 ]
356 });
357
358 let inputs = Inputs::from_json_with_content(&json, dir.path()).unwrap();
359
360 let pages = inputs.dict.get(&Str::from("pages")).unwrap();
362 let pages_arr = pages.clone().cast::<Array>().unwrap();
363 assert_eq!(pages_arr.len(), 1);
364
365 let page = pages_arr.at(0, None).unwrap();
367 let page_dict = page.clone().cast::<Dict>().unwrap();
368
369 let summary = page_dict.get(&Str::from("summary")).unwrap();
371 assert!(
372 summary.clone().cast::<Dict>().is_err(),
373 "summary should be Content, not Dict"
374 );
375 }
376
377 #[test]
378 fn test_from_json_with_content_complex() {
379 let dir = TempDir::new().unwrap();
380
381 let json = json!({
383 "summary": {
384 "func": "sequence",
385 "children": [
386 {"func": "text", "text": "Check out "},
387 {
388 "func": "link",
389 "dest": "https://example.com",
390 "body": {"func": "text", "text": "this link"}
391 },
392 {"func": "text", "text": "!"}
393 ]
394 }
395 });
396
397 let inputs = Inputs::from_json_with_content(&json, dir.path()).unwrap();
398
399 let summary = inputs.dict.get(&Str::from("summary")).unwrap();
400 assert!(summary.clone().cast::<Dict>().is_err());
402 }
403}