summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-06-06 21:13:59 +0200
committerLaurenz <laurmaedje@gmail.com>2023-06-06 22:06:16 +0200
commitfd417da04f7ca4b995de7f6510abafd3e9c31307 (patch)
tree3675529c75ca7363701ac8ea306de2cc1d3cbcb3 /src
parent168bdf35bd773e67343c965cb473492cc5cae9e7 (diff)
Improve value casting infrastructure
Diffstat (limited to 'src')
-rw-r--r--src/diag.rs32
-rw-r--r--src/doc.rs56
-rw-r--r--src/eval/args.rs43
-rw-r--r--src/eval/array.rs95
-rw-r--r--src/eval/auto.rs39
-rw-r--r--src/eval/cast.rs367
-rw-r--r--src/eval/datetime.rs6
-rw-r--r--src/eval/dict.rs19
-rw-r--r--src/eval/func.rs36
-rw-r--r--src/eval/int.rs81
-rw-r--r--src/eval/methods.rs153
-rw-r--r--src/eval/mod.rs71
-rw-r--r--src/eval/none.rs74
-rw-r--r--src/eval/ops.rs4
-rw-r--r--src/eval/scope.rs15
-rw-r--r--src/eval/str.rs80
-rw-r--r--src/eval/symbol.rs3
-rw-r--r--src/eval/value.rs63
-rw-r--r--src/font/mod.rs5
-rw-r--r--src/font/variant.rs38
-rw-r--r--src/geom/abs.rs5
-rw-r--r--src/geom/align.rs67
-rw-r--r--src/geom/axes.rs8
-rw-r--r--src/geom/color.rs386
-rw-r--r--src/geom/corners.rs82
-rw-r--r--src/geom/dir.rs4
-rw-r--r--src/geom/em.rs5
-rw-r--r--src/geom/length.rs2
-rw-r--r--src/geom/mod.rs52
-rw-r--r--src/geom/paint.rs393
-rw-r--r--src/geom/rel.rs5
-rw-r--r--src/geom/sides.rs81
-rw-r--r--src/geom/smart.rs42
-rw-r--r--src/geom/stroke.rs224
-rw-r--r--src/ide/mod.rs8
-rw-r--r--src/model/content.rs32
-rw-r--r--src/model/element.rs31
-rw-r--r--src/model/introspect.rs8
-rw-r--r--src/model/label.rs16
-rw-r--r--src/model/mod.rs20
-rw-r--r--src/model/selector.rs291
-rw-r--r--src/model/styles.rs313
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)]))
}
}
diff --git a/src/doc.rs b/src/doc.rs
index 3fdcf80f..1dc2a072 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -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(&regex::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(&regex::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,