summaryrefslogtreecommitdiff
path: root/src/model
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-12-15 22:51:55 +0100
committerLaurenz <laurmaedje@gmail.com>2022-12-15 23:11:20 +0100
commitb6202b646a0d5ecced301d9bac8bfcaf977d7ee4 (patch)
tree7d42cb50f9e66153e7e8b2217009684e25f54f42 /src/model
parentf3980c704544a464f9729cc8bc9f97d3a7454769 (diff)
Reflection for castables
Diffstat (limited to 'src/model')
-rw-r--r--src/model/cast.rs513
-rw-r--r--src/model/content.rs8
-rw-r--r--src/model/dict.rs23
-rw-r--r--src/model/func.rs59
-rw-r--r--src/model/mod.rs2
-rw-r--r--src/model/ops.rs4
-rw-r--r--src/model/str.rs8
-rw-r--r--src/model/styles.rs3
-rw-r--r--src/model/value.rs8
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 {