add support for loading data from yaml files (#447)

This commit is contained in:
P-Andersson 2023-04-01 14:33:42 +02:00 committed by GitHub
parent 9e69a7b161
commit 387bcc3879
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 131 additions and 0 deletions

1
Cargo.lock generated
View File

@ -1580,6 +1580,7 @@ dependencies = [
"roxmltree", "roxmltree",
"rustybuzz", "rustybuzz",
"serde_json", "serde_json",
"serde_yaml",
"smallvec", "smallvec",
"syntect", "syntect",
"ttf-parser 0.18.1", "ttf-parser 0.18.1",

1
assets/files/bad.yaml Normal file
View File

@ -0,0 +1 @@
this_will_break: [)

View File

@ -0,0 +1,11 @@
"Arthur C. Clarke":
- title: Against the Fall of Night
published: "1978"
- title: The songs of distant earth
published: "1986"
"Isaac Asimov":
- title: Quasar, Quasar, Burning Bright
published: "1977"
- title: Far as Human Eye Could See
published: 1987

View File

@ -0,0 +1,8 @@
null_key: [null, ~]
"string": text
integer: 5
float: 1.12
mapping: { '1': "one", '2': "two"}
seq: [1, 2, 3, 4]
bool: false
true: bool

View File

@ -23,6 +23,7 @@ once_cell = "1"
roxmltree = "0.14" roxmltree = "0.14"
rustybuzz = "0.5" rustybuzz = "0.5"
serde_json = "1" serde_json = "1"
serde_yaml = "0.8"
smallvec = "1.10" smallvec = "1.10"
syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] } syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] }
ttf-parser = "0.18.1" ttf-parser = "0.18.1"

View File

@ -207,6 +207,96 @@ fn format_json_error(error: serde_json::Error) -> String {
format!("failed to parse json file: syntax error in line {}", error.line()) format!("failed to parse json file: syntax error in line {}", error.line())
} }
/// Read structured data from a YAML file.
///
/// The file must contain a valid YAML object or array. YAML mappings will be
/// converted into Typst dictionaries, and YAML sequences will be converted into
/// Typst arrays. Strings and booleans will be converted into the Typst
/// equivalents, null-values (`null`, `~` or empty ``) will be converted into
/// `{none}`, and numbers will be converted to floats or integers depending on
/// whether they are whole numbers.
///
/// Note that mapping keys that are not a string cause the entry to be
/// discarded.
///
/// Custom YAML tags are ignored, though the loaded value will still be
/// present.
///
/// The function returns a dictionary or value or an array, depending on
/// the YAML file.
///
/// The YAML files in the example contain objects with authors as keys,
/// each with a sequence of their own submapping with the keys
/// "title" and "published"
///
/// ## Example
/// ```example
/// #let bookshelf(contents) = {
/// for author, works in contents {
/// author
/// for work in works [
/// - #work.title (#work.published)
/// ]
/// }
/// }
///
/// #bookshelf(yaml("scifi-authors.yaml"))
/// ```
///
/// Display: YAML
/// Category: data-loading
/// Returns: array or value or dictionary
#[func]
pub fn yaml(
/// Path to a YAML file.
path: Spanned<EcoString>,
) -> Value {
let Spanned { v: path, span } = path;
let path = vm.locate(&path).at(span)?;
let data = vm.world().file(&path).at(span)?;
let value: serde_yaml::Value =
serde_yaml::from_slice(&data).map_err(format_yaml_error).at(span)?;
convert_yaml(value)
}
/// Convert a YAML value to a Typst value.
fn convert_yaml(value: serde_yaml::Value) -> Value {
match value {
serde_yaml::Value::Null => Value::None,
serde_yaml::Value::Bool(v) => Value::Bool(v),
serde_yaml::Value::Number(v) => match v.as_i64() {
Some(int) => Value::Int(int),
None => Value::Float(v.as_f64().unwrap_or(f64::NAN)),
},
serde_yaml::Value::String(v) => Value::Str(v.into()),
serde_yaml::Value::Sequence(v) => {
Value::Array(v.into_iter().map(convert_yaml).collect())
}
serde_yaml::Value::Mapping(v) => Value::Dict(
v.into_iter()
.map(|(key, value)| (convert_yaml_key(key), convert_yaml(value)))
.filter_map(|(key, value)| key.map(|key|(key, value)))
.collect(),
)
}
}
/// Converts an arbitary YAML mapping key into a Typst Dict Key.
/// Currently it only does so for strings, everything else
/// returns None
fn convert_yaml_key(key: serde_yaml::Value) -> Option<Str> {
match key {
serde_yaml::Value::String(v) => Some(Str::from(v)),
_ => None,
}
}
/// Format the user-facing YAML error message.
#[track_caller]
fn format_yaml_error(error: serde_yaml::Error) -> String {
format!("failed to parse yaml file: {}", error.to_string().trim())
}
/// Read structured data from an XML file. /// Read structured data from an XML file.
/// ///
/// The XML file is parsed into an array of dictionaries and strings. XML nodes /// The XML file is parsed into an array of dictionaries and strings. XML nodes

View File

@ -124,6 +124,7 @@ fn global(math: Module, calc: Module) -> Module {
global.define("read", compute::read); global.define("read", compute::read);
global.define("csv", compute::csv); global.define("csv", compute::csv);
global.define("json", compute::json); global.define("json", compute::json);
global.define("yaml", compute::yaml);
global.define("xml", compute::xml); global.define("xml", compute::xml);
// Calc. // Calc.

View File

@ -41,6 +41,24 @@
// Error: 7-18 failed to parse json file: syntax error in line 3 // Error: 7-18 failed to parse json file: syntax error in line 3
#json("/bad.json") #json("/bad.json")
---
// Test reading YAML data
#let data = yaml("/yamltypes.yaml")
#test(data.len(), 7)
#test(data.null_key, (none, none))
#test(data.string, "text")
#test(data.integer, 5)
#test(data.float, 1.12)
#test(data.mapping, ("1": "one", "2": "two"))
#test(data.seq, (1,2,3,4))
#test(data.bool, false)
#test(data.keys().contains("true"), false)
---
---
// Error: 7-18 failed to parse yaml file: while parsing a flow sequence, expected ',' or ']' at line 2 column 1
#yaml("/bad.yaml")
--- ---
// Test reading XML data. // Test reading XML data.
#let data = xml("/data.xml") #let data = xml("/data.xml")