typst_library/loading/yaml.rs
1use ecow::eco_format;
2use typst_syntax::Spanned;
3
4use crate::diag::{At, LineCol, LoadError, LoadedWithin, ReportPos, SourceResult};
5use crate::engine::Engine;
6use crate::foundations::{Str, Value, func, scope};
7use crate::loading::{DataSource, Load, Readable};
8
9/// Reads structured data from a YAML file.
10///
11/// The file must contain a valid YAML object or array. The YAML values will be
12/// converted into corresponding Typst values as listed in the
13/// [table below](#conversion).
14///
15/// The function returns a dictionary, an array or, depending on the YAML file,
16/// another YAML data type.
17///
18/// The YAML files in the example contain objects with authors as keys,
19/// each with a sequence of their own submapping with the keys
20/// "title" and "published".
21///
22/// # Example
23/// ```example
24/// #let bookshelf(contents) = {
25/// for (author, works) in contents {
26/// author
27/// for work in works [
28/// - #work.title (#work.published)
29/// ]
30/// }
31/// }
32///
33/// #bookshelf(
34/// yaml("scifi-authors.yaml")
35/// )
36/// ```
37///
38/// # Conversion details { #conversion }
39///
40/// | YAML value | Converted into Typst |
41/// | -------------------------------------- | -------------------- |
42/// | null-values (`null`, `~` or empty ` `) | `{none}` |
43/// | boolean | [`bool`] |
44/// | number | [`float`] or [`int`] |
45/// | string | [`str`] |
46/// | sequence | [`array`] |
47/// | mapping | [`dictionary`] |
48///
49/// | Typst value | Converted into YAML |
50/// | ------------------------------------- | -------------------------------- |
51/// | types that can be converted from YAML | corresponding YAML value |
52/// | [`bytes`] | string via [`repr`] |
53/// | [`symbol`] | string |
54/// | [`content`] | a mapping describing the content |
55/// | other types ([`length`], etc.) | string via [`repr`] |
56///
57/// ## Notes
58/// - In most cases, YAML numbers will be converted to floats or integers
59/// depending on whether they are whole numbers. However, be aware that
60/// integers larger than 2<sup>63</sup>-1 or smaller than -2<sup>63</sup> will
61/// be converted to floating-point numbers, which may result in an
62/// approximative value.
63///
64/// - Custom YAML tags are ignored, though the loaded value will still be present.
65///
66/// - Bytes are not encoded as YAML sequences for performance and readability
67/// reasons. Consider using [`cbor.encode`] for binary data.
68///
69/// - The `repr` function is [for debugging purposes only]($repr/#debugging-only),
70/// and its output is not guaranteed to be stable across Typst versions.
71#[func(scope, title = "YAML")]
72pub fn yaml(
73 engine: &mut Engine,
74 /// A [path]($syntax/#paths) to a YAML file or raw YAML bytes.
75 source: Spanned<DataSource>,
76) -> SourceResult<Value> {
77 let loaded = source.load(engine.world)?;
78 serde_yaml::from_slice(loaded.data.as_slice())
79 .map_err(format_yaml_error)
80 .within(&loaded)
81}
82
83#[scope]
84impl yaml {
85 /// Reads structured data from a YAML string/bytes.
86 #[func(title = "Decode YAML")]
87 #[deprecated(
88 message = "`yaml.decode` is deprecated, directly pass bytes to `yaml` instead",
89 until = "0.15.0"
90 )]
91 pub fn decode(
92 engine: &mut Engine,
93 /// YAML data.
94 data: Spanned<Readable>,
95 ) -> SourceResult<Value> {
96 yaml(engine, data.map(Readable::into_source))
97 }
98
99 /// Encode structured data into a YAML string.
100 #[func(title = "Encode YAML")]
101 pub fn encode(
102 /// Value to be encoded.
103 value: Spanned<Value>,
104 ) -> SourceResult<Str> {
105 let Spanned { v: value, span } = value;
106 serde_yaml::to_string(&value)
107 .map(|v| v.into())
108 .map_err(|err| eco_format!("failed to encode value as YAML ({err})"))
109 .at(span)
110 }
111}
112
113/// Format the user-facing YAML error message.
114pub fn format_yaml_error(error: serde_yaml::Error) -> LoadError {
115 let pos = error
116 .location()
117 .map(|loc| {
118 let line_col = LineCol::one_based(loc.line(), loc.column());
119 let range = loc.index()..loc.index();
120 ReportPos::full(range, line_col)
121 })
122 .unwrap_or_default();
123 LoadError::new(pos, "failed to parse YAML", error)
124}