From 6134e3f4ee5298153c36d344df97f36279931c33 Mon Sep 17 00:00:00 2001 From: HarmoGlace <23212967+HarmoGlace@users.noreply.github.com> Date: Wed, 26 Apr 2023 11:31:32 +0200 Subject: [PATCH] Add toml support (#807) --- Cargo.lock | 53 ++++++++++++++++++++++ assets/files/bad.toml | 1 + assets/files/informations.toml | 11 +++++ assets/files/toml_types.toml | 11 +++++ library/Cargo.toml | 1 + library/src/compute/data.rs | 81 ++++++++++++++++++++++++++++++++++ library/src/lib.rs | 1 + tests/typ/compute/data.typ | 17 +++++++ 8 files changed, 176 insertions(+) create mode 100644 assets/files/bad.toml create mode 100644 assets/files/informations.toml create mode 100644 assets/files/toml_types.toml diff --git a/Cargo.lock b/Cargo.lock index efe42339d..e702406c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1848,6 +1848,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +dependencies = [ + "serde", +] + [[package]] name = "serde_yaml" version = "0.8.26" @@ -2139,6 +2148,40 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tracing" version = "0.1.38" @@ -2348,6 +2391,7 @@ dependencies = [ "serde_yaml", "smallvec", "syntect", + "toml", "tracing", "ttf-parser 0.18.1", "typed-arena", @@ -2882,6 +2926,15 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "winnow" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +dependencies = [ + "memchr", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/assets/files/bad.toml b/assets/files/bad.toml new file mode 100644 index 000000000..2d5da4d34 --- /dev/null +++ b/assets/files/bad.toml @@ -0,0 +1 @@ +"only a string" \ No newline at end of file diff --git a/assets/files/informations.toml b/assets/files/informations.toml new file mode 100644 index 000000000..f6e3b5fb0 --- /dev/null +++ b/assets/files/informations.toml @@ -0,0 +1,11 @@ +authors = ["Mr Robert", "Miss Enola", "Mr Jonathan"] +version = "1.0.3" + +series = [ + { name = "attack on titan", fans = 500}, + { name = "demon slayer", fans = 10} +] + +[informations] +location = "room A204" +pages = 47 \ No newline at end of file diff --git a/assets/files/toml_types.toml b/assets/files/toml_types.toml new file mode 100644 index 000000000..08f1118d5 --- /dev/null +++ b/assets/files/toml_types.toml @@ -0,0 +1,11 @@ +string = "wonderful" +integer = 42 +float = 3.14 +boolean = true +date_time = 2023-02-01T15:38:57Z +array = [1, "string", 3.0, false] +inline_table = { first = "amazing", second = "greater" } + +[table] +element = 5 +others = [false, "indeed", 7] \ No newline at end of file diff --git a/library/Cargo.toml b/library/Cargo.toml index 889ea70de..c0c20c411 100644 --- a/library/Cargo.toml +++ b/library/Cargo.toml @@ -40,3 +40,4 @@ unicode-segmentation = "1" xi-unicode = "0.3" chinese-number = { version = "0.7.2", default-features = false, features = ["number-to-chinese"] } tracing = "0.1.37" +toml = { version = "0.7.3", default-features = false, features = ["parse"]} diff --git a/library/src/compute/data.rs b/library/src/compute/data.rs index fc81435ca..02586ddc4 100644 --- a/library/src/compute/data.rs +++ b/library/src/compute/data.rs @@ -207,6 +207,87 @@ fn format_json_error(error: serde_json::Error) -> EcoString { eco_format!("failed to parse json file: syntax error in line {}", error.line()) } +/// Read structured data from a TOML file. +/// +/// The file must contain a valid TOML table. Tables will be +/// converted into Typst dictionaries, and TOML arrays will be converted into +/// Typst arrays. Strings and booleans will be converted into the Typst +/// equivalents, numbers will be converted to floats or integers depending on +/// whether they are whole numbers. TOML DateTim will be converted to strings. +/// +/// The function returns a dictionary. +/// +/// The JSON files in the example contain objects with the keys `temperature`, +/// `unit`, and `weather`. +/// +/// ## Example +/// ```example +/// #let informations(content) = { +/// [This work is made by #content.authors.join(", ", last: " and "). We are currently at version #content.version. +/// The favorites series of the audience are ] +/// for serie in content.series [ +/// - #serie.name with #serie.fans fans. +/// ] +/// [We need to submit our work in #content.informations.location, we currently have #content.informations.pages pages.] +/// } +/// +/// +/// #informations(toml("informations.toml")) +/// ``` +/// +/// Display: TOML +/// Category: data-loading +/// Returns: dictionary +#[func] +pub fn toml( + /// Path to a TOML file. + path: Spanned, +) -> Value { + let Spanned { v: path, span } = path; + let path = vm.locate(&path).at(span)?; + let data = vm.world().file(&path).at(span)?; + + let raw = std::str::from_utf8(&data) + .map_err(|_| "file is not valid utf-8") + .at(span)?; + + let value: toml::Value = toml::from_str(raw).map_err(format_toml_error).at(span)?; + convert_toml(value) +} + +/// Convert a TOML value to a Typst value. +fn convert_toml(value: toml::Value) -> Value { + match value { + toml::Value::String(v) => Value::Str(v.into()), + toml::Value::Integer(v) => Value::Int(v), + toml::Value::Float(v) => Value::Float(v), + toml::Value::Boolean(v) => Value::Bool(v), + toml::Value::Array(v) => Value::Array(v.into_iter().map(convert_toml).collect()), + toml::Value::Table(v) => Value::Dict( + v.into_iter() + .map(|(key, value)| (key.into(), convert_toml(value))) + .collect(), + ), + // Todo: make it use native date/time object(s) once it is implemented. + toml::Value::Datetime(v) => Value::Str(v.to_string().into()), + } +} + +/// Format the user-facing TOML error message. +#[track_caller] +fn format_toml_error(error: toml::de::Error) -> String { + if let Some(range) = error.span() { + format!( + "failed to parse toml file: {message}, index {start}-{end}", + message = error.message(), + start = range.start, + end = range.end + ) + } else { + format!("failed to parse toml file: {message}", message = error.message()) + } +} + /// Read structured data from a YAML file. /// /// The file must contain a valid YAML object or array. YAML mappings will be diff --git a/library/src/lib.rs b/library/src/lib.rs index e7a23cd7c..0350746dc 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -131,6 +131,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("toml", compute::toml); global.define("yaml", compute::yaml); global.define("xml", compute::xml); diff --git a/tests/typ/compute/data.typ b/tests/typ/compute/data.typ index cfd761df6..8b50f7c46 100644 --- a/tests/typ/compute/data.typ +++ b/tests/typ/compute/data.typ @@ -41,6 +41,23 @@ // Error: 7-18 failed to parse json file: syntax error in line 3 #json("/bad.json") +--- +// Test reading TOML data. +#let data = toml("/toml_types.toml") +#test(data.string, "wonderful") +#test(data.integer, 42) +#test(data.float, 3.14) +#test(data.boolean, true) +#test(data.date_time, "2023-02-01T15:38:57Z") +#test(data.array, (1, "string", 3.0, false)) +#test(data.inline_table, ("first": "amazing", "second": "greater") ) +#test(data.table.element, 5) +#test(data.table.others, (false, "indeed", 7)) + +--- +// Error: 7-18 failed to parse toml file: expected `.`, `=`, index 15-15 +#toml("/bad.toml") + --- // Test reading YAML data #let data = yaml("/yamltypes.yaml")