diff --git a/Cargo.lock b/Cargo.lock index ccaf93910..e4a8add70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1061,6 +1061,7 @@ dependencies = [ "rustybuzz", "same-file", "serde", + "serde_json", "siphasher", "subsetter", "svg2pdf", diff --git a/Cargo.toml b/Cargo.toml index 65afa214d..9e3137b13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ syntect = { version = "5", default-features = false, features = ["default-syntax rex = { git = "https://github.com/laurmaedje/ReX" } lipsum = { git = "https://github.com/reknih/lipsum" } csv = "1" +serde_json = "1" # PDF export miniz_oxide = "0.5" diff --git a/src/library/mod.rs b/src/library/mod.rs index 566a4f26d..d806f298e 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -97,6 +97,7 @@ pub fn new() -> Scope { std.def_fn("symbol", utility::symbol); std.def_fn("lorem", utility::lorem); std.def_fn("csv", utility::csv); + std.def_fn("json", utility::json); // Predefined colors. std.define("black", Color::BLACK); diff --git a/src/library/utility/data.rs b/src/library/utility/data.rs index 1ae8949aa..e3efe6e73 100644 --- a/src/library/utility/data.rs +++ b/src/library/utility/data.rs @@ -41,3 +41,46 @@ fn format_csv_error(error: csv::Error) -> String { _ => "failed to parse csv file".into(), } } + +/// Read structured data from a JSON file. +pub fn json(vm: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v: path, span } = + args.expect::>("path to json file")?; + + let path = vm.locate(&path).at(span)?; + let data = vm.world.file(&path).at(span)?; + let value: serde_json::Value = + serde_json::from_slice(&data).map_err(format_json_error).at(span)?; + + Ok(convert_json(value)) +} + +/// Convert a JSON value to a Typst value. +fn convert_json(value: serde_json::Value) -> Value { + match value { + serde_json::Value::Null => Value::None, + serde_json::Value::Bool(v) => Value::Bool(v), + serde_json::Value::Number(v) => match v.as_i64() { + Some(int) => Value::Int(int), + None => Value::Float(v.as_f64().unwrap_or(f64::NAN)), + }, + serde_json::Value::String(v) => Value::Str(v.into()), + serde_json::Value::Array(v) => { + Value::Array(v.into_iter().map(convert_json).collect()) + } + serde_json::Value::Object(v) => Value::Dict( + v.into_iter() + .map(|(key, value)| (key.into(), convert_json(value))) + .collect(), + ), + } +} + +/// Format the user-facing JSON error message. +fn format_json_error(error: serde_json::Error) -> String { + assert!(error.is_syntax() || error.is_eof()); + format!( + "failed to parse json file: syntax error in line {}", + error.line() + ) +} diff --git a/tests/ref/utility/csv.png b/tests/ref/utility/data.png similarity index 100% rename from tests/ref/utility/csv.png rename to tests/ref/utility/data.png diff --git a/tests/res/bad.json b/tests/res/bad.json new file mode 100644 index 000000000..cd5d0366b --- /dev/null +++ b/tests/res/bad.json @@ -0,0 +1,4 @@ +{ + "valid": true, + "invalid": True +} diff --git a/tests/res/zoo.json b/tests/res/zoo.json new file mode 100644 index 000000000..3b5a7ffc1 --- /dev/null +++ b/tests/res/zoo.json @@ -0,0 +1,20 @@ +[ + { + "name": "Debby", + "species": "Rhinoceros", + "weight": 1900, + "length": 390 + }, + { + "name": "Fluffy", + "species": "Tiger", + "weight": 115, + "length": 310 + }, + { + "name": "Sleepy", + "species": "Dolphin", + "weight": 150, + "length": 180 + } +] diff --git a/tests/typ/utility/csv.typ b/tests/typ/utility/data.typ similarity index 52% rename from tests/typ/utility/csv.typ rename to tests/typ/utility/data.typ index 146cafae4..4fdb84c27 100644 --- a/tests/typ/utility/csv.typ +++ b/tests/typ/utility/data.typ @@ -1,6 +1,9 @@ -// Test reading structured CSV data. +// Test reading structured data. +// Ref: false --- +// Test reading CSV data. +// Ref: true #set page(width: auto) #let data = csv("/res/zoo.csv") #let cells = data(0).map(strong) + data.slice(1).flatten() @@ -13,3 +16,14 @@ --- // Error: 6-20 failed to parse csv file: found 3 instead of 2 fields in line 3 #csv("/res/bad.csv") + +--- +// Test reading JSON data. +#let data = json("/res/zoo.json") +#test(data.len(), 3) +#test(data(0).name, "Debby") +#test(data(2).weight, 150) + +--- +// Error: 7-22 failed to parse json file: syntax error in line 3 +#json("/res/bad.json")