summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--assets/files/bad.yaml1
-rw-r--r--assets/files/scifi-authors.yaml11
-rw-r--r--assets/files/yamltypes.yaml8
-rw-r--r--library/Cargo.toml1
-rw-r--r--library/src/compute/data.rs90
-rw-r--r--library/src/lib.rs1
-rw-r--r--tests/typ/compute/data.typ18
8 files changed, 131 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 7394da54..2e11dc18 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1580,6 +1580,7 @@ dependencies = [
"roxmltree",
"rustybuzz",
"serde_json",
+ "serde_yaml",
"smallvec",
"syntect",
"ttf-parser 0.18.1",
diff --git a/assets/files/bad.yaml b/assets/files/bad.yaml
new file mode 100644
index 00000000..330ec7a9
--- /dev/null
+++ b/assets/files/bad.yaml
@@ -0,0 +1 @@
+this_will_break: [) \ No newline at end of file
diff --git a/assets/files/scifi-authors.yaml b/assets/files/scifi-authors.yaml
new file mode 100644
index 00000000..1cffc760
--- /dev/null
+++ b/assets/files/scifi-authors.yaml
@@ -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
diff --git a/assets/files/yamltypes.yaml b/assets/files/yamltypes.yaml
new file mode 100644
index 00000000..eb759777
--- /dev/null
+++ b/assets/files/yamltypes.yaml
@@ -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 \ No newline at end of file
diff --git a/library/Cargo.toml b/library/Cargo.toml
index a7f1f418..d1fe4267 100644
--- a/library/Cargo.toml
+++ b/library/Cargo.toml
@@ -23,6 +23,7 @@ once_cell = "1"
roxmltree = "0.14"
rustybuzz = "0.5"
serde_json = "1"
+serde_yaml = "0.8"
smallvec = "1.10"
syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] }
ttf-parser = "0.18.1"
diff --git a/library/src/compute/data.rs b/library/src/compute/data.rs
index 0c17972d..c2dc632b 100644
--- a/library/src/compute/data.rs
+++ b/library/src/compute/data.rs
@@ -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())
}
+/// 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.
///
/// The XML file is parsed into an array of dictionaries and strings. XML nodes
diff --git a/library/src/lib.rs b/library/src/lib.rs
index 178db8a2..cabafd8c 100644
--- a/library/src/lib.rs
+++ b/library/src/lib.rs
@@ -124,6 +124,7 @@ fn global(math: Module, calc: Module) -> Module {
global.define("read", compute::read);
global.define("csv", compute::csv);
global.define("json", compute::json);
+ global.define("yaml", compute::yaml);
global.define("xml", compute::xml);
// Calc.
diff --git a/tests/typ/compute/data.typ b/tests/typ/compute/data.typ
index 43746e18..cfd761df 100644
--- a/tests/typ/compute/data.typ
+++ b/tests/typ/compute/data.typ
@@ -42,6 +42,24 @@
#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.
#let data = xml("/data.xml")
#test(data, ((