typst/library/src/compute/construct.rs
2023-05-30 18:05:15 +02:00

703 lines
18 KiB
Rust

use std::num::NonZeroI64;
use std::str::FromStr;
use time::{Month, PrimitiveDateTime};
use typst::eval::{Datetime, Dynamic, Regex};
use crate::prelude::*;
/// Convert a value to an integer.
///
/// - Booleans are converted to `0` or `1`.
/// - Floats are floored to the next 64-bit integer.
/// - Strings are parsed in base 10.
///
/// ## Example { #example }
/// ```example
/// #int(false) \
/// #int(true) \
/// #int(2.7) \
/// #{ int("27") + int("4") }
/// ```
///
/// Display: Integer
/// Category: construct
/// Returns: integer
#[func]
pub fn int(
/// The value that should be converted to an integer.
value: ToInt,
) -> Value {
Value::Int(value.0)
}
/// A value that can be cast to an integer.
struct ToInt(i64);
cast_from_value! {
ToInt,
v: bool => Self(v as i64),
v: i64 => Self(v),
v: f64 => Self(v as i64),
v: EcoString => Self(v.parse().map_err(|_| eco_format!("invalid integer: {}", v))?),
}
/// Convert a value to a float.
///
/// - Booleans are converted to `0.0` or `1.0`.
/// - Integers are converted to the closest 64-bit float.
/// - Ratios are divided by 100%.
/// - Strings are parsed in base 10 to the closest 64-bit float.
/// Exponential notation is supported.
///
/// ## Example { #example }
/// ```example
/// #float(false) \
/// #float(true) \
/// #float(4) \
/// #float(40%) \
/// #float("2.7") \
/// #float("1e5")
/// ```
///
/// Display: Float
/// Category: construct
/// Returns: float
#[func]
pub fn float(
/// The value that should be converted to a float.
value: ToFloat,
) -> Value {
Value::Float(value.0)
}
/// A value that can be cast to a float.
struct ToFloat(f64);
cast_from_value! {
ToFloat,
v: bool => Self(v as i64 as f64),
v: i64 => Self(v as f64),
v: f64 => Self(v),
v: Ratio => Self(v.get()),
v: EcoString => Self(v.parse().map_err(|_| eco_format!("invalid float: {}", v))?),
}
/// Create a grayscale color.
///
/// ## Example { #example }
/// ```example
/// #for x in range(250, step: 50) {
/// box(square(fill: luma(x)))
/// }
/// ```
///
/// Display: Luma
/// Category: construct
/// Returns: color
#[func]
pub fn luma(
/// The gray component.
gray: Component,
) -> Value {
Value::Color(LumaColor::new(gray.0).into())
}
/// Create an RGB(A) color.
///
/// The color is specified in the sRGB color space.
///
/// _Note:_ While you can specify transparent colors and Typst's preview will
/// render them correctly, the PDF export does not handle them properly at the
/// moment. This will be fixed in the future.
///
/// ## Example { #example }
/// ```example
/// #square(fill: rgb("#b1f2eb"))
/// #square(fill: rgb(87, 127, 230))
/// #square(fill: rgb(25%, 13%, 65%))
/// ```
///
/// Display: RGB
/// Category: construct
/// Returns: color
#[func]
pub fn rgb(
/// The color in hexadecimal notation.
///
/// Accepts three, four, six or eight hexadecimal digits and optionally
/// a leading hashtag.
///
/// If this string is given, the individual components should not be given.
///
/// ```example
/// #text(16pt, rgb("#239dad"))[
/// *Typst*
/// ]
/// ```
#[external]
hex: EcoString,
/// The red component.
#[external]
red: Component,
/// The green component.
#[external]
green: Component,
/// The blue component.
#[external]
blue: Component,
/// The alpha component.
#[external]
alpha: Component,
) -> Value {
Value::Color(if let Some(string) = args.find::<Spanned<EcoString>>()? {
match RgbaColor::from_str(&string.v) {
Ok(color) => color.into(),
Err(msg) => bail!(string.span, msg),
}
} else {
let Component(r) = args.expect("red component")?;
let Component(g) = args.expect("green component")?;
let Component(b) = args.expect("blue component")?;
let Component(a) = args.eat()?.unwrap_or(Component(255));
RgbaColor::new(r, g, b, a).into()
})
}
/// An integer or ratio component.
struct Component(u8);
cast_from_value! {
Component,
v: i64 => match v {
0 ..= 255 => Self(v as u8),
_ => Err("number must be between 0 and 255")?,
},
v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
Self((v.get() * 255.0).round() as u8)
} else {
Err("ratio must be between 0% and 100%")?
},
}
/// Create a new datetime.
///
/// You can specify the [datetime]($type/datetime) using a year, month, day,
/// hour, minute, and second.
///
/// ## Example
/// ```example
/// #datetime(
/// year: 2012,
/// month: 8,
/// day: 3,
/// ).display()
/// ```
///
/// ## Format
/// _Note_: Depending on which components of the datetime you specify, Typst
/// will store it in one of the following three ways:
/// * If you specify year, month and day, Typst will store just a date.
/// * If you specify hour, minute and second, Typst will store just a time.
/// * If you specify all of year, month, day, hour, minute and second, Typst
/// will store a full datetime.
///
/// Depending on how it is stored, the [`display`]($type/datetime.display)
/// method will choose a different formatting by default.
///
/// Display: Datetime
/// Category: construct
/// Returns: datetime
#[func]
#[scope(
scope.define("today", datetime_today);
scope
)]
pub fn datetime(
/// The year of the datetime.
#[named]
year: Option<YearComponent>,
/// The month of the datetime.
#[named]
month: Option<MonthComponent>,
/// The day of the datetime.
#[named]
day: Option<DayComponent>,
/// The hour of the datetime.
#[named]
hour: Option<HourComponent>,
/// The minute of the datetime.
#[named]
minute: Option<MinuteComponent>,
/// The second of the datetime.
#[named]
second: Option<SecondComponent>,
) -> Value {
let time = match (hour, minute, second) {
(Some(hour), Some(minute), Some(second)) => {
match time::Time::from_hms(hour.0, minute.0, second.0) {
Ok(time) => Some(time),
Err(_) => bail!(args.span, "time is invalid"),
}
}
(None, None, None) => None,
_ => bail!(args.span, "time is incomplete"),
};
let date = match (year, month, day) {
(Some(year), Some(month), Some(day)) => {
match time::Date::from_calendar_date(year.0, month.0, day.0) {
Ok(date) => Some(date),
Err(_) => bail!(args.span, "date is invalid"),
}
}
(None, None, None) => None,
_ => bail!(args.span, "date is incomplete"),
};
match (date, time) {
(Some(date), Some(time)) => Value::Dyn(Dynamic::new(Datetime::Datetime(
PrimitiveDateTime::new(date, time),
))),
(Some(date), None) => Value::Dyn(Dynamic::new(Datetime::Date(date))),
(None, Some(time)) => Value::Dyn(Dynamic::new(Datetime::Time(time))),
(None, None) => {
bail!(args.span, "at least one of date or time must be fully specified")
}
}
}
struct YearComponent(i32);
struct MonthComponent(Month);
struct DayComponent(u8);
struct HourComponent(u8);
struct MinuteComponent(u8);
struct SecondComponent(u8);
cast_from_value!(
YearComponent,
v: i64 => match i32::try_from(v) {
Ok(n) => Self(n),
_ => Err("year is invalid")?
}
);
cast_from_value!(
MonthComponent,
v: i64 => match u8::try_from(v).ok().and_then(|n1| Month::try_from(n1).ok()).map(Self) {
Some(m) => m,
_ => Err("month is invalid")?
}
);
cast_from_value!(
DayComponent,
v: i64 => match u8::try_from(v) {
Ok(n) => Self(n),
_ => Err("day is invalid")?
}
);
cast_from_value!(
HourComponent,
v: i64 => match u8::try_from(v) {
Ok(n) => Self(n),
_ => Err("hour is invalid")?
}
);
cast_from_value!(
MinuteComponent,
v: i64 => match u8::try_from(v) {
Ok(n) => Self(n),
_ => Err("minute is invalid")?
}
);
cast_from_value!(
SecondComponent,
v: i64 => match u8::try_from(v) {
Ok(n) => Self(n),
_ => Err("second is invalid")?
}
);
/// Returns the current date.
///
/// ## Example
/// ```example
/// Today's date is
/// #datetime.today().display().
/// ```
///
/// Display: Today
/// Category: construct
/// Returns: datetime
#[func]
pub fn datetime_today(
/// An offset to apply to the current UTC date. If set to `{auto}`, the
/// offset will be the local offset.
#[named]
#[default]
offset: Smart<i64>,
) -> Value {
let current_date = match vm.vt.world.today(offset.as_custom()) {
Some(d) => d,
None => bail!(args.span, "unable to get the current date"),
};
Value::Dyn(Dynamic::new(current_date))
}
/// Create a CMYK color.
///
/// This is useful if you want to target a specific printer. The conversion
/// to RGB for display preview might differ from how your printer reproduces
/// the color.
///
/// ## Example { #example }
/// ```example
/// #square(
/// fill: cmyk(27%, 0%, 3%, 5%)
/// )
/// ````
///
/// Display: CMYK
/// Category: construct
/// Returns: color
#[func]
pub fn cmyk(
/// The cyan component.
cyan: RatioComponent,
/// The magenta component.
magenta: RatioComponent,
/// The yellow component.
yellow: RatioComponent,
/// The key component.
key: RatioComponent,
) -> Value {
Value::Color(CmykColor::new(cyan.0, magenta.0, yellow.0, key.0).into())
}
/// A component that must be a ratio.
struct RatioComponent(u8);
cast_from_value! {
RatioComponent,
v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
Self((v.get() * 255.0).round() as u8)
} else {
Err("ratio must be between 0% and 100%")?
},
}
/// Create a custom symbol with modifiers.
///
/// ## Example { #example }
/// ```example
/// #let envelope = symbol(
/// "🖂",
/// ("stamped", "🖃"),
/// ("stamped.pen", "🖆"),
/// ("lightning", "🖄"),
/// ("fly", "🖅"),
/// )
///
/// #envelope
/// #envelope.stamped
/// #envelope.stamped.pen
/// #envelope.lightning
/// #envelope.fly
/// ```
///
/// Display: Symbol
/// Category: construct
/// Returns: symbol
#[func]
pub fn symbol(
/// The variants of the symbol.
///
/// Can be a just a string consisting of a single character for the
/// modifierless variant or an array with two strings specifying the modifiers
/// and the symbol. Individual modifiers should be separated by dots. When
/// displaying a symbol, Typst selects the first from the variants that have
/// all attached modifiers and the minimum number of other modifiers.
#[variadic]
variants: Vec<Spanned<Variant>>,
) -> Value {
let mut list = Vec::new();
if variants.is_empty() {
bail!(args.span, "expected at least one variant");
}
for Spanned { v, span } in variants {
if list.iter().any(|(prev, _)| &v.0 == prev) {
bail!(span, "duplicate variant");
}
list.push((v.0, v.1));
}
Value::Symbol(Symbol::runtime(list.into_boxed_slice()))
}
/// A value that can be cast to a symbol.
struct Variant(EcoString, char);
cast_from_value! {
Variant,
c: char => Self(EcoString::new(), c),
array: Array => {
let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => Self(a.cast()?, b.cast()?),
_ => Err("point array must contain exactly two entries")?,
}
},
}
/// Convert a value to a string.
///
/// - Integers are formatted in base 10. This can be overridden with the
/// optional `base` parameter.
/// - Floats are formatted in base 10 and never in exponential notation.
/// - From labels the name is extracted.
///
/// ## Example { #example }
/// ```example
/// #str(10) \
/// #str(4000, base: 16) \
/// #str(2.7) \
/// #str(1e8) \
/// #str(<intro>)
/// ```
///
/// Display: String
/// Category: construct
/// Returns: string
#[func]
pub fn str(
/// The value that should be converted to a string.
value: ToStr,
/// The base (radix) to display integers in, between 2 and 36.
#[named]
#[default(Spanned::new(10, Span::detached()))]
base: Spanned<i64>,
) -> Value {
match value {
ToStr::Str(s) => {
if base.v != 10 {
bail!(base.span, "base is only supported for integers");
}
Value::Str(s)
}
ToStr::Int(n) => {
if base.v < 2 || base.v > 36 {
bail!(base.span, "base must be between 2 and 36");
}
int_to_base(n, base.v).into()
}
}
}
/// A value that can be cast to a string.
enum ToStr {
/// A string value ready to be used as-is.
Str(Str),
/// An integer about to be formatted in a given base.
Int(i64),
}
cast_from_value! {
ToStr,
v: i64 => Self::Int(v),
v: f64 => Self::Str(format_str!("{}", v)),
v: Label => Self::Str(v.0.into()),
v: Str => Self::Str(v),
}
/// Format an integer in a base.
fn int_to_base(mut n: i64, base: i64) -> EcoString {
if n == 0 {
return "0".into();
}
// In Rust, `format!("{:x}", -14i64)` is not `-e` but `fffffffffffffff2`.
// So we can only use the built-in for decimal, not bin/oct/hex.
if base == 10 {
return eco_format!("{n}");
}
// The largest output is `to_base(i64::MIN, 2)`, which is 65 chars long.
const SIZE: usize = 65;
let mut digits = [b'\0'; SIZE];
let mut i = SIZE;
// It's tempting to take the absolute value, but this will fail for i64::MIN.
// Instead, we turn n negative, as -i64::MAX is perfectly representable.
let negative = n < 0;
if n > 0 {
n = -n;
}
while n != 0 {
let digit = char::from_digit(-(n % base) as u32, base as u32);
i -= 1;
digits[i] = digit.unwrap_or('?') as u8;
n /= base;
}
if negative {
i -= 1;
digits[i] = b'-';
}
std::str::from_utf8(&digits[i..]).unwrap_or_default().into()
}
/// Create a label from a string.
///
/// Inserting a label into content attaches it to the closest previous element
/// that is not a space. Then, the element can be [referenced]($func/ref) and
/// styled through the label.
///
/// ## Example { #example }
/// ```example
/// #show <a>: set text(blue)
/// #show label("b"): set text(red)
///
/// = Heading <a>
/// *Strong* #label("b")
/// ```
///
/// ## Syntax { #syntax }
/// This function also has dedicated syntax: You can create a label by enclosing
/// its name in angle brackets. This works both in markup and code.
///
/// Display: Label
/// Category: construct
/// Returns: label
#[func]
pub fn label(
/// The name of the label.
name: EcoString,
) -> Value {
Value::Label(Label(name))
}
/// Create a regular expression from a string.
///
/// The result can be used as a
/// [show rule selector]($styling/#show-rules) and with
/// [string methods]($type/string) like `find`, `split`, and `replace`.
///
/// [See here](https://docs.rs/regex/latest/regex/#syntax) for a specification
/// of the supported syntax.
///
/// ## Example { #example }
/// ```example
/// // Works with show rules.
/// #show regex("\d+"): set text(red)
///
/// The numbers 1 to 10.
///
/// // Works with string methods.
/// #("a,b;c"
/// .split(regex("[,;]")))
/// ```
///
/// Display: Regex
/// Category: construct
/// Returns: regex
#[func]
pub fn regex(
/// The regular expression as a string.
///
/// Most regex escape sequences just work because they are not valid Typst
/// escape sequences. To produce regex escape sequences that are also valid in
/// Typst (e.g. `[\\]`), you need to escape twice. Thus, to match a verbatim
/// backslash, you would need to write `{regex("\\\\")}`.
///
/// If you need many escape sequences, you can also create a raw element
/// and extract its text to use it for your regular expressions:
/// ```{regex(`\d+\.\d+\.\d+`.text)}```.
regex: Spanned<EcoString>,
) -> Value {
Regex::new(&regex.v).at(regex.span)?.into()
}
/// Create an array consisting of a sequence of numbers.
///
/// If you pass just one positional parameter, it is interpreted as the `end` of
/// the range. If you pass two, they describe the `start` and `end` of the
/// range.
///
/// ## Example { #example }
/// ```example
/// #range(5) \
/// #range(2, 5) \
/// #range(20, step: 4) \
/// #range(21, step: 4) \
/// #range(5, 2, step: -1)
/// ```
///
/// Display: Range
/// Category: construct
/// Returns: array
#[func]
pub fn range(
/// The start of the range (inclusive).
#[external]
#[default]
start: i64,
/// The end of the range (exclusive).
#[external]
end: i64,
/// The distance between the generated numbers.
#[named]
#[default(NonZeroI64::new(1).unwrap())]
step: NonZeroI64,
) -> Value {
let first = args.expect::<i64>("end")?;
let (start, end) = match args.eat::<i64>()? {
Some(second) => (first, second),
None => (0, first),
};
let step = step.get();
let mut x = start;
let mut array = Array::new();
while x.cmp(&end) == 0.cmp(&step) {
array.push(Value::Int(x));
x += step;
}
Value::Array(array)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_base() {
assert_eq!(&int_to_base(0, 10), "0");
assert_eq!(&int_to_base(0, 16), "0");
assert_eq!(&int_to_base(0, 36), "0");
assert_eq!(
&int_to_base(i64::MAX, 2),
"111111111111111111111111111111111111111111111111111111111111111"
);
assert_eq!(
&int_to_base(i64::MIN, 2),
"-1000000000000000000000000000000000000000000000000000000000000000"
);
assert_eq!(&int_to_base(i64::MAX, 10), "9223372036854775807");
assert_eq!(&int_to_base(i64::MIN, 10), "-9223372036854775808");
assert_eq!(&int_to_base(i64::MAX, 16), "7fffffffffffffff");
assert_eq!(&int_to_base(i64::MIN, 16), "-8000000000000000");
assert_eq!(&int_to_base(i64::MAX, 36), "1y2p0ij32e8e7");
assert_eq!(&int_to_base(i64::MIN, 36), "-1y2p0ij32e8e8");
}
}