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}