Skip to main content

typst_library/loading/
json.rs

1use ecow::eco_format;
2use typst_syntax::Spanned;
3
4use crate::diag::{At, LineCol, LoadError, LoadedWithin, SourceResult, bail};
5use crate::engine::Engine;
6use crate::foundations::{Str, Value, func, scope};
7use crate::loading::{DataSource, Load};
8
9/// Reads structured data from a JSON file.
10///
11/// The file must contain a valid JSON value, such as object or array. The JSON
12/// values will be converted into corresponding Typst values as listed in the
13/// @json:conversion[table below].
14///
15/// The function returns a dictionary, an array or, depending on the JSON file,
16/// another JSON data type.
17///
18/// The JSON files in the example contain objects with the keys `temperature`,
19/// `unit`, and `weather`.
20///
21/// = Example <example>
22/// ```example
23/// #let forecast(day) = block[
24///   #box(square(
25///     width: 2cm,
26///     inset: 8pt,
27///     fill: if day.weather == "sunny" {
28///       yellow
29///     } else {
30///       aqua
31///     },
32///     align(
33///       bottom + right,
34///       strong(day.weather),
35///     ),
36///   ))
37///   #h(6pt)
38///   #set text(22pt, baseline: -8pt)
39///   #day.temperature °#day.unit
40/// ]
41///
42/// #forecast(json("monday.json"))
43/// #forecast(json("tuesday.json"))
44/// ```
45///
46/// = #short-or-long[Conversion][Conversion details] <conversion>
47/// #docs-table(
48///   table.header[JSON value][Converted into Typst],
49///
50///   [`null`],
51///   [`{none}`],
52///
53///   [bool],
54///   [@bool],
55///
56///   [number],
57///   [@float or @int],
58///
59///   [string],
60///   [@str],
61///
62///   [array],
63///   [@array],
64///
65///   [object],
66///   [@dictionary],
67/// )
68///
69/// #docs-table(
70///   table.header[Typst value][Converted into JSON],
71///
72///   [types that can be converted from JSON],
73///   [corresponding JSON value],
74///
75///   [@bytes],
76///   [string via @repr],
77///
78///   [@symbol],
79///   [string],
80///
81///   [@content],
82///   [an object describing the content],
83///
84///   [other types (@length, etc.)],
85///   [string via @repr],
86/// )
87///
88/// == Notes <notes>
89/// - In most cases, JSON numbers will be converted to floats or integers
90///   depending on whether they are whole numbers. However, be aware that
91///   integers larger than 2#super[63]-1 or smaller than -2#super[63] will be
92///   converted to floating-point numbers, which may result in an approximative
93///   value.
94///
95/// - Bytes are not encoded as JSON arrays for performance and readability
96///   reasons. Consider using @cbor.encode for binary data.
97///
98/// - The `repr` function is @repr:debugging-only[for debugging purposes only],
99///   and its output is not guaranteed to be stable across Typst versions.
100#[func(scope, title = "JSON")]
101pub fn json(
102    engine: &mut Engine,
103    /// A path to a JSON file or raw JSON bytes.
104    source: Spanned<DataSource>,
105) -> SourceResult<Value> {
106    let loaded = source.load(engine.world)?;
107    let raw = loaded.data.as_slice();
108    // If the file starts with a UTF-8 Byte Order Mark (BOM), return a
109    // friendly error message.
110    if raw.starts_with(b"\xef\xbb\xbf") {
111        bail!(
112            LoadError::text(
113                LineCol::one_based(1, 1),
114                "failed to parse JSON",
115                "unexpected Byte Order Mark",
116            )
117            .within(&loaded)
118            .with_hint("JSON requires UTF-8 without a BOM")
119        );
120    }
121
122    serde_json::from_slice(raw)
123        .map_err(|err| {
124            let pos = LineCol::one_based(err.line(), err.column());
125            LoadError::text(pos, "failed to parse JSON", err)
126        })
127        .within(&loaded)
128}
129
130#[scope]
131impl json {
132    /// Encodes structured data into a JSON string.
133    #[func(title = "Encode JSON")]
134    pub fn encode(
135        /// Value to be encoded.
136        value: Spanned<Value>,
137        /// Whether to pretty print the JSON with newlines and indentation.
138        #[named]
139        #[default(true)]
140        pretty: bool,
141    ) -> SourceResult<Str> {
142        let Spanned { v: value, span } = value;
143        if pretty {
144            serde_json::to_string_pretty(&value)
145        } else {
146            serde_json::to_string(&value)
147        }
148        .map(|v| v.into())
149        .map_err(|err| eco_format!("failed to encode value as JSON ({err})"))
150        .at(span)
151    }
152}