Skip to main content

typst_library/loading/
yaml.rs

1use ecow::eco_format;
2use typst_syntax::Spanned;
3
4use crate::diag::{At, LineCol, LoadError, LoadedWithin, ReportTextPos, SourceResult};
5use crate::engine::Engine;
6use crate::foundations::{Str, Value, func, scope};
7use crate::loading::{DataSource, Load};
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/// @yaml:conversion[table below].
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, each
19/// with a sequence of their own submapping with the keys "title" and
20/// "published".
21///
22/// = Example <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/// = #short-or-long[Conversion][Conversion details] <conversion>
39/// #docs-table(
40///   table.header[YAML value][Converted into Typst],
41///
42///   [null-values (`null`, `~` or empty ` `)],
43///   [`{none}`],
44///
45///   [boolean],
46///   [@bool],
47///
48///   [number],
49///   [@float or @int],
50///
51///   [string],
52///   [@str],
53///
54///   [sequence],
55///   [@array],
56///
57///   [mapping],
58///   [@dictionary],
59/// )
60///
61/// #docs-table(
62///   table.header[Typst value][Converted into YAML],
63///
64///   [types that can be converted from YAML],
65///   [corresponding YAML value],
66///
67///   [@bytes],
68///   [string via @repr],
69///
70///   [@symbol],
71///   [string],
72///
73///   [@content],
74///   [a mapping describing the content],
75///
76///   [other types (@length, etc.)],
77///   [string via @repr],
78/// )
79///
80/// == Notes <notes>
81/// - In most cases, YAML numbers will be converted to floats or integers
82///   depending on whether they are whole numbers. However, be aware that
83///   integers larger than 2#super[63]-1 or smaller than -2#super[63] will be
84///   converted to floating-point numbers, which may result in an approximative
85///   value.
86///
87/// - Custom YAML tags are ignored, though the loaded value will still be
88///   present.
89///
90/// - Bytes are not encoded as YAML sequences for performance and readability
91///   reasons. Consider using @cbor.encode for binary data.
92///
93/// - The `repr` function is @repr:debugging-only[for debugging purposes only],
94///   and its output is not guaranteed to be stable across Typst versions.
95#[func(scope, title = "YAML")]
96pub fn yaml(
97    engine: &mut Engine,
98    /// A path to a YAML file or raw YAML bytes.
99    source: Spanned<DataSource>,
100) -> SourceResult<Value> {
101    let loaded = source.load(engine.world)?;
102    serde_yaml::from_slice(loaded.data.as_slice())
103        .map_err(format_yaml_error)
104        .within(&loaded)
105}
106
107#[scope]
108impl yaml {
109    /// Encode structured data into a YAML string.
110    #[func(title = "Encode YAML")]
111    pub fn encode(
112        /// Value to be encoded.
113        value: Spanned<Value>,
114    ) -> SourceResult<Str> {
115        let Spanned { v: value, span } = value;
116        serde_yaml::to_string(&value)
117            .map(|v| v.into())
118            .map_err(|err| eco_format!("failed to encode value as YAML ({err})"))
119            .at(span)
120    }
121}
122
123/// Format the user-facing YAML error message.
124pub fn format_yaml_error(error: serde_yaml::Error) -> LoadError {
125    let pos = error
126        .location()
127        .map(|loc| {
128            let line_col = LineCol::one_based(loc.line(), loc.column());
129            let range = loc.index()..loc.index();
130            ReportTextPos::full(range, line_col)
131        })
132        .unwrap_or_default();
133    LoadError::text(pos, "failed to parse YAML", error)
134}