diff options
Diffstat (limited to 'src')
42 files changed, 1766 insertions, 1589 deletions
diff --git a/src/diag.rs b/src/diag.rs index a122fe1f..88924310 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -8,44 +8,53 @@ use std::str::Utf8Error; use std::string::FromUtf8Error; use comemo::Tracked; -use ecow::EcoString; use crate::syntax::{ErrorPos, Span, Spanned}; use crate::World; -/// Early-return with a [`SourceError`]. +/// Early-return with a [`StrError`] or [`SourceError`]. #[macro_export] #[doc(hidden)] macro_rules! __bail { + ($fmt:literal $(, $arg:expr)* $(,)?) => { + return Err($crate::diag::eco_format!($fmt, $($arg),*)) + }; + ($error:expr) => { return Err(Box::new(vec![$error])) }; - ($($tts:tt)*) => { - $crate::diag::bail!($crate::diag::error!($($tts)*)) + ($span:expr, $fmt:literal $(, $arg:expr)* $(,)?) => { + return Err(Box::new(vec![$crate::diag::SourceError::new( + $span, + $crate::diag::eco_format!($fmt, $($arg),*), + )])) }; } #[doc(inline)] pub use crate::__bail as bail; -/// Construct a [`SourceError`]. +/// Construct a [`StrError`] or [`SourceError`]. #[macro_export] #[doc(hidden)] macro_rules! __error { - ($span:expr, $message:expr $(,)?) => { - $crate::diag::SourceError::new($span, $message) + ($fmt:literal $(, $arg:expr)* $(,)?) => { + $crate::diag::eco_format!($fmt, $($arg),*) }; - ($span:expr, $fmt:expr, $($arg:expr),+ $(,)?) => { - $crate::diag::error!($span, $crate::diag::eco_format!($fmt, $($arg),+)) + ($span:expr, $fmt:literal $(, $arg:expr)* $(,)?) => { + $crate::diag::SourceError::new( + $span, + $crate::diag::eco_format!($fmt, $($arg),*), + ) }; } #[doc(inline)] pub use crate::__error as error; #[doc(hidden)] -pub use ecow::eco_format; +pub use ecow::{eco_format, EcoString}; /// A result that can carry multiple source errors. pub type SourceResult<T> = Result<T, Box<Vec<SourceError>>>; @@ -68,7 +77,6 @@ pub struct SourceError { impl SourceError { /// Create a new, bare error. - #[track_caller] pub fn new(span: Span, message: impl Into<EcoString>) -> Self { Self { span, @@ -173,7 +181,7 @@ where S: Into<EcoString>, { fn at(self, span: Span) -> SourceResult<T> { - self.map_err(|message| Box::new(vec![error!(span, message)])) + self.map_err(|message| Box::new(vec![SourceError::new(span, message)])) } } @@ -8,7 +8,7 @@ use std::sync::Arc; use ecow::EcoString; -use crate::eval::{cast_from_value, cast_to_value, dict, Dict, Value}; +use crate::eval::{cast, dict, Dict, Value}; use crate::font::Font; use crate::geom::{ self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Length, @@ -567,15 +567,12 @@ impl FromStr for Lang { } } -cast_from_value! { +cast! { Lang, + self => self.as_str().into_value(), string: EcoString => Self::from_str(&string)?, } -cast_to_value! { - v: Lang => v.as_str().into() -} - /// An identifier for a region somewhere in the world. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Region([u8; 2]); @@ -608,15 +605,12 @@ impl FromStr for Region { } } -cast_from_value! { +cast! { Region, + self => self.as_str().into_value(), string: EcoString => Self::from_str(&string)?, } -cast_to_value! { - v: Region => v.as_str().into() -} - /// Meta information that isn't visible or renderable. #[derive(Clone, PartialEq, Hash)] pub enum Meta { @@ -633,6 +627,10 @@ pub enum Meta { Hide, } +cast! { + type Meta: "meta", +} + impl Debug for Meta { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { @@ -644,10 +642,6 @@ impl Debug for Meta { } } -cast_from_value! { - Meta: "meta", -} - /// A link destination. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Destination { @@ -659,21 +653,18 @@ pub enum Destination { Location(Location), } -cast_from_value! { +cast! { Destination, + self => match self { + Self::Url(v) => v.into_value(), + Self::Position(v) => v.into_value(), + Self::Location(v) => v.into_value(), + }, v: EcoString => Self::Url(v), v: Position => Self::Position(v), v: Location => Self::Location(v), } -cast_to_value! { - v: Destination => match v { - Destination::Url(v) => v.into(), - Destination::Position(v) => v.into(), - Destination::Location(v) => v.into(), - } -} - /// A physical position in a document. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Position { @@ -683,8 +674,9 @@ pub struct Position { pub point: Point, } -cast_from_value! { +cast! { Position, + self => Value::Dict(self.into()), mut dict: Dict => { let page = dict.take("page")?.cast()?; let x: Length = dict.take("x")?.cast()?; @@ -694,12 +686,14 @@ cast_from_value! { }, } -cast_to_value! { - v: Position => Value::Dict(dict! { - "page" => Value::Int(v.page.get() as i64), - "x" => Value::Length(v.point.x.into()), - "y" => Value::Length(v.point.y.into()), - }) +impl From<Position> for Dict { + fn from(pos: Position) -> Self { + dict! { + "page" => pos.page, + "x" => pos.point.x, + "y" => pos.point.y, + } + } } #[cfg(test)] diff --git a/src/eval/args.rs b/src/eval/args.rs index bea2baa1..da29eeaf 100644 --- a/src/eval/args.rs +++ b/src/eval/args.rs @@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter}; use ecow::{eco_format, EcoVec}; -use super::{Array, Cast, Dict, Str, Value}; +use super::{Array, Dict, FromValue, IntoValue, Str, Value}; use crate::diag::{bail, At, SourceResult}; use crate::syntax::{Span, Spanned}; use crate::util::pretty_array_like; @@ -29,10 +29,14 @@ pub struct Arg { impl Args { /// Create positional arguments from a span and values. - pub fn new(span: Span, values: impl IntoIterator<Item = Value>) -> Self { + pub fn new<T: IntoValue>(span: Span, values: impl IntoIterator<Item = T>) -> Self { let items = values .into_iter() - .map(|value| Arg { span, name: None, value: Spanned::new(value, span) }) + .map(|value| Arg { + span, + name: None, + value: Spanned::new(value.into_value(), span), + }) .collect(); Self { span, items } } @@ -49,13 +53,13 @@ impl Args { /// Consume and cast the first positional argument if there is one. pub fn eat<T>(&mut self) -> SourceResult<Option<T>> where - T: Cast<Spanned<Value>>, + T: FromValue<Spanned<Value>>, { for (i, slot) in self.items.iter().enumerate() { if slot.name.is_none() { let value = self.items.remove(i).value; let span = value.span; - return T::cast(value).at(span).map(Some); + return T::from_value(value).at(span).map(Some); } } Ok(None) @@ -87,24 +91,24 @@ impl Args { /// left. pub fn expect<T>(&mut self, what: &str) -> SourceResult<T> where - T: Cast<Spanned<Value>>, + T: FromValue<Spanned<Value>>, { match self.eat()? { Some(v) => Ok(v), - None => bail!(self.span, "missing argument: {}", what), + None => bail!(self.span, "missing argument: {what}"), } } /// Find and consume the first castable positional argument. pub fn find<T>(&mut self) -> SourceResult<Option<T>> where - T: Cast<Spanned<Value>>, + T: FromValue<Spanned<Value>>, { for (i, slot) in self.items.iter().enumerate() { - if slot.name.is_none() && T::is(&slot.value) { + if slot.name.is_none() && T::castable(&slot.value.v) { let value = self.items.remove(i).value; let span = value.span; - return T::cast(value).at(span).map(Some); + return T::from_value(value).at(span).map(Some); } } Ok(None) @@ -113,7 +117,7 @@ impl Args { /// Find and consume all castable positional arguments. pub fn all<T>(&mut self) -> SourceResult<Vec<T>> where - T: Cast<Spanned<Value>>, + T: FromValue<Spanned<Value>>, { let mut list = vec![]; while let Some(value) = self.find()? { @@ -126,7 +130,7 @@ impl Args { /// error if the conversion fails. pub fn named<T>(&mut self, name: &str) -> SourceResult<Option<T>> where - T: Cast<Spanned<Value>>, + T: FromValue<Spanned<Value>>, { // We don't quit once we have a match because when multiple matches // exist, we want to remove all of them and use the last one. @@ -136,7 +140,7 @@ impl Args { if self.items[i].name.as_deref() == Some(name) { let value = self.items.remove(i).value; let span = value.span; - found = Some(T::cast(value).at(span)?); + found = Some(T::from_value(value).at(span)?); } else { i += 1; } @@ -147,7 +151,7 @@ impl Args { /// Same as named, but with fallback to find. pub fn named_or_find<T>(&mut self, name: &str) -> SourceResult<Option<T>> where - T: Cast<Spanned<Value>>, + T: FromValue<Spanned<Value>>, { match self.named(name)? { Some(value) => Ok(Some(value)), @@ -167,13 +171,10 @@ impl Args { /// argument. pub fn finish(self) -> SourceResult<()> { if let Some(arg) = self.items.first() { - bail!( - arg.span, - match &arg.name { - Some(name) => eco_format!("unexpected argument: {}", name), - _ => eco_format!("unexpected argument"), - } - ) + match &arg.name { + Some(name) => bail!(arg.span, "unexpected argument: {name}"), + _ => bail!(arg.span, "unexpected argument"), + } } Ok(()) } diff --git a/src/eval/array.rs b/src/eval/array.rs index d0b63b35..a7a1387b 100644 --- a/src/eval/array.rs +++ b/src/eval/array.rs @@ -4,7 +4,7 @@ use std::ops::{Add, AddAssign}; use ecow::{eco_format, EcoString, EcoVec}; -use super::{ops, Args, Func, Value, Vm}; +use super::{ops, Args, CastInfo, FromValue, Func, IntoValue, Reflect, Value, Vm}; use crate::diag::{At, SourceResult, StrResult}; use crate::syntax::Span; use crate::util::pretty_array_like; @@ -14,11 +14,16 @@ use crate::util::pretty_array_like; #[doc(hidden)] macro_rules! __array { ($value:expr; $count:expr) => { - $crate::eval::Array::from_vec($crate::eval::eco_vec![$value.into(); $count]) + $crate::eval::Array::from($crate::eval::eco_vec![ + $crate::eval::IntoValue::into_value($value); + $count + ]) }; ($($value:expr),* $(,)?) => { - $crate::eval::Array::from_vec($crate::eval::eco_vec![$($value.into()),*]) + $crate::eval::Array::from($crate::eval::eco_vec![$( + $crate::eval::IntoValue::into_value($value) + ),*]) }; } @@ -38,19 +43,14 @@ impl Array { Self::default() } - /// Create a new array from an eco vector of values. - pub fn from_vec(vec: EcoVec<Value>) -> Self { - Self(vec) - } - /// Return `true` if the length is 0. pub fn is_empty(&self) -> bool { self.0.len() == 0 } /// The length of the array. - pub fn len(&self) -> i64 { - self.0.len() as i64 + pub fn len(&self) -> usize { + self.0.len() } /// The first value in the array. @@ -134,14 +134,14 @@ impl Array { .filter(|&start| start <= self.0.len()) .ok_or_else(|| out_of_bounds(start, len))?; - let end = end.unwrap_or(self.len()); + let end = end.unwrap_or(self.len() as i64); let end = self .locate(end) .filter(|&end| end <= self.0.len()) .ok_or_else(|| out_of_bounds(end, len))? .max(start); - Ok(Self::from_vec(self.0[start..end].into())) + Ok(self.0[start..end].into()) } /// Whether the array contains a specific value. @@ -182,7 +182,7 @@ impl Array { kept.push(item.clone()) } } - Ok(Self::from_vec(kept)) + Ok(kept.into()) } /// Transform each item in the array with a function. @@ -273,7 +273,7 @@ impl Array { flat.push(item.clone()); } } - Self::from_vec(flat) + flat.into() } /// Returns a new array with reversed order. @@ -317,9 +317,7 @@ impl Array { pub fn zip(&self, other: Array) -> Array { self.iter() .zip(other) - .map(|(first, second)| { - Value::Array(Array::from_vec(eco_vec![first.clone(), second])) - }) + .map(|(first, second)| array![first.clone(), second].into_value()) .collect() } @@ -360,7 +358,7 @@ impl Array { } } }); - result.map(|_| Self::from_vec(vec)) + result.map(|_| vec.into()) } /// Repeat this array `n` times. @@ -385,19 +383,20 @@ impl Array { /// Resolve an index. fn locate(&self, index: i64) -> Option<usize> { - usize::try_from(if index >= 0 { index } else { self.len().checked_add(index)? }) - .ok() + usize::try_from(if index >= 0 { + index + } else { + (self.len() as i64).checked_add(index)? + }) + .ok() } /// Enumerate all items in the array. pub fn enumerate(&self) -> Self { - let v = self - .iter() + self.iter() .enumerate() - .map(|(i, value)| array![i, value.clone()]) - .map(Value::Array) - .collect(); - Self::from_vec(v) + .map(|(i, value)| array![i, value.clone()].into_value()) + .collect() } } @@ -453,6 +452,40 @@ impl<'a> IntoIterator for &'a Array { } } +impl From<EcoVec<Value>> for Array { + fn from(v: EcoVec<Value>) -> Self { + Array(v) + } +} + +impl From<&[Value]> for Array { + fn from(v: &[Value]) -> Self { + Array(v.into()) + } +} + +impl<T> Reflect for Vec<T> { + fn describe() -> CastInfo { + Array::describe() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) + } +} + +impl<T: IntoValue> IntoValue for Vec<T> { + fn into_value(self) -> Value { + Value::Array(self.into_iter().map(IntoValue::into_value).collect()) + } +} + +impl<T: FromValue> FromValue for Vec<T> { + fn from_value(value: Value) -> StrResult<Self> { + value.cast::<Array>()?.into_iter().map(Value::cast).collect() + } +} + /// The error message when the array is empty. #[cold] fn array_is_empty() -> EcoString { @@ -461,17 +494,15 @@ fn array_is_empty() -> EcoString { /// The out of bounds access error message. #[cold] -fn out_of_bounds(index: i64, len: i64) -> EcoString { - eco_format!("array index out of bounds (index: {}, len: {})", index, len) +fn out_of_bounds(index: i64, len: usize) -> EcoString { + eco_format!("array index out of bounds (index: {index}, len: {len})") } /// The out of bounds access error message when no default value was given. #[cold] -fn out_of_bounds_no_default(index: i64, len: i64) -> EcoString { +fn out_of_bounds_no_default(index: i64, len: usize) -> EcoString { eco_format!( - "array index out of bounds (index: {}, len: {}) \ + "array index out of bounds (index: {index}, len: {len}) \ and no default value was specified", - index, - len ) } diff --git a/src/eval/auto.rs b/src/eval/auto.rs new file mode 100644 index 00000000..e73b3f33 --- /dev/null +++ b/src/eval/auto.rs @@ -0,0 +1,39 @@ +use std::fmt::{self, Debug, Formatter}; + +use super::{CastInfo, FromValue, IntoValue, Reflect, Value}; +use crate::diag::StrResult; + +/// A value that indicates a smart default. +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct AutoValue; + +impl IntoValue for AutoValue { + fn into_value(self) -> Value { + Value::Auto + } +} + +impl FromValue for AutoValue { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::Auto => Ok(Self), + _ => Err(Self::error(&value)), + } + } +} + +impl Reflect for AutoValue { + fn describe() -> CastInfo { + CastInfo::Type("auto") + } + + fn castable(value: &Value) -> bool { + matches!(value, Value::Auto) + } +} + +impl Debug for AutoValue { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("auto") + } +} diff --git a/src/eval/cast.rs b/src/eval/cast.rs index 7ef2f1d0..29cf5f71 100644 --- a/src/eval/cast.rs +++ b/src/eval/cast.rs @@ -1,278 +1,185 @@ -pub use typst_macros::{cast_from_value, cast_to_value, Cast}; +pub use typst_macros::{cast, Cast}; -use std::num::{NonZeroI64, NonZeroU64, NonZeroUsize}; +use std::fmt::Write; use std::ops::Add; use ecow::EcoString; -use super::{Array, Str, Value}; -use crate::diag::StrResult; -use crate::eval::Type; -use crate::geom::Length; -use crate::syntax::Spanned; +use super::Value; +use crate::diag::{At, SourceResult, StrResult}; +use crate::syntax::{Span, Spanned}; use crate::util::separated_list; -/// Cast from a value to a specific type. -pub trait Cast<V = Value>: Sized { - /// Check whether the value is castable to `Self`. - fn is(value: &V) -> bool; - - /// Try to cast the value into an instance of `Self`. - fn cast(value: V) -> StrResult<Self>; - - /// Describe the acceptable values. +/// Determine details of a type. +/// +/// Type casting works as follows: +/// - [`Reflect for T`](Reflect) describes the possible Typst values for `T` +/// (for documentation and autocomplete). +/// - [`IntoValue for T`](IntoValue) is for conversion from `T -> Value` +/// (infallible) +/// - [`FromValue for T`](FromValue) is for conversion from `Value -> T` +/// (fallible). +/// +/// We can't use `TryFrom<Value>` due to conflicting impls. We could use +/// `From<T> for Value`, but that inverses the impl and leads to tons of +/// `.into()` all over the place that become hard to decipher. +pub trait Reflect { + /// Describe the acceptable values for this type. fn describe() -> CastInfo; - /// Produce an error for an inacceptable value. - fn error(value: Value) -> StrResult<Self> { - Err(Self::describe().error(&value)) + /// Whether the given value can be converted to `T`. + /// + /// This exists for performance. The check could also be done through the + /// [`CastInfo`], but it would be much more expensive (heap allocation + + /// dynamic checks instead of optimized machine code for each type). + fn castable(value: &Value) -> bool; + + /// Produce an error message for an inacceptable value. + /// + /// ``` + /// # use typst::eval::{Int, Reflect, Value}; + /// assert_eq!( + /// <Int as Reflect>::error(Value::None), + /// "expected integer, found none", + /// ); + /// ``` + fn error(found: &Value) -> EcoString { + Self::describe().error(found) } } -impl Cast for Value { - fn is(_: &Value) -> bool { - true - } - - fn cast(value: Value) -> StrResult<Self> { - Ok(value) - } - +impl Reflect for Value { fn describe() -> CastInfo { CastInfo::Any } -} -impl<T: Cast> Cast<Spanned<Value>> for T { - fn is(value: &Spanned<Value>) -> bool { - T::is(&value.v) - } - - fn cast(value: Spanned<Value>) -> StrResult<Self> { - T::cast(value.v) + fn castable(_: &Value) -> bool { + true } +} +impl<T: Reflect> Reflect for Spanned<T> { fn describe() -> CastInfo { T::describe() } -} - -impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> { - fn is(value: &Spanned<Value>) -> bool { - T::is(&value.v) - } - fn cast(value: Spanned<Value>) -> StrResult<Self> { - let span = value.span; - T::cast(value.v).map(|t| Spanned::new(t, span)) + fn castable(value: &Value) -> bool { + T::castable(value) } +} +impl<T: Reflect> Reflect for StrResult<T> { fn describe() -> CastInfo { T::describe() } -} - -cast_to_value! { - v: u8 => Value::Int(v as i64) -} - -cast_to_value! { - v: u16 => Value::Int(v as i64) -} - -cast_from_value! { - u32, - int: i64 => int.try_into().map_err(|_| { - if int < 0 { - "number must be at least zero" - } else { - "number too large" - } - })?, -} -cast_to_value! { - v: u32 => Value::Int(v as i64) -} - -cast_from_value! { - u64, - int: i64 => int.try_into().map_err(|_| { - if int < 0 { - "number must be at least zero" - } else { - "number too large" - } - })?, -} - -cast_to_value! { - v: u64 => Value::Int(v as i64) -} - -cast_from_value! { - usize, - int: i64 => int.try_into().map_err(|_| { - if int < 0 { - "number must be at least zero" - } else { - "number too large" - } - })?, -} - -cast_to_value! { - v: usize => Value::Int(v as i64) -} - -cast_to_value! { - v: i32 => Value::Int(v as i64) -} - -cast_from_value! { - NonZeroI64, - int: i64 => int.try_into() - .map_err(|_| if int == 0 { - "number must not be zero" - } else { - "number too large" - })?, -} - -cast_to_value! { - v: NonZeroI64 => Value::Int(v.get()) -} - -cast_from_value! { - NonZeroU64, - int: i64 => int - .try_into() - .and_then(|int: u64| int.try_into()) - .map_err(|_| if int <= 0 { - "number must be positive" - } else { - "number too large" - })?, + fn castable(value: &Value) -> bool { + T::castable(value) + } } -cast_to_value! { - v: NonZeroU64 => Value::Int(v.get() as i64) -} +impl<T: Reflect> Reflect for SourceResult<T> { + fn describe() -> CastInfo { + T::describe() + } -cast_from_value! { - NonZeroUsize, - int: i64 => int - .try_into() - .and_then(|int: usize| int.try_into()) - .map_err(|_| if int <= 0 { - "number must be positive" - } else { - "number too large" - })?, + fn castable(value: &Value) -> bool { + T::castable(value) + } } -cast_to_value! { - v: NonZeroUsize => Value::Int(v.get() as i64) -} +impl<T: Reflect> Reflect for &T { + fn describe() -> CastInfo { + T::describe() + } -cast_from_value! { - char, - string: Str => { - let mut chars = string.chars(); - match (chars.next(), chars.next()) { - (Some(c), None) => c, - _ => Err("expected exactly one character")?, - } - }, + fn castable(value: &Value) -> bool { + T::castable(value) + } } -cast_to_value! { - v: char => Value::Str(v.into()) -} +impl<T: Reflect> Reflect for &mut T { + fn describe() -> CastInfo { + T::describe() + } -cast_to_value! { - v: &str => Value::Str(v.into()) + fn castable(value: &Value) -> bool { + T::castable(value) + } } -cast_from_value! { - EcoString, - v: Str => v.into(), +/// Cast a Rust type into a Typst [`Value`]. +/// +/// See also: [`Reflect`]. +pub trait IntoValue { + /// Cast this type into a value. + fn into_value(self) -> Value; } -cast_to_value! { - v: EcoString => Value::Str(v.into()) +impl IntoValue for Value { + fn into_value(self) -> Value { + self + } } -cast_from_value! { - String, - v: Str => v.into(), +impl<T: IntoValue> IntoValue for Spanned<T> { + fn into_value(self) -> Value { + self.v.into_value() + } } -cast_to_value! { - v: String => Value::Str(v.into()) +/// Cast a Rust type or result into a [`SourceResult<Value>`]. +/// +/// Converts `T`, [`StrResult<T>`], or [`SourceResult<T>`] into +/// [`SourceResult<Value>`] by `Ok`-wrapping or adding span information. +pub trait IntoResult { + /// Cast this type into a value. + fn into_result(self, span: Span) -> SourceResult<Value>; } -impl<T: Cast> Cast for Option<T> { - fn is(value: &Value) -> bool { - matches!(value, Value::None) || T::is(value) - } - - fn cast(value: Value) -> StrResult<Self> { - match value { - Value::None => Ok(None), - v if T::is(&v) => Ok(Some(T::cast(v)?)), - _ => <Self as Cast>::error(value), - } - } - - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("none") +impl<T: IntoValue> IntoResult for T { + fn into_result(self, _: Span) -> SourceResult<Value> { + Ok(self.into_value()) } } -impl<T: Into<Value>> From<Spanned<T>> for Value { - fn from(spanned: Spanned<T>) -> Self { - spanned.v.into() +impl<T: IntoValue> IntoResult for StrResult<T> { + fn into_result(self, span: Span) -> SourceResult<Value> { + self.map(IntoValue::into_value).at(span) } } -impl<T: Into<Value>> From<Option<T>> for Value { - fn from(v: Option<T>) -> Self { - match v { - Some(v) => v.into(), - None => Value::None, - } +impl<T: IntoValue> IntoResult for SourceResult<T> { + fn into_result(self, _: Span) -> SourceResult<Value> { + self.map(IntoValue::into_value) } } -impl<T: Cast> Cast for Vec<T> { - fn is(value: &Value) -> bool { - Array::is(value) - } - - fn cast(value: Value) -> StrResult<Self> { - value.cast::<Array>()?.into_iter().map(Value::cast).collect() - } - - fn describe() -> CastInfo { - <Array as Cast>::describe() - } +/// Try to cast a Typst [`Value`] into a Rust type. +/// +/// See also: [`Reflect`]. +pub trait FromValue<V = Value>: Sized + Reflect { + /// Try to cast the value into an instance of `Self`. + fn from_value(value: V) -> StrResult<Self>; } -impl<T: Into<Value>> From<Vec<T>> for Value { - fn from(v: Vec<T>) -> Self { - Value::Array(v.into_iter().map(Into::into).collect()) +impl FromValue for Value { + fn from_value(value: Value) -> StrResult<Self> { + Ok(value) } } -/// A container for a variadic argument. -pub trait Variadics { - /// The contained type. - type Inner; +impl<T: FromValue> FromValue<Spanned<Value>> for T { + fn from_value(value: Spanned<Value>) -> StrResult<Self> { + T::from_value(value.v) + } } -impl<T> Variadics for Vec<T> { - type Inner = T; +impl<T: FromValue> FromValue<Spanned<Value>> for Spanned<T> { + fn from_value(value: Spanned<Value>) -> StrResult<Self> { + let span = value.span; + T::from_value(value.v).map(|t| Spanned::new(t, span)) + } } /// Describes a possible value for a cast. @@ -332,10 +239,10 @@ impl CastInfo { } if_chain::if_chain! { if let Value::Int(i) = found; - if parts.iter().any(|p| p == Length::TYPE_NAME); + if parts.iter().any(|p| p == "length"); if !matching_type; then { - msg.push_str(&format!(": a length needs a unit – did you mean {i}pt?")); + write!(msg, ": a length needs a unit – did you mean {i}pt?").unwrap(); } }; @@ -373,19 +280,37 @@ impl Add for CastInfo { } } -/// Castable from nothing. +/// A container for a variadic argument. +pub trait Variadics { + /// The contained type. + type Inner; +} + +impl<T> Variadics for Vec<T> { + type Inner = T; +} + +/// An uninhabitable type. pub enum Never {} -impl Cast for Never { - fn is(_: &Value) -> bool { +impl Reflect for Never { + fn describe() -> CastInfo { + CastInfo::Union(vec![]) + } + + fn castable(_: &Value) -> bool { false } +} - fn cast(value: Value) -> StrResult<Self> { - <Self as Cast>::error(value) +impl IntoValue for Never { + fn into_value(self) -> Value { + match self {} } +} - fn describe() -> CastInfo { - CastInfo::Union(vec![]) +impl FromValue for Never { + fn from_value(value: Value) -> StrResult<Self> { + Err(Self::error(&value)) } } diff --git a/src/eval/datetime.rs b/src/eval/datetime.rs index 47574bae..57d0683d 100644 --- a/src/eval/datetime.rs +++ b/src/eval/datetime.rs @@ -6,7 +6,7 @@ use ecow::{eco_format, EcoString, EcoVec}; use time::error::{Format, InvalidFormatDescription}; use time::{format_description, PrimitiveDateTime}; -use crate::eval::cast_from_value; +use crate::eval::cast; use crate::util::pretty_array_like; /// A datetime object that represents either a date, a time or a combination of @@ -153,8 +153,8 @@ impl Debug for Datetime { } } -cast_from_value! { - Datetime: "datetime", +cast! { + type Datetime: "datetime", } /// Format the `Format` error of the time crate in an appropriate way. diff --git a/src/eval/dict.rs b/src/eval/dict.rs index 49a60147..3e6233ae 100644 --- a/src/eval/dict.rs +++ b/src/eval/dict.rs @@ -17,8 +17,8 @@ macro_rules! __dict { ($($key:expr => $value:expr),* $(,)?) => {{ #[allow(unused_mut)] let mut map = $crate::eval::IndexMap::new(); - $(map.insert($key.into(), $value.into());)* - $crate::eval::Dict::from_map(map) + $(map.insert($key.into(), $crate::eval::IntoValue::into_value($value));)* + $crate::eval::Dict::from(map) }}; } @@ -38,19 +38,14 @@ impl Dict { Self::default() } - /// Create a new dictionary from a mapping of strings to values. - pub fn from_map(map: IndexMap<Str, Value>) -> Self { - Self(Arc::new(map)) - } - /// Whether the dictionary is empty. pub fn is_empty(&self) -> bool { self.0.is_empty() } /// The number of pairs in the dictionary. - pub fn len(&self) -> i64 { - self.0.len() as i64 + pub fn len(&self) -> usize { + self.0.len() } /// Borrow the value the given `key` maps to, @@ -217,6 +212,12 @@ impl<'a> IntoIterator for &'a Dict { } } +impl From<IndexMap<Str, Value>> for Dict { + fn from(map: IndexMap<Str, Value>) -> Self { + Self(Arc::new(map)) + } +} + /// The missing key access error message. #[cold] fn missing_key(key: &str) -> EcoString { diff --git a/src/eval/func.rs b/src/eval/func.rs index a224c5b8..86070fc7 100644 --- a/src/eval/func.rs +++ b/src/eval/func.rs @@ -1,5 +1,3 @@ -pub use typst_macros::func; - use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::sync::Arc; @@ -9,7 +7,7 @@ use ecow::eco_format; use once_cell::sync::Lazy; use super::{ - cast_to_value, Args, CastInfo, Eval, Flow, Route, Scope, Scopes, Tracer, Value, Vm, + cast, Args, CastInfo, Eval, Flow, IntoValue, Route, Scope, Scopes, Tracer, Value, Vm, }; use crate::diag::{bail, SourceResult, StrResult}; use crate::model::{ElemFunc, Introspector, Locator, Vt}; @@ -119,10 +117,10 @@ impl Func { /// Call the function with a Vt. #[tracing::instrument(skip_all)] - pub fn call_vt( + pub fn call_vt<T: IntoValue>( &self, vt: &mut Vt, - args: impl IntoIterator<Item = Value>, + args: impl IntoIterator<Item = T>, ) -> SourceResult<Value> { let route = Route::default(); let id = SourceId::detached(); @@ -233,13 +231,9 @@ impl From<&'static NativeFunc> for Func { } } -impl<F> From<F> for Value -where - F: Fn() -> &'static NativeFunc, -{ - fn from(f: F) -> Self { - Value::Func(f().into()) - } +cast! { + &'static NativeFunc, + self => Value::Func(self.into()), } /// Details about a function. @@ -249,16 +243,16 @@ pub struct FuncInfo { pub name: &'static str, /// The display name of the function. pub display: &'static str, - /// A string of keywords. + /// A string of search keywords. pub keywords: Option<&'static str>, + /// Which category the function is part of. + pub category: &'static str, /// Documentation for the function. pub docs: &'static str, /// Details about the function's parameters. pub params: Vec<ParamInfo>, - /// Valid types for the return value. - pub returns: Vec<&'static str>, - /// Which category the function is part of. - pub category: &'static str, + /// Valid values for the return value. + pub returns: CastInfo, /// The function's own scope of fields and sub-functions. pub scope: Scope, } @@ -311,6 +305,7 @@ pub(super) struct Closure { pub body: Expr, } +/// A closure parameter. #[derive(Hash)] pub enum Param { /// A positional parameter: `x`. @@ -362,7 +357,7 @@ impl Closure { // Parse the arguments according to the parameter list. let num_pos_params = closure.params.iter().filter(|p| matches!(p, Param::Pos(_))).count(); - let num_pos_args = args.to_pos().len() as usize; + let num_pos_args = args.to_pos().len(); let sink_size = num_pos_args.checked_sub(num_pos_params); let mut sink = None; @@ -425,8 +420,9 @@ impl From<Closure> for Func { } } -cast_to_value! { - v: Closure => Value::Func(v.into()) +cast! { + Closure, + self => Value::Func(self.into()), } /// A visitor that determines which variables to capture for a closure. diff --git a/src/eval/int.rs b/src/eval/int.rs new file mode 100644 index 00000000..4e081617 --- /dev/null +++ b/src/eval/int.rs @@ -0,0 +1,81 @@ +use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize}; + +use super::{cast, Value}; + +macro_rules! signed_int { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => Value::Int(self as i64), + v: i64 => v.try_into().map_err(|_| "number too large")?, + })* + } +} + +macro_rules! unsigned_int { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => Value::Int(self as i64), + v: i64 => v.try_into().map_err(|_| { + if v < 0 { + "number must be at least zero" + } else { + "number too large" + } + })?, + })* + } +} + +macro_rules! signed_nonzero { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => Value::Int(self.get() as i64), + v: i64 => v + .try_into() + .ok() + .and_then($ty::new) + .ok_or_else(|| if v == 0 { + "number must not be zero" + } else { + "number too large" + })?, + })* + } +} + +macro_rules! unsigned_nonzero { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => Value::Int(self.get() as i64), + v: i64 => v + .try_into() + .ok() + .and_then($ty::new) + .ok_or_else(|| if v <= 0 { + "number must be positive" + } else { + "number too large" + })?, + })* + } +} + +signed_int! { + i8 i16 i32 isize +} + +unsigned_int! { + u8 u16 u32 u64 usize +} + +signed_nonzero! { + NonZeroI64 NonZeroIsize +} + +unsigned_nonzero! { + NonZeroU64 NonZeroUsize +} diff --git a/src/eval/methods.rs b/src/eval/methods.rs index f57bf84d..62ac4095 100644 --- a/src/eval/methods.rs +++ b/src/eval/methods.rs @@ -2,7 +2,7 @@ use ecow::EcoString; -use super::{Args, Str, Value, Vm}; +use super::{Args, IntoValue, Str, Value, Vm}; use crate::diag::{At, SourceResult}; use crate::eval::Datetime; use crate::model::{Location, Selector}; @@ -21,20 +21,20 @@ pub fn call( let output = match value { Value::Color(color) => match method { - "lighten" => Value::Color(color.lighten(args.expect("amount")?)), - "darken" => Value::Color(color.darken(args.expect("amount")?)), - "negate" => Value::Color(color.negate()), + "lighten" => color.lighten(args.expect("amount")?).into_value(), + "darken" => color.darken(args.expect("amount")?).into_value(), + "negate" => color.negate().into_value(), _ => return missing(), }, Value::Str(string) => match method { - "len" => Value::Int(string.len()), - "first" => Value::Str(string.first().at(span)?), - "last" => Value::Str(string.last().at(span)?), + "len" => string.len().into_value(), + "first" => string.first().at(span)?.into_value(), + "last" => string.last().at(span)?.into_value(), "at" => { let index = args.expect("index")?; let default = args.named::<EcoString>("default")?; - Value::Str(string.at(index, default.as_deref()).at(span)?) + string.at(index, default.as_deref()).at(span)?.into_value() } "slice" => { let start = args.expect("start")?; @@ -42,56 +42,50 @@ pub fn call( if end.is_none() { end = args.named("count")?.map(|c: i64| start + c); } - Value::Str(string.slice(start, end).at(span)?) + string.slice(start, end).at(span)?.into_value() } - "clusters" => Value::Array(string.clusters()), - "codepoints" => Value::Array(string.codepoints()), - "contains" => Value::Bool(string.contains(args.expect("pattern")?)), - "starts-with" => Value::Bool(string.starts_with(args.expect("pattern")?)), - "ends-with" => Value::Bool(string.ends_with(args.expect("pattern")?)), - "find" => { - string.find(args.expect("pattern")?).map_or(Value::None, Value::Str) - } - "position" => string - .position(args.expect("pattern")?) - .map_or(Value::None, Value::Int), - "match" => string - .match_(args.expect("pattern")?) - .map_or(Value::None, Value::Dict), - "matches" => Value::Array(string.matches(args.expect("pattern")?)), + "clusters" => string.clusters().into_value(), + "codepoints" => string.codepoints().into_value(), + "contains" => string.contains(args.expect("pattern")?).into_value(), + "starts-with" => string.starts_with(args.expect("pattern")?).into_value(), + "ends-with" => string.ends_with(args.expect("pattern")?).into_value(), + "find" => string.find(args.expect("pattern")?).into_value(), + "position" => string.position(args.expect("pattern")?).into_value(), + "match" => string.match_(args.expect("pattern")?).into_value(), + "matches" => string.matches(args.expect("pattern")?).into_value(), "replace" => { let pattern = args.expect("pattern")?; let with = args.expect("string or function")?; let count = args.named("count")?; - Value::Str(string.replace(vm, pattern, with, count)?) + string.replace(vm, pattern, with, count)?.into_value() } "trim" => { let pattern = args.eat()?; let at = args.named("at")?; let repeat = args.named("repeat")?.unwrap_or(true); - Value::Str(string.trim(pattern, at, repeat)) + string.trim(pattern, at, repeat).into_value() } - "split" => Value::Array(string.split(args.eat()?)), + "split" => string.split(args.eat()?).into_value(), _ => return missing(), }, Value::Content(content) => match method { - "func" => content.func().into(), - "has" => Value::Bool(content.has(&args.expect::<EcoString>("field")?)), + "func" => content.func().into_value(), + "has" => content.has(&args.expect::<EcoString>("field")?).into_value(), "at" => content .at(&args.expect::<EcoString>("field")?, args.named("default")?) .at(span)?, - "fields" => Value::Dict(content.dict()), + "fields" => content.dict().into_value(), "location" => content .location() .ok_or("this method can only be called on content returned by query(..)") .at(span)? - .into(), + .into_value(), _ => return missing(), }, Value::Array(array) => match method { - "len" => Value::Int(array.len()), + "len" => array.len().into_value(), "first" => array.first().at(span)?.clone(), "last" => array.last().at(span)?.clone(), "at" => array @@ -104,117 +98,104 @@ pub fn call( if end.is_none() { end = args.named("count")?.map(|c: i64| start + c); } - Value::Array(array.slice(start, end).at(span)?) + array.slice(start, end).at(span)?.into_value() } - "contains" => Value::Bool(array.contains(&args.expect("value")?)), - "find" => array.find(vm, args.expect("function")?)?.unwrap_or(Value::None), - "position" => array - .position(vm, args.expect("function")?)? - .map_or(Value::None, Value::Int), - "filter" => Value::Array(array.filter(vm, args.expect("function")?)?), - "map" => Value::Array(array.map(vm, args.expect("function")?)?), + "contains" => array.contains(&args.expect("value")?).into_value(), + "find" => array.find(vm, args.expect("function")?)?.into_value(), + "position" => array.position(vm, args.expect("function")?)?.into_value(), + "filter" => array.filter(vm, args.expect("function")?)?.into_value(), + "map" => array.map(vm, args.expect("function")?)?.into_value(), "fold" => { array.fold(vm, args.expect("initial value")?, args.expect("function")?)? } "sum" => array.sum(args.named("default")?, span)?, "product" => array.product(args.named("default")?, span)?, - "any" => Value::Bool(array.any(vm, args.expect("function")?)?), - "all" => Value::Bool(array.all(vm, args.expect("function")?)?), - "flatten" => Value::Array(array.flatten()), - "rev" => Value::Array(array.rev()), - "split" => Value::Array(array.split(args.expect("separator")?)), + "any" => array.any(vm, args.expect("function")?)?.into_value(), + "all" => array.all(vm, args.expect("function")?)?.into_value(), + "flatten" => array.flatten().into_value(), + "rev" => array.rev().into_value(), + "split" => array.split(args.expect("separator")?).into_value(), "join" => { let sep = args.eat()?; let last = args.named("last")?; array.join(sep, last).at(span)? } - "sorted" => Value::Array(array.sorted(vm, span, args.named("key")?)?), - "zip" => Value::Array(array.zip(args.expect("other")?)), - "enumerate" => Value::Array(array.enumerate()), + "sorted" => array.sorted(vm, span, args.named("key")?)?.into_value(), + "zip" => array.zip(args.expect("other")?).into_value(), + "enumerate" => array.enumerate().into_value(), _ => return missing(), }, Value::Dict(dict) => match method { - "len" => Value::Int(dict.len()), + "len" => dict.len().into_value(), "at" => dict .at(&args.expect::<Str>("key")?, args.named("default")?.as_ref()) .at(span)? .clone(), - "keys" => Value::Array(dict.keys()), - "values" => Value::Array(dict.values()), - "pairs" => Value::Array(dict.pairs()), + "keys" => dict.keys().into_value(), + "values" => dict.values().into_value(), + "pairs" => dict.pairs().into_value(), _ => return missing(), }, Value::Func(func) => match method { - "with" => Value::Func(func.with(args.take())), + "with" => func.with(args.take()).into_value(), "where" => { let fields = args.to_named(); args.items.retain(|arg| arg.name.is_none()); - Value::dynamic( - func.element() - .ok_or("`where()` can only be called on element functions") - .at(span)? - .where_(fields), - ) + func.element() + .ok_or("`where()` can only be called on element functions") + .at(span)? + .where_(fields) + .into_value() } _ => return missing(), }, Value::Args(args) => match method { - "pos" => Value::Array(args.to_pos()), - "named" => Value::Dict(args.to_named()), + "pos" => args.to_pos().into_value(), + "named" => args.to_named().into_value(), _ => return missing(), }, Value::Dyn(dynamic) => { if let Some(location) = dynamic.downcast::<Location>() { match method { - "page" => vm.vt.introspector.page(*location).into(), - "position" => vm.vt.introspector.position(*location).into(), + "page" => vm.vt.introspector.page(*location).into_value(), + "position" => vm.vt.introspector.position(*location).into_value(), "page-numbering" => vm.vt.introspector.page_numbering(*location), _ => return missing(), } } else if let Some(selector) = dynamic.downcast::<Selector>() { match method { - "or" => selector.clone().or(args.all::<Selector>()?).into(), - "and" => selector.clone().and(args.all::<Selector>()?).into(), + "or" => selector.clone().or(args.all::<Selector>()?).into_value(), + "and" => selector.clone().and(args.all::<Selector>()?).into_value(), "before" => { let location = args.expect::<Selector>("selector")?; let inclusive = args.named_or_find::<bool>("inclusive")?.unwrap_or(true); - selector.clone().before(location, inclusive).into() + selector.clone().before(location, inclusive).into_value() } "after" => { let location = args.expect::<Selector>("selector")?; let inclusive = args.named_or_find::<bool>("inclusive")?.unwrap_or(true); - selector.clone().after(location, inclusive).into() + selector.clone().after(location, inclusive).into_value() } _ => return missing(), } } else if let Some(&datetime) = dynamic.downcast::<Datetime>() { match method { - "display" => datetime.display(args.eat()?).at(args.span)?.into(), - "year" => { - datetime.year().map_or(Value::None, |y| Value::Int(y.into())) - } - "month" => { - datetime.month().map_or(Value::None, |m| Value::Int(m.into())) - } - "weekday" => { - datetime.weekday().map_or(Value::None, |w| Value::Int(w.into())) - } - "day" => datetime.day().map_or(Value::None, |d| Value::Int(d.into())), - "hour" => { - datetime.hour().map_or(Value::None, |h| Value::Int(h.into())) - } - "minute" => { - datetime.minute().map_or(Value::None, |m| Value::Int(m.into())) - } - "second" => { - datetime.second().map_or(Value::None, |s| Value::Int(s.into())) + "display" => { + datetime.display(args.eat()?).at(args.span)?.into_value() } + "year" => datetime.year().into_value(), + "month" => datetime.month().into_value(), + "weekday" => datetime.weekday().into_value(), + "day" => datetime.day().into_value(), + "hour" => datetime.hour().into_value(), + "minute" => datetime.minute().into_value(), + "second" => datetime.second().into_value(), _ => return missing(), } } else { diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 2499ae22..ab75bfb4 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -13,31 +13,45 @@ mod str; #[macro_use] mod value; mod args; +mod auto; mod datetime; mod func; +mod int; mod methods; mod module; +mod none; pub mod ops; mod scope; mod symbol; #[doc(hidden)] -pub use once_cell::sync::Lazy; - -pub use self::args::*; -pub use self::array::*; -pub use self::cast::*; -pub use self::datetime::*; -pub use self::dict::*; -pub use self::func::*; -pub use self::library::*; -pub use self::module::*; -pub use self::scope::*; -pub use self::str::*; -pub use self::symbol::*; -pub use self::value::*; - -pub(crate) use self::methods::methods_on; +pub use { + self::library::LANG_ITEMS, + ecow::{eco_format, eco_vec}, + indexmap::IndexMap, + once_cell::sync::Lazy, +}; + +#[doc(inline)] +pub use typst_macros::{func, symbols}; + +pub use self::args::{Arg, Args}; +pub use self::array::{array, Array}; +pub use self::auto::AutoValue; +pub use self::cast::{ + cast, Cast, CastInfo, FromValue, IntoResult, IntoValue, Never, Reflect, Variadics, +}; +pub use self::datetime::Datetime; +pub use self::dict::{dict, Dict}; +pub use self::func::{Func, FuncInfo, NativeFunc, Param, ParamInfo}; +pub use self::library::{set_lang_items, LangItems, Library}; +pub use self::methods::methods_on; +pub use self::module::Module; +pub use self::none::NoneValue; +pub use self::scope::{Scope, Scopes}; +pub use self::str::{format_str, Regex, Str}; +pub use self::symbol::Symbol; +pub use self::value::{Dynamic, Type, Value}; use std::collections::HashSet; use std::mem; @@ -47,6 +61,7 @@ use comemo::{Track, Tracked, TrackedMut, Validate}; use ecow::{EcoString, EcoVec}; use unicode_segmentation::UnicodeSegmentation; +use self::func::{CapturesVisitor, Closure}; use crate::diag::{ bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint, }; @@ -214,8 +229,8 @@ impl<'a> Vm<'a> { /// Define a variable in the current scope. #[tracing::instrument(skip_all)] - pub fn define(&mut self, var: ast::Ident, value: impl Into<Value>) { - let value = value.into(); + pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) { + let value = value.into_value(); if self.traced == Some(var.span()) { self.vt.tracer.trace(value.clone()); } @@ -934,7 +949,7 @@ impl Eval for ast::Array { } } - Ok(Array::from_vec(vec)) + Ok(vec.into()) } } @@ -965,7 +980,7 @@ impl Eval for ast::Dict { } } - Ok(Dict::from_map(map)) + Ok(map.into()) } } @@ -1285,23 +1300,23 @@ impl ast::Pattern { for p in destruct.bindings() { match p { ast::DestructuringKind::Normal(expr) => { - let Ok(v) = value.at(i, None) else { + let Ok(v) = value.at(i as i64, None) else { bail!(expr.span(), "not enough elements to destructure"); }; f(vm, expr, v.clone())?; i += 1; } ast::DestructuringKind::Sink(spread) => { - let sink_size = (1 + value.len() as usize) - .checked_sub(destruct.bindings().count()); - let sink = - sink_size.and_then(|s| value.slice(i, Some(i + s as i64)).ok()); + let sink_size = + (1 + value.len()).checked_sub(destruct.bindings().count()); + let sink = sink_size + .and_then(|s| value.slice(i as i64, Some((i + s) as i64)).ok()); if let (Some(sink_size), Some(sink)) = (sink_size, sink) { if let Some(expr) = spread.expr() { f(vm, expr, Value::Array(sink.clone()))?; } - i += sink_size as i64; + i += sink_size; } else { bail!(self.span(), "not enough elements to destructure") } @@ -1590,7 +1605,7 @@ impl Eval for ast::ForLoop { #[allow(unused_parens)] for value in $iter { - $pat.define(vm, Value::from(value))?; + $pat.define(vm, value.into_value())?; let body = self.body(); let value = body.eval(vm)?; @@ -1644,7 +1659,7 @@ impl Eval for ast::ForLoop { } /// Applies imports from `import` to the current scope. -fn apply_imports<V: Into<Value>>( +fn apply_imports<V: IntoValue>( imports: Option<ast::Imports>, vm: &mut Vm, source_value: V, diff --git a/src/eval/none.rs b/src/eval/none.rs new file mode 100644 index 00000000..ab7644a7 --- /dev/null +++ b/src/eval/none.rs @@ -0,0 +1,74 @@ +use std::fmt::{self, Debug, Formatter}; + +use super::{cast, CastInfo, FromValue, IntoValue, Reflect, Value}; +use crate::diag::StrResult; + +/// A value that indicates the absence of any other value. +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct NoneValue; + +impl Reflect for NoneValue { + fn describe() -> CastInfo { + CastInfo::Type("none") + } + + fn castable(value: &Value) -> bool { + matches!(value, Value::None) + } +} + +impl IntoValue for NoneValue { + fn into_value(self) -> Value { + Value::None + } +} + +impl FromValue for NoneValue { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::None => Ok(Self), + _ => Err(Self::error(&value)), + } + } +} + +impl Debug for NoneValue { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("none") + } +} + +cast! { + (), + self => Value::None, + _: NoneValue => (), +} + +impl<T: Reflect> Reflect for Option<T> { + fn describe() -> CastInfo { + T::describe() + NoneValue::describe() + } + + fn castable(value: &Value) -> bool { + NoneValue::castable(value) || T::castable(value) + } +} + +impl<T: IntoValue> IntoValue for Option<T> { + fn into_value(self) -> Value { + match self { + Some(v) => v.into_value(), + None => Value::None, + } + } +} + +impl<T: FromValue> FromValue for Option<T> { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::None => Ok(None), + v if T::castable(&v) => Ok(Some(T::from_value(v)?)), + _ => Err(Self::error(&value)), + } + } +} diff --git a/src/eval/ops.rs b/src/eval/ops.rs index 3e397c30..bea2ea82 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -209,8 +209,8 @@ pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> { (Int(a), Str(b)) => Str(b.repeat(a)?), (Array(a), Int(b)) => Array(a.repeat(b)?), (Int(a), Array(b)) => Array(b.repeat(a)?), - (Content(a), Int(b)) => Content(a.repeat(b)?), - (Int(a), Content(b)) => Content(b.repeat(a)?), + (Content(a), b @ Int(_)) => Content(a.repeat(b.cast()?)), + (a @ Int(_), Content(b)) => Content(b.repeat(a.cast()?)), (a, b) => mismatch!("cannot multiply {} with {}", a, b), }) diff --git a/src/eval/scope.rs b/src/eval/scope.rs index 83d1703e..bc62feb1 100644 --- a/src/eval/scope.rs +++ b/src/eval/scope.rs @@ -4,7 +4,7 @@ use std::hash::Hash; use ecow::{eco_format, EcoString}; -use super::{Library, Value}; +use super::{IntoValue, Library, Value}; use crate::diag::StrResult; /// A stack of scopes. @@ -95,7 +95,7 @@ impl Scope { /// Bind a value to a name. #[track_caller] - pub fn define(&mut self, name: impl Into<EcoString>, value: impl Into<Value>) { + pub fn define(&mut self, name: impl Into<EcoString>, value: impl IntoValue) { let name = name.into(); #[cfg(debug_assertions)] @@ -103,16 +103,13 @@ impl Scope { panic!("duplicate definition: {name}"); } - self.0.insert(name, Slot::new(value.into(), Kind::Normal)); + self.0.insert(name, Slot::new(value.into_value(), Kind::Normal)); } /// Define a captured, immutable binding. - pub fn define_captured( - &mut self, - var: impl Into<EcoString>, - value: impl Into<Value>, - ) { - self.0.insert(var.into(), Slot::new(value.into(), Kind::Captured)); + pub fn define_captured(&mut self, var: impl Into<EcoString>, value: impl IntoValue) { + self.0 + .insert(var.into(), Slot::new(value.into_value(), Kind::Captured)); } /// Try to access a variable immutably. diff --git a/src/eval/str.rs b/src/eval/str.rs index 567ee5bd..48ba8311 100644 --- a/src/eval/str.rs +++ b/src/eval/str.rs @@ -6,9 +6,8 @@ use std::ops::{Add, AddAssign, Deref, Range}; use ecow::EcoString; use unicode_segmentation::UnicodeSegmentation; -use super::{cast_from_value, dict, Array, Dict, Func, Value, Vm}; +use super::{cast, dict, Args, Array, Dict, Func, IntoValue, Value, Vm}; use crate::diag::{At, SourceResult, StrResult}; -use crate::eval::Args; use crate::geom::GenAlign; /// Create a new [`Str`] from a format string. @@ -41,8 +40,8 @@ impl Str { } /// The length of the string in bytes. - pub fn len(&self) -> i64 { - self.0.len() as i64 + pub fn len(&self) -> usize { + self.0.len() } /// A string slice containing the entire string. @@ -82,7 +81,7 @@ impl Str { /// Extract a contiguous substring. pub fn slice(&self, start: i64, end: Option<i64>) -> StrResult<Self> { let start = self.locate(start)?; - let end = self.locate(end.unwrap_or(self.len()))?.max(start); + let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); Ok(self.0[start..end].into()) } @@ -283,7 +282,7 @@ impl Str { match &with { Replacement::Str(s) => output.push_str(s), Replacement::Func(func) => { - let args = Args::new(func.span(), [dict.into()]); + let args = Args::new(func.span(), [dict]); let piece = func.call_vm(vm, args)?.cast::<Str>().at(func.span())?; output.push_str(&piece); } @@ -329,7 +328,7 @@ impl Str { /// Errors on invalid char boundaries. fn locate_opt(&self, index: i64) -> StrResult<Option<usize>> { let wrapped = - if index >= 0 { Some(index) } else { self.len().checked_add(index) }; + if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; let resolved = wrapped .and_then(|v| usize::try_from(v).ok()) @@ -351,13 +350,13 @@ impl Str { /// The out of bounds access error message. #[cold] -fn out_of_bounds(index: i64, len: i64) -> EcoString { +fn out_of_bounds(index: i64, len: usize) -> EcoString { eco_format!("string index out of bounds (index: {}, len: {})", index, len) } /// The out of bounds access error message when no default value was given. #[cold] -fn no_default_and_out_of_bounds(index: i64, len: i64) -> EcoString { +fn no_default_and_out_of_bounds(index: i64, len: usize) -> EcoString { eco_format!("no default value was specified and string index out of bounds (index: {}, len: {})", index, len) } @@ -376,10 +375,10 @@ fn string_is_empty() -> EcoString { /// Convert an item of std's `match_indices` to a dictionary. fn match_to_dict((start, text): (usize, &str)) -> Dict { dict! { - "start" => Value::Int(start as i64), - "end" => Value::Int((start + text.len()) as i64), - "text" => Value::Str(text.into()), - "captures" => Value::Array(Array::new()), + "start" => start, + "end" => start + text.len(), + "text" => text, + "captures" => Array::new(), } } @@ -387,15 +386,13 @@ fn match_to_dict((start, text): (usize, &str)) -> Dict { fn captures_to_dict(cap: regex::Captures) -> Dict { let m = cap.get(0).expect("missing first match"); dict! { - "start" => Value::Int(m.start() as i64), - "end" => Value::Int(m.end() as i64), - "text" => Value::Str(m.as_str().into()), - "captures" => Value::Array( - cap.iter() - .skip(1) - .map(|opt| opt.map_or(Value::None, |m| m.as_str().into())) - .collect(), - ), + "start" => m.start(), + "end" => m.end(), + "text" => m.as_str(), + "captures" => cap.iter() + .skip(1) + .map(|opt| opt.map_or(Value::None, |m| m.as_str().into_value())) + .collect::<Array>(), } } @@ -503,6 +500,35 @@ impl From<Str> for String { } } +cast! { + char, + self => Value::Str(self.into()), + string: Str => { + let mut chars = string.chars(); + match (chars.next(), chars.next()) { + (Some(c), None) => c, + _ => Err("expected exactly one character")?, + } + }, +} + +cast! { + &str, + self => Value::Str(self.into()), +} + +cast! { + EcoString, + self => Value::Str(self.into()), + v: Str => v.into(), +} + +cast! { + String, + self => Value::Str(self.into()), + v: Str => v.into(), +} + /// A regular expression. #[derive(Clone)] pub struct Regex(regex::Regex); @@ -540,8 +566,8 @@ impl Hash for Regex { } } -cast_from_value! { - Regex: "regex", +cast! { + type Regex: "regex", } /// A pattern which can be searched for in a string. @@ -553,7 +579,7 @@ pub enum StrPattern { Regex(Regex), } -cast_from_value! { +cast! { StrPattern, text: Str => Self::Str(text), regex: Regex => Self::Regex(regex), @@ -569,7 +595,7 @@ pub enum StrSide { End, } -cast_from_value! { +cast! { StrSide, align: GenAlign => match align { GenAlign::Start => Self::Start, @@ -587,7 +613,7 @@ pub enum Replacement { Func(Func), } -cast_from_value! { +cast! { Replacement, text: Str => Self::Str(text), func: Func => Self::Func(func) diff --git a/src/eval/symbol.rs b/src/eval/symbol.rs index 5c8951b1..8f4c93f8 100644 --- a/src/eval/symbol.rs +++ b/src/eval/symbol.rs @@ -7,9 +7,6 @@ use ecow::EcoString; use crate::diag::StrResult; -#[doc(inline)] -pub use typst_macros::symbols; - /// A symbol, possibly with variants. #[derive(Clone, Eq, PartialEq, Hash)] pub struct Symbol(Repr); diff --git a/src/eval/value.rs b/src/eval/value.rs index 6d947866..91fdadbe 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -8,12 +8,12 @@ use ecow::eco_format; use siphasher::sip128::{Hasher128, SipHasher13}; use super::{ - cast_to_value, format_str, ops, Args, Array, Cast, CastInfo, Content, Dict, Func, - Label, Module, Str, Symbol, + cast, format_str, ops, Args, Array, CastInfo, Content, Dict, FromValue, Func, + IntoValue, Module, Reflect, Str, Symbol, }; use crate::diag::StrResult; use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel}; -use crate::model::Styles; +use crate::model::{Label, Styles}; use crate::syntax::{ast, Span}; /// A computational value. @@ -79,11 +79,11 @@ impl Value { pub fn numeric(pair: (f64, ast::Unit)) -> Self { let (v, unit) = pair; match unit { - ast::Unit::Length(unit) => Abs::with_unit(v, unit).into(), - ast::Unit::Angle(unit) => Angle::with_unit(v, unit).into(), - ast::Unit::Em => Em::new(v).into(), - ast::Unit::Fr => Fr::new(v).into(), - ast::Unit::Percent => Ratio::new(v / 100.0).into(), + ast::Unit::Length(unit) => Abs::with_unit(v, unit).into_value(), + ast::Unit::Angle(unit) => Angle::with_unit(v, unit).into_value(), + ast::Unit::Em => Em::new(v).into_value(), + ast::Unit::Fr => Fr::new(v).into_value(), + ast::Unit::Percent => Ratio::new(v / 100.0).into_value(), } } @@ -116,8 +116,8 @@ impl Value { } /// Try to cast the value into a specific type. - pub fn cast<T: Cast>(self) -> StrResult<T> { - T::cast(self) + pub fn cast<T: FromValue>(self) -> StrResult<T> { + T::from_value(self) } /// Try to access a field on the value. @@ -283,8 +283,9 @@ impl PartialEq for Dynamic { } } -cast_to_value! { - v: Dynamic => Value::Dyn(v) +cast! { + Dynamic, + self => Value::Dyn(self), } trait Bounds: Debug + Sync + Send + 'static { @@ -337,20 +338,32 @@ pub trait Type { /// Implement traits for primitives. macro_rules! primitive { ( - $type:ty: $name:literal, $variant:ident + $ty:ty: $name:literal, $variant:ident $(, $other:ident$(($binding:ident))? => $out:expr)* ) => { - impl Type for $type { + impl Type for $ty { const TYPE_NAME: &'static str = $name; } - impl Cast for $type { - fn is(value: &Value) -> bool { + impl Reflect for $ty { + fn describe() -> CastInfo { + CastInfo::Type(Self::TYPE_NAME) + } + + fn castable(value: &Value) -> bool { matches!(value, Value::$variant(_) $(| primitive!(@$other $(($binding))?))*) } + } - fn cast(value: Value) -> StrResult<Self> { + impl IntoValue for $ty { + fn into_value(self) -> Value { + Value::$variant(self) + } + } + + impl FromValue for $ty { + fn from_value(value: Value) -> StrResult<Self> { match value { Value::$variant(v) => Ok(v), $(Value::$other$(($binding))? => Ok($out),)* @@ -361,16 +374,6 @@ macro_rules! primitive { )), } } - - fn describe() -> CastInfo { - CastInfo::Type(Self::TYPE_NAME) - } - } - - impl From<$type> for Value { - fn from(v: $type) -> Self { - Value::$variant(v) - } } }; @@ -408,8 +411,8 @@ primitive! { Styles: "styles", Styles } primitive! { Array: "array", Array } primitive! { Dict: "dictionary", Dict } primitive! { Func: "function", Func } -primitive! { Module: "module", Module } primitive! { Args: "arguments", Args } +primitive! { Module: "module", Module } #[cfg(test)] mod tests { @@ -418,8 +421,8 @@ mod tests { use crate::geom::RgbaColor; #[track_caller] - fn test(value: impl Into<Value>, exp: &str) { - assert_eq!(format!("{:?}", value.into()), exp); + fn test(value: impl IntoValue, exp: &str) { + assert_eq!(format!("{:?}", value.into_value()), exp); } #[test] diff --git a/src/font/mod.rs b/src/font/mod.rs index bfb790bb..dbd7dd60 100644 --- a/src/font/mod.rs +++ b/src/font/mod.rs @@ -3,8 +3,8 @@ mod book; mod variant; -pub use self::book::*; -pub use self::variant::*; +pub use self::book::{FontBook, FontFlags, FontInfo}; +pub use self::variant::{FontStretch, FontStyle, FontVariant, FontWeight}; use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; @@ -12,6 +12,7 @@ use std::sync::Arc; use ttf_parser::GlyphId; +use self::book::find_name; use crate::eval::Cast; use crate::geom::Em; use crate::util::Buffer; diff --git a/src/font/variant.rs b/src/font/variant.rs index 9391aae1..d4508a59 100644 --- a/src/font/variant.rs +++ b/src/font/variant.rs @@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter}; use serde::{Deserialize, Serialize}; -use crate::eval::{cast_from_value, cast_to_value, Cast, Value}; +use crate::eval::{cast, Cast, IntoValue}; use crate::geom::Ratio; /// Properties that distinguish a font from other fonts in the same family. @@ -130,8 +130,20 @@ impl Debug for FontWeight { } } -cast_from_value! { +cast! { FontWeight, + self => IntoValue::into_value(match self { + FontWeight::THIN => "thin", + FontWeight::EXTRALIGHT => "extralight", + FontWeight::LIGHT => "light", + FontWeight::REGULAR => "regular", + FontWeight::MEDIUM => "medium", + FontWeight::SEMIBOLD => "semibold", + FontWeight::BOLD => "bold", + FontWeight::EXTRABOLD => "extrabold", + FontWeight::BLACK => "black", + _ => return self.to_number().into_value(), + }), v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16), /// Thin weight (100). "thin" => Self::THIN, @@ -153,21 +165,6 @@ cast_from_value! { "black" => Self::BLACK, } -cast_to_value! { - v: FontWeight => Value::from(match v { - FontWeight::THIN => "thin", - FontWeight::EXTRALIGHT => "extralight", - FontWeight::LIGHT => "light", - FontWeight::REGULAR => "regular", - FontWeight::MEDIUM => "medium", - FontWeight::SEMIBOLD => "semibold", - FontWeight::BOLD => "bold", - FontWeight::EXTRABOLD => "extrabold", - FontWeight::BLACK => "black", - _ => return v.to_number().into(), - }) -} - /// The width of a font. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Serialize, Deserialize)] @@ -247,15 +244,12 @@ impl Debug for FontStretch { } } -cast_from_value! { +cast! { FontStretch, + self => self.to_ratio().into_value(), v: Ratio => Self::from_ratio(v), } -cast_to_value! { - v: FontStretch => v.to_ratio().into() -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/geom/abs.rs b/src/geom/abs.rs index 34c3d010..4ca3a9a1 100644 --- a/src/geom/abs.rs +++ b/src/geom/abs.rs @@ -214,8 +214,9 @@ impl<'a> Sum<&'a Self> for Abs { } } -cast_to_value! { - v: Abs => Value::Length(v.into()) +cast! { + Abs, + self => Value::Length(self.into()), } /// Different units of absolute measurement. diff --git a/src/geom/align.rs b/src/geom/align.rs index 42fc493e..dca35891 100644 --- a/src/geom/align.rs +++ b/src/geom/align.rs @@ -128,16 +128,27 @@ impl Debug for GenAlign { } } -cast_from_value! { - GenAlign: "alignment", +cast! { + type GenAlign: "alignment", } -cast_from_value! { - Axes<GenAlign>: "2d alignment", +cast! { + type Axes<GenAlign>: "2d alignment", } -cast_from_value! { +cast! { + Axes<Align>, + self => self.map(GenAlign::from).into_value(), +} + +cast! { Axes<Option<GenAlign>>, + self => match (self.x, self.y) { + (Some(x), Some(y)) => Axes::new(x, y).into_value(), + (Some(x), None) => x.into_value(), + (None, Some(y)) => y.into_value(), + (None, None) => Value::None, + }, align: GenAlign => { let mut aligns = Axes::default(); aligns.set(align.axis(), Some(align)); @@ -146,19 +157,6 @@ cast_from_value! { aligns: Axes<GenAlign> => aligns.map(Some), } -cast_to_value! { - v: Axes<Align> => v.map(GenAlign::from).into() -} - -cast_to_value! { - v: Axes<Option<GenAlign>> => match (v.x, v.y) { - (Some(x), Some(y)) => Axes::new(x, y).into(), - (Some(x), None) => x.into(), - (None, Some(y)) => y.into(), - (None, None) => Value::None, - } -} - impl From<Axes<GenAlign>> for Axes<Option<GenAlign>> { fn from(axes: Axes<GenAlign>) -> Self { axes.map(Some) @@ -213,8 +211,9 @@ impl Fold for Align { #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct HorizontalAlign(pub GenAlign); -cast_from_value! { +cast! { HorizontalAlign, + self => self.0.into_value(), align: GenAlign => { if align.axis() != Axis::X { Err("alignment must be horizontal")?; @@ -223,17 +222,14 @@ cast_from_value! { }, } -cast_to_value! { - v: HorizontalAlign => v.0.into() -} - /// Utility struct to restrict a passed alignment value to the vertical axis on /// cast. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct VerticalAlign(pub GenAlign); -cast_from_value! { +cast! { VerticalAlign, + self => self.0.into_value(), align: GenAlign => { if align.axis() != Axis::Y { Err("alignment must be vertical")?; @@ -241,26 +237,3 @@ cast_from_value! { Self(align) }, } - -cast_to_value! { - v: VerticalAlign => v.0.into() -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum LeftRightAlternator { - Left, - Right, -} - -impl Iterator for LeftRightAlternator { - type Item = LeftRightAlternator; - - fn next(&mut self) -> Option<Self::Item> { - let r = Some(*self); - match self { - Self::Left => *self = Self::Right, - Self::Right => *self = Self::Left, - } - r - } -} diff --git a/src/geom/axes.rs b/src/geom/axes.rs index 511e6ff5..35c94129 100644 --- a/src/geom/axes.rs +++ b/src/geom/axes.rs @@ -2,7 +2,6 @@ use std::any::Any; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; use super::*; -use crate::eval::Array; /// A container with a horizontal and vertical component. #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] @@ -274,8 +273,9 @@ impl BitAndAssign for Axes<bool> { } } -cast_from_value! { +cast! { Axes<Rel<Length>>, + self => array![self.x, self.y].into_value(), array: Array => { let mut iter = array.into_iter(); match (iter.next(), iter.next(), iter.next()) { @@ -285,10 +285,6 @@ cast_from_value! { }, } -cast_to_value! { - v: Axes<Rel<Length>> => Value::Array(array![v.x, v.y]) -} - impl<T: Resolve> Resolve for Axes<T> { type Output = Axes<T::Output>; diff --git a/src/geom/color.rs b/src/geom/color.rs new file mode 100644 index 00000000..c7676c2b --- /dev/null +++ b/src/geom/color.rs @@ -0,0 +1,386 @@ +use std::str::FromStr; + +use super::*; + +/// A color in a dynamic format. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub enum Color { + /// An 8-bit luma color. + Luma(LumaColor), + /// An 8-bit RGBA color. + Rgba(RgbaColor), + /// An 8-bit CMYK color. + Cmyk(CmykColor), +} + +impl Color { + pub const BLACK: Self = Self::Rgba(RgbaColor::new(0x00, 0x00, 0x00, 0xFF)); + pub const GRAY: Self = Self::Rgba(RgbaColor::new(0xAA, 0xAA, 0xAA, 0xFF)); + pub const SILVER: Self = Self::Rgba(RgbaColor::new(0xDD, 0xDD, 0xDD, 0xFF)); + pub const WHITE: Self = Self::Rgba(RgbaColor::new(0xFF, 0xFF, 0xFF, 0xFF)); + pub const NAVY: Self = Self::Rgba(RgbaColor::new(0x00, 0x1f, 0x3f, 0xFF)); + pub const BLUE: Self = Self::Rgba(RgbaColor::new(0x00, 0x74, 0xD9, 0xFF)); + pub const AQUA: Self = Self::Rgba(RgbaColor::new(0x7F, 0xDB, 0xFF, 0xFF)); + pub const TEAL: Self = Self::Rgba(RgbaColor::new(0x39, 0xCC, 0xCC, 0xFF)); + pub const EASTERN: Self = Self::Rgba(RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); + pub const PURPLE: Self = Self::Rgba(RgbaColor::new(0xB1, 0x0D, 0xC9, 0xFF)); + pub const FUCHSIA: Self = Self::Rgba(RgbaColor::new(0xF0, 0x12, 0xBE, 0xFF)); + pub const MAROON: Self = Self::Rgba(RgbaColor::new(0x85, 0x14, 0x4b, 0xFF)); + pub const RED: Self = Self::Rgba(RgbaColor::new(0xFF, 0x41, 0x36, 0xFF)); + pub const ORANGE: Self = Self::Rgba(RgbaColor::new(0xFF, 0x85, 0x1B, 0xFF)); + pub const YELLOW: Self = Self::Rgba(RgbaColor::new(0xFF, 0xDC, 0x00, 0xFF)); + pub const OLIVE: Self = Self::Rgba(RgbaColor::new(0x3D, 0x99, 0x70, 0xFF)); + pub const GREEN: Self = Self::Rgba(RgbaColor::new(0x2E, 0xCC, 0x40, 0xFF)); + pub const LIME: Self = Self::Rgba(RgbaColor::new(0x01, 0xFF, 0x70, 0xFF)); + + /// Convert this color to RGBA. + pub fn to_rgba(self) -> RgbaColor { + match self { + Self::Luma(luma) => luma.to_rgba(), + Self::Rgba(rgba) => rgba, + Self::Cmyk(cmyk) => cmyk.to_rgba(), + } + } + + /// Lighten this color by the given factor. + pub fn lighten(self, factor: Ratio) -> Self { + match self { + Self::Luma(luma) => Self::Luma(luma.lighten(factor)), + Self::Rgba(rgba) => Self::Rgba(rgba.lighten(factor)), + Self::Cmyk(cmyk) => Self::Cmyk(cmyk.lighten(factor)), + } + } + + /// Darken this color by the given factor. + pub fn darken(self, factor: Ratio) -> Self { + match self { + Self::Luma(luma) => Self::Luma(luma.darken(factor)), + Self::Rgba(rgba) => Self::Rgba(rgba.darken(factor)), + Self::Cmyk(cmyk) => Self::Cmyk(cmyk.darken(factor)), + } + } + + /// Negate this color. + pub fn negate(self) -> Self { + match self { + Self::Luma(luma) => Self::Luma(luma.negate()), + Self::Rgba(rgba) => Self::Rgba(rgba.negate()), + Self::Cmyk(cmyk) => Self::Cmyk(cmyk.negate()), + } + } +} + +impl Debug for Color { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Luma(c) => Debug::fmt(c, f), + Self::Rgba(c) => Debug::fmt(c, f), + Self::Cmyk(c) => Debug::fmt(c, f), + } + } +} + +/// An 8-bit grayscale color. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct LumaColor(pub u8); + +impl LumaColor { + /// Construct a new luma color. + pub const fn new(luma: u8) -> Self { + Self(luma) + } + + /// Convert to an opque RGBA color. + pub const fn to_rgba(self) -> RgbaColor { + RgbaColor::new(self.0, self.0, self.0, u8::MAX) + } + + /// Convert to CMYK as a fraction of true black. + pub fn to_cmyk(self) -> CmykColor { + CmykColor::new( + round_u8(self.0 as f64 * 0.75), + round_u8(self.0 as f64 * 0.68), + round_u8(self.0 as f64 * 0.67), + round_u8(self.0 as f64 * 0.90), + ) + } + + /// Lighten this color by a factor. + pub fn lighten(self, factor: Ratio) -> Self { + let inc = round_u8((u8::MAX - self.0) as f64 * factor.get()); + Self(self.0.saturating_add(inc)) + } + + /// Darken this color by a factor. + pub fn darken(self, factor: Ratio) -> Self { + let dec = round_u8(self.0 as f64 * factor.get()); + Self(self.0.saturating_sub(dec)) + } + + /// Negate this color. + pub fn negate(self) -> Self { + Self(u8::MAX - self.0) + } +} + +impl Debug for LumaColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "luma({})", self.0) + } +} + +impl From<LumaColor> for Color { + fn from(luma: LumaColor) -> Self { + Self::Luma(luma) + } +} + +/// An 8-bit RGBA color. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct RgbaColor { + /// Red channel. + pub r: u8, + /// Green channel. + pub g: u8, + /// Blue channel. + pub b: u8, + /// Alpha channel. + pub a: u8, +} + +impl RgbaColor { + /// Construct a new RGBA color. + pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { r, g, b, a } + } + + /// Lighten this color by a factor. + /// + /// The alpha channel is not affected. + pub fn lighten(self, factor: Ratio) -> Self { + let lighten = + |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); + Self { + r: lighten(self.r), + g: lighten(self.g), + b: lighten(self.b), + a: self.a, + } + } + + /// Darken this color by a factor. + /// + /// The alpha channel is not affected. + pub fn darken(self, factor: Ratio) -> Self { + let darken = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); + Self { + r: darken(self.r), + g: darken(self.g), + b: darken(self.b), + a: self.a, + } + } + + /// Negate this color. + /// + /// The alpha channel is not affected. + pub fn negate(self) -> Self { + Self { + r: u8::MAX - self.r, + g: u8::MAX - self.g, + b: u8::MAX - self.b, + a: self.a, + } + } +} + +impl FromStr for RgbaColor { + type Err = &'static str; + + /// Constructs a new color from hex strings like the following: + /// - `#aef` (shorthand, with leading hashtag), + /// - `7a03c2` (without alpha), + /// - `abcdefff` (with alpha). + /// + /// The hashtag is optional and both lower and upper case are fine. + fn from_str(hex_str: &str) -> Result<Self, Self::Err> { + let hex_str = hex_str.strip_prefix('#').unwrap_or(hex_str); + if hex_str.chars().any(|c| !c.is_ascii_hexdigit()) { + return Err("color string contains non-hexadecimal letters"); + } + + let len = hex_str.len(); + let long = len == 6 || len == 8; + let short = len == 3 || len == 4; + let alpha = len == 4 || len == 8; + if !long && !short { + return Err("color string has wrong length"); + } + + let mut values: [u8; 4] = [u8::MAX; 4]; + for elem in if alpha { 0..4 } else { 0..3 } { + let item_len = if long { 2 } else { 1 }; + let pos = elem * item_len; + + let item = &hex_str[pos..(pos + item_len)]; + values[elem] = u8::from_str_radix(item, 16).unwrap(); + + if short { + // Duplicate number for shorthand notation, i.e. `a` -> `aa` + values[elem] += values[elem] * 16; + } + } + + Ok(Self::new(values[0], values[1], values[2], values[3])) + } +} + +impl Debug for RgbaColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?; + } else { + write!(f, "rgb(\"#{:02x}{:02x}{:02x}", self.r, self.g, self.b)?; + if self.a != 255 { + write!(f, "{:02x}", self.a)?; + } + write!(f, "\")")?; + } + Ok(()) + } +} + +impl<T: Into<RgbaColor>> From<T> for Color { + fn from(rgba: T) -> Self { + Self::Rgba(rgba.into()) + } +} + +cast! { + RgbaColor, + self => Value::Color(self.into()), +} + +/// An 8-bit CMYK color. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct CmykColor { + /// The cyan component. + pub c: u8, + /// The magenta component. + pub m: u8, + /// The yellow component. + pub y: u8, + /// The key (black) component. + pub k: u8, +} + +impl CmykColor { + /// Construct a new CMYK color. + pub const fn new(c: u8, m: u8, y: u8, k: u8) -> Self { + Self { c, m, y, k } + } + + /// Convert this color to RGBA. + pub fn to_rgba(self) -> RgbaColor { + let k = self.k as f64 / 255.0; + let f = |c| { + let c = c as f64 / 255.0; + round_u8(255.0 * (1.0 - c) * (1.0 - k)) + }; + + RgbaColor { r: f(self.c), g: f(self.m), b: f(self.y), a: 255 } + } + + /// Lighten this color by a factor. + pub fn lighten(self, factor: Ratio) -> Self { + let lighten = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); + Self { + c: lighten(self.c), + m: lighten(self.m), + y: lighten(self.y), + k: lighten(self.k), + } + } + + /// Darken this color by a factor. + pub fn darken(self, factor: Ratio) -> Self { + let darken = + |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); + Self { + c: darken(self.c), + m: darken(self.m), + y: darken(self.y), + k: darken(self.k), + } + } + + /// Negate this color. + /// + /// Does not affect the key component. + pub fn negate(self) -> Self { + Self { + c: u8::MAX - self.c, + m: u8::MAX - self.m, + y: u8::MAX - self.y, + k: self.k, + } + } +} + +impl Debug for CmykColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let g = |c| 100.0 * (c as f64 / 255.0); + write!( + f, + "cmyk({:.1}%, {:.1}%, {:.1}%, {:.1}%)", + g(self.c), + g(self.m), + g(self.y), + g(self.k), + ) + } +} + +impl From<CmykColor> for Color { + fn from(cmyk: CmykColor) -> Self { + Self::Cmyk(cmyk) + } +} + +/// Convert to the closest u8. +fn round_u8(value: f64) -> u8 { + value.round() as u8 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_color_strings() { + #[track_caller] + fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) { + assert_eq!(RgbaColor::from_str(hex), Ok(RgbaColor::new(r, g, b, a))); + } + + test("f61243ff", 0xf6, 0x12, 0x43, 0xff); + test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff); + test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad); + test("233", 0x22, 0x33, 0x33, 0xff); + test("111b", 0x11, 0x11, 0x11, 0xbb); + } + + #[test] + fn test_parse_invalid_colors() { + #[track_caller] + fn test(hex: &str, message: &str) { + assert_eq!(RgbaColor::from_str(hex), Err(message)); + } + + test("a5", "color string has wrong length"); + test("12345", "color string has wrong length"); + test("f075ff011", "color string has wrong length"); + test("hmmm", "color string contains non-hexadecimal letters"); + test("14B2AH", "color string contains non-hexadecimal letters"); + } +} diff --git a/src/geom/corners.rs b/src/geom/corners.rs index 20e9bed0..5ee1e063 100644 --- a/src/geom/corners.rs +++ b/src/geom/corners.rs @@ -1,3 +1,5 @@ +use crate::eval::{CastInfo, FromValue, IntoValue, Reflect}; + use super::*; /// A container with components for the four corners of a rectangle. @@ -108,15 +110,47 @@ pub enum Corner { BottomLeft, } -impl<T> Cast for Corners<Option<T>> +impl<T: Reflect> Reflect for Corners<Option<T>> { + fn describe() -> CastInfo { + T::describe() + Dict::describe() + } + + fn castable(value: &Value) -> bool { + Dict::castable(value) || T::castable(value) + } +} + +impl<T> IntoValue for Corners<T> where - T: Cast + Clone, + T: PartialEq + IntoValue, { - fn is(value: &Value) -> bool { - matches!(value, Value::Dict(_)) || T::is(value) + fn into_value(self) -> Value { + if self.is_uniform() { + return self.top_left.into_value(); + } + + let mut dict = Dict::new(); + let mut handle = |key: &str, component: T| { + let value = component.into_value(); + if value != Value::None { + dict.insert(key.into(), value); + } + }; + + handle("top-left", self.top_left); + handle("top-right", self.top_right); + handle("bottom-right", self.bottom_right); + handle("bottom-left", self.bottom_left); + + Value::Dict(dict) } +} - fn cast(mut value: Value) -> StrResult<Self> { +impl<T> FromValue for Corners<Option<T>> +where + T: FromValue + Clone, +{ + fn from_value(mut value: Value) -> StrResult<Self> { let keys = [ "top-left", "top-right", @@ -131,7 +165,7 @@ where if let Value::Dict(dict) = &mut value { if dict.iter().any(|(key, _)| keys.contains(&key.as_str())) { - let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); + let mut take = |key| dict.take(key).ok().map(T::from_value).transpose(); let rest = take("rest")?; let left = take("left")?.or_else(|| rest.clone()); let top = take("top")?.or_else(|| rest.clone()); @@ -157,16 +191,12 @@ where } } - if T::is(&value) { - Ok(Self::splat(Some(T::cast(value)?))) + if T::castable(&value) { + Ok(Self::splat(Some(T::from_value(value)?))) } else { - <Self as Cast>::error(value) + Err(Self::error(&value)) } } - - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("dictionary") - } } impl<T: Resolve> Resolve for Corners<T> { @@ -187,29 +217,3 @@ impl<T: Fold> Fold for Corners<Option<T>> { }) } } - -impl<T> From<Corners<T>> for Value -where - T: PartialEq + Into<Value>, -{ - fn from(corners: Corners<T>) -> Self { - if corners.is_uniform() { - return corners.top_left.into(); - } - - let mut dict = Dict::new(); - let mut handle = |key: &str, component: T| { - let value = component.into(); - if value != Value::None { - dict.insert(key.into(), value); - } - }; - - handle("top-left", corners.top_left); - handle("top-right", corners.top_right); - handle("bottom-right", corners.bottom_right); - handle("bottom-left", corners.bottom_left); - - Value::Dict(dict) - } -} diff --git a/src/geom/dir.rs b/src/geom/dir.rs index bc4d66e1..48915471 100644 --- a/src/geom/dir.rs +++ b/src/geom/dir.rs @@ -74,6 +74,6 @@ impl Debug for Dir { } } -cast_from_value! { - Dir: "direction", +cast! { + type Dir: "direction", } diff --git a/src/geom/em.rs b/src/geom/em.rs index 2c63c81d..8dda9ff6 100644 --- a/src/geom/em.rs +++ b/src/geom/em.rs @@ -135,8 +135,9 @@ impl Sum for Em { } } -cast_to_value! { - v: Em => Value::Length(v.into()) +cast! { + Em, + self => Value::Length(self.into()), } impl Resolve for Em { diff --git a/src/geom/length.rs b/src/geom/length.rs index 9d6552da..7d0a9841 100644 --- a/src/geom/length.rs +++ b/src/geom/length.rs @@ -1,6 +1,6 @@ use super::*; -/// A length, possibly expressed with contextual units. +/// A size or distance, possibly expressed with contextual units. /// /// Currently supports absolute and font-relative units, but support could quite /// easily be extended to other units. diff --git a/src/geom/mod.rs b/src/geom/mod.rs index 8896c24c..4a9ecfe1 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -6,6 +6,7 @@ mod abs; mod align; mod angle; mod axes; +mod color; mod corners; mod dir; mod ellipse; @@ -26,29 +27,32 @@ mod smart; mod stroke; mod transform; -pub use self::abs::*; -pub use self::align::*; -pub use self::angle::*; -pub use self::axes::*; -pub use self::corners::*; -pub use self::dir::*; -pub use self::ellipse::*; -pub use self::em::*; -pub use self::fr::*; -pub use self::length::*; -pub use self::paint::*; -pub use self::path::*; -pub use self::point::*; -pub use self::ratio::*; -pub use self::rel::*; -pub use self::rounded::*; -pub use self::scalar::*; -pub use self::shape::*; -pub use self::sides::*; -pub use self::size::*; -pub use self::smart::*; -pub use self::stroke::*; -pub use self::transform::*; +pub use self::abs::{Abs, AbsUnit}; +pub use self::align::{Align, GenAlign, HorizontalAlign, VerticalAlign}; +pub use self::angle::{Angle, AngleUnit}; +pub use self::axes::{Axes, Axis}; +pub use self::color::{CmykColor, Color, LumaColor, RgbaColor}; +pub use self::corners::{Corner, Corners}; +pub use self::dir::Dir; +pub use self::ellipse::ellipse; +pub use self::em::Em; +pub use self::fr::Fr; +pub use self::length::Length; +pub use self::paint::Paint; +pub use self::path::{Path, PathItem}; +pub use self::point::Point; +pub use self::ratio::Ratio; +pub use self::rel::Rel; +pub use self::rounded::rounded_rect; +pub use self::scalar::Scalar; +pub use self::shape::{Geometry, Shape}; +pub use self::sides::{Side, Sides}; +pub use self::size::Size; +pub use self::smart::Smart; +pub use self::stroke::{ + DashLength, DashPattern, LineCap, LineJoin, PartialStroke, Stroke, +}; +pub use self::transform::Transform; use std::cmp::Ordering; use std::f64::consts::PI; @@ -58,7 +62,7 @@ use std::iter::Sum; use std::ops::*; use crate::diag::StrResult; -use crate::eval::{array, cast_from_value, cast_to_value, Cast, CastInfo, Dict, Value}; +use crate::eval::{array, cast, Array, Dict, Value}; use crate::model::{Fold, Resolve, StyleChain}; /// Generic access to a structure's components. diff --git a/src/geom/paint.rs b/src/geom/paint.rs index e9bd3a2e..10fa9fde 100644 --- a/src/geom/paint.rs +++ b/src/geom/paint.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use super::*; /// How a fill or stroke should be painted. @@ -23,393 +21,10 @@ impl Debug for Paint { } } -cast_from_value! { +cast! { Paint, + self => match self { + Self::Solid(color) => Value::Color(color), + }, color: Color => Self::Solid(color), } - -cast_to_value! { - Paint::Solid(color): Paint => Value::Color(color) -} - -/// A color in a dynamic format. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub enum Color { - /// An 8-bit luma color. - Luma(LumaColor), - /// An 8-bit RGBA color. - Rgba(RgbaColor), - /// An 8-bit CMYK color. - Cmyk(CmykColor), -} - -impl Color { - pub const BLACK: Self = Self::Rgba(RgbaColor::new(0x00, 0x00, 0x00, 0xFF)); - pub const GRAY: Self = Self::Rgba(RgbaColor::new(0xAA, 0xAA, 0xAA, 0xFF)); - pub const SILVER: Self = Self::Rgba(RgbaColor::new(0xDD, 0xDD, 0xDD, 0xFF)); - pub const WHITE: Self = Self::Rgba(RgbaColor::new(0xFF, 0xFF, 0xFF, 0xFF)); - pub const NAVY: Self = Self::Rgba(RgbaColor::new(0x00, 0x1f, 0x3f, 0xFF)); - pub const BLUE: Self = Self::Rgba(RgbaColor::new(0x00, 0x74, 0xD9, 0xFF)); - pub const AQUA: Self = Self::Rgba(RgbaColor::new(0x7F, 0xDB, 0xFF, 0xFF)); - pub const TEAL: Self = Self::Rgba(RgbaColor::new(0x39, 0xCC, 0xCC, 0xFF)); - pub const EASTERN: Self = Self::Rgba(RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); - pub const PURPLE: Self = Self::Rgba(RgbaColor::new(0xB1, 0x0D, 0xC9, 0xFF)); - pub const FUCHSIA: Self = Self::Rgba(RgbaColor::new(0xF0, 0x12, 0xBE, 0xFF)); - pub const MAROON: Self = Self::Rgba(RgbaColor::new(0x85, 0x14, 0x4b, 0xFF)); - pub const RED: Self = Self::Rgba(RgbaColor::new(0xFF, 0x41, 0x36, 0xFF)); - pub const ORANGE: Self = Self::Rgba(RgbaColor::new(0xFF, 0x85, 0x1B, 0xFF)); - pub const YELLOW: Self = Self::Rgba(RgbaColor::new(0xFF, 0xDC, 0x00, 0xFF)); - pub const OLIVE: Self = Self::Rgba(RgbaColor::new(0x3D, 0x99, 0x70, 0xFF)); - pub const GREEN: Self = Self::Rgba(RgbaColor::new(0x2E, 0xCC, 0x40, 0xFF)); - pub const LIME: Self = Self::Rgba(RgbaColor::new(0x01, 0xFF, 0x70, 0xFF)); - - /// Convert this color to RGBA. - pub fn to_rgba(self) -> RgbaColor { - match self { - Self::Luma(luma) => luma.to_rgba(), - Self::Rgba(rgba) => rgba, - Self::Cmyk(cmyk) => cmyk.to_rgba(), - } - } - - /// Lighten this color by the given factor. - pub fn lighten(self, factor: Ratio) -> Self { - match self { - Self::Luma(luma) => Self::Luma(luma.lighten(factor)), - Self::Rgba(rgba) => Self::Rgba(rgba.lighten(factor)), - Self::Cmyk(cmyk) => Self::Cmyk(cmyk.lighten(factor)), - } - } - - /// Darken this color by the given factor. - pub fn darken(self, factor: Ratio) -> Self { - match self { - Self::Luma(luma) => Self::Luma(luma.darken(factor)), - Self::Rgba(rgba) => Self::Rgba(rgba.darken(factor)), - Self::Cmyk(cmyk) => Self::Cmyk(cmyk.darken(factor)), - } - } - - /// Negate this color. - pub fn negate(self) -> Self { - match self { - Self::Luma(luma) => Self::Luma(luma.negate()), - Self::Rgba(rgba) => Self::Rgba(rgba.negate()), - Self::Cmyk(cmyk) => Self::Cmyk(cmyk.negate()), - } - } -} - -impl Debug for Color { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Luma(c) => Debug::fmt(c, f), - Self::Rgba(c) => Debug::fmt(c, f), - Self::Cmyk(c) => Debug::fmt(c, f), - } - } -} - -/// An 8-bit grayscale color. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct LumaColor(pub u8); - -impl LumaColor { - /// Construct a new luma color. - pub const fn new(luma: u8) -> Self { - Self(luma) - } - - /// Convert to an opque RGBA color. - pub const fn to_rgba(self) -> RgbaColor { - RgbaColor::new(self.0, self.0, self.0, u8::MAX) - } - - /// Convert to CMYK as a fraction of true black. - pub fn to_cmyk(self) -> CmykColor { - CmykColor::new( - round_u8(self.0 as f64 * 0.75), - round_u8(self.0 as f64 * 0.68), - round_u8(self.0 as f64 * 0.67), - round_u8(self.0 as f64 * 0.90), - ) - } - - /// Lighten this color by a factor. - pub fn lighten(self, factor: Ratio) -> Self { - let inc = round_u8((u8::MAX - self.0) as f64 * factor.get()); - Self(self.0.saturating_add(inc)) - } - - /// Darken this color by a factor. - pub fn darken(self, factor: Ratio) -> Self { - let dec = round_u8(self.0 as f64 * factor.get()); - Self(self.0.saturating_sub(dec)) - } - - /// Negate this color. - pub fn negate(self) -> Self { - Self(u8::MAX - self.0) - } -} - -impl Debug for LumaColor { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "luma({})", self.0) - } -} - -impl From<LumaColor> for Color { - fn from(luma: LumaColor) -> Self { - Self::Luma(luma) - } -} - -/// An 8-bit RGBA color. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct RgbaColor { - /// Red channel. - pub r: u8, - /// Green channel. - pub g: u8, - /// Blue channel. - pub b: u8, - /// Alpha channel. - pub a: u8, -} - -impl RgbaColor { - /// Construct a new RGBA color. - pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { - Self { r, g, b, a } - } - - /// Lighten this color by a factor. - /// - /// The alpha channel is not affected. - pub fn lighten(self, factor: Ratio) -> Self { - let lighten = - |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); - Self { - r: lighten(self.r), - g: lighten(self.g), - b: lighten(self.b), - a: self.a, - } - } - - /// Darken this color by a factor. - /// - /// The alpha channel is not affected. - pub fn darken(self, factor: Ratio) -> Self { - let darken = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); - Self { - r: darken(self.r), - g: darken(self.g), - b: darken(self.b), - a: self.a, - } - } - - /// Negate this color. - /// - /// The alpha channel is not affected. - pub fn negate(self) -> Self { - Self { - r: u8::MAX - self.r, - g: u8::MAX - self.g, - b: u8::MAX - self.b, - a: self.a, - } - } -} - -impl FromStr for RgbaColor { - type Err = &'static str; - - /// Constructs a new color from hex strings like the following: - /// - `#aef` (shorthand, with leading hashtag), - /// - `7a03c2` (without alpha), - /// - `abcdefff` (with alpha). - /// - /// The hashtag is optional and both lower and upper case are fine. - fn from_str(hex_str: &str) -> Result<Self, Self::Err> { - let hex_str = hex_str.strip_prefix('#').unwrap_or(hex_str); - if hex_str.chars().any(|c| !c.is_ascii_hexdigit()) { - return Err("color string contains non-hexadecimal letters"); - } - - let len = hex_str.len(); - let long = len == 6 || len == 8; - let short = len == 3 || len == 4; - let alpha = len == 4 || len == 8; - if !long && !short { - return Err("color string has wrong length"); - } - - let mut values: [u8; 4] = [u8::MAX; 4]; - for elem in if alpha { 0..4 } else { 0..3 } { - let item_len = if long { 2 } else { 1 }; - let pos = elem * item_len; - - let item = &hex_str[pos..(pos + item_len)]; - values[elem] = u8::from_str_radix(item, 16).unwrap(); - - if short { - // Duplicate number for shorthand notation, i.e. `a` -> `aa` - values[elem] += values[elem] * 16; - } - } - - Ok(Self::new(values[0], values[1], values[2], values[3])) - } -} - -impl Debug for RgbaColor { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - if f.alternate() { - write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?; - } else { - write!(f, "rgb(\"#{:02x}{:02x}{:02x}", self.r, self.g, self.b)?; - if self.a != 255 { - write!(f, "{:02x}", self.a)?; - } - write!(f, "\")")?; - } - Ok(()) - } -} - -impl<T: Into<RgbaColor>> From<T> for Color { - fn from(rgba: T) -> Self { - Self::Rgba(rgba.into()) - } -} - -cast_to_value! { - v: RgbaColor => Value::Color(v.into()) -} - -/// An 8-bit CMYK color. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct CmykColor { - /// The cyan component. - pub c: u8, - /// The magenta component. - pub m: u8, - /// The yellow component. - pub y: u8, - /// The key (black) component. - pub k: u8, -} - -impl CmykColor { - /// Construct a new CMYK color. - pub const fn new(c: u8, m: u8, y: u8, k: u8) -> Self { - Self { c, m, y, k } - } - - /// Convert this color to RGBA. - pub fn to_rgba(self) -> RgbaColor { - let k = self.k as f64 / 255.0; - let f = |c| { - let c = c as f64 / 255.0; - round_u8(255.0 * (1.0 - c) * (1.0 - k)) - }; - - RgbaColor { r: f(self.c), g: f(self.m), b: f(self.y), a: 255 } - } - - /// Lighten this color by a factor. - pub fn lighten(self, factor: Ratio) -> Self { - let lighten = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); - Self { - c: lighten(self.c), - m: lighten(self.m), - y: lighten(self.y), - k: lighten(self.k), - } - } - - /// Darken this color by a factor. - pub fn darken(self, factor: Ratio) -> Self { - let darken = - |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); - Self { - c: darken(self.c), - m: darken(self.m), - y: darken(self.y), - k: darken(self.k), - } - } - - /// Negate this color. - /// - /// Does not affect the key component. - pub fn negate(self) -> Self { - Self { - c: u8::MAX - self.c, - m: u8::MAX - self.m, - y: u8::MAX - self.y, - k: self.k, - } - } -} - -impl Debug for CmykColor { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let g = |c| 100.0 * (c as f64 / 255.0); - write!( - f, - "cmyk({:.1}%, {:.1}%, {:.1}%, {:.1}%)", - g(self.c), - g(self.m), - g(self.y), - g(self.k), - ) - } -} - -impl From<CmykColor> for Color { - fn from(cmyk: CmykColor) -> Self { - Self::Cmyk(cmyk) - } -} - -/// Convert to the closest u8. -fn round_u8(value: f64) -> u8 { - value.round() as u8 -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_color_strings() { - #[track_caller] - fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) { - assert_eq!(RgbaColor::from_str(hex), Ok(RgbaColor::new(r, g, b, a))); - } - - test("f61243ff", 0xf6, 0x12, 0x43, 0xff); - test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff); - test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad); - test("233", 0x22, 0x33, 0x33, 0xff); - test("111b", 0x11, 0x11, 0x11, 0xbb); - } - - #[test] - fn test_parse_invalid_colors() { - #[track_caller] - fn test(hex: &str, message: &str) { - assert_eq!(RgbaColor::from_str(hex), Err(message)); - } - - test("a5", "color string has wrong length"); - test("12345", "color string has wrong length"); - test("f075ff011", "color string has wrong length"); - test("hmmm", "color string contains non-hexadecimal letters"); - test("14B2AH", "color string contains non-hexadecimal letters"); - } -} diff --git a/src/geom/rel.rs b/src/geom/rel.rs index cf1e73ef..88972222 100644 --- a/src/geom/rel.rs +++ b/src/geom/rel.rs @@ -240,6 +240,7 @@ impl Fold for Rel<Length> { } } -cast_to_value! { - v: Rel<Abs> => v.map(Length::from).into() +cast! { + Rel<Abs>, + self => self.map(Length::from).into_value(), } diff --git a/src/geom/sides.rs b/src/geom/sides.rs index a905a5f8..d4b72a9d 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -1,3 +1,5 @@ +use crate::eval::{CastInfo, FromValue, IntoValue, Reflect}; + use super::*; /// A container with left, top, right and bottom components. @@ -178,19 +180,51 @@ impl Side { } } -impl<T> Cast for Sides<Option<T>> +impl<T: Reflect> Reflect for Sides<Option<T>> { + fn describe() -> CastInfo { + T::describe() + Dict::describe() + } + + fn castable(value: &Value) -> bool { + Dict::castable(value) || T::castable(value) + } +} + +impl<T> IntoValue for Sides<T> where - T: Default + Cast + Clone, + T: PartialEq + IntoValue, { - fn is(value: &Value) -> bool { - matches!(value, Value::Dict(_)) || T::is(value) + fn into_value(self) -> Value { + if self.is_uniform() { + return self.left.into_value(); + } + + let mut dict = Dict::new(); + let mut handle = |key: &str, component: T| { + let value = component.into_value(); + if value != Value::None { + dict.insert(key.into(), value); + } + }; + + handle("left", self.left); + handle("top", self.top); + handle("right", self.right); + handle("bottom", self.bottom); + + Value::Dict(dict) } +} - fn cast(mut value: Value) -> StrResult<Self> { +impl<T> FromValue for Sides<Option<T>> +where + T: Default + FromValue + Clone, +{ + fn from_value(mut value: Value) -> StrResult<Self> { let keys = ["left", "top", "right", "bottom", "x", "y", "rest"]; if let Value::Dict(dict) = &mut value { if dict.iter().any(|(key, _)| keys.contains(&key.as_str())) { - let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); + let mut take = |key| dict.take(key).ok().map(T::from_value).transpose(); let rest = take("rest")?; let x = take("x")?.or_else(|| rest.clone()); let y = take("y")?.or_else(|| rest.clone()); @@ -206,43 +240,14 @@ where } } - if T::is(&value) { - Ok(Self::splat(Some(T::cast(value)?))) + if T::castable(&value) { + Ok(Self::splat(Some(T::from_value(value)?))) } else { - <Self as Cast>::error(value) + Err(Self::error(&value)) } } - - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("dictionary") - } } -impl<T> From<Sides<T>> for Value -where - T: PartialEq + Into<Value>, -{ - fn from(sides: Sides<T>) -> Self { - if sides.is_uniform() { - return sides.left.into(); - } - - let mut dict = Dict::new(); - let mut handle = |key: &str, component: T| { - let value = component.into(); - if value != Value::None { - dict.insert(key.into(), value); - } - }; - - handle("left", sides.left); - handle("top", sides.top); - handle("right", sides.right); - handle("bottom", sides.bottom); - - Value::Dict(dict) - } -} impl<T: Resolve> Resolve for Sides<T> { type Output = Sides<T::Output>; diff --git a/src/geom/smart.rs b/src/geom/smart.rs index d9f8fd16..a6271c20 100644 --- a/src/geom/smart.rs +++ b/src/geom/smart.rs @@ -1,3 +1,5 @@ +use crate::eval::{AutoValue, CastInfo, FromValue, IntoValue, Reflect}; + use super::*; /// A value that can be automatically determined. @@ -94,21 +96,32 @@ impl<T> Default for Smart<T> { } } -impl<T: Cast> Cast for Smart<T> { - fn is(value: &Value) -> bool { - matches!(value, Value::Auto) || T::is(value) +impl<T: Reflect> Reflect for Smart<T> { + fn castable(value: &Value) -> bool { + AutoValue::castable(value) || T::castable(value) } - fn cast(value: Value) -> StrResult<Self> { - match value { - Value::Auto => Ok(Self::Auto), - v if T::is(&v) => Ok(Self::Custom(T::cast(v)?)), - _ => <Self as Cast>::error(value), + fn describe() -> CastInfo { + T::describe() + AutoValue::describe() + } +} + +impl<T: IntoValue> IntoValue for Smart<T> { + fn into_value(self) -> Value { + match self { + Smart::Custom(v) => v.into_value(), + Smart::Auto => Value::Auto, } } +} - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("auto") +impl<T: FromValue> FromValue for Smart<T> { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::Auto => Ok(Self::Auto), + v if T::castable(&v) => Ok(Self::Custom(T::from_value(v)?)), + _ => Err(Self::error(&value)), + } } } @@ -131,12 +144,3 @@ where self.map(|inner| inner.fold(outer.unwrap_or_default())) } } - -impl<T: Into<Value>> From<Smart<T>> for Value { - fn from(v: Smart<T>) -> Self { - match v { - Smart::Custom(v) => v.into(), - Smart::Auto => Value::Auto, - } - } -} diff --git a/src/geom/stroke.rs b/src/geom/stroke.rs index 6539922c..66264d5d 100644 --- a/src/geom/stroke.rs +++ b/src/geom/stroke.rs @@ -1,3 +1,5 @@ +use crate::eval::{Cast, FromValue}; + use super::*; /// A stroke of a geometric shape. @@ -169,8 +171,78 @@ impl<T: Debug> Debug for PartialStroke<T> { } } +impl Resolve for PartialStroke { + type Output = PartialStroke<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + PartialStroke { + paint: self.paint, + thickness: self.thickness.resolve(styles), + line_cap: self.line_cap, + line_join: self.line_join, + dash_pattern: self.dash_pattern.resolve(styles), + miter_limit: self.miter_limit, + } + } +} + +impl Fold for PartialStroke<Abs> { + type Output = Self; + + fn fold(self, outer: Self::Output) -> Self::Output { + Self { + paint: self.paint.or(outer.paint), + thickness: self.thickness.or(outer.thickness), + line_cap: self.line_cap.or(outer.line_cap), + line_join: self.line_join.or(outer.line_join), + dash_pattern: self.dash_pattern.or(outer.dash_pattern), + miter_limit: self.miter_limit.or(outer.miter_limit), + } + } +} + +cast! { + type PartialStroke: "stroke", + thickness: Length => Self { + thickness: Smart::Custom(thickness), + ..Default::default() + }, + color: Color => Self { + paint: Smart::Custom(color.into()), + ..Default::default() + }, + mut dict: Dict => { + fn take<T: FromValue>(dict: &mut Dict, key: &str) -> StrResult<Smart<T>> { + Ok(dict.take(key).ok().map(T::from_value) + .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto)) + } + + let paint = take::<Paint>(&mut dict, "paint")?; + let thickness = take::<Length>(&mut dict, "thickness")?; + let line_cap = take::<LineCap>(&mut dict, "cap")?; + let line_join = take::<LineJoin>(&mut dict, "join")?; + let dash_pattern = take::<Option<DashPattern>>(&mut dict, "dash")?; + let miter_limit = take::<f64>(&mut dict, "miter-limit")?; + dict.finish(&["paint", "thickness", "cap", "join", "dash", "miter-limit"])?; + + Self { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit: miter_limit.map(Scalar), + } + }, +} + +cast! { + PartialStroke<Abs>, + self => self.map(Length::from).into_value(), +} + /// The line cap of a stroke -#[derive(Cast, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum LineCap { Butt, Round, @@ -188,7 +260,7 @@ impl Debug for LineCap { } /// The line join of a stroke -#[derive(Cast, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum LineJoin { Miter, Round, @@ -235,49 +307,22 @@ impl<T: Default> From<Vec<DashLength<T>>> for DashPattern<T> { } } -/// The length of a dash in a line dash pattern -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum DashLength<T = Length> { - LineWidth, - Length(T), -} - -impl From<Abs> for DashLength { - fn from(l: Abs) -> Self { - DashLength::Length(l.into()) - } -} - -impl<T> DashLength<T> { - fn finish(self, line_width: T) -> T { - match self { - Self::LineWidth => line_width, - Self::Length(l) => l, - } - } -} - -cast_from_value! { - DashLength: "dash length", - "dot" => Self::LineWidth, - l: Length => Self::Length(l), -} - -impl Resolve for DashLength { - type Output = DashLength<Abs>; +impl Resolve for DashPattern { + type Output = DashPattern<Abs>; fn resolve(self, styles: StyleChain) -> Self::Output { - match self { - Self::LineWidth => DashLength::LineWidth, - Self::Length(l) => DashLength::Length(l.resolve(styles)), + DashPattern { + array: self.array.into_iter().map(|l| l.resolve(styles)).collect(), + phase: self.phase.resolve(styles), } } } -cast_from_value! { - DashPattern: "dash pattern", - // Use same names as tikz: - // https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns +// Same names as tikz: +// https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns +cast! { + DashPattern, + "solid" => Vec::new().into(), "dotted" => vec![DashLength::LineWidth, Abs::pt(2.0).into()].into(), "densely-dotted" => vec![DashLength::LineWidth, Abs::pt(1.0).into()].into(), @@ -288,19 +333,13 @@ cast_from_value! { "dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into(), DashLength::LineWidth, Abs::pt(2.0).into()].into(), "densely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(1.0).into(), DashLength::LineWidth, Abs::pt(1.0).into()].into(), "loosely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(4.0).into(), DashLength::LineWidth, Abs::pt(4.0).into()].into(), - array: Vec<DashLength> => { - Self { - array, - phase: Length::zero(), - } - }, + + array: Vec<DashLength> => Self { array, phase: Length::zero() }, mut dict: Dict => { let array: Vec<DashLength> = dict.take("array")?.cast()?; - let phase = dict.take("phase").ok().map(Length::cast) + let phase = dict.take("phase").ok().map(Value::cast) .transpose()?.unwrap_or(Length::zero()); - dict.finish(&["array", "phase"])?; - Self { array, phase, @@ -308,82 +347,41 @@ cast_from_value! { }, } -impl Resolve for DashPattern { - type Output = DashPattern<Abs>; - - fn resolve(self, styles: StyleChain) -> Self::Output { - DashPattern { - array: self.array.into_iter().map(|l| l.resolve(styles)).collect(), - phase: self.phase.resolve(styles), - } - } +/// The length of a dash in a line dash pattern +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum DashLength<T = Length> { + LineWidth, + Length(T), } -cast_from_value! { - PartialStroke: "stroke", - thickness: Length => Self { - thickness: Smart::Custom(thickness), - ..Default::default() - }, - color: Color => Self { - paint: Smart::Custom(color.into()), - ..Default::default() - }, - mut dict: Dict => { - fn take<T: Cast<Value>>(dict: &mut Dict, key: &str) -> StrResult<Smart<T>> { - Ok(dict.take(key).ok().map(T::cast) - .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto)) - } - - let paint = take::<Paint>(&mut dict, "paint")?; - let thickness = take::<Length>(&mut dict, "thickness")?; - let line_cap = take::<LineCap>(&mut dict, "cap")?; - let line_join = take::<LineJoin>(&mut dict, "join")?; - let dash_pattern = take::<Option<DashPattern>>(&mut dict, "dash")?; - let miter_limit = take::<f64>(&mut dict, "miter-limit")?; - dict.finish(&["paint", "thickness", "cap", "join", "dash", "miter-limit"])?; - - Self { - paint, - thickness, - line_cap, - line_join, - dash_pattern, - miter_limit: miter_limit.map(Scalar), - } - }, +impl From<Abs> for DashLength { + fn from(l: Abs) -> Self { + DashLength::Length(l.into()) + } } -impl Resolve for PartialStroke { - type Output = PartialStroke<Abs>; - - fn resolve(self, styles: StyleChain) -> Self::Output { - PartialStroke { - paint: self.paint, - thickness: self.thickness.resolve(styles), - line_cap: self.line_cap, - line_join: self.line_join, - dash_pattern: self.dash_pattern.resolve(styles), - miter_limit: self.miter_limit, +impl<T> DashLength<T> { + fn finish(self, line_width: T) -> T { + match self { + Self::LineWidth => line_width, + Self::Length(l) => l, } } } -impl Fold for PartialStroke<Abs> { - type Output = Self; +impl Resolve for DashLength { + type Output = DashLength<Abs>; - fn fold(self, outer: Self::Output) -> Self::Output { - Self { - paint: self.paint.or(outer.paint), - thickness: self.thickness.or(outer.thickness), - line_cap: self.line_cap.or(outer.line_cap), - line_join: self.line_join.or(outer.line_join), - dash_pattern: self.dash_pattern.or(outer.dash_pattern), - miter_limit: self.miter_limit.or(outer.miter_limit), + fn resolve(self, styles: StyleChain) -> Self::Output { + match self { + Self::LineWidth => DashLength::LineWidth, + Self::Length(v) => DashLength::Length(v.resolve(styles)), } } } -cast_to_value! { - v: PartialStroke<Abs> => v.map(Length::from).into() +cast! { + DashLength, + "dot" => Self::LineWidth, + v: Length => Self::Length(v), } diff --git a/src/ide/mod.rs b/src/ide/mod.rs index 38bede0b..4b08b66b 100644 --- a/src/ide/mod.rs +++ b/src/ide/mod.rs @@ -7,10 +7,10 @@ mod jump; mod tooltip; pub use self::analyze::analyze_labels; -pub use self::complete::*; -pub use self::highlight::*; -pub use self::jump::*; -pub use self::tooltip::*; +pub use self::complete::{autocomplete, Completion, CompletionKind}; +pub use self::highlight::{highlight, highlight_html, Tag}; +pub use self::jump::{jump_from_click, jump_from_cursor, Jump}; +pub use self::tooltip::{tooltip, Tooltip}; use std::fmt::Write; diff --git a/src/model/content.rs b/src/model/content.rs index f262d027..015f8b76 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -8,11 +8,11 @@ use ecow::{eco_format, EcoString, EcoVec}; use super::{ element, Behave, Behaviour, ElemFunc, Element, Fold, Guard, Label, Locatable, - Location, PlainText, Recipe, Selector, Style, Styles, Synthesize, + Location, Recipe, Selector, Style, Styles, Synthesize, }; use crate::diag::{SourceResult, StrResult}; use crate::doc::Meta; -use crate::eval::{Cast, Dict, Str, Value, Vm}; +use crate::eval::{Dict, FromValue, IntoValue, Str, Value, Vm}; use crate::syntax::Span; use crate::util::pretty_array_like; @@ -153,23 +153,24 @@ impl Content { pub fn with_field( mut self, name: impl Into<EcoString>, - value: impl Into<Value>, + value: impl IntoValue, ) -> Self { self.push_field(name, value); self } /// Attach a field to the content. - pub fn push_field(&mut self, name: impl Into<EcoString>, value: impl Into<Value>) { + pub fn push_field(&mut self, name: impl Into<EcoString>, value: impl IntoValue) { let name = name.into(); if let Some(i) = self.attrs.iter().position(|attr| match attr { Attr::Field(field) => *field == name, _ => false, }) { - self.attrs.make_mut()[i + 1] = Attr::Value(Prehashed::new(value.into())); + self.attrs.make_mut()[i + 1] = + Attr::Value(Prehashed::new(value.into_value())); } else { self.attrs.push(Attr::Field(name)); - self.attrs.push(Attr::Value(Prehashed::new(value.into()))); + self.attrs.push(Attr::Value(Prehashed::new(value.into_value()))); } } @@ -226,7 +227,7 @@ impl Content { } /// Try to access a field on the content as a specified type. - pub fn cast_field<T: Cast>(&self, name: &str) -> Option<T> { + pub fn cast_field<T: FromValue>(&self, name: &str) -> Option<T> { match self.field(name) { Some(value) => value.cast().ok(), None => None, @@ -235,7 +236,7 @@ impl Content { /// Expect a field on the content to exist as a specified type. #[track_caller] - pub fn expect_field<T: Cast>(&self, name: &str) -> T { + pub fn expect_field<T: FromValue>(&self, name: &str) -> T { self.field(name).unwrap().cast().unwrap() } @@ -311,12 +312,9 @@ impl Content { } } - /// Repeat this content `n` times. - pub fn repeat(&self, n: i64) -> StrResult<Self> { - let count = usize::try_from(n) - .map_err(|_| format!("cannot repeat this content {} times", n))?; - - Ok(Self::sequence(vec![self.clone(); count])) + /// Repeat this content `count` times. + pub fn repeat(&self, count: usize) -> Self { + Self::sequence(vec![self.clone(); count]) } /// Disable a show rule recipe. @@ -599,6 +597,12 @@ impl Fold for Vec<Meta> { } } +/// Tries to extract the plain-text representation of the element. +pub trait PlainText { + /// Write this element's plain text into the given buffer. + fn plain_text(&self, text: &mut EcoString); +} + /// The missing key access error message when no default value was given. #[cold] fn missing_field_no_default(key: &str) -> EcoString { diff --git a/src/model/element.rs b/src/model/element.rs index e26848b1..c673ee41 100644 --- a/src/model/element.rs +++ b/src/model/element.rs @@ -2,14 +2,11 @@ use std::any::TypeId; use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; -use ecow::EcoString; use once_cell::sync::Lazy; use super::{Content, Selector, Styles}; use crate::diag::SourceResult; -use crate::eval::{ - cast_from_value, cast_to_value, Args, Dict, Func, FuncInfo, Value, Vm, -}; +use crate::eval::{cast, Args, Dict, Func, FuncInfo, Value, Vm}; /// A document element. pub trait Element: Construct + Set + Sized + 'static { @@ -110,15 +107,12 @@ impl Hash for ElemFunc { } } -cast_from_value! { +cast! { ElemFunc, + self => Value::Func(self.into()), v: Func => v.element().ok_or("expected element function")?, } -cast_to_value! { - v: ElemFunc => Value::Func(v.into()) -} - impl From<&'static NativeElemFunc> for ElemFunc { fn from(native: &'static NativeElemFunc) -> Self { Self(native) @@ -138,22 +132,3 @@ pub struct NativeElemFunc { /// Details about the function. pub info: Lazy<FuncInfo>, } - -/// A label for an element. -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Label(pub EcoString); - -impl Debug for Label { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "<{}>", self.0) - } -} - -/// Indicates that an element cannot be labelled. -pub trait Unlabellable {} - -/// Tries to extract the plain-text representation of the element. -pub trait PlainText { - /// Write this element's plain text into the given buffer. - fn plain_text(&self, text: &mut EcoString); -} diff --git a/src/model/introspect.rs b/src/model/introspect.rs index 87a17a8e..f00f89f5 100644 --- a/src/model/introspect.rs +++ b/src/model/introspect.rs @@ -11,12 +11,12 @@ use indexmap::IndexMap; use super::{Content, Selector}; use crate::diag::StrResult; use crate::doc::{Frame, FrameItem, Meta, Position}; -use crate::eval::{cast_from_value, Value}; +use crate::eval::{cast, Value}; use crate::geom::{Point, Transform}; use crate::model::Label; use crate::util::NonZeroExt; -/// Uniquely identifies an element in the document across multiple layout passes. +/// Identifies the location of an element in the document. /// /// This struct is created by [`Locator::locate`]. #[derive(Copy, Clone, Eq, PartialEq, Hash)] @@ -46,8 +46,8 @@ impl Debug for Location { } } -cast_from_value! { - Location: "location", +cast! { + type Location: "location", } /// Provides locations for elements in the document. diff --git a/src/model/label.rs b/src/model/label.rs new file mode 100644 index 00000000..ef8f3edd --- /dev/null +++ b/src/model/label.rs @@ -0,0 +1,16 @@ +use std::fmt::{self, Debug, Formatter}; + +use ecow::EcoString; + +/// A label for an element. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Label(pub EcoString); + +impl Debug for Label { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "<{}>", self.0) + } +} + +/// Indicates that an element cannot be labelled. +pub trait Unlabellable {} diff --git a/src/model/mod.rs b/src/model/mod.rs index e541cd01..632b691f 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -3,16 +3,26 @@ mod content; mod element; mod introspect; +mod label; mod realize; +mod selector; mod styles; +#[doc(inline)] pub use typst_macros::element; -pub use self::content::*; -pub use self::element::*; -pub use self::introspect::*; -pub use self::realize::*; -pub use self::styles::*; +pub use self::content::{Content, MetaElem, PlainText}; +pub use self::element::{Construct, ElemFunc, Element, NativeElemFunc, Set}; +pub use self::introspect::{Introspector, Location, Locator}; +pub use self::label::{Label, Unlabellable}; +pub use self::realize::{ + applicable, realize, Behave, Behaviour, Finalize, Guard, Locatable, Show, Synthesize, +}; +pub use self::selector::{LocatableSelector, Selector, ShowableSelector}; +pub use self::styles::{ + Fold, Property, Recipe, Resolve, Style, StyleChain, StyleVec, StyleVecBuilder, + Styles, Transform, +}; use std::mem::ManuallyDrop; diff --git a/src/model/selector.rs b/src/model/selector.rs new file mode 100644 index 00000000..c1528c0a --- /dev/null +++ b/src/model/selector.rs @@ -0,0 +1,291 @@ +use std::any::{Any, TypeId}; +use std::fmt::{self, Debug, Formatter, Write}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString, EcoVec}; + +use super::{Content, ElemFunc, Label, Location}; +use crate::diag::StrResult; +use crate::eval::{ + cast, CastInfo, Dict, FromValue, Func, IntoValue, Reflect, Regex, Value, +}; +use crate::model::Locatable; +use crate::util::pretty_array_like; + +/// A selector in a show rule. +#[derive(Clone, PartialEq, Hash)] +pub enum Selector { + /// Matches a specific type of element. + /// + /// If there is a dictionary, only elements with the fields from the + /// dictionary match. + Elem(ElemFunc, Option<Dict>), + /// Matches the element at the specified location. + Location(Location), + /// Matches elements with a specific label. + Label(Label), + /// Matches text elements through a regular expression. + Regex(Regex), + /// Matches elements with a specific capability. + Can(TypeId), + /// Matches if any of the subselectors match. + Or(EcoVec<Self>), + /// Matches if all of the subselectors match. + And(EcoVec<Self>), + /// Matches all matches of `selector` before `end`. + Before { selector: Arc<Self>, end: Arc<Self>, inclusive: bool }, + /// Matches all matches of `selector` after `start`. + After { selector: Arc<Self>, start: Arc<Self>, inclusive: bool }, +} + +impl Selector { + /// Define a simple text selector. + pub fn text(text: &str) -> Self { + Self::Regex(Regex::new(®ex::escape(text)).unwrap()) + } + + /// Define a simple [`Selector::Can`] selector. + pub fn can<T: ?Sized + Any>() -> Self { + Self::Can(TypeId::of::<T>()) + } + + /// Transforms this selector and an iterator of other selectors into a + /// [`Selector::Or`] selector. + pub fn and(self, others: impl IntoIterator<Item = Self>) -> Self { + Self::And(others.into_iter().chain(Some(self)).collect()) + } + + /// Transforms this selector and an iterator of other selectors into a + /// [`Selector::And`] selector. + pub fn or(self, others: impl IntoIterator<Item = Self>) -> Self { + Self::Or(others.into_iter().chain(Some(self)).collect()) + } + + /// Transforms this selector into a [`Selector::Before`] selector. + pub fn before(self, location: impl Into<Self>, inclusive: bool) -> Self { + Self::Before { + selector: Arc::new(self), + end: Arc::new(location.into()), + inclusive, + } + } + + /// Transforms this selector into a [`Selector::After`] selector. + pub fn after(self, location: impl Into<Self>, inclusive: bool) -> Self { + Self::After { + selector: Arc::new(self), + start: Arc::new(location.into()), + inclusive, + } + } + + /// Whether the selector matches for the target. + pub fn matches(&self, target: &Content) -> bool { + match self { + Self::Elem(element, dict) => { + target.func() == *element + && dict + .iter() + .flat_map(|dict| dict.iter()) + .all(|(name, value)| target.field_ref(name) == Some(value)) + } + Self::Label(label) => target.label() == Some(label), + Self::Regex(regex) => { + target.func() == item!(text_func) + && item!(text_str)(target).map_or(false, |text| regex.is_match(&text)) + } + Self::Can(cap) => target.can_type_id(*cap), + Self::Or(selectors) => selectors.iter().any(move |sel| sel.matches(target)), + Self::And(selectors) => selectors.iter().all(move |sel| sel.matches(target)), + Self::Location(location) => target.location() == Some(*location), + // Not supported here. + Self::Before { .. } | Self::After { .. } => false, + } + } +} + +impl From<Location> for Selector { + fn from(value: Location) -> Self { + Self::Location(value) + } +} + +impl Debug for Selector { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Elem(elem, dict) => { + f.write_str(elem.name())?; + if let Some(dict) = dict { + f.write_str(".where")?; + dict.fmt(f)?; + } + Ok(()) + } + Self::Label(label) => label.fmt(f), + Self::Regex(regex) => regex.fmt(f), + Self::Can(cap) => cap.fmt(f), + Self::Or(selectors) | Self::And(selectors) => { + f.write_str(if matches!(self, Self::Or(_)) { "or" } else { "and" })?; + let pieces: Vec<_> = + selectors.iter().map(|sel| eco_format!("{sel:?}")).collect(); + f.write_str(&pretty_array_like(&pieces, false)) + } + Self::Location(loc) => loc.fmt(f), + Self::Before { selector, end: split, inclusive } + | Self::After { selector, start: split, inclusive } => { + selector.fmt(f)?; + + if matches!(self, Self::Before { .. }) { + f.write_str(".before(")?; + } else { + f.write_str(".after(")?; + } + + split.fmt(f)?; + if !*inclusive { + f.write_str(", inclusive: false")?; + } + f.write_char(')') + } + } + } +} + +cast! { + type Selector: "selector", + func: Func => func + .element() + .ok_or("only element functions can be used as selectors")? + .select(), + label: Label => Self::Label(label), + text: EcoString => Self::text(&text), + regex: Regex => Self::Regex(regex), + location: Location => Self::Location(location), +} + +/// A selector that can be used with `query`. +/// +/// Hopefully, this is made obsolete by a more powerful query mechanism in the +/// future. +#[derive(Clone, PartialEq, Hash)] +pub struct LocatableSelector(pub Selector); + +impl Reflect for LocatableSelector { + fn describe() -> CastInfo { + CastInfo::Union(vec![ + CastInfo::Type("function"), + CastInfo::Type("label"), + CastInfo::Type("selector"), + ]) + } + + fn castable(value: &Value) -> bool { + matches!(value.type_name(), "function" | "label" | "selector") + } +} + +impl IntoValue for LocatableSelector { + fn into_value(self) -> Value { + self.0.into_value() + } +} + +impl FromValue for LocatableSelector { + fn from_value(value: Value) -> StrResult<Self> { + fn validate(selector: &Selector) -> StrResult<()> { + match selector { + Selector::Elem(elem, _) => { + if !elem.can::<dyn Locatable>() { + Err(eco_format!("{} is not locatable", elem.name()))? + } + } + Selector::Location(_) => {} + Selector::Label(_) => {} + Selector::Regex(_) => Err("text is not locatable")?, + Selector::Can(_) => Err("capability is not locatable")?, + Selector::Or(list) | Selector::And(list) => { + for selector in list { + validate(selector)?; + } + } + Selector::Before { selector, end: split, .. } + | Selector::After { selector, start: split, .. } => { + for selector in [selector, split] { + validate(selector)?; + } + } + } + Ok(()) + } + + if !Self::castable(&value) { + return Err(Self::error(&value)); + } + + let selector = Selector::from_value(value)?; + validate(&selector)?; + Ok(Self(selector)) + } +} + +/// A selector that can be used with show rules. +/// +/// Hopefully, this is made obsolete by a more powerful showing mechanism in the +/// future. +#[derive(Clone, PartialEq, Hash)] +pub struct ShowableSelector(pub Selector); + +impl Reflect for ShowableSelector { + fn describe() -> CastInfo { + CastInfo::Union(vec![ + CastInfo::Type("function"), + CastInfo::Type("label"), + CastInfo::Type("string"), + CastInfo::Type("regex"), + CastInfo::Type("symbol"), + CastInfo::Type("selector"), + ]) + } + + fn castable(value: &Value) -> bool { + matches!( + value.type_name(), + "symbol" | "string" | "label" | "function" | "regex" | "selector" + ) + } +} + +impl IntoValue for ShowableSelector { + fn into_value(self) -> Value { + self.0.into_value() + } +} + +impl FromValue for ShowableSelector { + fn from_value(value: Value) -> StrResult<Self> { + fn validate(selector: &Selector) -> StrResult<()> { + match selector { + Selector::Elem(_, _) => {} + Selector::Label(_) => {} + Selector::Regex(_) => {} + Selector::Or(_) + | Selector::And(_) + | Selector::Location(_) + | Selector::Can(_) + | Selector::Before { .. } + | Selector::After { .. } => { + Err("this selector cannot be used with show")? + } + } + Ok(()) + } + + if !Self::castable(&value) { + return Err(Self::error(&value)); + } + + let selector = Selector::from_value(value)?; + validate(&selector)?; + Ok(Self(selector)) + } +} diff --git a/src/model/styles.rs b/src/model/styles.rs index e30a8a92..5b6430c2 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -1,19 +1,15 @@ -use std::any::{Any, TypeId}; use std::fmt::{self, Debug, Formatter, Write}; use std::iter; use std::mem; use std::ptr; -use std::sync::Arc; use comemo::Prehashed; -use ecow::{eco_format, eco_vec, EcoString, EcoVec}; +use ecow::{eco_vec, EcoString, EcoVec}; -use super::{Content, ElemFunc, Element, Label, Location, Vt}; -use crate::diag::{SourceResult, StrResult, Trace, Tracepoint}; -use crate::eval::{cast_from_value, Args, Cast, CastInfo, Dict, Func, Regex, Value, Vm}; -use crate::model::Locatable; +use super::{Content, ElemFunc, Element, Selector, Vt}; +use crate::diag::{SourceResult, Trace, Tracepoint}; +use crate::eval::{cast, Args, FromValue, Func, IntoValue, Value, Vm}; use crate::syntax::Span; -use crate::util::pretty_array_like; /// A list of style properties. #[derive(Default, PartialEq, Clone, Hash)] @@ -158,8 +154,17 @@ pub struct Property { impl Property { /// Create a new property from a key-value pair. - pub fn new(element: ElemFunc, name: EcoString, value: Value) -> Self { - Self { element, name, value, span: None } + pub fn new( + element: ElemFunc, + name: impl Into<EcoString>, + value: impl IntoValue, + ) -> Self { + Self { + element, + name: name.into(), + value: value.into_value(), + span: None, + } } /// Whether this property is the given one. @@ -254,282 +259,6 @@ impl Debug for Recipe { } } -/// A selector in a show rule. -#[derive(Clone, PartialEq, Hash)] -pub enum Selector { - /// Matches a specific type of element. - /// - /// If there is a dictionary, only elements with the fields from the - /// dictionary match. - Elem(ElemFunc, Option<Dict>), - /// Matches the element at the specified location. - Location(Location), - /// Matches elements with a specific label. - Label(Label), - /// Matches text elements through a regular expression. - Regex(Regex), - /// Matches elements with a specific capability. - Can(TypeId), - /// Matches if any of the subselectors match. - Or(EcoVec<Self>), - /// Matches if all of the subselectors match. - And(EcoVec<Self>), - /// Matches all matches of `selector` before `end`. - Before { selector: Arc<Self>, end: Arc<Self>, inclusive: bool }, - /// Matches all matches of `selector` after `start`. - After { selector: Arc<Self>, start: Arc<Self>, inclusive: bool }, -} - -impl Selector { - /// Define a simple text selector. - pub fn text(text: &str) -> Self { - Self::Regex(Regex::new(®ex::escape(text)).unwrap()) - } - - /// Define a simple [`Selector::Can`] selector. - pub fn can<T: ?Sized + Any>() -> Self { - Self::Can(TypeId::of::<T>()) - } - - /// Transforms this selector and an iterator of other selectors into a - /// [`Selector::Or`] selector. - pub fn and(self, others: impl IntoIterator<Item = Self>) -> Self { - Self::And(others.into_iter().chain(Some(self)).collect()) - } - - /// Transforms this selector and an iterator of other selectors into a - /// [`Selector::And`] selector. - pub fn or(self, others: impl IntoIterator<Item = Self>) -> Self { - Self::Or(others.into_iter().chain(Some(self)).collect()) - } - - /// Transforms this selector into a [`Selector::Before`] selector. - pub fn before(self, location: impl Into<Self>, inclusive: bool) -> Self { - Self::Before { - selector: Arc::new(self), - end: Arc::new(location.into()), - inclusive, - } - } - - /// Transforms this selector into a [`Selector::After`] selector. - pub fn after(self, location: impl Into<Self>, inclusive: bool) -> Self { - Self::After { - selector: Arc::new(self), - start: Arc::new(location.into()), - inclusive, - } - } - - /// Whether the selector matches for the target. - pub fn matches(&self, target: &Content) -> bool { - match self { - Self::Elem(element, dict) => { - target.func() == *element - && dict - .iter() - .flat_map(|dict| dict.iter()) - .all(|(name, value)| target.field_ref(name) == Some(value)) - } - Self::Label(label) => target.label() == Some(label), - Self::Regex(regex) => { - target.func() == item!(text_func) - && item!(text_str)(target).map_or(false, |text| regex.is_match(&text)) - } - Self::Can(cap) => target.can_type_id(*cap), - Self::Or(selectors) => selectors.iter().any(move |sel| sel.matches(target)), - Self::And(selectors) => selectors.iter().all(move |sel| sel.matches(target)), - Self::Location(location) => target.location() == Some(*location), - // Not supported here. - Self::Before { .. } | Self::After { .. } => false, - } - } -} - -impl From<Location> for Selector { - fn from(value: Location) -> Self { - Self::Location(value) - } -} - -impl Debug for Selector { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Elem(elem, dict) => { - f.write_str(elem.name())?; - if let Some(dict) = dict { - f.write_str(".where")?; - dict.fmt(f)?; - } - Ok(()) - } - Self::Label(label) => label.fmt(f), - Self::Regex(regex) => regex.fmt(f), - Self::Can(cap) => cap.fmt(f), - Self::Or(selectors) | Self::And(selectors) => { - f.write_str(if matches!(self, Self::Or(_)) { "or" } else { "and" })?; - let pieces: Vec<_> = - selectors.iter().map(|sel| eco_format!("{sel:?}")).collect(); - f.write_str(&pretty_array_like(&pieces, false)) - } - Self::Location(loc) => loc.fmt(f), - Self::Before { selector, end: split, inclusive } - | Self::After { selector, start: split, inclusive } => { - selector.fmt(f)?; - - if matches!(self, Self::Before { .. }) { - f.write_str(".before(")?; - } else { - f.write_str(".after(")?; - } - - split.fmt(f)?; - if !*inclusive { - f.write_str(", inclusive: false")?; - } - f.write_char(')') - } - } - } -} - -cast_from_value! { - Selector: "selector", - func: Func => func - .element() - .ok_or("only element functions can be used as selectors")? - .select(), - label: Label => Self::Label(label), - text: EcoString => Self::text(&text), - regex: Regex => Self::Regex(regex), - location: Location => Self::Location(location), -} - -/// A selector that can be used with `query`. -/// -/// Hopefully, this is made obsolete by a more powerful query mechanism in the -/// future. -#[derive(Clone, PartialEq, Hash)] -pub struct LocatableSelector(pub Selector); - -impl Cast for LocatableSelector { - fn is(value: &Value) -> bool { - matches!(value, Value::Label(_) | Value::Func(_)) - || value.type_name() == "selector" - } - - fn cast(value: Value) -> StrResult<Self> { - fn validate(selector: &Selector) -> StrResult<()> { - match selector { - Selector::Elem(elem, _) => { - if !elem.can::<dyn Locatable>() { - Err(eco_format!("{} is not locatable", elem.name()))? - } - } - Selector::Location(_) => {} - Selector::Label(_) => {} - Selector::Regex(_) => Err("text is not locatable")?, - Selector::Can(_) => Err("capability is not locatable")?, - Selector::Or(list) | Selector::And(list) => { - for selector in list { - validate(selector)?; - } - } - Selector::Before { selector, end: split, .. } - | Selector::After { selector, start: split, .. } => { - for selector in [selector, split] { - validate(selector)?; - } - } - } - Ok(()) - } - - if !Self::is(&value) { - return <Self as Cast>::error(value); - } - - let selector = Selector::cast(value)?; - validate(&selector)?; - Ok(Self(selector)) - } - - fn describe() -> CastInfo { - CastInfo::Union(vec![ - CastInfo::Type("label"), - CastInfo::Type("function"), - CastInfo::Type("selector"), - ]) - } -} - -impl From<LocatableSelector> for Value { - fn from(value: LocatableSelector) -> Self { - value.0.into() - } -} - -/// A selector that can be used with show rules. -/// -/// Hopefully, this is made obsolete by a more powerful showing mechanism in the -/// future. -#[derive(Clone, PartialEq, Hash)] -pub struct ShowableSelector(pub Selector); - -impl Cast for ShowableSelector { - fn is(value: &Value) -> bool { - matches!( - value, - Value::Symbol(_) | Value::Str(_) | Value::Label(_) | Value::Func(_) - ) || value.type_name() == "regex" - || value.type_name() == "selector" - } - - fn cast(value: Value) -> StrResult<Self> { - fn validate(selector: &Selector) -> StrResult<()> { - match selector { - Selector::Elem(_, _) => {} - Selector::Label(_) => {} - Selector::Regex(_) => {} - Selector::Or(_) - | Selector::And(_) - | Selector::Location(_) - | Selector::Can(_) - | Selector::Before { .. } - | Selector::After { .. } => { - Err("this selector cannot be used with show")? - } - } - Ok(()) - } - - if !Self::is(&value) { - return <Self as Cast>::error(value); - } - - let selector = Selector::cast(value)?; - validate(&selector)?; - Ok(Self(selector)) - } - - fn describe() -> CastInfo { - CastInfo::Union(vec![ - CastInfo::Type("function"), - CastInfo::Type("label"), - CastInfo::Type("string"), - CastInfo::Type("regex"), - CastInfo::Type("symbol"), - CastInfo::Type("selector"), - ]) - } -} - -impl From<ShowableSelector> for Value { - fn from(value: ShowableSelector) -> Self { - value.0.into() - } -} - /// A show rule transformation that can be applied to a match. #[derive(Clone, PartialEq, Hash)] pub enum Transform { @@ -551,7 +280,7 @@ impl Debug for Transform { } } -cast_from_value! { +cast! { Transform, content: Content => Self::Content(content), func: Func => Self::Func(func), @@ -592,7 +321,7 @@ impl<'a> StyleChain<'a> { } /// Cast the first value for the given property in the chain. - pub fn get<T: Cast>( + pub fn get<T: FromValue>( self, func: ElemFunc, name: &'a str, @@ -605,7 +334,7 @@ impl<'a> StyleChain<'a> { } /// Cast the first value for the given property in the chain. - pub fn get_resolve<T: Cast + Resolve>( + pub fn get_resolve<T: FromValue + Resolve>( self, func: ElemFunc, name: &'a str, @@ -616,7 +345,7 @@ impl<'a> StyleChain<'a> { } /// Cast the first value for the given property in the chain. - pub fn get_fold<T: Cast + Fold>( + pub fn get_fold<T: FromValue + Fold>( self, func: ElemFunc, name: &'a str, @@ -645,7 +374,7 @@ impl<'a> StyleChain<'a> { default: impl Fn() -> <T::Output as Fold>::Output, ) -> <T::Output as Fold>::Output where - T: Cast + Resolve, + T: FromValue + Resolve, T::Output: Fold, { fn next<T>( @@ -671,7 +400,7 @@ impl<'a> StyleChain<'a> { } /// Iterate over all values for the given property in the chain. - pub fn properties<T: Cast + 'a>( + pub fn properties<T: FromValue + 'a>( self, func: ElemFunc, name: &'a str, |
