summaryrefslogtreecommitdiff
path: root/library/src/utility
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-11-03 11:44:53 +0100
committerLaurenz <laurmaedje@gmail.com>2022-11-03 13:35:39 +0100
commit37a7afddfaffd44cb9bc013c9506599267e08983 (patch)
tree20e7d62d3c5418baff01a21d0406b91bf3096214 /library/src/utility
parent56342bd972a13ffe21beaf2b87ab7eb1597704b4 (diff)
Split crates
Diffstat (limited to 'library/src/utility')
-rw-r--r--library/src/utility/color.rs66
-rw-r--r--library/src/utility/data.rs132
-rw-r--r--library/src/utility/math.rs144
-rw-r--r--library/src/utility/mod.rs54
-rw-r--r--library/src/utility/string.rs141
5 files changed, 537 insertions, 0 deletions
diff --git a/library/src/utility/color.rs b/library/src/utility/color.rs
new file mode 100644
index 00000000..8bb12334
--- /dev/null
+++ b/library/src/utility/color.rs
@@ -0,0 +1,66 @@
+use std::str::FromStr;
+
+use crate::prelude::*;
+
+/// Create a grayscale color.
+pub fn luma(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let Component(luma) = args.expect("gray component")?;
+ Ok(Value::Color(LumaColor::new(luma).into()))
+}
+
+/// Create an RGB(A) color.
+pub fn rgb(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ Ok(Value::Color(
+ if let Some(string) = args.find::<Spanned<EcoString>>()? {
+ match RgbaColor::from_str(&string.v) {
+ Ok(color) => color.into(),
+ Err(msg) => bail!(string.span, msg),
+ }
+ } else {
+ let Component(r) = args.expect("red component")?;
+ let Component(g) = args.expect("green component")?;
+ let Component(b) = args.expect("blue component")?;
+ let Component(a) = args.eat()?.unwrap_or(Component(255));
+ RgbaColor::new(r, g, b, a).into()
+ },
+ ))
+}
+
+/// Create a CMYK color.
+pub fn cmyk(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let RatioComponent(c) = args.expect("cyan component")?;
+ let RatioComponent(m) = args.expect("magenta component")?;
+ let RatioComponent(y) = args.expect("yellow component")?;
+ let RatioComponent(k) = args.expect("key component")?;
+ Ok(Value::Color(CmykColor::new(c, m, y, k).into()))
+}
+
+/// An integer or ratio component.
+struct Component(u8);
+
+castable! {
+ Component,
+ Expected: "integer or ratio",
+ Value::Int(v) => match v {
+ 0 ..= 255 => Self(v as u8),
+ _ => Err("must be between 0 and 255")?,
+ },
+ Value::Ratio(v) => if (0.0 ..= 1.0).contains(&v.get()) {
+ Self((v.get() * 255.0).round() as u8)
+ } else {
+ Err("must be between 0% and 100%")?
+ },
+}
+
+/// A component that must be a ratio.
+struct RatioComponent(u8);
+
+castable! {
+ RatioComponent,
+ Expected: "ratio",
+ Value::Ratio(v) => if (0.0 ..= 1.0).contains(&v.get()) {
+ Self((v.get() * 255.0).round() as u8)
+ } else {
+ Err("must be between 0% and 100%")?
+ },
+}
diff --git a/library/src/utility/data.rs b/library/src/utility/data.rs
new file mode 100644
index 00000000..3edade55
--- /dev/null
+++ b/library/src/utility/data.rs
@@ -0,0 +1,132 @@
+use std::fmt::Write;
+
+use typst::diag::format_xml_like_error;
+
+use crate::prelude::*;
+
+/// Read structured data from a CSV file.
+pub fn csv(vm: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let Spanned { v: path, span } =
+ args.expect::<Spanned<EcoString>>("path to csv file")?;
+
+ let path = vm.locate(&path).at(span)?;
+ let data = vm.world.file(&path).at(span)?;
+
+ 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() {
+ let row = result.map_err(format_csv_error).at(span)?;
+ let array = row.iter().map(|field| Value::Str(field.into())).collect();
+ vec.push(Value::Array(array))
+ }
+
+ Ok(Value::Array(Array::from_vec(vec)))
+}
+
+/// Format the user-facing CSV error message.
+fn format_csv_error(error: csv::Error) -> String {
+ match error.kind() {
+ csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(),
+ csv::ErrorKind::UnequalLengths { pos, expected_len, len } => {
+ let mut msg = format!(
+ "failed to parse csv file: found {len} instead of {expected_len} fields"
+ );
+ if let Some(pos) = pos {
+ write!(msg, " in line {}", pos.line()).unwrap();
+ }
+ msg
+ }
+ _ => "failed to parse csv file".into(),
+ }
+}
+
+/// Read structured data from a JSON file.
+pub fn json(vm: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let Spanned { v: path, span } =
+ args.expect::<Spanned<EcoString>>("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()
+ )
+}
+
+/// Read structured data from an XML file.
+pub fn xml(vm: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let Spanned { v: path, span } =
+ args.expect::<Spanned<EcoString>>("path to xml file")?;
+
+ let path = vm.locate(&path).at(span)?;
+ let data = vm.world.file(&path).at(span)?;
+ let text = std::str::from_utf8(&data).map_err(FileError::from).at(span)?;
+
+ let document = roxmltree::Document::parse(text).map_err(format_xml_error).at(span)?;
+
+ Ok(convert_xml(document.root()))
+}
+
+/// Convert an XML node to a Typst value.
+fn convert_xml(node: roxmltree::Node) -> Value {
+ if node.is_text() {
+ return Value::Str(node.text().unwrap_or_default().into());
+ }
+
+ let children: Array = node.children().map(convert_xml).collect();
+ if node.is_root() {
+ return Value::Array(children);
+ }
+
+ let tag: Str = node.tag_name().name().into();
+ let attrs: Dict = node
+ .attributes()
+ .iter()
+ .map(|attr| (attr.name().into(), attr.value().into()))
+ .collect();
+
+ Value::Dict(dict! {
+ "tag" => tag,
+ "attrs" => attrs,
+ "children" => children,
+ })
+}
+
+/// Format the user-facing XML error message.
+fn format_xml_error(error: roxmltree::Error) -> String {
+ format_xml_like_error("xml file", error)
+}
diff --git a/library/src/utility/math.rs b/library/src/utility/math.rs
new file mode 100644
index 00000000..dd37e8e7
--- /dev/null
+++ b/library/src/utility/math.rs
@@ -0,0 +1,144 @@
+use std::cmp::Ordering;
+
+use crate::prelude::*;
+
+/// Convert a value to an integer.
+pub fn int(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let Spanned { v, span } = args.expect("value")?;
+ Ok(Value::Int(match v {
+ Value::Bool(v) => v as i64,
+ Value::Int(v) => v,
+ Value::Float(v) => v as i64,
+ Value::Str(v) => match v.parse() {
+ Ok(v) => v,
+ Err(_) => bail!(span, "invalid integer"),
+ },
+ v => bail!(span, "cannot convert {} to integer", v.type_name()),
+ }))
+}
+
+/// Convert a value to a float.
+pub fn float(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let Spanned { v, span } = args.expect("value")?;
+ Ok(Value::Float(match v {
+ Value::Int(v) => v as f64,
+ Value::Float(v) => v,
+ Value::Str(v) => match v.parse() {
+ Ok(v) => v,
+ Err(_) => bail!(span, "invalid float"),
+ },
+ v => bail!(span, "cannot convert {} to float", v.type_name()),
+ }))
+}
+
+/// The absolute value of a numeric value.
+pub fn abs(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let Spanned { v, span } = args.expect("numeric value")?;
+ Ok(match v {
+ Value::Int(v) => Value::Int(v.abs()),
+ Value::Float(v) => Value::Float(v.abs()),
+ Value::Angle(v) => Value::Angle(v.abs()),
+ Value::Ratio(v) => Value::Ratio(v.abs()),
+ Value::Fraction(v) => Value::Fraction(v.abs()),
+ Value::Length(_) | Value::Relative(_) => {
+ bail!(span, "cannot take absolute value of a length")
+ }
+ v => bail!(span, "expected numeric value, found {}", v.type_name()),
+ })
+}
+
+/// The minimum of a sequence of values.
+pub fn min(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ minmax(args, Ordering::Less)
+}
+
+/// The maximum of a sequence of values.
+pub fn max(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ minmax(args, Ordering::Greater)
+}
+
+/// Find the minimum or maximum of a sequence of values.
+fn minmax(args: &mut Args, goal: Ordering) -> SourceResult<Value> {
+ let mut extremum = args.expect::<Value>("value")?;
+ for Spanned { v, span } in args.all::<Spanned<Value>>()? {
+ match v.partial_cmp(&extremum) {
+ Some(ordering) => {
+ if ordering == goal {
+ extremum = v;
+ }
+ }
+ None => bail!(
+ span,
+ "cannot compare {} and {}",
+ extremum.type_name(),
+ v.type_name(),
+ ),
+ }
+ }
+ Ok(extremum)
+}
+
+/// Whether an integer is even.
+pub fn even(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ Ok(Value::Bool(args.expect::<i64>("integer")? % 2 == 0))
+}
+
+/// Whether an integer is odd.
+pub fn odd(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ Ok(Value::Bool(args.expect::<i64>("integer")? % 2 != 0))
+}
+
+/// The modulo of two numbers.
+pub fn mod_(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let Spanned { v: v1, span: span1 } = args.expect("integer or float")?;
+ let Spanned { v: v2, span: span2 } = args.expect("integer or float")?;
+
+ let (a, b) = match (v1, v2) {
+ (Value::Int(a), Value::Int(b)) => match a.checked_rem(b) {
+ Some(res) => return Ok(Value::Int(res)),
+ None => bail!(span2, "divisor must not be zero"),
+ },
+ (Value::Int(a), Value::Float(b)) => (a as f64, b),
+ (Value::Float(a), Value::Int(b)) => (a, b as f64),
+ (Value::Float(a), Value::Float(b)) => (a, b),
+ (Value::Int(_), b) | (Value::Float(_), b) => bail!(
+ span2,
+ format!("expected integer or float, found {}", b.type_name())
+ ),
+ (a, _) => bail!(
+ span1,
+ format!("expected integer or float, found {}", a.type_name())
+ ),
+ };
+
+ if b == 0.0 {
+ bail!(span2, "divisor must not be zero");
+ }
+
+ Ok(Value::Float(a % b))
+}
+
+/// Create a sequence of numbers.
+pub fn range(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let first = args.expect::<i64>("end")?;
+ let (start, end) = match args.eat::<i64>()? {
+ Some(second) => (first, second),
+ None => (0, first),
+ };
+
+ let step: i64 = match args.named("step")? {
+ Some(Spanned { v: 0, span }) => bail!(span, "step must not be zero"),
+ Some(Spanned { v, .. }) => v,
+ None => 1,
+ };
+
+ let mut x = start;
+ let mut seq = vec![];
+
+ while x.cmp(&end) == 0.cmp(&step) {
+ seq.push(Value::Int(x));
+ x += step;
+ }
+
+ Ok(Value::Array(Array::from_vec(seq)))
+}
diff --git a/library/src/utility/mod.rs b/library/src/utility/mod.rs
new file mode 100644
index 00000000..402944cd
--- /dev/null
+++ b/library/src/utility/mod.rs
@@ -0,0 +1,54 @@
+//! Computational utility functions.
+
+mod color;
+mod data;
+mod math;
+mod string;
+
+pub use color::*;
+pub use data::*;
+pub use math::*;
+pub use string::*;
+
+use comemo::Track;
+use typst::model::{Eval, Route, Scopes, Vm};
+use typst::syntax::Source;
+
+use crate::prelude::*;
+
+/// The name of a value's type.
+pub fn type_(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ Ok(args.expect::<Value>("value")?.type_name().into())
+}
+
+/// Ensure that a condition is fulfilled.
+pub fn assert(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let Spanned { v, span } = args.expect::<Spanned<bool>>("condition")?;
+ if !v {
+ bail!(span, "assertion failed");
+ }
+ Ok(Value::None)
+}
+
+/// Evaluate a string as Typst markup.
+pub fn eval(vm: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let Spanned { v: text, span } = args.expect::<Spanned<String>>("source")?;
+
+ // Parse the source and set a synthetic span for all nodes.
+ let source = Source::synthesized(text, span);
+ let ast = source.ast()?;
+
+ // Evaluate the source.
+ let std = &vm.world.config().scope;
+ let scopes = Scopes::new(Some(std));
+ let route = Route::default();
+ let mut sub = Vm::new(vm.world, route.track(), None, scopes);
+ let result = ast.eval(&mut sub);
+
+ // Handle control flow.
+ if let Some(flow) = sub.flow {
+ bail!(flow.forbidden());
+ }
+
+ Ok(Value::Content(result?))
+}
diff --git a/library/src/utility/string.rs b/library/src/utility/string.rs
new file mode 100644
index 00000000..ed444d35
--- /dev/null
+++ b/library/src/utility/string.rs
@@ -0,0 +1,141 @@
+use typst::model::Regex;
+
+use crate::prelude::*;
+
+/// The string representation of a value.
+pub fn repr(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ Ok(args.expect::<Value>("value")?.repr().into())
+}
+
+/// Convert a value to a string.
+pub fn str(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let Spanned { v, span } = args.expect("value")?;
+ Ok(Value::Str(match v {
+ Value::Int(v) => format_str!("{}", v),
+ Value::Float(v) => format_str!("{}", v),
+ Value::Str(v) => v,
+ v => bail!(span, "cannot convert {} to string", v.type_name()),
+ }))
+}
+
+/// Create blind text.
+pub fn lorem(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let words: usize = args.expect("number of words")?;
+ Ok(Value::Str(lipsum::lipsum(words).into()))
+}
+
+/// Create a regular expression.
+pub fn regex(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ let Spanned { v, span } = args.expect::<Spanned<EcoString>>("regular expression")?;
+ Ok(Regex::new(&v).at(span)?.into())
+}
+
+/// Converts an integer into one or multiple letters.
+pub fn letter(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ numbered(Numbering::Letter, args)
+}
+
+/// Converts an integer into a roman numeral.
+pub fn roman(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ numbered(Numbering::Roman, args)
+}
+
+/// Convert a number into a symbol.
+pub fn symbol(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
+ numbered(Numbering::Symbol, args)
+}
+
+fn numbered(numbering: Numbering, args: &mut Args) -> SourceResult<Value> {
+ let n = args.expect::<usize>("non-negative integer")?;
+ Ok(Value::Str(numbering.apply(n).into()))
+}
+
+/// Allows to convert a number into letters, roman numerals and symbols.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Numbering {
+ Arabic,
+ Letter,
+ Roman,
+ Symbol,
+}
+
+impl Numbering {
+ /// Apply the numbering to the given number.
+ pub fn apply(self, mut n: usize) -> EcoString {
+ match self {
+ Self::Arabic => {
+ format_eco!("{}", n)
+ }
+ Self::Letter => {
+ if n == 0 {
+ return '-'.into();
+ }
+
+ n -= 1;
+
+ let mut letters = vec![];
+ loop {
+ letters.push(b'a' + (n % 26) as u8);
+ n /= 26;
+ if n == 0 {
+ break;
+ }
+ }
+
+ letters.reverse();
+ String::from_utf8(letters).unwrap().into()
+ }
+ Self::Roman => {
+ if n == 0 {
+ return 'N'.into();
+ }
+
+ // Adapted from Yann Villessuzanne's roman.rs under the Unlicense, at
+ // https://github.com/linfir/roman.rs/
+ let mut fmt = EcoString::new();
+ for &(name, value) in ROMANS {
+ while n >= value {
+ n -= value;
+ fmt.push_str(name);
+ }
+ }
+
+ fmt
+ }
+ Self::Symbol => {
+ if n == 0 {
+ return '-'.into();
+ }
+
+ let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()];
+ let amount = ((n - 1) / SYMBOLS.len()) + 1;
+ std::iter::repeat(symbol).take(amount).collect()
+ }
+ }
+ }
+}
+
+const ROMANS: &[(&str, usize)] = &[
+ ("M̅", 1000000),
+ ("D̅", 500000),
+ ("C̅", 100000),
+ ("L̅", 50000),
+ ("X̅", 10000),
+ ("V̅", 5000),
+ ("I̅V̅", 4000),
+ ("M", 1000),
+ ("CM", 900),
+ ("D", 500),
+ ("CD", 400),
+ ("C", 100),
+ ("XC", 90),
+ ("L", 50),
+ ("XL", 40),
+ ("X", 10),
+ ("IX", 9),
+ ("V", 5),
+ ("IV", 4),
+ ("I", 1),
+];
+
+const SYMBOLS: &[char] = &['*', '†', '‡', '§', '‖', '¶'];