Skip to main content

typst_batch/codegen/
serialize.rs

1//! Typst → JSON serialization.
2
3use serde_json::{Map, Value as JsonValue};
4use typst::foundations::{Content, Value};
5
6/// Serialize Content to JSON.
7///
8/// Note: `null` values are stripped from the output for cleaner JSON.
9pub fn content_to_json(content: &Content) -> JsonValue {
10    let json = serde_json::to_value(content).unwrap_or(JsonValue::Null);
11    strip_nulls(json)
12}
13
14/// Serialize Value to JSON.
15///
16/// Note: `null` values are stripped from the output for cleaner JSON.
17pub fn value_to_json(value: &Value) -> JsonValue {
18    let json = serde_json::to_value(value).unwrap_or(JsonValue::Null);
19    strip_nulls(json)
20}
21
22/// Recursively strip `null` values from JSON objects.
23fn strip_nulls(json: JsonValue) -> JsonValue {
24    match json {
25        JsonValue::Object(obj) => {
26            let filtered: Map<String, JsonValue> = obj
27                .into_iter()
28                .filter(|(_, v)| !v.is_null())
29                .map(|(k, v)| (k, strip_nulls(v)))
30                .collect();
31            JsonValue::Object(filtered)
32        }
33        JsonValue::Array(arr) => JsonValue::Array(arr.into_iter().map(strip_nulls).collect()),
34        other => other,
35    }
36}
37
38// =============================================================================
39// Content simplification (extract text from Content JSON)
40// =============================================================================
41
42/// Simplify JSON by extracting text from Content objects.
43///
44/// Content objects (with "func" field) are converted to plain text strings.
45pub fn json_to_simple_text(json: &JsonValue) -> JsonValue {
46    match json {
47        JsonValue::Object(obj) => {
48            if obj.contains_key("func") {
49                // Content: extract text only
50                JsonValue::String(extract_content_text(json))
51            } else {
52                // Regular dict: recurse into values
53                let simplified: Map<String, JsonValue> = obj
54                    .iter()
55                    .map(|(k, v)| (k.clone(), json_to_simple_text(v)))
56                    .collect();
57                JsonValue::Object(simplified)
58            }
59        }
60        JsonValue::Array(arr) => {
61            JsonValue::Array(arr.iter().map(json_to_simple_text).collect())
62        }
63        other => other.clone(),
64    }
65}
66
67/// Recursively extract text from Typst Content JSON.
68fn extract_content_text(json: &JsonValue) -> String {
69    match json {
70        JsonValue::Object(obj) => {
71            // "text" field (text element)
72            if let Some(text) = obj.get("text").and_then(|v| v.as_str()) {
73                return text.to_string();
74            }
75            // "body" field (link, strong, emph, etc.)
76            if let Some(body) = obj.get("body") {
77                return extract_content_text(body);
78            }
79            // "children" array (sequence)
80            if let Some(children) = obj.get("children").and_then(|v| v.as_array()) {
81                return children.iter().map(extract_content_text).collect();
82            }
83            // "child" field
84            if let Some(child) = obj.get("child") {
85                return extract_content_text(child);
86            }
87            String::new()
88        }
89        JsonValue::Array(arr) => arr.iter().map(extract_content_text).collect(),
90        JsonValue::String(s) => s.clone(),
91        _ => String::new(),
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use serde_json::json;
99
100    #[test]
101    fn test_simplify_text_content() {
102        let json = json!({"func": "text", "text": "Hello World"});
103        assert_eq!(json_to_simple_text(&json), json!("Hello World"));
104    }
105
106    #[test]
107    fn test_simplify_link_content() {
108        let json = json!({
109            "func": "link",
110            "dest": "/posts/hello",
111            "body": {"func": "text", "text": "Click here"}
112        });
113        assert_eq!(json_to_simple_text(&json), json!("Click here"));
114    }
115
116    #[test]
117    fn test_simplify_sequence_content() {
118        let json = json!({
119            "func": "sequence",
120            "children": [
121                {"func": "text", "text": "Hello "},
122                {"func": "strong", "body": {"func": "text", "text": "World"}}
123            ]
124        });
125        assert_eq!(json_to_simple_text(&json), json!("Hello World"));
126    }
127
128    #[test]
129    fn test_simplify_nested_dict() {
130        let json = json!({
131            "title": "My Post",
132            "summary": {"func": "text", "text": "A summary"},
133            "next": {
134                "func": "link",
135                "dest": "/next",
136                "body": {"func": "text", "text": "Next Post"}
137            }
138        });
139        let result = json_to_simple_text(&json);
140        assert_eq!(result["title"], "My Post");
141        assert_eq!(result["summary"], "A summary");
142        assert_eq!(result["next"], "Next Post");
143    }
144
145    #[test]
146    fn test_simplify_array_with_content() {
147        let json = json!([
148            {"title": "A", "summary": {"func": "text", "text": "Summary A"}},
149            {"title": "B", "summary": {"func": "text", "text": "Summary B"}}
150        ]);
151        let result = json_to_simple_text(&json);
152        assert_eq!(result[0]["summary"], "Summary A");
153        assert_eq!(result[1]["summary"], "Summary B");
154    }
155}