diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-12-15 22:51:55 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-12-15 23:11:20 +0100 |
| commit | b6202b646a0d5ecced301d9bac8bfcaf977d7ee4 (patch) | |
| tree | 7d42cb50f9e66153e7e8b2217009684e25f54f42 /src/model | |
| parent | f3980c704544a464f9729cc8bc9f97d3a7454769 (diff) | |
Reflection for castables
Diffstat (limited to 'src/model')
| -rw-r--r-- | src/model/cast.rs | 513 | ||||
| -rw-r--r-- | src/model/content.rs | 8 | ||||
| -rw-r--r-- | src/model/dict.rs | 23 | ||||
| -rw-r--r-- | src/model/func.rs | 59 | ||||
| -rw-r--r-- | src/model/mod.rs | 2 | ||||
| -rw-r--r-- | src/model/ops.rs | 4 | ||||
| -rw-r--r-- | src/model/str.rs | 8 | ||||
| -rw-r--r-- | src/model/styles.rs | 3 | ||||
| -rw-r--r-- | src/model/value.rs | 8 |
9 files changed, 348 insertions, 280 deletions
diff --git a/src/model/cast.rs b/src/model/cast.rs index bfde1bdd..833b9e9e 100644 --- a/src/model/cast.rs +++ b/src/model/cast.rs @@ -1,15 +1,19 @@ use std::num::NonZeroUsize; +use std::ops::Add; use std::str::FromStr; -use super::{Content, Regex, Selector, Transform, Value}; -use crate::diag::{with_alternative, StrResult}; +use super::{ + castable, Array, Content, Dict, Func, Label, Regex, Selector, Str, Transform, Value, +}; +use crate::diag::StrResult; use crate::doc::{Destination, Lang, Location, Region}; use crate::font::{FontStretch, FontStyle, FontWeight}; use crate::geom::{ - Axes, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Rel, Sides, + Axes, Color, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Ratio, + Rel, Sides, Smart, }; use crate::syntax::Spanned; -use crate::util::{format_eco, EcoString}; +use crate::util::EcoString; /// Cast from a value to a specific type. pub trait Cast<V = Value>: Sized { @@ -18,95 +22,98 @@ pub trait Cast<V = Value>: Sized { /// Try to cast the value into an instance of `Self`. fn cast(value: V) -> StrResult<Self>; + + /// Describe the acceptable values. + fn describe() -> CastInfo; + + /// Produce an error for an inacceptable value. + fn error(value: Value) -> StrResult<Self> { + Err(Self::describe().error(&value)) + } } -/// Implement traits for dynamic types. -#[macro_export] -#[doc(hidden)] -macro_rules! __dynamic { - ($type:ty: $name:literal, $($tts:tt)*) => { - impl $crate::model::Type for $type { - const TYPE_NAME: &'static str = $name; +/// Describes a possible value for a cast. +#[derive(Debug, Clone)] +pub enum CastInfo { + /// Any value is okay. + Any, + /// A specific value, plus short documentation for that value. + Value(Value, &'static str), + /// Any value of a type. + Type(&'static str), + /// Multiple alternatives. + Union(Vec<Self>), +} + +impl CastInfo { + /// Produce an error message describing what was expected and what was + /// found. + pub fn error(&self, found: &Value) -> EcoString { + fn accumulate( + info: &CastInfo, + found: &Value, + parts: &mut Vec<EcoString>, + matching_type: &mut bool, + ) { + match info { + CastInfo::Any => parts.push("anything".into()), + CastInfo::Value(value, _) => { + parts.push(value.repr().into()); + if value.type_name() == found.type_name() { + *matching_type = true; + } + } + CastInfo::Type(ty) => parts.push((*ty).into()), + CastInfo::Union(options) => { + for option in options { + accumulate(option, found, parts, matching_type); + } + } + } } - castable! { - $type, - Expected: <Self as $crate::model::Type>::TYPE_NAME, - $($tts)* - @this: Self => this.clone(), + let mut matching_type = false; + let mut parts = vec![]; + accumulate(self, found, &mut parts, &mut matching_type); + + let mut msg = String::from("expected "); + if parts.is_empty() { + msg.push_str(" nothing"); } - impl From<$type> for $crate::model::Value { - fn from(v: $type) -> Self { - $crate::model::Value::Dyn($crate::model::Dynamic::new(v)) - } + crate::diag::comma_list(&mut msg, &parts, "or"); + + if !matching_type { + msg.push_str(", found "); + msg.push_str(found.type_name()); } - }; + + msg.into() + } } -#[doc(inline)] -pub use crate::__dynamic as dynamic; +impl Add for CastInfo { + type Output = Self; -/// Make a type castable from a value. -#[macro_export] -#[doc(hidden)] -macro_rules! __castable { - ($type:ty: $inner:ty) => { - impl $crate::model::Cast<$crate::model::Value> for $type { - fn is(value: &$crate::model::Value) -> bool { - <$inner>::is(value) + fn add(self, rhs: Self) -> Self { + Self::Union(match (self, rhs) { + (Self::Union(mut lhs), Self::Union(rhs)) => { + lhs.extend(rhs); + lhs } - - fn cast(value: $crate::model::Value) -> $crate::diag::StrResult<Self> { - <$inner>::cast(value).map(Self) + (Self::Union(mut lhs), rhs) => { + lhs.push(rhs); + lhs } - } - }; - - ( - $type:ty, - Expected: $expected:expr, - $($pattern:pat => $out:expr,)* - $(@$dyn_in:ident: $dyn_type:ty => $dyn_out:expr,)* - ) => { - #[allow(unreachable_patterns)] - impl $crate::model::Cast<$crate::model::Value> for $type { - fn is(value: &$crate::model::Value) -> bool { - #[allow(unused_variables)] - match value { - $($pattern => true,)* - $crate::model::Value::Dyn(dynamic) => { - false $(|| dynamic.is::<$dyn_type>())* - } - _ => false, - } - } - - fn cast(value: $crate::model::Value) -> $crate::diag::StrResult<Self> { - let found = match value { - $($pattern => return Ok($out),)* - $crate::model::Value::Dyn(dynamic) => { - $(if let Some($dyn_in) = dynamic.downcast::<$dyn_type>() { - return Ok($dyn_out); - })* - dynamic.type_name() - } - v => v.type_name(), - }; - - Err($crate::util::format_eco!( - "expected {}, found {}", - $expected, - found, - )) + (lhs, Self::Union(mut rhs)) => { + rhs.insert(0, lhs); + rhs } - } - }; + (lhs, rhs) => vec![lhs, rhs], + }) + } } -#[doc(inline)] -pub use crate::__castable as castable; - impl Cast for Value { fn is(_: &Value) -> bool { true @@ -115,6 +122,10 @@ impl Cast for Value { fn cast(value: Value) -> StrResult<Self> { Ok(value) } + + fn describe() -> CastInfo { + CastInfo::Any + } } impl<T: Cast> Cast<Spanned<Value>> for T { @@ -125,6 +136,10 @@ impl<T: Cast> Cast<Spanned<Value>> for T { fn cast(value: Spanned<Value>) -> StrResult<Self> { T::cast(value.v) } + + fn describe() -> CastInfo { + T::describe() + } } impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> { @@ -136,14 +151,64 @@ impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> { let span = value.span; T::cast(value.v).map(|t| Spanned::new(t, span)) } + + fn describe() -> CastInfo { + T::describe() + } +} + +castable! { + Dir: "direction", +} + +castable! { + GenAlign: "alignment", +} + +castable! { + Regex: "regular expression", +} + +castable! { + Selector: "selector", + text: EcoString => Self::text(&text), + label: Label => Self::Label(label), + func: Func => func.select(None)?, + regex: Regex => Self::Regex(regex), +} + +castable! { + Axes<GenAlign>: "2d alignment", +} + +castable! { + PartialStroke: "stroke", + thickness: Length => Self { + paint: Smart::Auto, + thickness: Smart::Custom(thickness), + }, + color: Color => Self { + paint: Smart::Custom(color.into()), + thickness: Smart::Auto, + }, +} + +castable! { + u32, + int: i64 => int.try_into().map_err(|_| { + if int < 0 { + "number must be at least zero" + } else { + "number too large" + } + })?, } castable! { usize, - Expected: "non-negative integer", - Value::Int(int) => int.try_into().map_err(|_| { + int: i64 => int.try_into().map_err(|_| { if int < 0 { - "must be at least zero" + "number must be at least zero" } else { "number too large" } @@ -152,12 +217,11 @@ castable! { castable! { NonZeroUsize, - Expected: "positive integer", - Value::Int(int) => int + int: i64 => int .try_into() .and_then(|int: usize| int.try_into()) .map_err(|_| if int <= 0 { - "must be positive" + "number must be positive" } else { "number too large" })?, @@ -165,41 +229,23 @@ castable! { castable! { Paint, - Expected: "color", - Value::Color(color) => Paint::Solid(color), + color: Color => Self::Solid(color), } castable! { EcoString, - Expected: "string", - Value::Str(str) => str.into(), + string: Str => string.into(), } castable! { String, - Expected: "string", - Value::Str(string) => string.into(), -} - -dynamic! { - Regex: "regular expression", -} - -dynamic! { - Selector: "selector", - Value::Str(text) => Self::text(&text), - Value::Label(label) => Self::Label(label), - Value::Func(func) => func.select(None)?, - @regex: Regex => Self::Regex(regex.clone()), + string: Str => string.into(), } castable! { Transform, - Expected: "content or function", - Value::None => Self::Content(Content::empty()), - Value::Str(text) => Self::Content(item!(text)(text.into())), - Value::Content(content) => Self::Content(content), - Value::Func(func) => { + content: Content => Self::Content(content), + func: Func => { if func.argc().map_or(false, |count| count != 1) { Err("function must have exactly one parameter")? } @@ -207,45 +253,19 @@ castable! { }, } -dynamic! { - Dir: "direction", -} - -dynamic! { - GenAlign: "alignment", -} - -dynamic! { - Axes<GenAlign>: "2d alignment", -} - castable! { Axes<Option<GenAlign>>, - Expected: "1d or 2d alignment", - @align: GenAlign => { + align: GenAlign => { let mut aligns = Axes::default(); - aligns.set(align.axis(), Some(*align)); + aligns.set(align.axis(), Some(align)); aligns }, - @aligns: Axes<GenAlign> => aligns.map(Some), -} - -dynamic! { - PartialStroke: "stroke", - Value::Length(thickness) => Self { - paint: Smart::Auto, - thickness: Smart::Custom(thickness), - }, - Value::Color(color) => Self { - paint: Smart::Custom(color.into()), - thickness: Smart::Auto, - }, + aligns: Axes<GenAlign> => aligns.map(Some), } castable! { Axes<Rel<Length>>, - Expected: "array of two relative lengths", - Value::Array(array) => { + array: Array => { let mut iter = array.into_iter(); match (iter.next(), iter.next(), iter.next()) { (Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?), @@ -256,142 +276,124 @@ castable! { castable! { Location, - Expected: "dictionary with `page`, `x`, and `y` keys", - Value::Dict(dict) => { - let page = dict.get("page")?.clone().cast()?; - let x: Length = dict.get("x")?.clone().cast()?; - let y: Length = dict.get("y")?.clone().cast()?; + mut dict: Dict => { + let page = dict.take("page")?.cast()?; + let x: Length = dict.take("x")?.cast()?; + let y: Length = dict.take("y")?.cast()?; + dict.finish(&["page", "x", "y"])?; Self { page, pos: Point::new(x.abs, y.abs) } }, } castable! { Destination, - Expected: "string or dictionary with `page`, `x`, and `y` keys", - Value::Str(string) => Self::Url(string.into()), - v @ Value::Dict(_) => Self::Internal(v.cast()?), + loc: Location => Self::Internal(loc), + string: EcoString => Self::Url(string), } castable! { FontStyle, - Expected: "string", - Value::Str(string) => match string.as_str() { - "normal" => Self::Normal, - "italic" => Self::Italic, - "oblique" => Self::Oblique, - _ => Err(r#"expected "normal", "italic" or "oblique""#)?, - }, + /// The default style. + "normal" => Self::Normal, + /// A cursive style. + "italic" => Self::Italic, + /// A slanted style. + "oblique" => Self::Oblique, } castable! { FontWeight, - Expected: "integer or string", - Value::Int(v) => Self::from_number(v.clamp(0, u16::MAX as i64) as u16), - Value::Str(string) => match string.as_str() { - "thin" => Self::THIN, - "extralight" => Self::EXTRALIGHT, - "light" => Self::LIGHT, - "regular" => Self::REGULAR, - "medium" => Self::MEDIUM, - "semibold" => Self::SEMIBOLD, - "bold" => Self::BOLD, - "extrabold" => Self::EXTRABOLD, - "black" => Self::BLACK, - _ => Err("unknown font weight")?, - }, + v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16), + /// Thin weight (100). + "thin" => Self::THIN, + /// Extra light weight (200). + "extralight" => Self::EXTRALIGHT, + /// Light weight (300). + "light" => Self::LIGHT, + /// Regular weight (400). + "regular" => Self::REGULAR, + /// Medium weight (500). + "medium" => Self::MEDIUM, + /// Semibold weight (600). + "semibold" => Self::SEMIBOLD, + /// Bold weight (700). + "bold" => Self::BOLD, + /// Extrabold weight (800). + "extrabold" => Self::EXTRABOLD, + /// Black weight (900). + "black" => Self::BLACK, } castable! { FontStretch, - Expected: "ratio", - Value::Ratio(v) => Self::from_ratio(v.get() as f32), + v: Ratio => Self::from_ratio(v.get() as f32), } castable! { Lang, - Expected: "string", - Value::Str(string) => Self::from_str(&string)?, + string: EcoString => Self::from_str(&string)?, } castable! { Region, - Expected: "string", - Value::Str(string) => Self::from_str(&string)?, + string: EcoString => Self::from_str(&string)?, } -impl<T: Cast> Cast for Option<T> { +/// Castable from [`Value::None`]. +pub struct NoneValue; + +impl Cast for NoneValue { fn is(value: &Value) -> bool { - matches!(value, Value::None) || T::is(value) + matches!(value, Value::None) } fn cast(value: Value) -> StrResult<Self> { match value { - Value::None => Ok(None), - v => T::cast(v).map(Some).map_err(|msg| with_alternative(msg, "none")), + Value::None => Ok(Self), + _ => <Self as Cast>::error(value), } } + + fn describe() -> CastInfo { + CastInfo::Type("none") + } } -/// A value that can be automatically determined. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum Smart<T> { - /// The value should be determined smartly based on the circumstances. - Auto, - /// A specific value. - Custom(T), -} - -impl<T> Smart<T> { - /// Map the contained custom value with `f`. - pub fn map<F, U>(self, f: F) -> Smart<U> - where - F: FnOnce(T) -> U, - { - match self { - Self::Auto => Smart::Auto, - Self::Custom(x) => Smart::Custom(f(x)), - } +impl<T: Cast> Cast for Option<T> { + fn is(value: &Value) -> bool { + matches!(value, Value::None) || T::is(value) } - /// Keeps `self` if it contains a custom value, otherwise returns `other`. - pub fn or(self, other: Smart<T>) -> Self { - match self { - Self::Custom(x) => Self::Custom(x), - Self::Auto => other, + 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), } } - /// Returns the contained custom value or a provided default value. - pub fn unwrap_or(self, default: T) -> T { - match self { - Self::Auto => default, - Self::Custom(x) => x, - } + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("none") } +} - /// Returns the contained custom value or computes a default value. - pub fn unwrap_or_else<F>(self, f: F) -> T - where - F: FnOnce() -> T, - { - match self { - Self::Auto => f(), - Self::Custom(x) => x, - } +/// Castable from [`Value::Auto`]. +pub struct AutoValue; + +impl Cast for AutoValue { + fn is(value: &Value) -> bool { + matches!(value, Value::Auto) } - /// Returns the contained custom value or the default value. - pub fn unwrap_or_default(self) -> T - where - T: Default, - { - self.unwrap_or_else(T::default) + fn cast(value: Value) -> StrResult<Self> { + match value { + Value::Auto => Ok(Self), + _ => <Self as Cast>::error(value), + } } -} -impl<T> Default for Smart<T> { - fn default() -> Self { - Self::Auto + fn describe() -> CastInfo { + CastInfo::Type("auto") } } @@ -403,11 +405,14 @@ impl<T: Cast> Cast for Smart<T> { fn cast(value: Value) -> StrResult<Self> { match value { Value::Auto => Ok(Self::Auto), - v => T::cast(v) - .map(Self::Custom) - .map_err(|msg| with_alternative(msg, "auto")), + v if T::is(&v) => Ok(Self::Custom(T::cast(v)?)), + _ => <Self as Cast>::error(value), } } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("auto") + } } impl<T> Cast for Sides<T> @@ -420,7 +425,7 @@ where fn cast(mut value: Value) -> StrResult<Self> { if let Value::Dict(dict) = &mut value { - let mut take = |key| dict.take(key).map(T::cast).transpose(); + let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); let rest = take("rest")?; let x = take("x")?.or(rest); @@ -432,22 +437,19 @@ where bottom: take("bottom")?.or(y), }; - if let Some((key, _)) = dict.iter().next() { - return Err(format_eco!("unexpected key {key:?}")); - } + dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?; Ok(sides.map(Option::unwrap_or_default)) + } else if T::is(&value) { + Ok(Self::splat(T::cast(value)?)) } else { - T::cast(value).map(Self::splat).map_err(|msg| { - with_alternative( - msg, - "dictionary with any of \ - `left`, `top`, `right`, `bottom`, \ - `x`, `y`, or `rest` as keys", - ) - }) + <Self as Cast>::error(value) } } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("dictionary") + } } impl<T> Cast for Corners<T> @@ -460,7 +462,7 @@ where fn cast(mut value: Value) -> StrResult<Self> { if let Value::Dict(dict) = &mut value { - let mut take = |key| dict.take(key).map(T::cast).transpose(); + let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); let rest = take("rest")?; let left = take("left")?.or(rest); @@ -474,20 +476,27 @@ where bottom_left: take("bottom-left")?.or(bottom).or(left), }; - if let Some((key, _)) = dict.iter().next() { - return Err(format_eco!("unexpected key {key:?}")); - } + dict.finish(&[ + "top-left", + "top-right", + "bottom-right", + "bottom-left", + "left", + "top", + "right", + "bottom", + "rest", + ])?; Ok(corners.map(Option::unwrap_or_default)) + } else if T::is(&value) { + Ok(Self::splat(T::cast(value)?)) } else { - T::cast(value).map(Self::splat).map_err(|msg| { - with_alternative( - msg, - "dictionary with any of \ - `top-left`, `top-right`, `bottom-right`, `bottom-left`, \ - `left`, `top`, `right`, `bottom`, or `rest` as keys", - ) - }) + <Self as Cast>::error(value) } } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("dictionary") + } } diff --git a/src/model/content.rs b/src/model/content.rs index e73fa4a8..df910a58 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -11,7 +11,8 @@ use thin_vec::ThinVec; use typst_macros::node; use super::{ - capability, capable, Args, Guard, Key, Property, Recipe, Style, StyleMap, Value, Vm, + capability, capable, Args, Guard, Key, ParamInfo, Property, Recipe, Style, StyleMap, + Value, Vm, }; use crate::diag::{SourceResult, StrResult}; use crate::syntax::Span; @@ -426,6 +427,11 @@ pub trait Node: 'static + Capable { where Self: Sized; + /// List the settable properties. + fn properties() -> Vec<ParamInfo> + where + Self: Sized; + /// Access a field on this node. fn field(&self, name: &str) -> Option<Value>; } diff --git a/src/model/dict.rs b/src/model/dict.rs index d54a0e82..6e014d7e 100644 --- a/src/model/dict.rs +++ b/src/model/dict.rs @@ -62,6 +62,13 @@ impl Dict { Arc::make_mut(&mut self.0).entry(key).or_default() } + /// Remove the value if the dictionary contains the given key. + pub fn take(&mut self, key: &str) -> StrResult<Value> { + Arc::make_mut(&mut self.0) + .remove(key) + .ok_or_else(|| format_eco!("missing key: {:?}", Str::from(key))) + } + /// Whether the dictionary contains a specific key. pub fn contains(&self, key: &str) -> bool { self.0.contains_key(key) @@ -80,11 +87,6 @@ impl Dict { } } - /// Remove the value if the dictionary contains the given key. - pub fn take(&mut self, key: &str) -> Option<Value> { - Arc::make_mut(&mut self.0).remove(key) - } - /// Clear the dictionary. pub fn clear(&mut self) { if Arc::strong_count(&self.0) == 1 { @@ -118,6 +120,17 @@ impl Dict { pub fn iter(&self) -> std::collections::btree_map::Iter<Str, Value> { self.0.iter() } + + /// Return an "unexpected key" error if there is any remaining pair. + pub fn finish(&self, expected: &[&str]) -> StrResult<()> { + if let Some((key, _)) = self.iter().next() { + let parts: Vec<_> = expected.iter().map(|s| format_eco!("\"{s}\"")).collect(); + let mut msg = format!("unexpected key {key:?}, valid keys are "); + crate::diag::comma_list(&mut msg, &parts, "and"); + return Err(msg.into()); + } + Ok(()) + } } /// The missing key access error message. diff --git a/src/model/func.rs b/src/model/func.rs index 0261b5e2..5b38b700 100644 --- a/src/model/func.rs +++ b/src/model/func.rs @@ -5,8 +5,8 @@ use std::sync::Arc; use comemo::{Track, Tracked}; use super::{ - Args, Dict, Eval, Flow, Node, NodeId, Route, Scope, Scopes, Selector, StyleMap, - Value, Vm, + Args, CastInfo, Dict, Eval, Flow, Node, NodeId, Route, Scope, Scopes, Selector, + StyleMap, Value, Vm, }; use crate::diag::{bail, SourceResult, StrResult}; use crate::syntax::ast::{self, AstNode, Expr}; @@ -39,13 +39,14 @@ impl Func { pub fn from_fn( name: &'static str, func: fn(&Vm, &mut Args) -> SourceResult<Value>, - doc: &'static str, + info: FuncInfo, ) -> Self { - Self(Arc::new(Repr::Native(Native { name, func, set: None, node: None, doc }))) + Self(Arc::new(Repr::Native(Native { name, func, set: None, node: None, info }))) } /// Create a new function from a native rust node. - pub fn from_node<T: Node>(name: &'static str, doc: &'static str) -> Self { + pub fn from_node<T: Node>(name: &'static str, mut info: FuncInfo) -> Self { + info.params.extend(T::properties()); Self(Arc::new(Repr::Native(Native { name, func: |ctx, args| { @@ -55,7 +56,7 @@ impl Func { }, set: Some(|args| T::set(args, false)), node: Some(NodeId::of::<T>()), - doc, + info, }))) } @@ -73,11 +74,11 @@ impl Func { } } - /// Documentation for the function. - pub fn doc(&self) -> Option<&str> { + /// Extract details the function. + pub fn info(&self) -> Option<&FuncInfo> { match self.0.as_ref() { - Repr::Native(native) => Some(native.doc), - Repr::With(func, _) => func.doc(), + Repr::Native(native) => Some(&native.info), + Repr::With(func, _) => func.info(), _ => None, } } @@ -192,7 +193,7 @@ struct Native { /// The id of the node to customize with this function's show rule. node: Option<NodeId>, /// Documentation of the function. - doc: &'static str, + info: FuncInfo, } impl Hash for Native { @@ -201,10 +202,44 @@ impl Hash for Native { (self.func as usize).hash(state); self.set.map(|set| set as usize).hash(state); self.node.hash(state); - self.doc.hash(state); } } +/// Details about a function. +#[derive(Debug, Clone)] +pub struct FuncInfo { + /// The function's name. + pub name: &'static str, + /// Tags that categorize the function. + pub tags: &'static [&'static str], + /// Documentation for the function. + pub docs: &'static str, + /// Details about the function's parameters. + pub params: Vec<ParamInfo>, +} + +impl FuncInfo { + /// Get the parameter info for a parameter with the given name + pub fn param(&self, name: &str) -> Option<&ParamInfo> { + self.params.iter().find(|param| param.name == name) + } +} + +/// Describes a named parameter. +#[derive(Debug, Clone)] +pub struct ParamInfo { + /// The parameter's name. + pub name: &'static str, + /// Documentation for the parameter. + pub docs: &'static str, + /// Is the parameter settable with a set rule? + pub settable: bool, + /// Can the name be omitted? + pub shorthand: bool, + /// Valid values for the parameter. + pub cast: CastInfo, +} + /// A user-defined closure. #[derive(Hash)] pub(super) struct Closure { diff --git a/src/model/mod.rs b/src/model/mod.rs index 015df9b3..6ba8014c 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -26,7 +26,7 @@ mod typeset; #[doc(hidden)] pub use once_cell; -pub use typst_macros::{capability, capable, func, node}; +pub use typst_macros::{capability, capable, castable, func, node}; pub use self::args::*; pub use self::array::*; diff --git a/src/model/ops.rs b/src/model/ops.rs index 1a8dcb6b..9da9b0cc 100644 --- a/src/model/ops.rs +++ b/src/model/ops.rs @@ -1,8 +1,8 @@ //! Operations on values. -use super::{Regex, Smart, Value}; +use super::{Regex, Value}; use crate::diag::StrResult; -use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel}; +use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel, Smart}; use crate::util::format_eco; use std::cmp::Ordering; use Value::*; diff --git a/src/model/str.rs b/src/model/str.rs index 0c288d9b..d1bf9d23 100644 --- a/src/model/str.rs +++ b/src/model/str.rs @@ -442,9 +442,8 @@ pub enum StrPattern { castable! { StrPattern, - Expected: "string or regular expression", - Value::Str(text) => Self::Str(text), - @regex: Regex => Self::Regex(regex.clone()), + text: Str => Self::Str(text), + regex: Regex => Self::Regex(regex), } /// A side of a string. @@ -459,8 +458,7 @@ pub enum StrSide { castable! { StrSide, - Expected: "start or end", - @align: GenAlign => match align { + align: GenAlign => match align { GenAlign::Start => Self::Start, GenAlign::End => Self::End, _ => Err("expected either `start` or `end`")?, diff --git a/src/model/styles.rs b/src/model/styles.rs index b2c328fa..1eaf5128 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -7,10 +7,11 @@ use std::sync::Arc; use comemo::{Prehashed, Tracked}; -use super::{Args, Content, Dict, Func, Label, NodeId, Regex, Smart, Value}; +use super::{Args, Content, Dict, Func, Label, NodeId, Regex, Value}; use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::geom::{ Abs, Align, Axes, Corners, Em, GenAlign, Length, Numeric, PartialStroke, Rel, Sides, + Smart, }; use crate::syntax::Span; use crate::util::ReadableTypeId; diff --git a/src/model/value.rs b/src/model/value.rs index 98d11e15..1c687d8d 100644 --- a/src/model/value.rs +++ b/src/model/value.rs @@ -6,7 +6,9 @@ use std::sync::Arc; use siphasher::sip128::{Hasher128, SipHasher}; -use super::{format_str, ops, Args, Array, Cast, Content, Dict, Func, Label, Str}; +use super::{ + format_str, ops, Args, Array, Cast, CastInfo, Content, Dict, Func, Label, Str, +}; use crate::diag::StrResult; use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel, RgbaColor}; use crate::util::{format_eco, EcoString}; @@ -351,6 +353,10 @@ macro_rules! primitive { )), } } + + fn describe() -> CastInfo { + CastInfo::Type(Self::TYPE_NAME) + } } impl From<$type> for Value { |
