summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/compute/construct.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-07-02 19:59:52 +0200
committerLaurenz <laurmaedje@gmail.com>2023-07-02 20:07:43 +0200
commitebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch)
tree2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-library/src/compute/construct.rs
parent3ab19185093d7709f824b95b979060ce125389d8 (diff)
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-library/src/compute/construct.rs')
-rw-r--r--crates/typst-library/src/compute/construct.rs743
1 files changed, 743 insertions, 0 deletions
diff --git a/crates/typst-library/src/compute/construct.rs b/crates/typst-library/src/compute/construct.rs
new file mode 100644
index 00000000..956212ee
--- /dev/null
+++ b/crates/typst-library/src/compute/construct.rs
@@ -0,0 +1,743 @@
+use std::num::NonZeroI64;
+use std::str::FromStr;
+
+use time::{Month, PrimitiveDateTime};
+
+use typst::eval::{Datetime, Regex};
+
+use crate::prelude::*;
+
+/// Converts 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
+#[func]
+pub fn int(
+ /// The value that should be converted to an integer.
+ value: ToInt,
+) -> i64 {
+ value.0
+}
+
+/// A value that can be cast to an integer.
+pub struct ToInt(i64);
+
+cast! {
+ 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))?),
+}
+
+/// Converts 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
+#[func]
+pub fn float(
+ /// The value that should be converted to a float.
+ value: ToFloat,
+) -> f64 {
+ value.0
+}
+
+/// A value that can be cast to a float.
+pub struct ToFloat(f64);
+
+cast! {
+ 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))?),
+}
+
+/// Creates a grayscale color.
+///
+/// ## Example { #example }
+/// ```example
+/// #for x in range(250, step: 50) {
+/// box(square(fill: luma(x)))
+/// }
+/// ```
+///
+/// Display: Luma
+/// Category: construct
+#[func]
+pub fn luma(
+ /// The gray component.
+ gray: Component,
+) -> Color {
+ LumaColor::new(gray.0).into()
+}
+
+/// Creates 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
+#[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,
+ /// The arguments.
+ args: Args,
+) -> SourceResult<Color> {
+ let mut args = args;
+ Ok(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.
+pub struct Component(u8);
+
+cast! {
+ Component,
+ v: i64 => match v {
+ 0 ..= 255 => Self(v as u8),
+ _ => bail!("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 {
+ bail!("ratio must be between 0% and 100%");
+ },
+}
+
+/// Creates a new datetime.
+///
+/// You can specify the [datetime]($type/datetime) using a year, month, day,
+/// hour, minute, and second. You can also get the current date with
+/// [`datetime.today`]($func/datetime.today).
+///
+/// ## Example
+/// ```example
+/// #let date = datetime(
+/// year: 2012,
+/// month: 8,
+/// day: 3,
+/// )
+///
+/// #date.display() \
+/// #date.display(
+/// "[day].[month].[year]"
+/// )
+/// ```
+///
+/// ## 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
+#[func]
+#[scope(
+ scope.define("today", datetime_today_func());
+ 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>,
+) -> StrResult<Datetime> {
+ 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!("time is invalid"),
+ }
+ }
+ (None, None, None) => None,
+ _ => bail!("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!("date is invalid"),
+ }
+ }
+ (None, None, None) => None,
+ _ => bail!("date is incomplete"),
+ };
+
+ Ok(match (date, time) {
+ (Some(date), Some(time)) => {
+ Datetime::Datetime(PrimitiveDateTime::new(date, time))
+ }
+ (Some(date), None) => Datetime::Date(date),
+ (None, Some(time)) => Datetime::Time(time),
+ (None, None) => {
+ bail!("at least one of date or time must be fully specified")
+ }
+ })
+}
+
+pub struct YearComponent(i32);
+pub struct MonthComponent(Month);
+pub struct DayComponent(u8);
+pub struct HourComponent(u8);
+pub struct MinuteComponent(u8);
+pub struct SecondComponent(u8);
+
+cast! {
+ YearComponent,
+ v: i32 => Self(v),
+}
+
+cast! {
+ MonthComponent,
+ v: u8 => Self(Month::try_from(v).map_err(|_| "month is invalid")?)
+}
+
+cast! {
+ DayComponent,
+ v: u8 => Self(v),
+}
+
+cast! {
+ HourComponent,
+ v: u8 => Self(v),
+}
+
+cast! {
+ MinuteComponent,
+ v: u8 => Self(v),
+}
+
+cast! {
+ SecondComponent,
+ v: u8 => Self(v),
+}
+
+/// Returns the current date.
+///
+/// Refer to the documentation of the [`display`]($type/datetime.display) method
+/// for details on how to affect the formatting of the date.
+///
+/// ## Example
+/// ```example
+/// Today's date is
+/// #datetime.today().display().
+/// ```
+///
+/// Display: Today
+/// Category: construct
+#[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>,
+ /// The virtual machine.
+ vt: &mut Vt,
+) -> StrResult<Datetime> {
+ Ok(vt
+ .world
+ .today(offset.as_custom())
+ .ok_or("unable to get the current date")?)
+}
+
+/// Creates 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
+#[func]
+pub fn cmyk(
+ /// The cyan component.
+ cyan: RatioComponent,
+ /// The magenta component.
+ magenta: RatioComponent,
+ /// The yellow component.
+ yellow: RatioComponent,
+ /// The key component.
+ key: RatioComponent,
+) -> Color {
+ CmykColor::new(cyan.0, magenta.0, yellow.0, key.0).into()
+}
+
+/// A component that must be a ratio.
+pub struct RatioComponent(u8);
+
+cast! {
+ RatioComponent,
+ v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
+ Self((v.get() * 255.0).round() as u8)
+ } else {
+ bail!("ratio must be between 0% and 100%");
+ },
+}
+
+/// Creates 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
+#[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>>,
+ /// The callsite span.
+ span: Span,
+) -> SourceResult<Symbol> {
+ let mut list = Vec::new();
+ if variants.is_empty() {
+ bail!(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));
+ }
+ Ok(Symbol::runtime(list.into_boxed_slice()))
+}
+
+/// A value that can be cast to a symbol.
+pub struct Variant(EcoString, char);
+
+cast! {
+ 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()?),
+ _ => bail!("point array must contain exactly two entries"),
+ }
+ },
+}
+
+/// Converts 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.
+///
+/// If you wish to convert from and to Unicode code points, see
+/// [`str.to-unicode`]($func/str.to-unicode) and
+/// [`str.from-unicode`]($func/str.from-unicode).
+///
+/// ## Example { #example }
+/// ```example
+/// #str(10) \
+/// #str(4000, base: 16) \
+/// #str(2.7) \
+/// #str(1e8) \
+/// #str(<intro>)
+/// ```
+///
+/// Display: String
+/// Category: construct
+#[func]
+#[scope(
+ scope.define("to-unicode", str_to_unicode_func());
+ scope.define("from-unicode", str_from_unicode_func());
+ scope
+)]
+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>,
+) -> SourceResult<Str> {
+ Ok(match value {
+ ToStr::Str(s) => {
+ if base.v != 10 {
+ bail!(base.span, "base is only supported for integers");
+ }
+ 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.
+pub 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! {
+ 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()
+}
+
+/// Converts a character into its corresponding code point.
+///
+/// ## Example
+/// ```example
+/// #str.to-unicode("a") \
+/// #"a\u{0300}".codepoints().map(str.to-unicode)
+/// ```
+///
+/// Display: String To Unicode
+/// Category: construct
+#[func]
+pub fn str_to_unicode(
+ /// The character that should be converted.
+ value: char,
+) -> u32 {
+ value.into()
+}
+
+/// Converts a Unicode code point into its corresponding string.
+///
+/// ```example
+/// #str.from-unicode(97)
+/// ```
+///
+/// Display: String From Unicode
+/// Category: construct
+#[func]
+pub fn str_from_unicode(
+ /// The code point that should be converted.
+ value: CodePoint,
+) -> Str {
+ format_str!("{}", value.0)
+}
+
+/// The numeric representation of a single unicode code point.
+pub struct CodePoint(char);
+
+cast! {
+ CodePoint,
+ v: i64 => {
+ Self(v.try_into().ok().and_then(|v: u32| v.try_into().ok()).ok_or_else(
+ || eco_format!("{:#x} is not a valid codepoint", v),
+ )?)
+ },
+}
+
+/// Creates 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
+#[func]
+pub fn label(
+ /// The name of the label.
+ name: EcoString,
+) -> Label {
+ Label(name)
+}
+
+/// Creates 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 [the specification of the supported syntax](https://docs.rs/regex/latest/regex/#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
+#[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>,
+) -> SourceResult<Regex> {
+ Regex::new(&regex.v).at(regex.span)
+}
+
+/// Creates an array consisting of consecutive integers.
+///
+/// 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
+#[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,
+ /// The arguments.
+ args: Args,
+) -> SourceResult<Array> {
+ let mut args = args;
+ 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;
+ }
+
+ Ok(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");
+ }
+}