diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-11-29 13:37:25 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-11-29 14:18:13 +0100 |
| commit | 0efe669278a5e1c3f2985eba2f3360e91159c54a (patch) | |
| tree | 502712857c48f0decb5e698257c0a96d358a436e /library/src/compute | |
| parent | 836692e73cff0356e409a9ba5b4887b86809d4ca (diff) | |
Reorganize library and tests
Diffstat (limited to 'library/src/compute')
| -rw-r--r-- | library/src/compute/calc.rs | 88 | ||||
| -rw-r--r-- | library/src/compute/create.rs | 143 | ||||
| -rw-r--r-- | library/src/compute/data.rs | 129 | ||||
| -rw-r--r-- | library/src/compute/foundations.rs | 34 | ||||
| -rw-r--r-- | library/src/compute/mod.rs | 13 | ||||
| -rw-r--r-- | library/src/compute/utility.rs | 160 |
6 files changed, 567 insertions, 0 deletions
diff --git a/library/src/compute/calc.rs b/library/src/compute/calc.rs new file mode 100644 index 00000000..3541e08c --- /dev/null +++ b/library/src/compute/calc.rs @@ -0,0 +1,88 @@ +use std::cmp::Ordering; + +use crate::prelude::*; + +/// The absolute value of a numeric value. +pub fn abs(_: &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(_: &Vm, args: &mut Args) -> SourceResult<Value> { + minmax(args, Ordering::Less) +} + +/// The maximum of a sequence of values. +pub fn max(_: &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(_: &Vm, args: &mut Args) -> SourceResult<Value> { + Ok(Value::Bool(args.expect::<i64>("integer")? % 2 == 0)) +} + +/// Whether an integer is odd. +pub fn odd(_: &Vm, args: &mut Args) -> SourceResult<Value> { + Ok(Value::Bool(args.expect::<i64>("integer")? % 2 != 0)) +} + +/// The modulo of two numbers. +pub fn mod_(_: &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)) +} diff --git a/library/src/compute/create.rs b/library/src/compute/create.rs new file mode 100644 index 00000000..4fd27499 --- /dev/null +++ b/library/src/compute/create.rs @@ -0,0 +1,143 @@ +use std::str::FromStr; + +use typst::model::Regex; + +use crate::prelude::*; + +/// Convert a value to an integer. +pub fn int(_: &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(_: &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()), + })) +} + +/// Create a grayscale color. +pub fn luma(_: &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(_: &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(_: &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%")? + }, +} + +/// Convert a value to a string. +pub fn str(_: &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::Label(label) => label.0.into(), + Value::Str(v) => v, + v => bail!(span, "cannot convert {} to string", v.type_name()), + })) +} + +/// Create a label from a string. +pub fn label(_: &Vm, args: &mut Args) -> SourceResult<Value> { + Ok(Value::Label(Label(args.expect("string")?))) +} + +/// Create a regular expression from a string. +pub fn regex(_: &Vm, args: &mut Args) -> SourceResult<Value> { + let Spanned { v, span } = args.expect::<Spanned<EcoString>>("regular expression")?; + Ok(Regex::new(&v).at(span)?.into()) +} + +/// Create an array consisting of a sequence of numbers. +pub fn range(_: &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/compute/data.rs b/library/src/compute/data.rs new file mode 100644 index 00000000..4f6e3b67 --- /dev/null +++ b/library/src/compute/data.rs @@ -0,0 +1,129 @@ +use std::fmt::Write; + +use typst::diag::{format_xml_like_error, FileError}; + +use crate::prelude::*; + +/// Read structured data from a CSV file. +pub fn csv(vm: &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: &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: &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/compute/foundations.rs b/library/src/compute/foundations.rs new file mode 100644 index 00000000..3e410185 --- /dev/null +++ b/library/src/compute/foundations.rs @@ -0,0 +1,34 @@ +use crate::prelude::*; + +use comemo::Track; + +use typst::model; +use typst::syntax::Source; + +/// The name of a value's type. +pub fn type_(_: &Vm, args: &mut Args) -> SourceResult<Value> { + Ok(args.expect::<Value>("value")?.type_name().into()) +} + +/// The string representation of a value. +pub fn repr(_: &Vm, args: &mut Args) -> SourceResult<Value> { + Ok(args.expect::<Value>("value")?.repr().into()) +} + +/// Ensure that a condition is fulfilled. +pub fn assert(_: &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: &Vm, args: &mut Args) -> SourceResult<Value> { + let Spanned { v: text, span } = args.expect::<Spanned<String>>("source")?; + let source = Source::synthesized(text, span); + let route = model::Route::default(); + let module = model::eval(vm.world(), route.track(), &source)?; + Ok(Value::Content(module.content)) +} diff --git a/library/src/compute/mod.rs b/library/src/compute/mod.rs new file mode 100644 index 00000000..70690d44 --- /dev/null +++ b/library/src/compute/mod.rs @@ -0,0 +1,13 @@ +//! Computational functions. + +mod calc; +mod create; +mod data; +mod foundations; +mod utility; + +pub use self::calc::*; +pub use self::create::*; +pub use self::data::*; +pub use self::foundations::*; +pub use self::utility::*; diff --git a/library/src/compute/utility.rs b/library/src/compute/utility.rs new file mode 100644 index 00000000..2b04dfd6 --- /dev/null +++ b/library/src/compute/utility.rs @@ -0,0 +1,160 @@ +use std::str::FromStr; + +use unscanny::Scanner; + +use crate::prelude::*; + +/// Create a blind text string. +pub fn lorem(_: &Vm, args: &mut Args) -> SourceResult<Value> { + let words: usize = args.expect("number of words")?; + Ok(Value::Str(lipsum::lipsum(words).into())) +} + +/// Apply a numbering pattern to a number. +pub fn numbering(_: &Vm, args: &mut Args) -> SourceResult<Value> { + let number = args.expect::<usize>("number")?; + let pattern = args.expect::<NumberingPattern>("pattern")?; + Ok(Value::Str(pattern.apply(number).into())) +} + +/// How to turn a number into text. +/// +/// A pattern consists of a prefix, followed by one of `1`, `a`, `A`, `i`, `I` +/// or `*`, and then a suffix. +/// +/// Examples of valid patterns: +/// - `1)` +/// - `a.` +/// - `(I)` +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct NumberingPattern { + prefix: EcoString, + numbering: NumberingKind, + upper: bool, + suffix: EcoString, +} + +impl NumberingPattern { + /// Apply the pattern to the given number. + pub fn apply(&self, n: usize) -> EcoString { + let fmt = self.numbering.apply(n); + let mid = if self.upper { fmt.to_uppercase() } else { fmt.to_lowercase() }; + format_eco!("{}{}{}", self.prefix, mid, self.suffix) + } +} + +impl FromStr for NumberingPattern { + type Err = &'static str; + + fn from_str(pattern: &str) -> Result<Self, Self::Err> { + let mut s = Scanner::new(pattern); + let mut prefix; + let numbering = loop { + prefix = s.before(); + match s.eat().map(|c| c.to_ascii_lowercase()) { + Some('1') => break NumberingKind::Arabic, + Some('a') => break NumberingKind::Letter, + Some('i') => break NumberingKind::Roman, + Some('*') => break NumberingKind::Symbol, + Some(_) => {} + None => Err("invalid numbering pattern")?, + } + }; + let upper = s.scout(-1).map_or(false, char::is_uppercase); + let suffix = s.after().into(); + Ok(Self { prefix: prefix.into(), numbering, upper, suffix }) + } +} + +castable! { + NumberingPattern, + Expected: "numbering pattern", + Value::Str(s) => s.parse()?, +} + +/// Different kinds of numberings. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +enum NumberingKind { + Arabic, + Letter, + Roman, + Symbol, +} + +impl NumberingKind { + /// 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 &[ + ("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), + ] { + while n >= value { + n -= value; + fmt.push_str(name); + } + } + + fmt + } + Self::Symbol => { + if n == 0 { + return '-'.into(); + } + + const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖']; + let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()]; + let amount = ((n - 1) / SYMBOLS.len()) + 1; + std::iter::repeat(symbol).take(amount).collect() + } + } + } +} |
