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}