diff --git a/Cargo.lock b/Cargo.lock index d9d959d3a..9365ce7ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,6 +83,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bytemuck" version = "1.9.1" @@ -126,6 +138,28 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "csv" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +dependencies = [ + "bstr", + "csv-core", + "itoa 0.4.8", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + [[package]] name = "data-url" version = "0.1.1" @@ -272,6 +306,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "itoa" version = "1.0.2" @@ -431,7 +471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "249f9b33a3192626f2cd9f4b0cd66c1ec32d65968d58cf4d8239977feddddead" dependencies = [ "bitflags", - "itoa", + "itoa 1.0.2", "ryu", ] @@ -547,6 +587,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "regex-syntax" version = "0.6.26" @@ -665,7 +711,7 @@ version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" dependencies = [ - "itoa", + "itoa 1.0.2", "ryu", "serde", ] @@ -816,6 +862,7 @@ version = "0.1.0" dependencies = [ "bytemuck", "codespan-reporting", + "csv", "dirs", "flate2", "fxhash", diff --git a/Cargo.toml b/Cargo.toml index db9fb5c7e..998177163 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ usvg = { version = "0.22", default-features = false } # External implementation of user-facing features syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] } rex = { git = "https://github.com/laurmaedje/ReX" } +csv = "1" # PDF export miniz_oxide = "0.5" diff --git a/src/lib.rs b/src/lib.rs index 1a39a14d4..72c7fb51e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,6 +79,8 @@ pub fn typeset(ctx: &mut Context, id: SourceId) -> TypResult> { /// The core context which holds the configuration and stores. pub struct Context { + /// The loader for fonts and files. + pub loader: Arc, /// Stores loaded source files. pub sources: SourceStore, /// Stores parsed font faces. @@ -97,6 +99,7 @@ impl Context { /// Create a new context. pub fn new(loader: Arc, config: Config) -> Self { Self { + loader: Arc::clone(&loader), sources: SourceStore::new(Arc::clone(&loader)), fonts: FontStore::new(Arc::clone(&loader)), images: ImageStore::new(loader), diff --git a/src/library/mod.rs b/src/library/mod.rs index d78e38caa..bd34590a4 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -97,6 +97,7 @@ pub fn new() -> Scope { std.def_fn("roman", utility::roman); std.def_fn("symbol", utility::symbol); std.def_fn("lorem", utility::lorem); + std.def_fn("csv", utility::csv); // Predefined colors. std.define("black", Color::BLACK); diff --git a/src/library/prelude.rs b/src/library/prelude.rs index c033b6316..f55447c38 100644 --- a/src/library/prelude.rs +++ b/src/library/prelude.rs @@ -2,12 +2,15 @@ pub use std::fmt::{self, Debug, Formatter}; pub use std::hash::Hash; +pub use std::io; pub use std::num::NonZeroUsize; pub use std::sync::Arc; pub use typst_macros::node; -pub use crate::diag::{with_alternative, At, Error, StrResult, TypError, TypResult}; +pub use crate::diag::{ + failed_to_load, with_alternative, At, Error, StrResult, TypError, TypResult, +}; pub use crate::eval::{ Arg, Args, Array, Cast, Dict, Dynamic, Func, Machine, Node, RawAlign, RawLength, RawStroke, Scope, Smart, Value, diff --git a/src/library/utility/data.rs b/src/library/utility/data.rs new file mode 100644 index 000000000..0f9e6bf09 --- /dev/null +++ b/src/library/utility/data.rs @@ -0,0 +1,30 @@ +use crate::library::prelude::*; + +/// Read structured data from a CSV file. +pub fn csv(vm: &mut Machine, args: &mut Args) -> TypResult { + let Spanned { v: path, span } = + args.expect::>("path to csv file")?; + + let path = vm.locate(&path).at(span)?; + let try_load = || -> io::Result { + let data = vm.ctx.loader.load(&path)?; + + let mut builder = csv::ReaderBuilder::new(); + builder.has_headers(false); + + let mut reader = builder.from_reader(data.as_slice()); + let mut vec = vec![]; + + for result in reader.records() { + vec.push(Value::Array( + result?.iter().map(|field| Value::Str(field.into())).collect(), + )) + } + + Ok(Value::Array(Array::from_vec(vec))) + }; + + try_load() + .map_err(|err| failed_to_load("csv file", &path, err)) + .at(span) +} diff --git a/src/library/utility/mod.rs b/src/library/utility/mod.rs index 10aa7c7a1..9c95e60cd 100644 --- a/src/library/utility/mod.rs +++ b/src/library/utility/mod.rs @@ -1,10 +1,12 @@ //! Computational utility functions. mod color; +mod data; mod math; mod string; pub use color::*; +pub use data::*; pub use math::*; pub use string::*; diff --git a/tests/ref/utility/csv.png b/tests/ref/utility/csv.png new file mode 100644 index 000000000..69e0ae38d Binary files /dev/null and b/tests/ref/utility/csv.png differ diff --git a/tests/res/bad.csv b/tests/res/bad.csv new file mode 100644 index 000000000..2c2696e96 --- /dev/null +++ b/tests/res/bad.csv @@ -0,0 +1,4 @@ +A,B +1,2 +3,4,5 +6,7 diff --git a/tests/res/zoo.csv b/tests/res/zoo.csv new file mode 100644 index 000000000..42ff06c7d --- /dev/null +++ b/tests/res/zoo.csv @@ -0,0 +1,4 @@ +Name,Species,Weight,Length +Debby,Rhinoceros,1900kg,390cm +Fluffy,Tiger,115kg,310cm +Sleepy,Dolphin,150kg,180cm diff --git a/tests/typ/utility/csv.typ b/tests/typ/utility/csv.typ new file mode 100644 index 000000000..ab955ab07 --- /dev/null +++ b/tests/typ/utility/csv.typ @@ -0,0 +1,15 @@ +// Test reading structured CSV data. + +--- +#set page(width: auto) +#let data = csv("/res/zoo.csv") +#let cells = data(0).map(strong) + data.slice(1).flatten() +#table(columns: data(0).len(), ..cells) + +--- +// Error: 6-16 file not found (searched at typ/utility/nope.csv) +#csv("nope.csv") + +--- +// Error: 6-20 failed to load csv file (CSV error: record 2 (line: 3, byte: 8): found record with 3 fields, but the previous record has 2 fields) +#csv("/res/bad.csv")