From 33928a00dc58250e24da1dae4e5db17e7b598d70 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 3 Nov 2022 16:50:26 +0100 Subject: Tidy up library --- cli/src/main.rs | 3 +- library/src/base/calc.rs | 144 +++++++++++++++++++++++++++++++++ library/src/base/color.rs | 66 +++++++++++++++ library/src/base/data.rs | 132 ++++++++++++++++++++++++++++++ library/src/base/mod.rs | 54 +++++++++++++ library/src/base/string.rs | 141 ++++++++++++++++++++++++++++++++ library/src/ext.rs | 2 +- library/src/layout/flow.rs | 12 +-- library/src/layout/grid.rs | 6 +- library/src/layout/mod.rs | 40 +++++---- library/src/layout/page.rs | 1 + library/src/layout/stack.rs | 22 ++--- library/src/lib.rs | 58 ++++++------- library/src/math/tex.rs | 2 +- library/src/prelude.rs | 17 ++-- library/src/structure/list.rs | 4 +- library/src/structure/reference.rs | 1 + library/src/text/deco.rs | 4 +- library/src/text/mod.rs | 14 ++-- library/src/text/quotes.rs | 4 +- library/src/text/shaping.rs | 8 +- library/src/text/shift.rs | 16 ++-- library/src/utility/color.rs | 66 --------------- library/src/utility/data.rs | 132 ------------------------------ library/src/utility/math.rs | 144 --------------------------------- library/src/utility/mod.rs | 54 ------------- library/src/utility/string.rs | 141 -------------------------------- tests/ref/base/blind.png | Bin 0 -> 27575 bytes tests/ref/base/collection.png | Bin 0 -> 1384 bytes tests/ref/base/color.png | Bin 0 -> 595 bytes tests/ref/base/data.png | Bin 0 -> 8603 bytes tests/ref/base/eval.png | Bin 0 -> 5429 bytes tests/ref/base/numbering.png | Bin 0 -> 10634 bytes tests/ref/base/string.png | Bin 0 -> 10614 bytes tests/ref/layout/math.png | Bin 0 -> 6554 bytes tests/ref/math/basic.png | Bin 6554 -> 0 bytes tests/ref/text/basic.png | Bin 58344 -> 0 bytes tests/ref/text/plain.png | Bin 0 -> 58344 bytes tests/ref/utility/blind.png | Bin 27575 -> 0 bytes tests/ref/utility/collection.png | Bin 1384 -> 0 bytes tests/ref/utility/color.png | Bin 595 -> 0 bytes tests/ref/utility/data.png | Bin 8603 -> 0 bytes tests/ref/utility/eval.png | Bin 5429 -> 0 bytes tests/ref/utility/numbering.png | Bin 10634 -> 0 bytes tests/ref/utility/string.png | Bin 10614 -> 0 bytes tests/src/benches.rs | 3 +- tests/src/tests.rs | 5 +- tests/typ/base/assert.typ | 23 ++++++ tests/typ/base/blind.typ | 32 ++++++++ tests/typ/base/calc.typ | 117 +++++++++++++++++++++++++++ tests/typ/base/collection.typ | 115 ++++++++++++++++++++++++++ tests/typ/base/color.typ | 63 +++++++++++++++ tests/typ/base/data.typ | 58 +++++++++++++ tests/typ/base/eval.typ | 52 ++++++++++++ tests/typ/base/string.typ | 161 +++++++++++++++++++++++++++++++++++++ tests/typ/base/type.typ | 7 ++ tests/typ/layout/math.typ | 20 +++++ tests/typ/math/basic.typ | 20 ----- tests/typ/text/basic.typ | 20 ----- tests/typ/text/plain.typ | 20 +++++ tests/typ/utility/basics.typ | 24 ------ tests/typ/utility/blind.typ | 32 -------- tests/typ/utility/collection.typ | 115 -------------------------- tests/typ/utility/color.typ | 63 --------------- tests/typ/utility/data.typ | 58 ------------- tests/typ/utility/eval.typ | 52 ------------ tests/typ/utility/math.typ | 117 --------------------------- tests/typ/utility/string.typ | 161 ------------------------------------- 68 files changed, 1310 insertions(+), 1316 deletions(-) create mode 100644 library/src/base/calc.rs create mode 100644 library/src/base/color.rs create mode 100644 library/src/base/data.rs create mode 100644 library/src/base/mod.rs create mode 100644 library/src/base/string.rs delete mode 100644 library/src/utility/color.rs delete mode 100644 library/src/utility/data.rs delete mode 100644 library/src/utility/math.rs delete mode 100644 library/src/utility/mod.rs delete mode 100644 library/src/utility/string.rs create mode 100644 tests/ref/base/blind.png create mode 100644 tests/ref/base/collection.png create mode 100644 tests/ref/base/color.png create mode 100644 tests/ref/base/data.png create mode 100644 tests/ref/base/eval.png create mode 100644 tests/ref/base/numbering.png create mode 100644 tests/ref/base/string.png create mode 100644 tests/ref/layout/math.png delete mode 100644 tests/ref/math/basic.png delete mode 100644 tests/ref/text/basic.png create mode 100644 tests/ref/text/plain.png delete mode 100644 tests/ref/utility/blind.png delete mode 100644 tests/ref/utility/collection.png delete mode 100644 tests/ref/utility/color.png delete mode 100644 tests/ref/utility/data.png delete mode 100644 tests/ref/utility/eval.png delete mode 100644 tests/ref/utility/numbering.png delete mode 100644 tests/ref/utility/string.png create mode 100644 tests/typ/base/assert.typ create mode 100644 tests/typ/base/blind.typ create mode 100644 tests/typ/base/calc.typ create mode 100644 tests/typ/base/collection.typ create mode 100644 tests/typ/base/color.typ create mode 100644 tests/typ/base/data.typ create mode 100644 tests/typ/base/eval.typ create mode 100644 tests/typ/base/string.typ create mode 100644 tests/typ/base/type.typ create mode 100644 tests/typ/layout/math.typ delete mode 100644 tests/typ/math/basic.typ delete mode 100644 tests/typ/text/basic.typ create mode 100644 tests/typ/text/plain.typ delete mode 100644 tests/typ/utility/basics.typ delete mode 100644 tests/typ/utility/blind.typ delete mode 100644 tests/typ/utility/collection.typ delete mode 100644 tests/typ/utility/color.typ delete mode 100644 tests/typ/utility/data.typ delete mode 100644 tests/typ/utility/eval.typ delete mode 100644 tests/typ/utility/math.typ delete mode 100644 tests/typ/utility/string.typ diff --git a/cli/src/main.rs b/cli/src/main.rs index 62cad8ab..c7eb2977 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -17,13 +17,12 @@ use pico_args::Arguments; use same_file::{is_same_file, Handle}; use siphasher::sip128::{Hasher128, SipHasher}; use termcolor::{ColorChoice, StandardStream, WriteColor}; -use walkdir::WalkDir; - use typst::diag::{FileError, FileResult, SourceError, StrResult}; use typst::font::{Font, FontBook, FontInfo, FontVariant}; use typst::syntax::{Source, SourceId}; use typst::util::{Buffer, PathExt}; use typst::{Config, World}; +use walkdir::WalkDir; type CodespanResult = Result; type CodespanError = codespan_reporting::files::Error; diff --git a/library/src/base/calc.rs b/library/src/base/calc.rs new file mode 100644 index 00000000..dd37e8e7 --- /dev/null +++ b/library/src/base/calc.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 { + 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 { + 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 { + 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 { + minmax(args, Ordering::Less) +} + +/// The maximum of a sequence of values. +pub fn max(_: &mut Vm, args: &mut Args) -> SourceResult { + minmax(args, Ordering::Greater) +} + +/// Find the minimum or maximum of a sequence of values. +fn minmax(args: &mut Args, goal: Ordering) -> SourceResult { + let mut extremum = args.expect::("value")?; + for Spanned { v, span } in args.all::>()? { + 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 { + Ok(Value::Bool(args.expect::("integer")? % 2 == 0)) +} + +/// Whether an integer is odd. +pub fn odd(_: &mut Vm, args: &mut Args) -> SourceResult { + Ok(Value::Bool(args.expect::("integer")? % 2 != 0)) +} + +/// The modulo of two numbers. +pub fn mod_(_: &mut Vm, args: &mut Args) -> SourceResult { + 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 { + let first = args.expect::("end")?; + let (start, end) = match args.eat::()? { + 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/base/color.rs b/library/src/base/color.rs new file mode 100644 index 00000000..8bb12334 --- /dev/null +++ b/library/src/base/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 { + 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 { + Ok(Value::Color( + if let Some(string) = args.find::>()? { + 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 { + 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/base/data.rs b/library/src/base/data.rs new file mode 100644 index 00000000..1199056f --- /dev/null +++ b/library/src/base/data.rs @@ -0,0 +1,132 @@ +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: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v: path, span } = + args.expect::>("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 { + 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() + ) +} + +/// Read structured data from an XML file. +pub fn xml(vm: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v: path, span } = + args.expect::>("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/base/mod.rs b/library/src/base/mod.rs new file mode 100644 index 00000000..bb1c8c51 --- /dev/null +++ b/library/src/base/mod.rs @@ -0,0 +1,54 @@ +//! Foundational functions. + +mod calc; +mod color; +mod data; +mod string; + +pub use calc::*; +pub use color::*; +pub use data::*; +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 { + Ok(args.expect::("value")?.type_name().into()) +} + +/// Ensure that a condition is fulfilled. +pub fn assert(_: &mut Vm, args: &mut Args) -> SourceResult { + let Spanned { v, span } = args.expect::>("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 { + let Spanned { v: text, span } = args.expect::>("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/base/string.rs b/library/src/base/string.rs new file mode 100644 index 00000000..ed444d35 --- /dev/null +++ b/library/src/base/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 { + Ok(args.expect::("value")?.repr().into()) +} + +/// Convert a value to a string. +pub fn str(_: &mut Vm, args: &mut Args) -> SourceResult { + 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 { + 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 { + let Spanned { v, span } = args.expect::>("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 { + numbered(Numbering::Letter, args) +} + +/// Converts an integer into a roman numeral. +pub fn roman(_: &mut Vm, args: &mut Args) -> SourceResult { + numbered(Numbering::Roman, args) +} + +/// Convert a number into a symbol. +pub fn symbol(_: &mut Vm, args: &mut Args) -> SourceResult { + numbered(Numbering::Symbol, args) +} + +fn numbered(numbering: Numbering, args: &mut Args) -> SourceResult { + let n = args.expect::("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] = &['*', '†', '‡', '§', '‖', '¶']; diff --git a/library/src/ext.rs b/library/src/ext.rs index 72ef484b..6f5b1b67 100644 --- a/library/src/ext.rs +++ b/library/src/ext.rs @@ -115,7 +115,7 @@ impl StyleMapExt for StyleMap { fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain) { self.set( text::TextNode::FAMILY, - FallbackList( + text::FallbackList( std::iter::once(preferred) .chain(existing.get(text::TextNode::FAMILY).0.iter().cloned()) .collect(), diff --git a/library/src/layout/flow.rs b/library/src/layout/flow.rs index a5992796..347c1dd8 100644 --- a/library/src/layout/flow.rs +++ b/library/src/layout/flow.rs @@ -80,7 +80,7 @@ impl PartialOrd for FlowChild { } /// Performs flow layout. -pub struct FlowLayouter { +struct FlowLayouter { /// The regions to layout children into. regions: Regions, /// Whether the flow should expand to fill the region. @@ -112,7 +112,7 @@ enum FlowItem { impl FlowLayouter { /// Create a new flow layouter. - pub fn new(regions: &Regions) -> Self { + fn new(regions: &Regions) -> Self { let expand = regions.expand; let full = regions.first; @@ -132,7 +132,7 @@ impl FlowLayouter { } /// Layout spacing. - pub fn layout_spacing(&mut self, spacing: Spacing, styles: StyleChain) { + fn layout_spacing(&mut self, spacing: Spacing, styles: StyleChain) { match spacing { Spacing::Relative(v) => { // Resolve the spacing and limit it to the remaining space. @@ -150,7 +150,7 @@ impl FlowLayouter { } /// Layout a block. - pub fn layout_block( + fn layout_block( &mut self, world: Tracked, block: &Content, @@ -206,7 +206,7 @@ impl FlowLayouter { } /// Finish the frame for one region. - pub fn finish_region(&mut self) { + fn finish_region(&mut self) { // Determine the size of the flow in this region dependening on whether // the region expands. let mut size = self.expand.select(self.full, self.used); @@ -254,7 +254,7 @@ impl FlowLayouter { } /// Finish layouting and return the resulting frames. - pub fn finish(mut self) -> Vec { + fn finish(mut self) -> Vec { if self.expand.y { while !self.regions.backlog.is_empty() { self.finish_region(); diff --git a/library/src/layout/grid.rs b/library/src/layout/grid.rs index f6610d78..a6f93ab6 100644 --- a/library/src/layout/grid.rs +++ b/library/src/layout/grid.rs @@ -99,7 +99,7 @@ castable! { } /// Performs grid layout. -pub struct GridLayouter<'a> { +struct GridLayouter<'a> { /// The core context. world: Tracked<'a, dyn World>, /// The grid cells. @@ -140,7 +140,7 @@ impl<'a> GridLayouter<'a> { /// Create a new grid layouter. /// /// This prepares grid layout by unifying content and gutter tracks. - pub fn new( + fn new( world: Tracked<'a, dyn World>, tracks: Axes<&[TrackSizing]>, gutter: Axes<&[TrackSizing]>, @@ -211,7 +211,7 @@ impl<'a> GridLayouter<'a> { } /// Determines the columns sizes and then layouts the grid row-by-row. - pub fn layout(mut self) -> SourceResult> { + fn layout(mut self) -> SourceResult> { self.measure_columns()?; for y in 0 .. self.rows.len() { diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs index ddfaa351..5ab5f42e 100644 --- a/library/src/layout/mod.rs +++ b/library/src/layout/mod.rs @@ -44,16 +44,16 @@ use crate::text::{ LinebreakNode, ParChild, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode, }; -/// The root-level layout. +/// Root-level layout. #[capability] -pub trait Layout: 'static + Sync + Send { +pub trait LayoutRoot: 'static + Sync + Send { /// Layout into one frame per page. - fn layout(&self, world: Tracked) -> SourceResult>; + fn layout_root(&self, world: Tracked) -> SourceResult>; } -impl Layout for Content { +impl LayoutRoot for Content { #[comemo::memoize] - fn layout(&self, world: Tracked) -> SourceResult> { + fn layout_root(&self, world: Tracked) -> SourceResult> { let styles = StyleChain::with_root(&world.config().styles); let scratch = Scratch::default(); @@ -259,7 +259,7 @@ struct Scratch<'a> { /// Determines whether a style could interrupt some composable structure. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub enum Interruption { +enum Interruption { /// The style forces a list break. List, /// The style forces a paragraph break. @@ -269,11 +269,7 @@ pub enum Interruption { } impl<'a> Builder<'a> { - pub fn new( - world: Tracked<'a, dyn World>, - scratch: &'a Scratch<'a>, - top: bool, - ) -> Self { + fn new(world: Tracked<'a, dyn World>, scratch: &'a Scratch<'a>, top: bool) -> Self { Self { world, scratch, @@ -284,7 +280,7 @@ impl<'a> Builder<'a> { } } - pub fn into_doc( + fn into_doc( mut self, styles: StyleChain<'a>, ) -> SourceResult<(DocNode, StyleChain<'a>)> { @@ -293,7 +289,7 @@ impl<'a> Builder<'a> { Ok((DocNode(pages), shared)) } - pub fn into_flow( + fn into_flow( mut self, styles: StyleChain<'a>, ) -> SourceResult<(FlowNode, StyleChain<'a>)> { @@ -302,7 +298,7 @@ impl<'a> Builder<'a> { Ok((FlowNode(children), shared)) } - pub fn accept( + fn accept( &mut self, content: &'a Content, styles: StyleChain<'a>, @@ -740,7 +736,7 @@ enum Last { impl<'a, T> CollapsingBuilder<'a, T> { /// Create a new style-vec builder. - pub fn new() -> Self { + fn new() -> Self { Self { builder: StyleVecBuilder::new(), staged: vec![], @@ -749,7 +745,7 @@ impl<'a, T> CollapsingBuilder<'a, T> { } /// Whether the builder is empty. - pub fn is_empty(&self) -> bool { + fn is_empty(&self) -> bool { self.builder.is_empty() && self.staged.is_empty() } @@ -760,7 +756,7 @@ impl<'a, T> CollapsingBuilder<'a, T> { /// Between weak items, there may be at least one per layer and among the /// candidates the strongest one (smallest `weakness`) wins. When tied, /// the one that compares larger through `PartialOrd` wins. - pub fn weak(&mut self, item: T, styles: StyleChain<'a>, weakness: u8) + fn weak(&mut self, item: T, styles: StyleChain<'a>, weakness: u8) where T: PartialOrd, { @@ -788,31 +784,31 @@ impl<'a, T> CollapsingBuilder<'a, T> { } /// Forces nearby weak items to collapse. - pub fn destructive(&mut self, item: T, styles: StyleChain<'a>) { + fn destructive(&mut self, item: T, styles: StyleChain<'a>) { self.flush(false); self.builder.push(item, styles); self.last = Last::Destructive; } /// Allows nearby weak items to exist. - pub fn supportive(&mut self, item: T, styles: StyleChain<'a>) { + fn supportive(&mut self, item: T, styles: StyleChain<'a>) { self.flush(true); self.builder.push(item, styles); self.last = Last::Supportive; } /// Has no influence on other items. - pub fn ignorant(&mut self, item: T, styles: StyleChain<'a>) { + fn ignorant(&mut self, item: T, styles: StyleChain<'a>) { self.staged.push((item, styles, None)); } /// Iterate over the contained items. - pub fn items(&self) -> impl DoubleEndedIterator { + fn items(&self) -> impl DoubleEndedIterator { self.builder.items().chain(self.staged.iter().map(|(item, ..)| item)) } /// Return the finish style vec and the common prefix chain. - pub fn finish(mut self) -> (StyleVec, StyleChain<'a>) { + fn finish(mut self) -> (StyleVec, StyleChain<'a>) { self.flush(false); self.builder.finish() } diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs index 53a8cbc7..7ce0d633 100644 --- a/library/src/layout/page.rs +++ b/library/src/layout/page.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use super::ColumnsNode; use crate::prelude::*; +use crate::text::TextNode; /// Layouts its child onto one or multiple pages. #[derive(PartialEq, Clone, Hash)] diff --git a/library/src/layout/stack.rs b/library/src/layout/stack.rs index 44bcbf67..ec1063fd 100644 --- a/library/src/layout/stack.rs +++ b/library/src/layout/stack.rs @@ -89,7 +89,7 @@ castable! { } /// Performs stack layout. -pub struct StackLayouter<'a> { +struct StackLayouter<'a> { /// The stacking direction. dir: Dir, /// The axis of the stacking direction. @@ -125,7 +125,7 @@ enum StackItem { impl<'a> StackLayouter<'a> { /// Create a new stack layouter. - pub fn new(dir: Dir, regions: &Regions, styles: StyleChain<'a>) -> Self { + fn new(dir: Dir, regions: &Regions, styles: StyleChain<'a>) -> Self { let axis = dir.axis(); let expand = regions.expand; let full = regions.first; @@ -149,7 +149,7 @@ impl<'a> StackLayouter<'a> { } /// Add spacing along the spacing direction. - pub fn layout_spacing(&mut self, spacing: Spacing) { + fn layout_spacing(&mut self, spacing: Spacing) { match spacing { Spacing::Relative(v) => { // Resolve the spacing and limit it to the remaining space. @@ -169,7 +169,7 @@ impl<'a> StackLayouter<'a> { } /// Layout an arbitrary block. - pub fn layout_block( + fn layout_block( &mut self, world: Tracked, block: &Content, @@ -223,7 +223,7 @@ impl<'a> StackLayouter<'a> { } /// Advance to the next region. - pub fn finish_region(&mut self) { + fn finish_region(&mut self) { // Determine the size of the stack in this region dependening on whether // the region expands. let used = self.used.to_axes(self.axis); @@ -279,7 +279,7 @@ impl<'a> StackLayouter<'a> { } /// Finish layouting and return the resulting frames. - pub fn finish(mut self) -> Vec { + fn finish(mut self) -> Vec { self.finish_region(); self.finished } @@ -287,7 +287,7 @@ impl<'a> StackLayouter<'a> { /// A container with a main and cross component. #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Gen { +struct Gen { /// The main component. pub cross: T, /// The cross component. @@ -296,12 +296,12 @@ pub struct Gen { impl Gen { /// Create a new instance from the two components. - pub const fn new(cross: T, main: T) -> Self { + const fn new(cross: T, main: T) -> Self { Self { cross, main } } /// Convert to the specific representation, given the current main axis. - pub fn to_axes(self, main: Axis) -> Axes { + fn to_axes(self, main: Axis) -> Axes { match main { Axis::X => Axes::new(self.main, self.cross), Axis::Y => Axes::new(self.cross, self.main), @@ -311,12 +311,12 @@ impl Gen { impl Gen { /// The zero value. - pub fn zero() -> Self { + fn zero() -> Self { Self { cross: Abs::zero(), main: Abs::zero() } } /// Convert to a point. - pub fn to_point(self, main: Axis) -> Point { + fn to_point(self, main: Axis) -> Point { self.to_axes(main).to_point() } } diff --git a/library/src/lib.rs b/library/src/lib.rs index ed332a06..7ffb490c 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -1,19 +1,19 @@ //! Typst's standard library. +pub mod base; pub mod graphics; pub mod layout; pub mod math; pub mod prelude; pub mod structure; pub mod text; -pub mod utility; mod ext; use typst::geom::{Align, Color, Dir, GenAlign}; use typst::model::{LangItems, Node, Scope, StyleMap}; -use self::layout::Layout; +use self::layout::LayoutRoot; /// Construct the standard library scope. pub fn scope() -> Scope { @@ -83,32 +83,32 @@ pub fn scope() -> Scope { std.define("NN", "ℕ"); std.define("RR", "ℝ"); - // Utility. - std.def_fn("type", utility::type_); - std.def_fn("assert", utility::assert); - std.def_fn("eval", utility::eval); - std.def_fn("int", utility::int); - std.def_fn("float", utility::float); - std.def_fn("abs", utility::abs); - std.def_fn("min", utility::min); - std.def_fn("max", utility::max); - std.def_fn("even", utility::even); - std.def_fn("odd", utility::odd); - std.def_fn("mod", utility::mod_); - std.def_fn("range", utility::range); - std.def_fn("luma", utility::luma); - std.def_fn("rgb", utility::rgb); - std.def_fn("cmyk", utility::cmyk); - std.def_fn("repr", utility::repr); - std.def_fn("str", utility::str); - std.def_fn("regex", utility::regex); - std.def_fn("letter", utility::letter); - std.def_fn("roman", utility::roman); - std.def_fn("symbol", utility::symbol); - std.def_fn("lorem", utility::lorem); - std.def_fn("csv", utility::csv); - std.def_fn("json", utility::json); - std.def_fn("xml", utility::xml); + // Base. + std.def_fn("type", base::type_); + std.def_fn("assert", base::assert); + std.def_fn("eval", base::eval); + std.def_fn("int", base::int); + std.def_fn("float", base::float); + std.def_fn("abs", base::abs); + std.def_fn("min", base::min); + std.def_fn("max", base::max); + std.def_fn("even", base::even); + std.def_fn("odd", base::odd); + std.def_fn("mod", base::mod_); + std.def_fn("range", base::range); + std.def_fn("luma", base::luma); + std.def_fn("rgb", base::rgb); + std.def_fn("cmyk", base::cmyk); + std.def_fn("repr", base::repr); + std.def_fn("str", base::str); + std.def_fn("regex", base::regex); + std.def_fn("letter", base::letter); + std.def_fn("roman", base::roman); + std.def_fn("symbol", base::symbol); + std.def_fn("lorem", base::lorem); + std.def_fn("csv", base::csv); + std.def_fn("json", base::json); + std.def_fn("xml", base::xml); // Predefined colors. std.define("black", Color::BLACK); @@ -155,7 +155,7 @@ pub fn styles() -> StyleMap { /// Construct the standard lang item mapping. pub fn items() -> LangItems { LangItems { - root: |world, content| content.layout(world), + root: |world, content| content.layout_root(world), em: |styles| styles.get(text::TextNode::SIZE), dir: |styles| styles.get(text::TextNode::DIR), space: || text::SpaceNode.pack(), diff --git a/library/src/math/tex.rs b/library/src/math/tex.rs index 7b40aa2b..7de68d7b 100644 --- a/library/src/math/tex.rs +++ b/library/src/math/tex.rs @@ -5,7 +5,7 @@ use rex::parser::color::RGBA; use rex::render::{Backend, Cursor, Renderer}; use typst::font::Font; -use super::*; +use super::MathNode; use crate::prelude::*; use crate::text::{variant, LinebreakNode, SpaceNode, TextNode}; diff --git a/library/src/prelude.rs b/library/src/prelude.rs index 0c3b0eb1..11095c67 100644 --- a/library/src/prelude.rs +++ b/library/src/prelude.rs @@ -1,27 +1,20 @@ //! Helpful imports for creating library functionality. 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 comemo::Tracked; -pub use typst::diag::{ - bail, error, with_alternative, At, FileError, FileResult, SourceError, SourceResult, - StrResult, -}; +pub use typst::diag::{bail, error, with_alternative, At, SourceResult, StrResult}; pub use typst::frame::*; pub use typst::geom::*; pub use typst::model::{ - array, capability, castable, dict, dynamic, format_str, node, Args, Array, - Capability, Cast, Content, Dict, Dynamic, Fold, Func, Key, LangItems, Node, Resolve, - Scope, Selector, Show, Smart, Str, StyleChain, StyleMap, StyleVec, Value, Vm, + array, capability, castable, dict, dynamic, format_str, node, Args, Array, Cast, + Content, Dict, Fold, Func, Key, Node, Resolve, Scope, Selector, Show, Smart, Str, + StyleChain, StyleMap, StyleVec, Value, Vm, }; pub use typst::syntax::{Span, Spanned}; pub use typst::util::{format_eco, EcoString}; pub use typst::World; pub use super::ext::{ContentExt, StyleMapExt}; -pub use super::layout::{Layout, LayoutBlock, LayoutInline, Regions}; -pub use super::text::{FallbackList, TextNode}; +pub use super::layout::{LayoutBlock, LayoutInline, Regions}; diff --git a/library/src/structure/list.rs b/library/src/structure/list.rs index 2015f19b..d461ef2d 100644 --- a/library/src/structure/list.rs +++ b/library/src/structure/list.rs @@ -1,9 +1,9 @@ use unscanny::Scanner; +use crate::base::Numbering; use crate::layout::{BlockSpacing, GridNode, HNode, TrackSizing}; use crate::prelude::*; -use crate::text::{ParNode, SpaceNode}; -use crate::utility::Numbering; +use crate::text::{ParNode, SpaceNode, TextNode}; /// An unordered (bulleted) or ordered (numbered) list. #[derive(Debug, Hash)] diff --git a/library/src/structure/reference.rs b/library/src/structure/reference.rs index 632ecba5..56f8b8e3 100644 --- a/library/src/structure/reference.rs +++ b/library/src/structure/reference.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use crate::text::TextNode; /// A reference to a label. #[derive(Debug, Hash)] diff --git a/library/src/text/deco.rs b/library/src/text/deco.rs index cd3acef5..aaf6cfa8 100644 --- a/library/src/text/deco.rs +++ b/library/src/text/deco.rs @@ -70,7 +70,7 @@ impl Show for DecoNode { /// /// For more details, see [`DecoNode`]. #[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Decoration { +pub(super) struct Decoration { pub line: DecoLine, pub stroke: PartialStroke, pub offset: Smart, @@ -91,7 +91,7 @@ pub const STRIKETHROUGH: DecoLine = 1; pub const OVERLINE: DecoLine = 2; /// Add line decorations to a single run of shaped text. -pub fn decorate( +pub(super) fn decorate( frame: &mut Frame, deco: &Decoration, text: &Text, diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs index d793f614..61edacbe 100644 --- a/library/src/text/mod.rs +++ b/library/src/text/mod.rs @@ -11,7 +11,6 @@ mod shift; pub use deco::*; pub use link::*; pub use par::*; -pub use quotes::*; pub use raw::*; pub use shaping::*; pub use shift::*; @@ -22,6 +21,7 @@ use rustybuzz::Tag; use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; use typst::util::EcoString; +use self::quotes::*; use crate::prelude::*; /// A single run of text with the same style. @@ -107,22 +107,22 @@ impl TextNode { /// Whether the font weight should be increased by 300. #[property(skip, fold)] - pub const BOLD: Toggle = false; + pub(super) const BOLD: Toggle = false; /// Whether the font style should be inverted. #[property(skip, fold)] - pub const ITALIC: Toggle = false; + pub(super) const ITALIC: Toggle = false; /// A case transformation that should be applied to the text. #[property(skip)] - pub const CASE: Option = None; + pub(super) const CASE: Option = None; /// Whether small capital glyphs should be used. ("smcp") #[property(skip)] - pub const SMALLCAPS: bool = false; + pub(super) const SMALLCAPS: bool = false; /// A destination the text should be linked to. #[property(skip, referenced)] - pub const LINK: Option = None; + pub(crate) const LINK: Option = None; /// Decorative lines. #[property(skip, fold)] - pub const DECO: Decoration = vec![]; + pub(super) const DECO: Decoration = vec![]; fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { // The text constructor is special: It doesn't create a text node. diff --git a/library/src/text/quotes.rs b/library/src/text/quotes.rs index ab4d3f9d..af10de46 100644 --- a/library/src/text/quotes.rs +++ b/library/src/text/quotes.rs @@ -4,7 +4,7 @@ use super::{Lang, Region}; /// State machine for smart quote subtitution. #[derive(Debug, Clone)] -pub struct Quoter { +pub(super) struct Quoter { /// How many quotes have been opened. quote_depth: usize, /// Whether an opening quote might follow. @@ -68,7 +68,7 @@ fn is_opening_bracket(c: char) -> bool { } /// Decides which quotes to subtitute smart quotes with. -pub struct Quotes<'s> { +pub(super) struct Quotes<'s> { /// The opening single quote. pub single_open: &'s str, /// The closing single quote. diff --git a/library/src/text/shaping.rs b/library/src/text/shaping.rs index 32143862..bab02eca 100644 --- a/library/src/text/shaping.rs +++ b/library/src/text/shaping.rs @@ -13,7 +13,7 @@ use crate::prelude::*; /// This type contains owned or borrowed shaped text runs, which can be /// measured, used to reshape substrings more quickly and converted into a /// frame. -pub struct ShapedText<'a> { +pub(super) struct ShapedText<'a> { /// The text that was shaped. pub text: &'a str, /// The text direction. @@ -32,7 +32,7 @@ pub struct ShapedText<'a> { /// A single glyph resulting from shaping. #[derive(Debug, Clone)] -pub struct ShapedGlyph { +pub(super) struct ShapedGlyph { /// The font the glyph is contained in. pub font: Font, /// The glyph's index in the font. @@ -318,7 +318,7 @@ struct ShapingContext<'a> { } /// Shape text into [`ShapedText`]. -pub fn shape<'a>( +pub(super) fn shape<'a>( world: Tracked, text: &'a str, styles: StyleChain<'a>, @@ -534,7 +534,7 @@ fn nbsp_delta(font: &Font) -> Option { Some(font.advance(nbsp)? - font.advance(space)?) } -/// Resolve the font variant with `BOLD` and `ITALIC` factored in. +/// Resolve the font variant. pub fn variant(styles: StyleChain) -> FontVariant { let mut variant = FontVariant::new( styles.get(TextNode::STYLE), diff --git a/library/src/text/shift.rs b/library/src/text/shift.rs index e5f142dd..856d0f96 100644 --- a/library/src/text/shift.rs +++ b/library/src/text/shift.rs @@ -11,7 +11,7 @@ use crate::prelude::*; /// codepoints. If that fails, we fall back to rendering shrunk normal letters /// in a raised way. #[derive(Debug, Hash)] -pub struct ShiftNode(pub Content); +pub struct ShiftNode(pub Content); /// Shift the text into superscript. pub type SuperNode = ShiftNode; @@ -20,7 +20,7 @@ pub type SuperNode = ShiftNode; pub type SubNode = ShiftNode; #[node(Show)] -impl ShiftNode { +impl ShiftNode { /// Whether to prefer the dedicated sub- and superscript characters of the /// font. pub const TYPOGRAPHIC: bool = true; @@ -35,7 +35,7 @@ impl ShiftNode { } } -impl Show for ShiftNode { +impl Show for ShiftNode { fn unguard_parts(&self, _: Selector) -> Content { Self(self.0.clone()).pack() } @@ -72,7 +72,7 @@ impl Show for ShiftNode { /// Find and transform the text contained in `content` to the given script kind /// if and only if it only consists of `Text`, `Space`, and `Empty` leaf nodes. -fn search_text(content: &Content, mode: ScriptKind) -> Option { +fn search_text(content: &Content, mode: ShiftKind) -> Option { if content.is_empty() { Some(EcoString::new()) } else if content.is::() { @@ -114,7 +114,7 @@ fn is_shapable(world: Tracked, text: &str, styles: StyleChain) -> boo /// Convert a string to sub- or superscript codepoints if all characters /// can be mapped to such a codepoint. -fn convert_script(text: &str, mode: ScriptKind) -> Option { +fn convert_script(text: &str, mode: ShiftKind) -> Option { let mut result = EcoString::with_capacity(text.len()); let converter = match mode { SUPERSCRIPT => to_superscript_codepoint, @@ -179,10 +179,10 @@ fn to_subscript_codepoint(c: char) -> Option { } /// A category of script. -pub type ScriptKind = usize; +pub type ShiftKind = usize; /// Text that is rendered smaller and raised, also known as superior. -const SUPERSCRIPT: ScriptKind = 0; +const SUPERSCRIPT: ShiftKind = 0; /// Text that is rendered smaller and lowered, also known as inferior. -const SUBSCRIPT: ScriptKind = 1; +const SUBSCRIPT: ShiftKind = 1; diff --git a/library/src/utility/color.rs b/library/src/utility/color.rs deleted file mode 100644 index 8bb12334..00000000 --- a/library/src/utility/color.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::str::FromStr; - -use crate::prelude::*; - -/// Create a grayscale color. -pub fn luma(_: &mut Vm, args: &mut Args) -> SourceResult { - 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 { - Ok(Value::Color( - if let Some(string) = args.find::>()? { - 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 { - 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 deleted file mode 100644 index 3edade55..00000000 --- a/library/src/utility/data.rs +++ /dev/null @@ -1,132 +0,0 @@ -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 { - let Spanned { v: path, span } = - args.expect::>("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 { - 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() - ) -} - -/// Read structured data from an XML file. -pub fn xml(vm: &mut Vm, args: &mut Args) -> SourceResult { - let Spanned { v: path, span } = - args.expect::>("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 deleted file mode 100644 index dd37e8e7..00000000 --- a/library/src/utility/math.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::cmp::Ordering; - -use crate::prelude::*; - -/// Convert a value to an integer. -pub fn int(_: &mut Vm, args: &mut Args) -> SourceResult { - 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 { - 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 { - 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 { - minmax(args, Ordering::Less) -} - -/// The maximum of a sequence of values. -pub fn max(_: &mut Vm, args: &mut Args) -> SourceResult { - minmax(args, Ordering::Greater) -} - -/// Find the minimum or maximum of a sequence of values. -fn minmax(args: &mut Args, goal: Ordering) -> SourceResult { - let mut extremum = args.expect::("value")?; - for Spanned { v, span } in args.all::>()? { - 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 { - Ok(Value::Bool(args.expect::("integer")? % 2 == 0)) -} - -/// Whether an integer is odd. -pub fn odd(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(Value::Bool(args.expect::("integer")? % 2 != 0)) -} - -/// The modulo of two numbers. -pub fn mod_(_: &mut Vm, args: &mut Args) -> SourceResult { - 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 { - let first = args.expect::("end")?; - let (start, end) = match args.eat::()? { - 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 deleted file mode 100644 index 402944cd..00000000 --- a/library/src/utility/mod.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! 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 { - Ok(args.expect::("value")?.type_name().into()) -} - -/// Ensure that a condition is fulfilled. -pub fn assert(_: &mut Vm, args: &mut Args) -> SourceResult { - let Spanned { v, span } = args.expect::>("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 { - let Spanned { v: text, span } = args.expect::>("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 deleted file mode 100644 index ed444d35..00000000 --- a/library/src/utility/string.rs +++ /dev/null @@ -1,141 +0,0 @@ -use typst::model::Regex; - -use crate::prelude::*; - -/// The string representation of a value. -pub fn repr(_: &mut Vm, args: &mut Args) -> SourceResult { - Ok(args.expect::("value")?.repr().into()) -} - -/// Convert a value to a string. -pub fn str(_: &mut Vm, args: &mut Args) -> SourceResult { - 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 { - 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 { - let Spanned { v, span } = args.expect::>("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 { - numbered(Numbering::Letter, args) -} - -/// Converts an integer into a roman numeral. -pub fn roman(_: &mut Vm, args: &mut Args) -> SourceResult { - numbered(Numbering::Roman, args) -} - -/// Convert a number into a symbol. -pub fn symbol(_: &mut Vm, args: &mut Args) -> SourceResult { - numbered(Numbering::Symbol, args) -} - -fn numbered(numbering: Numbering, args: &mut Args) -> SourceResult { - let n = args.expect::("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] = &['*', '†', '‡', '§', '‖', '¶']; diff --git a/tests/ref/base/blind.png b/tests/ref/base/blind.png new file mode 100644 index 00000000..e972567e Binary files /dev/null and b/tests/ref/base/blind.png differ diff --git a/tests/ref/base/collection.png b/tests/ref/base/collection.png new file mode 100644 index 00000000..e93e2beb Binary files /dev/null and b/tests/ref/base/collection.png differ diff --git a/tests/ref/base/color.png b/tests/ref/base/color.png new file mode 100644 index 00000000..496013bb Binary files /dev/null and b/tests/ref/base/color.png differ diff --git a/tests/ref/base/data.png b/tests/ref/base/data.png new file mode 100644 index 00000000..69e0ae38 Binary files /dev/null and b/tests/ref/base/data.png differ diff --git a/tests/ref/base/eval.png b/tests/ref/base/eval.png new file mode 100644 index 00000000..38c1d64e Binary files /dev/null and b/tests/ref/base/eval.png differ diff --git a/tests/ref/base/numbering.png b/tests/ref/base/numbering.png new file mode 100644 index 00000000..d4d575d9 Binary files /dev/null and b/tests/ref/base/numbering.png differ diff --git a/tests/ref/base/string.png b/tests/ref/base/string.png new file mode 100644 index 00000000..02184316 Binary files /dev/null and b/tests/ref/base/string.png differ diff --git a/tests/ref/layout/math.png b/tests/ref/layout/math.png new file mode 100644 index 00000000..902354df Binary files /dev/null and b/tests/ref/layout/math.png differ diff --git a/tests/ref/math/basic.png b/tests/ref/math/basic.png deleted file mode 100644 index 902354df..00000000 Binary files a/tests/ref/math/basic.png and /dev/null differ diff --git a/tests/ref/text/basic.png b/tests/ref/text/basic.png deleted file mode 100644 index bfdf47a2..00000000 Binary files a/tests/ref/text/basic.png and /dev/null differ diff --git a/tests/ref/text/plain.png b/tests/ref/text/plain.png new file mode 100644 index 00000000..bfdf47a2 Binary files /dev/null and b/tests/ref/text/plain.png differ diff --git a/tests/ref/utility/blind.png b/tests/ref/utility/blind.png deleted file mode 100644 index e972567e..00000000 Binary files a/tests/ref/utility/blind.png and /dev/null differ diff --git a/tests/ref/utility/collection.png b/tests/ref/utility/collection.png deleted file mode 100644 index e93e2beb..00000000 Binary files a/tests/ref/utility/collection.png and /dev/null differ diff --git a/tests/ref/utility/color.png b/tests/ref/utility/color.png deleted file mode 100644 index 496013bb..00000000 Binary files a/tests/ref/utility/color.png and /dev/null differ diff --git a/tests/ref/utility/data.png b/tests/ref/utility/data.png deleted file mode 100644 index 69e0ae38..00000000 Binary files a/tests/ref/utility/data.png and /dev/null differ diff --git a/tests/ref/utility/eval.png b/tests/ref/utility/eval.png deleted file mode 100644 index 38c1d64e..00000000 Binary files a/tests/ref/utility/eval.png and /dev/null differ diff --git a/tests/ref/utility/numbering.png b/tests/ref/utility/numbering.png deleted file mode 100644 index d4d575d9..00000000 Binary files a/tests/ref/utility/numbering.png and /dev/null differ diff --git a/tests/ref/utility/string.png b/tests/ref/utility/string.png deleted file mode 100644 index 02184316..00000000 Binary files a/tests/ref/utility/string.png and /dev/null differ diff --git a/tests/src/benches.rs b/tests/src/benches.rs index 3af2db6e..e3c276bc 100644 --- a/tests/src/benches.rs +++ b/tests/src/benches.rs @@ -2,13 +2,12 @@ use std::path::{Path, PathBuf}; use comemo::{Prehashed, Track, Tracked}; use iai::{black_box, main, Iai}; -use unscanny::Scanner; - use typst::diag::{FileError, FileResult}; use typst::font::{Font, FontBook}; use typst::syntax::{Source, SourceId, TokenMode, Tokens}; use typst::util::Buffer; use typst::{Config, World}; +use unscanny::Scanner; const TEXT: &str = include_str!("../typ/benches/bench.typ"); const FONT: &[u8] = include_bytes!("../../fonts/IBMPlexSans-Regular.ttf"); diff --git a/tests/src/tests.rs b/tests/src/tests.rs index 620c1e7b..54afd034 100644 --- a/tests/src/tests.rs +++ b/tests/src/tests.rs @@ -11,9 +11,6 @@ use comemo::Prehashed; use elsa::FrozenVec; use once_cell::unsync::OnceCell; use tiny_skia as sk; -use unscanny::Scanner; -use walkdir::WalkDir; - use typst::diag::{bail, FileError, FileResult}; use typst::font::{Font, FontBook}; use typst::frame::{Element, Frame}; @@ -24,6 +21,8 @@ use typst::util::{Buffer, PathExt}; use typst::{Config, World}; use typst_library::layout::PageNode; use typst_library::text::{TextNode, TextSize}; +use unscanny::Scanner; +use walkdir::WalkDir; const TYP_DIR: &str = "typ"; const REF_DIR: &str = "ref"; diff --git a/tests/typ/base/assert.typ b/tests/typ/base/assert.typ new file mode 100644 index 00000000..b0c8aafd --- /dev/null +++ b/tests/typ/base/assert.typ @@ -0,0 +1,23 @@ +// Test the `assert` function. +// Ref: false + +--- +#assert(1 + 1 == 2) +#assert(range(2, 5) == (2, 3, 4)) +#assert(not false) + +--- +// Test failing assertions. +// Error: 9-15 assertion failed +#assert(1 == 2) + +--- +// Test failing assertions. +// Error: 9-15 expected boolean, found string +#assert("true") + +--- +// Test the `type` function. +#test(type(1), "integer") +#test(type(ltr), "direction") +#test(type(10 / 3), "float") diff --git a/tests/typ/base/blind.typ b/tests/typ/base/blind.typ new file mode 100644 index 00000000..17452dec --- /dev/null +++ b/tests/typ/base/blind.typ @@ -0,0 +1,32 @@ +// Test blind text. + +--- +// Test basic call. +#lorem(19) + +--- +// Test custom paragraphs with user code. +#set text(8pt) + +{ + let sentences = lorem(59) + .split(".") + .filter(s => s != "") + .map(s => s + ".") + + let used = 0 + for s in sentences { + if used < 2 { + used += 1 + } else { + parbreak() + used = 0 + } + s.trim() + [ ] + } +} + +--- +// Error: 7-9 missing argument: number of words +#lorem() diff --git a/tests/typ/base/calc.typ b/tests/typ/base/calc.typ new file mode 100644 index 00000000..4ccefa22 --- /dev/null +++ b/tests/typ/base/calc.typ @@ -0,0 +1,117 @@ +// Test math functions. +// Ref: false + +--- +// Test conversion to numbers. +#test(int(false), 0) +#test(int(true), 1) +#test(int(10), 10) +#test(int("150"), 150) +#test(int(10 / 3), 3) +#test(float(10), 10.0) +#test(float("31.4e-1"), 3.14) +#test(type(float(10)), "float") + +--- +// Error: 6-10 cannot convert length to integer +#int(10pt) + +--- +// Error: 8-13 cannot convert function to float +#float(float) + +--- +// Error: 6-12 invalid integer +#int("nope") + +--- +// Error: 8-15 invalid float +#float("1.2.3") + +--- +// Test the `abs` function. +#test(abs(-3), 3) +#test(abs(3), 3) +#test(abs(-0.0), 0.0) +#test(abs(0.0), -0.0) +#test(abs(-3.14), 3.14) +#test(abs(50%), 50%) +#test(abs(-25%), 25%) + +--- +// Error: 6-17 expected numeric value, found string +#abs("no number") + +--- +// Error: 6-11 cannot take absolute value of a length +#abs(-12pt) + +--- +// Error: 6-16 cannot take absolute value of a length +#abs(50% - 12pt) + +--- +// Test the `even` and `odd` functions. +#test(even(2), true) +#test(odd(2), false) +#test(odd(-1), true) +#test(even(-11), false) + +--- +// Test the `mod` function. +#test(mod(1, 1), 0) +#test(mod(5, 3), 2) +#test(mod(5, -3), 2) +#test(mod(22.5, 10), 2.5) +#test(mod(9, 4.5), 0) + +--- +// Error: 9-10 divisor must not be zero +#mod(5, 0) + +--- +// Error: 11-14 divisor must not be zero +#mod(3.0, 0.0) + +--- +// Test the `min` and `max` functions. +#test(min(2, -4), -4) +#test(min(3.5, 1e2, -0.1, 3), -0.1) +#test(max(-3, 11), 11) +#test(min("hi"), "hi") + +--- +// Error: 5-7 missing argument: value +#min() + +--- +// Error: 9-13 cannot compare integer and string +#min(1, "hi") + +--- +// Test the `range` function. +#test(range(4), (0, 1, 2, 3)) +#test(range(1, 4), (1, 2, 3)) +#test(range(-4, 2), (-4, -3, -2, -1, 0, 1)) +#test(range(10, 5), ()) +#test(range(10, step: 3), (0, 3, 6, 9)) +#test(range(1, 4, step: 1), (1, 2, 3)) +#test(range(1, 8, step: 2), (1, 3, 5, 7)) +#test(range(5, 2, step: -1), (5, 4, 3)) +#test(range(10, 0, step: -3), (10, 7, 4, 1)) + +--- +// Error: 7-9 missing argument: end +#range() + +--- +// Error: 11-14 expected integer, found float +#range(1, 2.0) + +--- +// Error: 17-22 expected integer, found string +#range(4, step: "one") + +--- +// Error: 18-19 step must not be zero +#range(10, step: 0) diff --git a/tests/typ/base/collection.typ b/tests/typ/base/collection.typ new file mode 100644 index 00000000..46ff97ab --- /dev/null +++ b/tests/typ/base/collection.typ @@ -0,0 +1,115 @@ +// Test collection functions. +// Ref: false + +--- +// Test the `len` method. +#test(().len(), 0) +#test(("A", "B", "C").len(), 3) +#test("Hello World!".len(), 12) +#test((a: 1, b: 2).len(), 2) + +--- +// The the `first` and `last` methods. +#test(().first(), none) +#test(().last(), none) +#test((1,).first(), 1) +#test((2,).last(), 2) +#test((1, 2, 3).first(), 1) +#test((1, 2, 3).last(), 3) + +--- +// Test the `push` and `pop` methods. +{ + let tasks = (a: (1, 2, 3), b: (4, 5, 6)) + tasks("a").pop() + tasks("b").push(7) + test(tasks("a"), (1, 2)) + test(tasks("b"), (4, 5, 6, 7)) +} + +--- +// Test the `insert` and `remove` methods. +{ + let array = (0, 1, 2, 4, 5) + array.insert(3, 3) + test(array, range(6)) + array.remove(1) + test(array, (0, 2, 3, 4, 5)) +} + +--- +// Error: 2:17-2:19 missing argument: index +#let numbers = () +{ numbers.insert() } + +--- +// Test the `slice` method. +#test((1, 2, 3, 4).slice(2), (3, 4)) +#test(range(10).slice(2, 6), (2, 3, 4, 5)) +#test(range(10).slice(4, count: 3), (4, 5, 6)) +#test(range(10).slice(-5, count: 2), (5, 6)) +#test((1, 2, 3).slice(2, -2), ()) +#test((1, 2, 3).slice(-2, 2), (2,)) +#test((1, 2, 3).slice(-3, 2), (1, 2)) +#test("ABCD".split("").slice(1, -1).join("-"), "A-B-C-D") + +--- +// Error: 3-31 array index out of bounds (index: 12, len: 10) +{ range(10).slice(9, count: 3) } + +--- +// Error: 3-25 array index out of bounds (index: -4, len: 3) +{ (1, 2, 3).slice(0, -4) } + +--- +// Test the `position` method. +#test(("Hi", "❤️", "Love").position(s => s == "❤️"), 1) +#test(("Bye", "💘", "Apart").position(s => s == "❤️"), none) +#test(("A", "B", "CDEF", "G").position(v => v.len() > 2), 2) + +--- +// Test the `rev` method. +#test(range(3).rev(), (2, 1, 0)) + +--- +// Test the `join` method. +#test(().join(), none) +#test((1,).join(), 1) +#test(("a", "b", "c").join(), "abc") +#test("(" + ("a", "b", "c").join(", ") + ")", "(a, b, c)") + +--- +// Error: 2-22 cannot join boolean with boolean +{(true, false).join()} + +--- +// Error: 2-20 cannot join string with integer +{("a", "b").join(1)} + +--- +// Test joining content. +// Ref: true +{([One], [Two], [Three]).join([, ], last: [ and ])}. + +--- +// Test the `sorted` method. +#test(().sorted(), ()) +#test(((true, false) * 10).sorted(), (false,) * 10 + (true,) * 10) +#test(("it", "the", "hi", "text").sorted(), ("hi", "it", "text", "the")) +#test((2, 1, 3, 10, 5, 8, 6, -7, 2).sorted(), (-7, 1, 2, 2, 3, 5, 6, 8, 10)) + +--- +// Error: 2-26 cannot order content and content +{([Hi], [There]).sorted()} + +--- +// Test dictionary methods. +#let dict = (a: 3, c: 2, b: 1) +#test("c" in dict, true) +#test(dict.len(), 3) +#test(dict.values(), (3, 1, 2)) +#test(dict.pairs((k, v) => k + str(v)).join(), "a3b1c2") + +{ dict.remove("c") } +#test("c" in dict, false) +#test(dict, (a: 3, b: 1)) diff --git a/tests/typ/base/color.typ b/tests/typ/base/color.typ new file mode 100644 index 00000000..96d76063 --- /dev/null +++ b/tests/typ/base/color.typ @@ -0,0 +1,63 @@ +// Test color creation functions. +// Ref: false + +--- +// Compare both ways. +#test(rgb(0%, 30%, 70%), rgb("004db3")) + +// Alpha channel. +#test(rgb(255, 0, 0, 50%), rgb("ff000080")) + +// Test color modification methods. +#test(rgb(25, 35, 45).lighten(10%), rgb(48, 57, 66)) +#test(rgb(40, 30, 20).darken(10%), rgb(36, 27, 18)) +#test(rgb("#133337").negate(), rgb(236, 204, 200)) +#test(white.lighten(100%), white) + +--- +// Test gray color conversion. +// Ref: true +#rect(fill: luma(0)) +#rect(fill: luma(80%)) + +--- +// Test gray color modification. +#test(luma(20%).lighten(50%), luma(60%)) +#test(luma(80%).darken(20%), luma(63.9%)) +#test(luma(80%).negate(), luma(20%)) + +--- +// Test CMYK color conversion. +// Ref: true +#let c = cmyk(50%, 64%, 16%, 17%) +#rect(width: 1cm, fill: cmyk(69%, 11%, 69%, 41%)) +#rect(width: 1cm, fill: c) +#rect(width: 1cm, fill: c.negate()) + +#for x in range(0, 11) { + square(width: 9pt, fill: c.lighten(x * 10%)) +} +#for x in range(0, 11) { + square(width: 9pt, fill: c.darken(x * 10%)) +} + +--- +// Error for values that are out of range. +// Error: 11-14 must be between 0 and 255 +#test(rgb(-30, 15, 50)) + +--- +// Error: 6-11 string contains non-hexadecimal letters +#rgb("lol") + +--- +// Error: 5-7 missing argument: red component +#rgb() + +--- +// Error: 5-11 missing argument: blue component +#rgb(0, 1) + +--- +// Error: 21-26 expected integer or ratio, found boolean +#rgb(10%, 20%, 30%, false) diff --git a/tests/typ/base/data.typ b/tests/typ/base/data.typ new file mode 100644 index 00000000..96b12ff5 --- /dev/null +++ b/tests/typ/base/data.typ @@ -0,0 +1,58 @@ +// 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() +#table(columns: data(0).len(), ..cells) + +--- +// Error: 6-16 file not found (searched at typ/base/nope.csv) +#csv("nope.csv") + +--- +// 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") + +--- +// Test reading XML data. +#let data = xml("/res/data.xml") +#test(data, (( + tag: "data", + attrs: (:), + children: ( + "\n ", + (tag: "hello", attrs: (name: "hi"), children: ("1",)), + "\n ", + ( + tag: "data", + attrs: (:), + children: ( + "\n ", + (tag: "hello", attrs: (:), children: ("World",)), + "\n ", + (tag: "hello", attrs: (:), children: ("World",)), + "\n ", + ), + ), + "\n", + ), +),)) + +--- +// Error: 6-20 failed to parse xml file: found closing tag 'data' instead of 'hello' in line 3 +#xml("/res/bad.xml") diff --git a/tests/typ/base/eval.typ b/tests/typ/base/eval.typ new file mode 100644 index 00000000..86b1f0c4 --- /dev/null +++ b/tests/typ/base/eval.typ @@ -0,0 +1,52 @@ +// Test the `eval` function. + +--- +#eval("_Hello" + " World!_") + +--- +// Error: 7-13 expected identifier +#eval("#let") + +--- +#set raw(around: none) +#show it: raw as text("IBM Plex Sans", eval(it.text)) + +Interacting +``` +#set text(blue) +Blue #move(dy: -0.15em)[🌊] +``` + +--- +// Error: 7-19 cannot continue outside of loop +#eval("{continue}") + +--- +// Error: 7-33 cannot access file system from here +#eval("#include \"../coma.typ\"") + +--- +// Error: 7-35 cannot access file system from here +#eval("#image(\"/res/tiger.jpg\")") + +--- +// Error: 23-30 cannot access file system from here +#show it: raw as eval(it.text) + +``` +#show strong as image("/res/tiger.jpg") +*No absolute tiger!* +``` + +--- +// Error: 23-30 cannot access file system from here +#show it: raw as eval(it.text) + +``` +#show emph as image("../../res/giraffe.jpg") +_No relative giraffe!_ +``` + +--- +// Error: 7-16 expected comma +#eval("{(1 2)}") diff --git a/tests/typ/base/string.typ b/tests/typ/base/string.typ new file mode 100644 index 00000000..3104a3ea --- /dev/null +++ b/tests/typ/base/string.typ @@ -0,0 +1,161 @@ +// Test string related methods. +// Ref: false + +--- +// Test conversion to string. +#test(str(123), "123") +#test(str(50.14), "50.14") +#test(str(10 / 3).len() > 10, true) +#test(repr(ltr), "ltr") +#test(repr((1, 2, false, )), "(1, 2, false)") + +--- +// Error: 6-8 cannot convert content to string +#str([]) + +--- +// Test the `slice` method. +#test("abc".slice(1, 2), "b") +#test("abc🏡def".slice(2, 7), "c🏡") +#test("abc🏡def".slice(2, -2), "c🏡d") +#test("abc🏡def".slice(-3, -1), "de") + +--- +// Test the `contains` method. +#test("abc".contains("b"), true) +#test("b" in "abc", true) +#test("1234f".contains(regex("\d")), true) +#test(regex("\d") in "1234f", true) +#test("abc".contains("d"), false) +#test("1234g" in "1234f", false) +#test("abc".contains(regex("^[abc]$")), false) +#test("abc".contains(regex("^[abc]+$")), true) + +--- +// Test the `starts-with` and `ends-with` methods. +#test("Typst".starts-with("Ty"), true) +#test("Typst".starts-with(regex("[Tt]ys")), false) +#test("Typst".starts-with("st"), false) +#test("Typst".ends-with("st"), true) +#test("Typst".ends-with(regex("\d*")), true) +#test("Typst".ends-with(regex("\d+")), false) +#test("Typ12".ends-with(regex("\d+")), true) + +--- +// Test the `find` and `position` methods. +#let date = regex("\d{2}:\d{2}") +#test("Hello World".find("World"), "World") +#test("Hello World".position("World"), 6) +#test("It's 12:13 now".find(date), "12:13") +#test("It's 12:13 now".position(date), 5) + +--- +// Test the `match` method. +#test("Is there a".match("for this?"), none) +#test( + "The time of my life.".match(regex("[mit]+e")), + (start: 4, end: 8, text: "time", captures: ()), +) + +// Test the `matches` method. +#test("Hello there".matches("\d"), ()) +#test("Day by Day.".matches("Day"), ( + (start: 0, end: 3, text: "Day", captures: ()), + (start: 7, end: 10, text: "Day", captures: ()), +)) + +// Compute the sum of all timestamps in the text. +#let timesum(text) = { + let time = 0 + for match in text.matches(regex("(\d+):(\d+)")) { + let caps = match.captures + time += 60 * int(caps(0)) + int(caps(1)) + } + str(int(time / 60)) + ":" + str(mod(time, 60)) +} + +#test(timesum(""), "0:0") +#test(timesum("2:70"), "3:10") +#test(timesum("1:20, 2:10, 0:40"), "4:10") + +--- +// Test the `replace` method. +#test("ABC".replace("", "-"), "-A-B-C-") +#test("Ok".replace("Ok", "Nope", count: 0), "Ok") +#test("to add?".replace("", "How ", count: 1), "How to add?") +#test("AB C DEF GH J".replace(" ", ",", count: 2), "AB,C,DEF GH J") +#test("Walcemo" + .replace("o", "k") + .replace("e", "o") + .replace("k", "e") + .replace("a", "e"), + "Welcome" +) +#test("123".replace(regex("\d$"), "_"), "12_") +#test("123".replace(regex("\d{1,2}$"), "__"), "1__") + +--- +// Test the `trim` method. +#let str = "Typst, LaTeX, Word, InDesign" +#let array = ("Typst", "LaTeX", "Word", "InDesign") +#test(str.split(",").map(s => s.trim()), array) +#test("".trim(), "") +#test(" abc ".trim(at: start), "abc ") +#test(" abc ".trim(at: end, repeat: true), " abc") +#test(" abc".trim(at: start, repeat: false), "abc") +#test("aabcaa".trim("a", repeat: false), "abca") +#test("aabca".trim("a", at: start), "bca") +#test("aabcaa".trim("a", at: end, repeat: false), "aabca") +#test("".trim(regex(".")), "") +#test("123abc456".trim(regex("\d")), "abc") +#test("123abc456".trim(regex("\d"), repeat: false), "23abc45") +#test("123a4b5c678".trim(regex("\d"), repeat: true), "a4b5c") +#test("123a4b5c678".trim(regex("\d"), repeat: false), "23a4b5c67") +#test("123abc456".trim(regex("\d"), at: start), "abc456") +#test("123abc456".trim(regex("\d"), at: end), "123abc") +#test("123abc456".trim(regex("\d+"), at: end, repeat: false), "123abc") +#test("123abc456".trim(regex("\d{1,2}$"), repeat: false), "123abc4") +#test("hello world".trim(regex(".")), "") + +--- +// Error: 17-21 expected either `start` or `end` +{"abc".trim(at: left)} + +--- +// Test the `split` method. +#test("abc".split(""), ("", "a", "b", "c", "")) +#test("abc".split("b"), ("a", "c")) +#test("a123c".split(regex("\d")), ("a", "", "", "c")) +#test("a123c".split(regex("\d+")), ("a", "c")) + +--- +// Test the `upper` and `lower` functions. +#let memes = "ArE mEmEs gReAt?"; +#test(lower(memes), "are memes great?") +#test(upper(memes), "ARE MEMES GREAT?") +#test(upper("Ελλάδα"), "ΕΛΛΆΔΑ") + +--- +// Test integrated lower, upper and symbols. +// Ref: true + +#upper("Abc 8") +#upper[def] + +#lower("SCREAMING MUST BE SILENCED in " + roman(1672) + " years") + +#for i in range(9) { + symbol(i) + [ and ] + roman(i) + [ for #i] + parbreak() +} + +--- +// Error: 8-9 expected string or content, found integer +#upper(1) + +--- +// Error: 9-11 must be at least zero +#symbol(-1) diff --git a/tests/typ/base/type.typ b/tests/typ/base/type.typ new file mode 100644 index 00000000..37cf8623 --- /dev/null +++ b/tests/typ/base/type.typ @@ -0,0 +1,7 @@ +// Test the `type` function. +// Ref: false + +--- +#test(type(1), "integer") +#test(type(ltr), "direction") +#test(type(10 / 3), "float") diff --git a/tests/typ/layout/math.typ b/tests/typ/layout/math.typ new file mode 100644 index 00000000..55a853cf --- /dev/null +++ b/tests/typ/layout/math.typ @@ -0,0 +1,20 @@ +// Test math formulas. + +--- +The sum of $a$ and $b$ is $a + b$. + +--- +We will show that: +$ a^2 + b^2 = c^2 $ + +--- +Prove by induction: +$ sum_(k=0)^n k = (n(n+1))/2 $ + +--- +// Test that blackboard style looks nice. +$ f: NN arrow RR $ + +--- +// Error: 1:3 expected dollar sign +$a diff --git a/tests/typ/math/basic.typ b/tests/typ/math/basic.typ deleted file mode 100644 index 55a853cf..00000000 --- a/tests/typ/math/basic.typ +++ /dev/null @@ -1,20 +0,0 @@ -// Test math formulas. - ---- -The sum of $a$ and $b$ is $a + b$. - ---- -We will show that: -$ a^2 + b^2 = c^2 $ - ---- -Prove by induction: -$ sum_(k=0)^n k = (n(n+1))/2 $ - ---- -// Test that blackboard style looks nice. -$ f: NN arrow RR $ - ---- -// Error: 1:3 expected dollar sign -$a diff --git a/tests/typ/text/basic.typ b/tests/typ/text/basic.typ deleted file mode 100644 index a074a046..00000000 --- a/tests/typ/text/basic.typ +++ /dev/null @@ -1,20 +0,0 @@ -// Test simple text. - ---- -#set page(width: 250pt, height: 120pt) - -But, soft! what light through yonder window breaks? It is the east, and Juliet -is the sun. Arise, fair sun, and kill the envious moon, Who is already sick and -pale with grief, That thou her maid art far more fair than she: Be not her maid, -since she is envious; Her vestal livery is but sick and green And none but fools -do wear it; cast it off. It is my lady, O, it is my love! O, that she knew she -were! She speaks yet she says nothing: what of that? Her eye discourses; I will -answer it. - -I am too bold, 'tis not to me she speaks: Two of the fairest stars in all the -heaven, Having some business, do entreat her eyes To twinkle in their spheres -till they return. What if her eyes were there, they in her head? The brightness -of her cheek would shame those stars, As daylight doth a lamp; her eyes in -heaven Would through the airy region stream so bright That birds would sing and -think it were not night. See, how she leans her cheek upon her hand! O, that I -were a glove upon that hand, That I might touch that cheek! diff --git a/tests/typ/text/plain.typ b/tests/typ/text/plain.typ new file mode 100644 index 00000000..34a2d626 --- /dev/null +++ b/tests/typ/text/plain.typ @@ -0,0 +1,20 @@ +// Test plain text. + +--- +#set page(width: 250pt, height: 120pt) + +But, soft! what light through yonder window breaks? It is the east, and Juliet +is the sun. Arise, fair sun, and kill the envious moon, Who is already sick and +pale with grief, That thou her maid art far more fair than she: Be not her maid, +since she is envious; Her vestal livery is but sick and green And none but fools +do wear it; cast it off. It is my lady, O, it is my love! O, that she knew she +were! She speaks yet she says nothing: what of that? Her eye discourses; I will +answer it. + +I am too bold, 'tis not to me she speaks: Two of the fairest stars in all the +heaven, Having some business, do entreat her eyes To twinkle in their spheres +till they return. What if her eyes were there, they in her head? The brightness +of her cheek would shame those stars, As daylight doth a lamp; her eyes in +heaven Would through the airy region stream so bright That birds would sing and +think it were not night. See, how she leans her cheek upon her hand! O, that I +were a glove upon that hand, That I might touch that cheek! diff --git a/tests/typ/utility/basics.typ b/tests/typ/utility/basics.typ deleted file mode 100644 index 83d192c4..00000000 --- a/tests/typ/utility/basics.typ +++ /dev/null @@ -1,24 +0,0 @@ -// Test basic functions. -// Ref: false - ---- -// Test the `assert` function. -#assert(1 + 1 == 2) -#assert(range(2, 5) == (2, 3, 4)) -#assert(not false) - ---- -// Test failing assertions. -// Error: 9-15 assertion failed -#assert(1 == 2) - ---- -// Test failing assertions. -// Error: 9-15 expected boolean, found string -#assert("true") - ---- -// Test the `type` function. -#test(type(1), "integer") -#test(type(ltr), "direction") -#test(type(10 / 3), "float") diff --git a/tests/typ/utility/blind.typ b/tests/typ/utility/blind.typ deleted file mode 100644 index 17452dec..00000000 --- a/tests/typ/utility/blind.typ +++ /dev/null @@ -1,32 +0,0 @@ -// Test blind text. - ---- -// Test basic call. -#lorem(19) - ---- -// Test custom paragraphs with user code. -#set text(8pt) - -{ - let sentences = lorem(59) - .split(".") - .filter(s => s != "") - .map(s => s + ".") - - let used = 0 - for s in sentences { - if used < 2 { - used += 1 - } else { - parbreak() - used = 0 - } - s.trim() - [ ] - } -} - ---- -// Error: 7-9 missing argument: number of words -#lorem() diff --git a/tests/typ/utility/collection.typ b/tests/typ/utility/collection.typ deleted file mode 100644 index 46ff97ab..00000000 --- a/tests/typ/utility/collection.typ +++ /dev/null @@ -1,115 +0,0 @@ -// Test collection functions. -// Ref: false - ---- -// Test the `len` method. -#test(().len(), 0) -#test(("A", "B", "C").len(), 3) -#test("Hello World!".len(), 12) -#test((a: 1, b: 2).len(), 2) - ---- -// The the `first` and `last` methods. -#test(().first(), none) -#test(().last(), none) -#test((1,).first(), 1) -#test((2,).last(), 2) -#test((1, 2, 3).first(), 1) -#test((1, 2, 3).last(), 3) - ---- -// Test the `push` and `pop` methods. -{ - let tasks = (a: (1, 2, 3), b: (4, 5, 6)) - tasks("a").pop() - tasks("b").push(7) - test(tasks("a"), (1, 2)) - test(tasks("b"), (4, 5, 6, 7)) -} - ---- -// Test the `insert` and `remove` methods. -{ - let array = (0, 1, 2, 4, 5) - array.insert(3, 3) - test(array, range(6)) - array.remove(1) - test(array, (0, 2, 3, 4, 5)) -} - ---- -// Error: 2:17-2:19 missing argument: index -#let numbers = () -{ numbers.insert() } - ---- -// Test the `slice` method. -#test((1, 2, 3, 4).slice(2), (3, 4)) -#test(range(10).slice(2, 6), (2, 3, 4, 5)) -#test(range(10).slice(4, count: 3), (4, 5, 6)) -#test(range(10).slice(-5, count: 2), (5, 6)) -#test((1, 2, 3).slice(2, -2), ()) -#test((1, 2, 3).slice(-2, 2), (2,)) -#test((1, 2, 3).slice(-3, 2), (1, 2)) -#test("ABCD".split("").slice(1, -1).join("-"), "A-B-C-D") - ---- -// Error: 3-31 array index out of bounds (index: 12, len: 10) -{ range(10).slice(9, count: 3) } - ---- -// Error: 3-25 array index out of bounds (index: -4, len: 3) -{ (1, 2, 3).slice(0, -4) } - ---- -// Test the `position` method. -#test(("Hi", "❤️", "Love").position(s => s == "❤️"), 1) -#test(("Bye", "💘", "Apart").position(s => s == "❤️"), none) -#test(("A", "B", "CDEF", "G").position(v => v.len() > 2), 2) - ---- -// Test the `rev` method. -#test(range(3).rev(), (2, 1, 0)) - ---- -// Test the `join` method. -#test(().join(), none) -#test((1,).join(), 1) -#test(("a", "b", "c").join(), "abc") -#test("(" + ("a", "b", "c").join(", ") + ")", "(a, b, c)") - ---- -// Error: 2-22 cannot join boolean with boolean -{(true, false).join()} - ---- -// Error: 2-20 cannot join string with integer -{("a", "b").join(1)} - ---- -// Test joining content. -// Ref: true -{([One], [Two], [Three]).join([, ], last: [ and ])}. - ---- -// Test the `sorted` method. -#test(().sorted(), ()) -#test(((true, false) * 10).sorted(), (false,) * 10 + (true,) * 10) -#test(("it", "the", "hi", "text").sorted(), ("hi", "it", "text", "the")) -#test((2, 1, 3, 10, 5, 8, 6, -7, 2).sorted(), (-7, 1, 2, 2, 3, 5, 6, 8, 10)) - ---- -// Error: 2-26 cannot order content and content -{([Hi], [There]).sorted()} - ---- -// Test dictionary methods. -#let dict = (a: 3, c: 2, b: 1) -#test("c" in dict, true) -#test(dict.len(), 3) -#test(dict.values(), (3, 1, 2)) -#test(dict.pairs((k, v) => k + str(v)).join(), "a3b1c2") - -{ dict.remove("c") } -#test("c" in dict, false) -#test(dict, (a: 3, b: 1)) diff --git a/tests/typ/utility/color.typ b/tests/typ/utility/color.typ deleted file mode 100644 index 96d76063..00000000 --- a/tests/typ/utility/color.typ +++ /dev/null @@ -1,63 +0,0 @@ -// Test color creation functions. -// Ref: false - ---- -// Compare both ways. -#test(rgb(0%, 30%, 70%), rgb("004db3")) - -// Alpha channel. -#test(rgb(255, 0, 0, 50%), rgb("ff000080")) - -// Test color modification methods. -#test(rgb(25, 35, 45).lighten(10%), rgb(48, 57, 66)) -#test(rgb(40, 30, 20).darken(10%), rgb(36, 27, 18)) -#test(rgb("#133337").negate(), rgb(236, 204, 200)) -#test(white.lighten(100%), white) - ---- -// Test gray color conversion. -// Ref: true -#rect(fill: luma(0)) -#rect(fill: luma(80%)) - ---- -// Test gray color modification. -#test(luma(20%).lighten(50%), luma(60%)) -#test(luma(80%).darken(20%), luma(63.9%)) -#test(luma(80%).negate(), luma(20%)) - ---- -// Test CMYK color conversion. -// Ref: true -#let c = cmyk(50%, 64%, 16%, 17%) -#rect(width: 1cm, fill: cmyk(69%, 11%, 69%, 41%)) -#rect(width: 1cm, fill: c) -#rect(width: 1cm, fill: c.negate()) - -#for x in range(0, 11) { - square(width: 9pt, fill: c.lighten(x * 10%)) -} -#for x in range(0, 11) { - square(width: 9pt, fill: c.darken(x * 10%)) -} - ---- -// Error for values that are out of range. -// Error: 11-14 must be between 0 and 255 -#test(rgb(-30, 15, 50)) - ---- -// Error: 6-11 string contains non-hexadecimal letters -#rgb("lol") - ---- -// Error: 5-7 missing argument: red component -#rgb() - ---- -// Error: 5-11 missing argument: blue component -#rgb(0, 1) - ---- -// Error: 21-26 expected integer or ratio, found boolean -#rgb(10%, 20%, 30%, false) diff --git a/tests/typ/utility/data.typ b/tests/typ/utility/data.typ deleted file mode 100644 index e90c1b0d..00000000 --- a/tests/typ/utility/data.typ +++ /dev/null @@ -1,58 +0,0 @@ -// 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() -#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 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") - ---- -// Test reading XML data. -#let data = xml("/res/data.xml") -#test(data, (( - tag: "data", - attrs: (:), - children: ( - "\n ", - (tag: "hello", attrs: (name: "hi"), children: ("1",)), - "\n ", - ( - tag: "data", - attrs: (:), - children: ( - "\n ", - (tag: "hello", attrs: (:), children: ("World",)), - "\n ", - (tag: "hello", attrs: (:), children: ("World",)), - "\n ", - ), - ), - "\n", - ), -),)) - ---- -// Error: 6-20 failed to parse xml file: found closing tag 'data' instead of 'hello' in line 3 -#xml("/res/bad.xml") diff --git a/tests/typ/utility/eval.typ b/tests/typ/utility/eval.typ deleted file mode 100644 index 86b1f0c4..00000000 --- a/tests/typ/utility/eval.typ +++ /dev/null @@ -1,52 +0,0 @@ -// Test the `eval` function. - ---- -#eval("_Hello" + " World!_") - ---- -// Error: 7-13 expected identifier -#eval("#let") - ---- -#set raw(around: none) -#show it: raw as text("IBM Plex Sans", eval(it.text)) - -Interacting -``` -#set text(blue) -Blue #move(dy: -0.15em)[🌊] -``` - ---- -// Error: 7-19 cannot continue outside of loop -#eval("{continue}") - ---- -// Error: 7-33 cannot access file system from here -#eval("#include \"../coma.typ\"") - ---- -// Error: 7-35 cannot access file system from here -#eval("#image(\"/res/tiger.jpg\")") - ---- -// Error: 23-30 cannot access file system from here -#show it: raw as eval(it.text) - -``` -#show strong as image("/res/tiger.jpg") -*No absolute tiger!* -``` - ---- -// Error: 23-30 cannot access file system from here -#show it: raw as eval(it.text) - -``` -#show emph as image("../../res/giraffe.jpg") -_No relative giraffe!_ -``` - ---- -// Error: 7-16 expected comma -#eval("{(1 2)}") diff --git a/tests/typ/utility/math.typ b/tests/typ/utility/math.typ deleted file mode 100644 index 4ccefa22..00000000 --- a/tests/typ/utility/math.typ +++ /dev/null @@ -1,117 +0,0 @@ -// Test math functions. -// Ref: false - ---- -// Test conversion to numbers. -#test(int(false), 0) -#test(int(true), 1) -#test(int(10), 10) -#test(int("150"), 150) -#test(int(10 / 3), 3) -#test(float(10), 10.0) -#test(float("31.4e-1"), 3.14) -#test(type(float(10)), "float") - ---- -// Error: 6-10 cannot convert length to integer -#int(10pt) - ---- -// Error: 8-13 cannot convert function to float -#float(float) - ---- -// Error: 6-12 invalid integer -#int("nope") - ---- -// Error: 8-15 invalid float -#float("1.2.3") - ---- -// Test the `abs` function. -#test(abs(-3), 3) -#test(abs(3), 3) -#test(abs(-0.0), 0.0) -#test(abs(0.0), -0.0) -#test(abs(-3.14), 3.14) -#test(abs(50%), 50%) -#test(abs(-25%), 25%) - ---- -// Error: 6-17 expected numeric value, found string -#abs("no number") - ---- -// Error: 6-11 cannot take absolute value of a length -#abs(-12pt) - ---- -// Error: 6-16 cannot take absolute value of a length -#abs(50% - 12pt) - ---- -// Test the `even` and `odd` functions. -#test(even(2), true) -#test(odd(2), false) -#test(odd(-1), true) -#test(even(-11), false) - ---- -// Test the `mod` function. -#test(mod(1, 1), 0) -#test(mod(5, 3), 2) -#test(mod(5, -3), 2) -#test(mod(22.5, 10), 2.5) -#test(mod(9, 4.5), 0) - ---- -// Error: 9-10 divisor must not be zero -#mod(5, 0) - ---- -// Error: 11-14 divisor must not be zero -#mod(3.0, 0.0) - ---- -// Test the `min` and `max` functions. -#test(min(2, -4), -4) -#test(min(3.5, 1e2, -0.1, 3), -0.1) -#test(max(-3, 11), 11) -#test(min("hi"), "hi") - ---- -// Error: 5-7 missing argument: value -#min() - ---- -// Error: 9-13 cannot compare integer and string -#min(1, "hi") - ---- -// Test the `range` function. -#test(range(4), (0, 1, 2, 3)) -#test(range(1, 4), (1, 2, 3)) -#test(range(-4, 2), (-4, -3, -2, -1, 0, 1)) -#test(range(10, 5), ()) -#test(range(10, step: 3), (0, 3, 6, 9)) -#test(range(1, 4, step: 1), (1, 2, 3)) -#test(range(1, 8, step: 2), (1, 3, 5, 7)) -#test(range(5, 2, step: -1), (5, 4, 3)) -#test(range(10, 0, step: -3), (10, 7, 4, 1)) - ---- -// Error: 7-9 missing argument: end -#range() - ---- -// Error: 11-14 expected integer, found float -#range(1, 2.0) - ---- -// Error: 17-22 expected integer, found string -#range(4, step: "one") - ---- -// Error: 18-19 step must not be zero -#range(10, step: 0) diff --git a/tests/typ/utility/string.typ b/tests/typ/utility/string.typ deleted file mode 100644 index 3104a3ea..00000000 --- a/tests/typ/utility/string.typ +++ /dev/null @@ -1,161 +0,0 @@ -// Test string related methods. -// Ref: false - ---- -// Test conversion to string. -#test(str(123), "123") -#test(str(50.14), "50.14") -#test(str(10 / 3).len() > 10, true) -#test(repr(ltr), "ltr") -#test(repr((1, 2, false, )), "(1, 2, false)") - ---- -// Error: 6-8 cannot convert content to string -#str([]) - ---- -// Test the `slice` method. -#test("abc".slice(1, 2), "b") -#test("abc🏡def".slice(2, 7), "c🏡") -#test("abc🏡def".slice(2, -2), "c🏡d") -#test("abc🏡def".slice(-3, -1), "de") - ---- -// Test the `contains` method. -#test("abc".contains("b"), true) -#test("b" in "abc", true) -#test("1234f".contains(regex("\d")), true) -#test(regex("\d") in "1234f", true) -#test("abc".contains("d"), false) -#test("1234g" in "1234f", false) -#test("abc".contains(regex("^[abc]$")), false) -#test("abc".contains(regex("^[abc]+$")), true) - ---- -// Test the `starts-with` and `ends-with` methods. -#test("Typst".starts-with("Ty"), true) -#test("Typst".starts-with(regex("[Tt]ys")), false) -#test("Typst".starts-with("st"), false) -#test("Typst".ends-with("st"), true) -#test("Typst".ends-with(regex("\d*")), true) -#test("Typst".ends-with(regex("\d+")), false) -#test("Typ12".ends-with(regex("\d+")), true) - ---- -// Test the `find` and `position` methods. -#let date = regex("\d{2}:\d{2}") -#test("Hello World".find("World"), "World") -#test("Hello World".position("World"), 6) -#test("It's 12:13 now".find(date), "12:13") -#test("It's 12:13 now".position(date), 5) - ---- -// Test the `match` method. -#test("Is there a".match("for this?"), none) -#test( - "The time of my life.".match(regex("[mit]+e")), - (start: 4, end: 8, text: "time", captures: ()), -) - -// Test the `matches` method. -#test("Hello there".matches("\d"), ()) -#test("Day by Day.".matches("Day"), ( - (start: 0, end: 3, text: "Day", captures: ()), - (start: 7, end: 10, text: "Day", captures: ()), -)) - -// Compute the sum of all timestamps in the text. -#let timesum(text) = { - let time = 0 - for match in text.matches(regex("(\d+):(\d+)")) { - let caps = match.captures - time += 60 * int(caps(0)) + int(caps(1)) - } - str(int(time / 60)) + ":" + str(mod(time, 60)) -} - -#test(timesum(""), "0:0") -#test(timesum("2:70"), "3:10") -#test(timesum("1:20, 2:10, 0:40"), "4:10") - ---- -// Test the `replace` method. -#test("ABC".replace("", "-"), "-A-B-C-") -#test("Ok".replace("Ok", "Nope", count: 0), "Ok") -#test("to add?".replace("", "How ", count: 1), "How to add?") -#test("AB C DEF GH J".replace(" ", ",", count: 2), "AB,C,DEF GH J") -#test("Walcemo" - .replace("o", "k") - .replace("e", "o") - .replace("k", "e") - .replace("a", "e"), - "Welcome" -) -#test("123".replace(regex("\d$"), "_"), "12_") -#test("123".replace(regex("\d{1,2}$"), "__"), "1__") - ---- -// Test the `trim` method. -#let str = "Typst, LaTeX, Word, InDesign" -#let array = ("Typst", "LaTeX", "Word", "InDesign") -#test(str.split(",").map(s => s.trim()), array) -#test("".trim(), "") -#test(" abc ".trim(at: start), "abc ") -#test(" abc ".trim(at: end, repeat: true), " abc") -#test(" abc".trim(at: start, repeat: false), "abc") -#test("aabcaa".trim("a", repeat: false), "abca") -#test("aabca".trim("a", at: start), "bca") -#test("aabcaa".trim("a", at: end, repeat: false), "aabca") -#test("".trim(regex(".")), "") -#test("123abc456".trim(regex("\d")), "abc") -#test("123abc456".trim(regex("\d"), repeat: false), "23abc45") -#test("123a4b5c678".trim(regex("\d"), repeat: true), "a4b5c") -#test("123a4b5c678".trim(regex("\d"), repeat: false), "23a4b5c67") -#test("123abc456".trim(regex("\d"), at: start), "abc456") -#test("123abc456".trim(regex("\d"), at: end), "123abc") -#test("123abc456".trim(regex("\d+"), at: end, repeat: false), "123abc") -#test("123abc456".trim(regex("\d{1,2}$"), repeat: false), "123abc4") -#test("hello world".trim(regex(".")), "") - ---- -// Error: 17-21 expected either `start` or `end` -{"abc".trim(at: left)} - ---- -// Test the `split` method. -#test("abc".split(""), ("", "a", "b", "c", "")) -#test("abc".split("b"), ("a", "c")) -#test("a123c".split(regex("\d")), ("a", "", "", "c")) -#test("a123c".split(regex("\d+")), ("a", "c")) - ---- -// Test the `upper` and `lower` functions. -#let memes = "ArE mEmEs gReAt?"; -#test(lower(memes), "are memes great?") -#test(upper(memes), "ARE MEMES GREAT?") -#test(upper("Ελλάδα"), "ΕΛΛΆΔΑ") - ---- -// Test integrated lower, upper and symbols. -// Ref: true - -#upper("Abc 8") -#upper[def] - -#lower("SCREAMING MUST BE SILENCED in " + roman(1672) + " years") - -#for i in range(9) { - symbol(i) - [ and ] - roman(i) - [ for #i] - parbreak() -} - ---- -// Error: 8-9 expected string or content, found integer -#upper(1) - ---- -// Error: 9-11 must be at least zero -#symbol(-1) -- cgit v1.2.3