Skip to main content

typst_batch/codegen/
deserialize.rs

1//! JSON → Typst deserialization.
2
3use serde_json::{Map, Value as JsonValue};
4use typst::comemo::Tracked;
5use typst::ecow::EcoVec;
6use typst::engine::Engine;
7use typst::foundations::{Arg, Args, CastInfo, Content, Context, Dict, Func, Str, Value};
8use typst::foundations::SymbolElem;
9use typst::syntax::{Span, Spanned};
10use typst::text::{SpaceElem, TextElem};
11use typst::Library;
12
13use super::error::ConvertError;
14use super::literal::{parse_typst_literal, parse_length, parse_angle, parse_ratio, parse_color};
15use super::lookup::{find_element_funcs, find_element_in_scope};
16
17/// Convert JSON to Typst Content.
18///
19/// Reconstructs a `Content` object from `typst query` JSON output.
20///
21/// # Special Elements
22///
23/// These elements require special handling because they are not registered
24/// as public element functions in Typst's global scope:
25///
26/// - `text` - Constructor expects `body: Content`, not `text: String`
27/// - `space` - Not registered in global scope
28/// - `sequence` - Internal representation, not a public function
29/// - `symbol` - Math symbol, not in global scope
30pub fn json_to_content(
31    engine: &mut Engine,
32    context: Tracked<Context>,
33    library: &Library,
34    json: &JsonValue,
35) -> Result<Content, ConvertError> {
36    json_to_content_with_ancestors(engine, context, library, json, &[])
37}
38
39/// Convert JSON to Typst Content with ancestor context.
40///
41/// The ancestors are used for context-aware lookup of sub-elements.
42/// For example, when deserializing `grid.header.cell`, we look for `cell`
43/// in the ancestors' scopes (grid.header, then grid) to find `grid.cell`.
44fn json_to_content_with_ancestors(
45    engine: &mut Engine,
46    context: Tracked<Context>,
47    library: &Library,
48    json: &JsonValue,
49    ancestors: &[Func],
50) -> Result<Content, ConvertError> {
51    let obj = json.as_object().ok_or(ConvertError::NotObject(json_type_name(json)))?;
52    let func_name = obj
53        .get("func")
54        .and_then(|v| v.as_str())
55        .ok_or(ConvertError::MissingFunc)?;
56
57    // Special elements not in global scope
58    match func_name {
59        "text" => {
60            let text = obj
61                .get("text")
62                .and_then(|v| v.as_str())
63                .ok_or(ConvertError::MissingField("text"))?;
64            return Ok(TextElem::packed(text));
65        }
66        "space" => {
67            return Ok(SpaceElem::shared().clone());
68        }
69        "symbol" => {
70            // Math symbol (e.g., x, y, alpha)
71            let text = obj
72                .get("text")
73                .and_then(|v| v.as_str())
74                .ok_or(ConvertError::MissingField("text"))?;
75            return Ok(SymbolElem::packed(text.chars().next().unwrap_or('?')));
76        }
77        "sequence" => {
78            let children = obj
79                .get("children")
80                .and_then(|v| v.as_array())
81                .ok_or(ConvertError::MissingField("children"))?;
82            let contents: Result<Vec<Content>, _> = children
83                .iter()
84                .map(|c| json_to_content_with_ancestors(engine, context, library, c, ancestors))
85                .collect();
86            return Ok(Content::sequence(contents?));
87        }
88        "styled" => {
89            // Styled element: styles are lost in JSON serialization (".."),
90            // so we just return the child content without styles.
91            let child = obj
92                .get("child")
93                .ok_or(ConvertError::MissingField("child"))?;
94            return json_to_content_with_ancestors(engine, context, library, child, ancestors);
95        }
96        _ => {}
97    }
98
99    // Try to find element in ancestors' scopes (most recent first)
100    let func = ancestors
101        .iter()
102        .rev()
103        .find_map(|ancestor| find_element_in_scope(ancestor, func_name))
104        // Fall back to global lookup with field-based disambiguation
105        .or_else(|| find_best_matching_element(library, func_name, obj))
106        .ok_or_else(|| ConvertError::UnknownElement(func_name.to_string()))?;
107
108    let args = build_args(&func, obj, engine, context, library, ancestors)?;
109
110    func.call(engine, context, args)
111        .map_err(|e| ConvertError::CallFailed {
112            func: func_name.to_string(),
113            reason: e.iter().map(|d| d.message.to_string()).collect::<Vec<_>>().join("; "),
114        })?
115        .cast::<Content>()
116        .map_err(|_| ConvertError::CallFailed {
117            func: func_name.to_string(),
118            reason: "result is not Content".to_string(),
119        })
120}
121
122/// Convert JSON to Typst Value.
123///
124/// - Objects with "func" field → Content
125/// - Objects without "func" → Dict
126/// - Arrays → Array
127/// - Primitives → corresponding Value variant
128pub fn json_to_value(
129    engine: &mut Engine,
130    context: Tracked<Context>,
131    library: &Library,
132    json: &JsonValue,
133) -> Result<Value, ConvertError> {
134    json_to_value_with_ancestors(engine, context, library, json, &[])
135}
136
137/// Convert JSON to Typst Value with ancestor context.
138///
139/// # Type Markers
140///
141/// Supports explicit type markers using `_typst_type` to disambiguate
142/// serialized values that lose type information:
143///
144/// ```json
145/// {"_typst_type": "length", "value": "12pt"}
146/// {"_typst_type": "angle", "value": "90deg"}
147/// {"_typst_type": "ratio", "value": "50%"}
148/// {"_typst_type": "color", "value": "#ff0000"}
149/// ```
150fn json_to_value_with_ancestors(
151    engine: &mut Engine,
152    context: Tracked<Context>,
153    library: &Library,
154    json: &JsonValue,
155    ancestors: &[Func],
156) -> Result<Value, ConvertError> {
157    match json {
158        JsonValue::Null => Ok(Value::None),
159        JsonValue::Bool(b) => Ok(Value::Bool(*b)),
160        JsonValue::Number(n) => {
161            if let Some(i) = n.as_i64() {
162                Ok(Value::Int(i))
163            } else {
164                Ok(Value::Float(n.as_f64().ok_or(ConvertError::ValueConversion)?))
165            }
166        }
167        JsonValue::String(s) => {
168            // Try to parse as Typst literal (length, angle, ratio, color, auto, none)
169            if let Some(value) = parse_typst_literal(s) {
170                return Ok(value);
171            }
172            Ok(Value::Str(s.as_str().into()))
173        }
174        JsonValue::Array(arr) => {
175            let items: Result<Vec<Value>, _> = arr
176                .iter()
177                .map(|v| json_to_value_with_ancestors(engine, context, library, v, ancestors))
178                .collect();
179            Ok(Value::Array(items?.into_iter().collect()))
180        }
181        JsonValue::Object(obj) => {
182            // Check for explicit type marker: {"_typst_type": "...", "value": "..."}
183            if let Some(type_tag) = obj.get("_typst_type").and_then(|v| v.as_str()) {
184                return parse_typed_value(type_tag, obj);
185            }
186
187            // Check for Content marker: {"func": "..."}
188            if obj.contains_key("func") {
189                let content = json_to_content_with_ancestors(engine, context, library, json, ancestors)?;
190                Ok(Value::Content(content))
191            } else {
192                // Regular Dict
193                let dict: Result<Dict, _> = obj
194                    .iter()
195                    .map(|(k, v)| {
196                        let value = json_to_value_with_ancestors(engine, context, library, v, ancestors)?;
197                        Ok((Str::from(k.as_str()), value))
198                    })
199                    .collect();
200                Ok(Value::Dict(dict?))
201            }
202        }
203    }
204}
205
206/// Parse a value with explicit type marker.
207///
208/// Handles objects like: `{"_typst_type": "length", "value": "12pt"}`
209fn parse_typed_value(type_tag: &str, obj: &Map<String, JsonValue>) -> Result<Value, ConvertError> {
210    let value_str = obj
211        .get("value")
212        .and_then(|v| v.as_str())
213        .ok_or(ConvertError::MissingField("value"))?;
214
215    match type_tag {
216        "length" => parse_length(value_str)
217            .map(Value::Length)
218            .ok_or_else(|| ConvertError::InvalidLiteral {
219                type_name: "length",
220                value: value_str.to_string(),
221            }),
222        "angle" => parse_angle(value_str)
223            .map(Value::Angle)
224            .ok_or_else(|| ConvertError::InvalidLiteral {
225                type_name: "angle",
226                value: value_str.to_string(),
227            }),
228        "ratio" => parse_ratio(value_str)
229            .map(Value::Ratio)
230            .ok_or_else(|| ConvertError::InvalidLiteral {
231                type_name: "ratio",
232                value: value_str.to_string(),
233            }),
234        "color" => parse_color(value_str)
235            .map(Value::Color)
236            .ok_or_else(|| ConvertError::InvalidLiteral {
237                type_name: "color",
238                value: value_str.to_string(),
239            }),
240        "str" | "string" => {
241            // Explicit string - do NOT parse as literal
242            Ok(Value::Str(value_str.into()))
243        }
244        _ => Err(ConvertError::UnknownTypeTag(type_tag.to_string())),
245    }
246}
247
248/// Build arguments from function's parameter info.
249///
250/// Strategy:
251/// - Positional-only parameters (positional=true, named=false) must be passed positionally
252///   - For optional positional-only params:
253///     - If the param type accepts `none`, pass `None` to maintain order
254///     - Otherwise, skip (Typst uses type inference)
255/// - Named-only parameters (positional=false, named=true) must be passed by name
256/// - Dual parameters (positional=true, named=true) are passed by name to avoid ordering issues
257/// - Variadic parameters are expanded into multiple positional arguments
258fn build_args(
259    func: &Func,
260    obj: &Map<String, JsonValue>,
261    engine: &mut Engine,
262    context: Tracked<Context>,
263    library: &Library,
264    ancestors: &[Func],
265) -> Result<Args, ConvertError> {
266    let span = Span::detached();
267    let mut items: EcoVec<Arg> = EcoVec::new();
268
269    let params = func.params().ok_or(ConvertError::ValueConversion)?;
270
271    // Build new ancestors list with current func
272    let mut new_ancestors = ancestors.to_vec();
273    new_ancestors.push(func.clone());
274
275    // Collect positional-only parameters in order
276    let positional_only: Vec<_> = params
277        .iter()
278        .filter(|p| p.positional && !p.named)
279        .collect();
280
281    // First, handle positional-only parameters (must be in order)
282    for param in &positional_only {
283        if let Some(value) = obj.get(param.name) {
284            if param.variadic {
285                // Expand variadic into multiple positional args
286                if let Some(arr) = value.as_array() {
287                    for item in arr {
288                        let typst_value = json_to_value_with_ancestors(
289                            engine, context, library, item, &new_ancestors,
290                        )?;
291                        items.push(Arg {
292                            span,
293                            name: None,
294                            value: Spanned::new(typst_value, span),
295                        });
296                    }
297                } else {
298                    let typst_value = json_to_value_with_ancestors(
299                        engine, context, library, value, &new_ancestors,
300                    )?;
301                    items.push(Arg {
302                        span,
303                        name: None,
304                        value: Spanned::new(typst_value, span),
305                    });
306                }
307            } else {
308                let typst_value = json_to_value_with_ancestors(
309                    engine, context, library, value, &new_ancestors,
310                )?;
311                items.push(Arg {
312                    span,
313                    name: None,
314                    value: Spanned::new(typst_value, span),
315                });
316            }
317        } else if !param.required && param_accepts_none(param) {
318            // For optional positional-only params that accept `none`,
319            // pass None to maintain argument order
320            items.push(Arg {
321                span,
322                name: None,
323                value: Spanned::new(Value::None, span),
324            });
325        }
326        // For optional params that don't accept `none`, skip them.
327        // Typst uses type inference to match arguments to parameters.
328    }
329
330    // Collect names of positional-only params to skip them in named processing
331    let positional_only_names: std::collections::HashSet<_> =
332        positional_only.iter().map(|p| p.name).collect();
333
334    // Then, handle all other parameters as named arguments
335    for (key, value) in obj.iter() {
336        if key == "func" || positional_only_names.contains(key.as_str()) {
337            continue;
338        }
339
340        // Check if this is a variadic parameter
341        let param = params.iter().find(|p| p.name == key);
342        if let Some(param) = param
343            && param.variadic {
344                // Expand variadic into multiple positional args
345                if let Some(arr) = value.as_array() {
346                    for item in arr {
347                        let typst_value = json_to_value_with_ancestors(
348                            engine, context, library, item, &new_ancestors,
349                        )?;
350                        items.push(Arg {
351                            span,
352                            name: None,
353                            value: Spanned::new(typst_value, span),
354                        });
355                    }
356                    continue;
357                }
358            }
359
360        // Regular named argument
361        let typst_value = json_to_value_with_ancestors(engine, context, library, value, &new_ancestors)?;
362        items.push(Arg {
363            span,
364            name: Some(Str::from(key.as_str())),
365            value: Spanned::new(typst_value, span),
366        });
367    }
368
369    Ok(Args { span, items })
370}
371
372/// Check if a parameter's type accepts `none`.
373fn param_accepts_none(param: &typst::foundations::ParamInfo) -> bool {
374    let mut accepts_none = false;
375    param.input.walk(|info| {
376        match info {
377            CastInfo::Any => accepts_none = true,
378            CastInfo::Type(ty) if ty.short_name() == "none" => accepts_none = true,
379            _ => {}
380        }
381    });
382    accepts_none
383}
384
385fn json_type_name(json: &JsonValue) -> &'static str {
386    match json {
387        JsonValue::Null => "null",
388        JsonValue::Bool(_) => "bool",
389        JsonValue::Number(_) => "number",
390        JsonValue::String(_) => "string",
391        JsonValue::Array(_) => "array",
392        JsonValue::Object(_) => "object",
393    }
394}
395
396/// Find the best matching element function based on JSON fields.
397///
398/// When multiple elements share the same name (e.g., `list.item`, `enum.item`, `terms.item`),
399/// we use the JSON fields to determine which element is the best match.
400fn find_best_matching_element(
401    library: &Library,
402    func_name: &str,
403    obj: &Map<String, JsonValue>,
404) -> Option<Func> {
405    let candidates: Vec<_> = find_element_funcs(library, func_name).collect();
406
407    if candidates.len() <= 1 {
408        return candidates.into_iter().next();
409    }
410
411    // Score each candidate based on how well its parameters match the JSON fields
412    let json_fields: std::collections::HashSet<_> = obj.keys().filter(|k| *k != "func").collect();
413
414    candidates
415        .into_iter()
416        .max_by_key(|func| {
417            let params = func.params().unwrap_or_default();
418            let param_names: std::collections::HashSet<_> = params.iter().map(|p| p.name).collect();
419
420            // Count how many JSON fields match parameter names
421            let matches = json_fields
422                .iter()
423                .filter(|f| param_names.contains(f.as_str()))
424                .count();
425
426            // Penalize if there are required params not in JSON
427            let missing_required = params
428                .iter()
429                .filter(|p| p.required && !json_fields.contains(&p.name.to_string()))
430                .count();
431
432            // Score: matches - missing_required
433            (matches as i32) - (missing_required as i32)
434        })
435}