summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/doc.rs132
-rw-r--r--src/eval/cast.rs474
-rw-r--r--src/eval/func.rs2
-rw-r--r--src/eval/library.rs2
-rw-r--r--src/eval/mod.rs2
-rw-r--r--src/eval/str.rs10
-rw-r--r--src/eval/value.rs68
-rw-r--r--src/font/mod.rs25
-rw-r--r--src/font/variant.rs80
-rw-r--r--src/geom/abs.rs4
-rw-r--r--src/geom/align.rs48
-rw-r--r--src/geom/axes.rs35
-rw-r--r--src/geom/corners.rs97
-rw-r--r--src/geom/dir.rs4
-rw-r--r--src/geom/em.rs16
-rw-r--r--src/geom/length.rs8
-rw-r--r--src/geom/mod.rs40
-rw-r--r--src/geom/paint.rs23
-rw-r--r--src/geom/rel.rs28
-rw-r--r--src/geom/shape.rs35
-rw-r--r--src/geom/sides.rs85
-rw-r--r--src/geom/smart.rs59
-rw-r--r--src/geom/stroke.rs34
-rw-r--r--src/ide/complete.rs5
-rw-r--r--src/lib.rs3
-rw-r--r--src/model/content.rs412
-rw-r--r--src/model/mod.rs2
-rw-r--r--src/model/realize.rs7
-rw-r--r--src/model/styles.rs337
-rw-r--r--src/model/typeset.rs3
-rw-r--r--src/util/mod.rs5
31 files changed, 1112 insertions, 973 deletions
diff --git a/src/doc.rs b/src/doc.rs
index cba3ca99..9ac2f68d 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -7,14 +7,14 @@ use std::sync::Arc;
use ecow::EcoString;
-use crate::eval::{dict, Dict, Value};
+use crate::eval::{cast_from_value, cast_to_value, dict, Dict, Value};
use crate::font::Font;
use crate::geom::{
- self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Numeric,
- Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform,
+ self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Length,
+ Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform,
};
use crate::image::Image;
-use crate::model::{capable, node, Content, Fold, StableId, StyleChain};
+use crate::model::{node, Content, Fold, StableId, StyleChain};
/// A finished document with metadata and page frames.
#[derive(Debug, Default, Clone, Hash)]
@@ -274,7 +274,7 @@ impl Frame {
if self.is_empty() {
return;
}
- for meta in styles.get(Meta::DATA) {
+ for meta in styles.get(MetaNode::DATA) {
if matches!(meta, Meta::Hidden) {
self.clear();
break;
@@ -283,6 +283,14 @@ impl Frame {
}
}
+ /// Add a background fill.
+ pub fn fill(&mut self, fill: Paint) {
+ self.prepend(
+ Point::zero(),
+ Element::Shape(Geometry::Rect(self.size()).filled(fill)),
+ );
+ }
+
/// Add a fill and stroke with optional radius and outset to the frame.
pub fn fill_and_stroke(
&mut self,
@@ -533,6 +541,15 @@ impl FromStr for Lang {
}
}
+cast_from_value! {
+ Lang,
+ 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]);
@@ -559,8 +576,16 @@ impl FromStr for Region {
}
}
+cast_from_value! {
+ Region,
+ string: EcoString => Self::from_str(&string)?,
+}
+
+cast_to_value! {
+ v: Region => v.as_str().into()
+}
+
/// Meta information that isn't visible or renderable.
-#[capable]
#[derive(Debug, Clone, Hash)]
pub enum Meta {
/// An internal or external link.
@@ -572,12 +597,16 @@ pub enum Meta {
Hidden,
}
+/// Host for metadata.
#[node]
-impl Meta {
+pub struct MetaNode {
/// Metadata that should be attached to all elements affected by this style
/// property.
- #[property(fold, skip)]
- pub const DATA: Vec<Meta> = vec![];
+ #[settable]
+ #[fold]
+ #[skip]
+ #[default]
+ pub data: Vec<Meta>,
}
impl Fold for Vec<Meta> {
@@ -589,6 +618,16 @@ impl Fold for Vec<Meta> {
}
}
+cast_from_value! {
+ Meta: "meta",
+}
+
+impl PartialEq for Meta {
+ fn eq(&self, other: &Self) -> bool {
+ crate::util::hash128(self) == crate::util::hash128(other)
+ }
+}
+
/// A link destination.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Destination {
@@ -598,6 +637,19 @@ pub enum Destination {
Url(EcoString),
}
+cast_from_value! {
+ Destination,
+ loc: Location => Self::Internal(loc),
+ string: EcoString => Self::Url(string),
+}
+
+cast_to_value! {
+ v: Destination => match v {
+ Destination::Internal(loc) => loc.into(),
+ Destination::Url(url) => url.into(),
+ }
+}
+
/// A physical location in a document.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Location {
@@ -607,53 +659,21 @@ pub struct Location {
pub pos: Point,
}
-impl Location {
- /// Encode into a user-facing dictionary.
- pub fn encode(&self) -> Dict {
- dict! {
- "page" => Value::Int(self.page.get() as i64),
- "x" => Value::Length(self.pos.x.into()),
- "y" => Value::Length(self.pos.y.into()),
- }
- }
+cast_from_value! {
+ Location,
+ 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) }
+ },
}
-/// Standard semantic roles.
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub enum Role {
- /// A paragraph.
- Paragraph,
- /// A heading of the given level and whether it should be part of the
- /// outline.
- Heading { level: NonZeroUsize, outlined: bool },
- /// A generic block-level subdivision.
- GenericBlock,
- /// A generic inline subdivision.
- GenericInline,
- /// A list and whether it is ordered.
- List { ordered: bool },
- /// A list item. Must have a list parent.
- ListItem,
- /// The label of a list item. Must have a list item parent.
- ListLabel,
- /// The body of a list item. Must have a list item parent.
- ListItemBody,
- /// A mathematical formula.
- Formula,
- /// A table.
- Table,
- /// A table row. Must have a table parent.
- TableRow,
- /// A table cell. Must have a table row parent.
- TableCell,
- /// A code fragment.
- Code,
- /// A page header.
- Header,
- /// A page footer.
- Footer,
- /// A page background.
- Background,
- /// A page foreground.
- Foreground,
+cast_to_value! {
+ v: Location => Value::Dict(dict! {
+ "page" => Value::Int(v.page.get() as i64),
+ "x" => Value::Length(v.pos.x.into()),
+ "y" => Value::Length(v.pos.y.into()),
+ })
}
diff --git a/src/eval/cast.rs b/src/eval/cast.rs
index 77521f7f..840ceb05 100644
--- a/src/eval/cast.rs
+++ b/src/eval/cast.rs
@@ -1,18 +1,12 @@
+pub use typst_macros::{cast_from_value, cast_to_value};
+
use std::num::NonZeroUsize;
use std::ops::Add;
-use std::str::FromStr;
use ecow::EcoString;
-use super::{castable, Array, Dict, Func, Regex, Str, Value};
+use super::{Array, Str, Value};
use crate::diag::StrResult;
-use crate::doc::{Destination, Lang, Location, Region};
-use crate::font::{FontStretch, FontStyle, FontWeight};
-use crate::geom::{
- Axes, Color, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Ratio,
- Rel, Sides, Smart,
-};
-use crate::model::{Content, Label, Selector, Transform};
use crate::syntax::Spanned;
/// Cast from a value to a specific type.
@@ -32,88 +26,6 @@ pub trait Cast<V = Value>: Sized {
}
}
-/// Describes a possible value for a cast.
-#[derive(Debug, Clone, Hash)]
-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);
- }
- }
- }
- }
-
- 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");
- }
-
- crate::diag::comma_list(&mut msg, &parts, "or");
-
- if !matching_type {
- msg.push_str(", found ");
- msg.push_str(found.type_name());
- }
-
- msg.into()
- }
-}
-
-impl Add for CastInfo {
- type Output = Self;
-
- fn add(self, rhs: Self) -> Self {
- Self::Union(match (self, rhs) {
- (Self::Union(mut lhs), Self::Union(rhs)) => {
- lhs.extend(rhs);
- lhs
- }
- (Self::Union(mut lhs), rhs) => {
- lhs.push(rhs);
- lhs
- }
- (lhs, Self::Union(mut rhs)) => {
- rhs.insert(0, lhs);
- rhs
- }
- (lhs, rhs) => vec![lhs, rhs],
- })
- }
-}
-
impl Cast for Value {
fn is(_: &Value) -> bool {
true
@@ -157,43 +69,15 @@ impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> {
}
}
-castable! {
- Dir: "direction",
-}
-
-castable! {
- GenAlign: "alignment",
-}
-
-castable! {
- Regex: "regular expression",
+cast_to_value! {
+ v: u8 => Value::Int(v as i64)
}
-castable! {
- Selector: "selector",
- text: EcoString => Self::text(&text),
- label: Label => Self::Label(label),
- func: Func => func.select(None)?,
- regex: Regex => Self::Regex(regex),
+cast_to_value! {
+ v: u16 => Value::Int(v as i64)
}
-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! {
+cast_from_value! {
u32,
int: i64 => int.try_into().map_err(|_| {
if int < 0 {
@@ -204,7 +88,15 @@ castable! {
})?,
}
-castable! {
+cast_to_value! {
+ v: u32 => Value::Int(v as i64)
+}
+
+cast_to_value! {
+ v: i32 => Value::Int(v as i64)
+}
+
+cast_from_value! {
usize,
int: i64 => int.try_into().map_err(|_| {
if int < 0 {
@@ -215,7 +107,11 @@ castable! {
})?,
}
-castable! {
+cast_to_value! {
+ v: usize => Value::Int(v as i64)
+}
+
+cast_from_value! {
NonZeroUsize,
int: i64 => int
.try_into()
@@ -227,12 +123,11 @@ castable! {
})?,
}
-castable! {
- Paint,
- color: Color => Self::Solid(color),
+cast_to_value! {
+ v: NonZeroUsize => Value::Int(v.get() as i64)
}
-castable! {
+cast_from_value! {
char,
string: Str => {
let mut chars = string.chars();
@@ -243,131 +138,30 @@ castable! {
},
}
-castable! {
- EcoString,
- string: Str => string.into(),
-}
-
-castable! {
- String,
- string: Str => string.into(),
-}
-
-castable! {
- Transform,
- content: Content => Self::Content(content),
- func: Func => {
- if func.argc().map_or(false, |count| count != 1) {
- Err("function must have exactly one parameter")?
- }
- Self::Func(func)
- },
-}
-
-castable! {
- Axes<Option<GenAlign>>,
- align: GenAlign => {
- let mut aligns = Axes::default();
- aligns.set(align.axis(), Some(align));
- aligns
- },
- aligns: Axes<GenAlign> => aligns.map(Some),
-}
-
-castable! {
- Axes<Rel<Length>>,
- 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()?),
- _ => Err("point array must contain exactly two entries")?,
- }
- },
-}
-
-castable! {
- Location,
- 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,
- loc: Location => Self::Internal(loc),
- string: EcoString => Self::Url(string),
+cast_to_value! {
+ v: char => Value::Str(v.into())
}
-castable! {
- FontStyle,
- /// The default, typically upright style.
- "normal" => Self::Normal,
- /// A cursive style with custom letterform.
- "italic" => Self::Italic,
- /// Just a slanted version of the normal style.
- "oblique" => Self::Oblique,
+cast_to_value! {
+ v: &str => Value::Str(v.into())
}
-castable! {
- FontWeight,
- 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,
+cast_from_value! {
+ EcoString,
+ v: Str => v.into(),
}
-castable! {
- FontStretch,
- v: Ratio => Self::from_ratio(v.get() as f32),
+cast_to_value! {
+ v: EcoString => Value::Str(v.into())
}
-castable! {
- Lang,
- string: EcoString => Self::from_str(&string)?,
+cast_from_value! {
+ String,
+ v: Str => v.into(),
}
-castable! {
- Region,
- string: EcoString => Self::from_str(&string)?,
-}
-
-/// Castable from [`Value::None`].
-pub struct NoneValue;
-
-impl Cast for NoneValue {
- fn is(value: &Value) -> bool {
- matches!(value, Value::None)
- }
-
- fn cast(value: Value) -> StrResult<Self> {
- match value {
- Value::None => Ok(Self),
- _ => <Self as Cast>::error(value),
- }
- }
-
- fn describe() -> CastInfo {
- CastInfo::Type("none")
- }
+cast_to_value! {
+ v: String => Value::Str(v.into())
}
impl<T: Cast> Cast for Option<T> {
@@ -388,126 +182,130 @@ impl<T: Cast> Cast for Option<T> {
}
}
-/// Castable from [`Value::Auto`].
-pub struct AutoValue;
+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 Cast for AutoValue {
+impl<T: Cast> Cast for Vec<T> {
fn is(value: &Value) -> bool {
- matches!(value, Value::Auto)
+ Array::is(value)
}
fn cast(value: Value) -> StrResult<Self> {
- match value {
- Value::Auto => Ok(Self),
- _ => <Self as Cast>::error(value),
- }
+ value.cast::<Array>()?.into_iter().map(Value::cast).collect()
}
fn describe() -> CastInfo {
- CastInfo::Type("auto")
+ <Array as Cast>::describe()
}
}
-impl<T: Cast> Cast for Smart<T> {
- fn is(value: &Value) -> bool {
- matches!(value, Value::Auto) || T::is(value)
+impl<T: Into<Value>> From<Vec<T>> for Value {
+ fn from(v: Vec<T>) -> Self {
+ Value::Array(v.into_iter().map(Into::into).collect())
}
+}
- 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),
+/// Describes a possible value for a cast.
+#[derive(Debug, Clone, Hash)]
+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);
+ }
+ }
+ }
}
- }
- fn describe() -> CastInfo {
- T::describe() + CastInfo::Type("auto")
- }
-}
+ let mut matching_type = false;
+ let mut parts = vec![];
+ accumulate(self, found, &mut parts, &mut matching_type);
-impl<T> Cast for Sides<Option<T>>
-where
- T: Cast + Copy,
-{
- fn is(value: &Value) -> bool {
- matches!(value, Value::Dict(_)) || T::is(value)
- }
+ let mut msg = String::from("expected ");
+ if parts.is_empty() {
+ msg.push_str(" nothing");
+ }
- fn cast(mut value: Value) -> StrResult<Self> {
- if let Value::Dict(dict) = &mut value {
- let mut take = |key| dict.take(key).ok().map(T::cast).transpose();
-
- let rest = take("rest")?;
- let x = take("x")?.or(rest);
- let y = take("y")?.or(rest);
- let sides = Sides {
- left: take("left")?.or(x),
- top: take("top")?.or(y),
- right: take("right")?.or(x),
- bottom: take("bottom")?.or(y),
- };
-
- dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?;
-
- Ok(sides)
- } else if T::is(&value) {
- Ok(Self::splat(Some(T::cast(value)?)))
- } else {
- <Self as Cast>::error(value)
+ crate::diag::comma_list(&mut msg, &parts, "or");
+
+ if !matching_type {
+ msg.push_str(", found ");
+ msg.push_str(found.type_name());
}
+
+ msg.into()
}
+}
- fn describe() -> CastInfo {
- T::describe() + CastInfo::Type("dictionary")
+impl Add for CastInfo {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self {
+ Self::Union(match (self, rhs) {
+ (Self::Union(mut lhs), Self::Union(rhs)) => {
+ lhs.extend(rhs);
+ lhs
+ }
+ (Self::Union(mut lhs), rhs) => {
+ lhs.push(rhs);
+ lhs
+ }
+ (lhs, Self::Union(mut rhs)) => {
+ rhs.insert(0, lhs);
+ rhs
+ }
+ (lhs, rhs) => vec![lhs, rhs],
+ })
}
}
-impl<T> Cast for Corners<Option<T>>
-where
- T: Cast + Copy,
-{
- fn is(value: &Value) -> bool {
- matches!(value, Value::Dict(_)) || T::is(value)
+/// Castable from nothing.
+pub enum Never {}
+
+impl Cast for Never {
+ fn is(_: &Value) -> bool {
+ false
}
- fn cast(mut value: Value) -> StrResult<Self> {
- if let Value::Dict(dict) = &mut value {
- let mut take = |key| dict.take(key).ok().map(T::cast).transpose();
-
- let rest = take("rest")?;
- let left = take("left")?.or(rest);
- let top = take("top")?.or(rest);
- let right = take("right")?.or(rest);
- let bottom = take("bottom")?.or(rest);
- let corners = Corners {
- top_left: take("top-left")?.or(top).or(left),
- top_right: take("top-right")?.or(top).or(right),
- bottom_right: take("bottom-right")?.or(bottom).or(right),
- bottom_left: take("bottom-left")?.or(bottom).or(left),
- };
-
- dict.finish(&[
- "top-left",
- "top-right",
- "bottom-right",
- "bottom-left",
- "left",
- "top",
- "right",
- "bottom",
- "rest",
- ])?;
-
- Ok(corners)
- } else if T::is(&value) {
- Ok(Self::splat(Some(T::cast(value)?)))
- } else {
- <Self as Cast>::error(value)
- }
+ fn cast(value: Value) -> StrResult<Self> {
+ <Self as Cast>::error(value)
}
fn describe() -> CastInfo {
- T::describe() + CastInfo::Type("dictionary")
+ CastInfo::Union(vec![])
}
}
diff --git a/src/eval/func.rs b/src/eval/func.rs
index e5280932..8243b4f6 100644
--- a/src/eval/func.rs
+++ b/src/eval/func.rs
@@ -1,3 +1,5 @@
+pub use typst_macros::func;
+
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
diff --git a/src/eval/library.rs b/src/eval/library.rs
index adfcc6e7..75787348 100644
--- a/src/eval/library.rs
+++ b/src/eval/library.rs
@@ -44,7 +44,7 @@ pub struct LangItems {
/// The id of the text node.
pub text_id: NodeId,
/// Get the string if this is a text node.
- pub text_str: fn(&Content) -> Option<&str>,
+ pub text_str: fn(&Content) -> Option<EcoString>,
/// A smart quote: `'` or `"`.
pub smart_quote: fn(double: bool) -> Content,
/// A paragraph break.
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index 2cf6f4d1..8180f11d 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -20,8 +20,6 @@ mod ops;
mod scope;
mod symbol;
-pub use typst_macros::{castable, func};
-
pub use self::args::*;
pub use self::array::*;
pub use self::cast::*;
diff --git a/src/eval/str.rs b/src/eval/str.rs
index 63ea5dc8..0d5d71b9 100644
--- a/src/eval/str.rs
+++ b/src/eval/str.rs
@@ -6,7 +6,7 @@ use std::ops::{Add, AddAssign, Deref};
use ecow::EcoString;
use unicode_segmentation::UnicodeSegmentation;
-use super::{castable, dict, Array, Dict, Value};
+use super::{cast_from_value, dict, Array, Dict, Value};
use crate::diag::StrResult;
use crate::geom::GenAlign;
@@ -479,6 +479,10 @@ impl Hash for Regex {
}
}
+cast_from_value! {
+ Regex: "regular expression",
+}
+
/// A pattern which can be searched for in a string.
#[derive(Debug, Clone)]
pub enum StrPattern {
@@ -488,7 +492,7 @@ pub enum StrPattern {
Regex(Regex),
}
-castable! {
+cast_from_value! {
StrPattern,
text: Str => Self::Str(text),
regex: Regex => Self::Regex(regex),
@@ -504,7 +508,7 @@ pub enum StrSide {
End,
}
-castable! {
+cast_from_value! {
StrSide,
align: GenAlign => match align {
GenAlign::Start => Self::Start,
diff --git a/src/eval/value.rs b/src/eval/value.rs
index 5e06da76..9b9bc314 100644
--- a/src/eval/value.rs
+++ b/src/eval/value.rs
@@ -4,15 +4,15 @@ use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
-use ecow::{eco_format, EcoString};
+use ecow::eco_format;
use siphasher::sip128::{Hasher128, SipHasher};
use super::{
- format_str, ops, Args, Array, Cast, CastInfo, Content, Dict, Func, Label, Module,
- Str, Symbol,
+ cast_to_value, format_str, ops, Args, Array, Cast, CastInfo, Content, Dict, Func,
+ Label, Module, Str, Symbol,
};
use crate::diag::StrResult;
-use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel, RgbaColor};
+use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel};
use crate::syntax::{ast, Span};
/// A computational value.
@@ -122,6 +122,7 @@ impl Value {
Self::Dict(dict) => dict.at(&field).cloned(),
Self::Content(content) => content
.field(&field)
+ .cloned()
.ok_or_else(|| eco_format!("unknown field `{field}`")),
Self::Module(module) => module.get(&field).cloned(),
v => Err(eco_format!("cannot access fields on type {}", v.type_name())),
@@ -241,60 +242,6 @@ impl Hash for Value {
}
}
-impl From<i32> for Value {
- fn from(v: i32) -> Self {
- Self::Int(v as i64)
- }
-}
-
-impl From<usize> for Value {
- fn from(v: usize) -> Self {
- Self::Int(v as i64)
- }
-}
-
-impl From<Abs> for Value {
- fn from(v: Abs) -> Self {
- Self::Length(v.into())
- }
-}
-
-impl From<Em> for Value {
- fn from(v: Em) -> Self {
- Self::Length(v.into())
- }
-}
-
-impl From<RgbaColor> for Value {
- fn from(v: RgbaColor) -> Self {
- Self::Color(v.into())
- }
-}
-
-impl From<&str> for Value {
- fn from(v: &str) -> Self {
- Self::Str(v.into())
- }
-}
-
-impl From<EcoString> for Value {
- fn from(v: EcoString) -> Self {
- Self::Str(v.into())
- }
-}
-
-impl From<String> for Value {
- fn from(v: String) -> Self {
- Self::Str(v.into())
- }
-}
-
-impl From<Dynamic> for Value {
- fn from(v: Dynamic) -> Self {
- Self::Dyn(v)
- }
-}
-
/// A dynamic value.
#[derive(Clone, Hash)]
pub struct Dynamic(Arc<dyn Bounds>);
@@ -336,6 +283,10 @@ impl PartialEq for Dynamic {
}
}
+cast_to_value! {
+ v: Dynamic => Value::Dyn(v)
+}
+
trait Bounds: Debug + Sync + Send + 'static {
fn as_any(&self) -> &dyn Any;
fn dyn_eq(&self, other: &Dynamic) -> bool;
@@ -462,6 +413,7 @@ primitive! { Args: "arguments", Args }
mod tests {
use super::*;
use crate::eval::{array, dict};
+ use crate::geom::RgbaColor;
#[track_caller]
fn test(value: impl Into<Value>, exp: &str) {
diff --git a/src/font/mod.rs b/src/font/mod.rs
index bedc107d..94ec170e 100644
--- a/src/font/mod.rs
+++ b/src/font/mod.rs
@@ -12,6 +12,7 @@ use std::sync::Arc;
use ttf_parser::GlyphId;
+use crate::eval::{cast_from_value, cast_to_value, Value};
use crate::geom::Em;
use crate::util::Buffer;
@@ -249,3 +250,27 @@ pub enum VerticalFontMetric {
/// present and falls back to the descender from the `hhea` table otherwise.
Descender,
}
+
+cast_from_value! {
+ VerticalFontMetric,
+ /// The font's ascender, which typically exceeds the height of all glyphs.
+ "ascender" => Self::Ascender,
+ /// The approximate height of uppercase letters.
+ "cap-height" => Self::CapHeight,
+ /// The approximate height of non-ascending lowercase letters.
+ "x-height" => Self::XHeight,
+ /// The baseline on which the letters rest.
+ "baseline" => Self::Baseline,
+ /// The font's ascender, which typically exceeds the depth of all glyphs.
+ "descender" => Self::Descender,
+}
+
+cast_to_value! {
+ v: VerticalFontMetric => Value::from(match v {
+ VerticalFontMetric::Ascender => "ascender",
+ VerticalFontMetric::CapHeight => "cap-height",
+ VerticalFontMetric::XHeight => "x-height",
+ VerticalFontMetric::Baseline => "baseline" ,
+ VerticalFontMetric::Descender => "descender",
+ })
+}
diff --git a/src/font/variant.rs b/src/font/variant.rs
index aa9ff141..4eda80ad 100644
--- a/src/font/variant.rs
+++ b/src/font/variant.rs
@@ -2,6 +2,9 @@ use std::fmt::{self, Debug, Formatter};
use serde::{Deserialize, Serialize};
+use crate::eval::{cast_from_value, cast_to_value, Value};
+use crate::geom::Ratio;
+
/// Properties that distinguish a font from other fonts in the same family.
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(Serialize, Deserialize)]
@@ -59,6 +62,24 @@ impl Default for FontStyle {
}
}
+cast_from_value! {
+ FontStyle,
+ /// The default, typically upright style.
+ "normal" => Self::Normal,
+ /// A cursive style with custom letterform.
+ "italic" => Self::Italic,
+ /// Just a slanted version of the normal style.
+ "oblique" => Self::Oblique,
+}
+
+cast_to_value! {
+ v: FontStyle => Value::from(match v {
+ FontStyle::Normal => "normal",
+ FontStyle::Italic => "italic",
+ FontStyle::Oblique => "oblique",
+ })
+}
+
/// The weight of a font.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[derive(Serialize, Deserialize)]
@@ -127,6 +148,44 @@ impl Debug for FontWeight {
}
}
+cast_from_value! {
+ FontWeight,
+ 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,
+}
+
+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)]
@@ -163,8 +222,8 @@ impl FontStretch {
/// Create a font stretch from a ratio between 0.5 and 2.0, clamping it if
/// necessary.
- pub fn from_ratio(ratio: f32) -> Self {
- Self((ratio.max(0.5).min(2.0) * 1000.0) as u16)
+ pub fn from_ratio(ratio: Ratio) -> Self {
+ Self((ratio.get().max(0.5).min(2.0) * 1000.0) as u16)
}
/// Create a font stretch from an OpenType-style number between 1 and 9,
@@ -184,12 +243,12 @@ impl FontStretch {
}
/// The ratio between 0.5 and 2.0 corresponding to this stretch.
- pub fn to_ratio(self) -> f32 {
- self.0 as f32 / 1000.0
+ pub fn to_ratio(self) -> Ratio {
+ Ratio::new(self.0 as f64 / 1000.0)
}
/// The absolute ratio distance between this and another font stretch.
- pub fn distance(self, other: Self) -> f32 {
+ pub fn distance(self, other: Self) -> Ratio {
(self.to_ratio() - other.to_ratio()).abs()
}
}
@@ -202,10 +261,19 @@ impl Default for FontStretch {
impl Debug for FontStretch {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "{}%", 100.0 * self.to_ratio())
+ self.to_ratio().fmt(f)
}
}
+cast_from_value! {
+ FontStretch,
+ 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 4429e46d..34c3d010 100644
--- a/src/geom/abs.rs
+++ b/src/geom/abs.rs
@@ -214,6 +214,10 @@ impl<'a> Sum<&'a Self> for Abs {
}
}
+cast_to_value! {
+ v: Abs => Value::Length(v.into())
+}
+
/// Different units of absolute measurement.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum AbsUnit {
diff --git a/src/geom/align.rs b/src/geom/align.rs
index 1e9bde52..b14e6775 100644
--- a/src/geom/align.rs
+++ b/src/geom/align.rs
@@ -115,3 +115,51 @@ impl Debug for GenAlign {
}
}
}
+
+cast_from_value! {
+ GenAlign: "alignment",
+}
+
+cast_from_value! {
+ Axes<GenAlign>: "2d alignment",
+}
+
+cast_from_value! {
+ Axes<Option<GenAlign>>,
+ align: GenAlign => {
+ let mut aligns = Axes::default();
+ aligns.set(align.axis(), Some(align));
+ aligns
+ },
+ aligns: Axes<GenAlign> => aligns.map(Some),
+}
+
+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 Resolve for GenAlign {
+ type Output = Align;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ let dir = item!(dir)(styles);
+ match self {
+ Self::Start => dir.start().into(),
+ Self::End => dir.end().into(),
+ Self::Specific(align) => align,
+ }
+ }
+}
+
+impl Fold for GenAlign {
+ type Output = Self;
+
+ fn fold(self, _: Self::Output) -> Self::Output {
+ self
+ }
+}
diff --git a/src/geom/axes.rs b/src/geom/axes.rs
index 48f8c0e8..92bd0388 100644
--- a/src/geom/axes.rs
+++ b/src/geom/axes.rs
@@ -2,6 +2,7 @@ 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)]
@@ -272,3 +273,37 @@ impl BitAndAssign for Axes<bool> {
self.y &= rhs.y;
}
}
+
+cast_from_value! {
+ Axes<Rel<Length>>,
+ 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()?),
+ _ => Err("point array must contain exactly two entries")?,
+ }
+ },
+}
+
+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>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|v| v.resolve(styles))
+ }
+}
+
+impl<T: Fold> Fold for Axes<Option<T>> {
+ type Output = Axes<T::Output>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.zip(outer).map(|(inner, outer)| match inner {
+ Some(value) => value.fold(outer),
+ None => outer,
+ })
+ }
+}
diff --git a/src/geom/corners.rs b/src/geom/corners.rs
index 386acbfb..844d3047 100644
--- a/src/geom/corners.rs
+++ b/src/geom/corners.rs
@@ -107,3 +107,100 @@ pub enum Corner {
/// The bottom left corner.
BottomLeft,
}
+
+impl<T> Cast for Corners<Option<T>>
+where
+ T: Cast + Copy,
+{
+ fn is(value: &Value) -> bool {
+ matches!(value, Value::Dict(_)) || T::is(value)
+ }
+
+ fn cast(mut value: Value) -> StrResult<Self> {
+ if let Value::Dict(dict) = &mut value {
+ let mut take = |key| dict.take(key).ok().map(T::cast).transpose();
+
+ let rest = take("rest")?;
+ let left = take("left")?.or(rest);
+ let top = take("top")?.or(rest);
+ let right = take("right")?.or(rest);
+ let bottom = take("bottom")?.or(rest);
+ let corners = Corners {
+ top_left: take("top-left")?.or(top).or(left),
+ top_right: take("top-right")?.or(top).or(right),
+ bottom_right: take("bottom-right")?.or(bottom).or(right),
+ bottom_left: take("bottom-left")?.or(bottom).or(left),
+ };
+
+ dict.finish(&[
+ "top-left",
+ "top-right",
+ "bottom-right",
+ "bottom-left",
+ "left",
+ "top",
+ "right",
+ "bottom",
+ "rest",
+ ])?;
+
+ Ok(corners)
+ } else if T::is(&value) {
+ Ok(Self::splat(Some(T::cast(value)?)))
+ } else {
+ <Self as Cast>::error(value)
+ }
+ }
+
+ fn describe() -> CastInfo {
+ T::describe() + CastInfo::Type("dictionary")
+ }
+}
+
+impl<T: Resolve> Resolve for Corners<T> {
+ type Output = Corners<T::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|v| v.resolve(styles))
+ }
+}
+
+impl<T: Fold> Fold for Corners<Option<T>> {
+ type Output = Corners<T::Output>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.zip(outer).map(|(inner, outer)| match inner {
+ Some(value) => value.fold(outer),
+ None => outer,
+ })
+ }
+}
+
+impl<T> From<Corners<Option<T>>> for Value
+where
+ T: PartialEq + Into<Value>,
+{
+ fn from(corners: Corners<Option<T>>) -> Self {
+ if corners.is_uniform() {
+ if let Some(value) = corners.top_left {
+ return value.into();
+ }
+ }
+
+ let mut dict = Dict::new();
+ if let Some(top_left) = corners.top_left {
+ dict.insert("top-left".into(), top_left.into());
+ }
+ if let Some(top_right) = corners.top_right {
+ dict.insert("top-right".into(), top_right.into());
+ }
+ if let Some(bottom_right) = corners.bottom_right {
+ dict.insert("bottom-right".into(), bottom_right.into());
+ }
+ if let Some(bottom_left) = corners.bottom_left {
+ dict.insert("bottom-left".into(), bottom_left.into());
+ }
+
+ Value::Dict(dict)
+ }
+}
diff --git a/src/geom/dir.rs b/src/geom/dir.rs
index b2fd6e5a..bc4d66e1 100644
--- a/src/geom/dir.rs
+++ b/src/geom/dir.rs
@@ -73,3 +73,7 @@ impl Debug for Dir {
})
}
}
+
+cast_from_value! {
+ Dir: "direction",
+}
diff --git a/src/geom/em.rs b/src/geom/em.rs
index 9f5aff39..2c63c81d 100644
--- a/src/geom/em.rs
+++ b/src/geom/em.rs
@@ -134,3 +134,19 @@ impl Sum for Em {
Self(iter.map(|s| s.0).sum())
}
}
+
+cast_to_value! {
+ v: Em => Value::Length(v.into())
+}
+
+impl Resolve for Em {
+ type Output = Abs;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ if self.is_zero() {
+ Abs::zero()
+ } else {
+ self.at(item!(em)(styles))
+ }
+ }
+}
diff --git a/src/geom/length.rs b/src/geom/length.rs
index ae615f14..f70ea263 100644
--- a/src/geom/length.rs
+++ b/src/geom/length.rs
@@ -124,3 +124,11 @@ assign_impl!(Length += Length);
assign_impl!(Length -= Length);
assign_impl!(Length *= f64);
assign_impl!(Length /= f64);
+
+impl Resolve for Length {
+ type Output = Abs;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.abs + self.em.resolve(styles)
+ }
+}
diff --git a/src/geom/mod.rs b/src/geom/mod.rs
index ebe4436c..b7daaa1b 100644
--- a/src/geom/mod.rs
+++ b/src/geom/mod.rs
@@ -19,6 +19,7 @@ mod ratio;
mod rel;
mod rounded;
mod scalar;
+mod shape;
mod sides;
mod size;
mod smart;
@@ -42,6 +43,7 @@ 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::*;
@@ -55,6 +57,10 @@ use std::hash::{Hash, Hasher};
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::model::{Fold, Resolve, StyleChain};
+
/// Generic access to a structure's components.
pub trait Get<Index> {
/// The structure's component type.
@@ -72,40 +78,6 @@ pub trait Get<Index> {
}
}
-/// A geometric shape with optional fill and stroke.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct Shape {
- /// The shape's geometry.
- pub geometry: Geometry,
- /// The shape's background fill.
- pub fill: Option<Paint>,
- /// The shape's border stroke.
- pub stroke: Option<Stroke>,
-}
-
-/// A shape's geometry.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub enum Geometry {
- /// A line to a point (relative to its position).
- Line(Point),
- /// A rectangle with its origin in the topleft corner.
- Rect(Size),
- /// A bezier path.
- Path(Path),
-}
-
-impl Geometry {
- /// Fill the geometry without a stroke.
- pub fn filled(self, fill: Paint) -> Shape {
- Shape { geometry: self, fill: Some(fill), stroke: None }
- }
-
- /// Stroke the geometry without a fill.
- pub fn stroked(self, stroke: Stroke) -> Shape {
- Shape { geometry: self, fill: None, stroke: Some(stroke) }
- }
-}
-
/// A numeric type.
pub trait Numeric:
Sized
diff --git a/src/geom/paint.rs b/src/geom/paint.rs
index b4064438..c01b21da 100644
--- a/src/geom/paint.rs
+++ b/src/geom/paint.rs
@@ -9,10 +9,7 @@ pub enum Paint {
Solid(Color),
}
-impl<T> From<T> for Paint
-where
- T: Into<Color>,
-{
+impl<T: Into<Color>> From<T> for Paint {
fn from(t: T) -> Self {
Self::Solid(t.into())
}
@@ -26,6 +23,15 @@ impl Debug for Paint {
}
}
+cast_from_value! {
+ Paint,
+ 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 {
@@ -274,15 +280,16 @@ impl Debug for RgbaColor {
}
}
-impl<T> From<T> for Color
-where
- T: Into<RgbaColor>,
-{
+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 {
diff --git a/src/geom/rel.rs b/src/geom/rel.rs
index a8e75d1c..7288f380 100644
--- a/src/geom/rel.rs
+++ b/src/geom/rel.rs
@@ -199,3 +199,31 @@ impl<T: Numeric> Add<Ratio> for Rel<T> {
self + Rel::from(other)
}
}
+
+impl<T> Resolve for Rel<T>
+where
+ T: Resolve + Numeric,
+ <T as Resolve>::Output: Numeric,
+{
+ type Output = Rel<<T as Resolve>::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|abs| abs.resolve(styles))
+ }
+}
+
+impl Fold for Rel<Abs> {
+ type Output = Self;
+
+ fn fold(self, _: Self::Output) -> Self::Output {
+ self
+ }
+}
+
+impl Fold for Rel<Length> {
+ type Output = Self;
+
+ fn fold(self, _: Self::Output) -> Self::Output {
+ self
+ }
+}
diff --git a/src/geom/shape.rs b/src/geom/shape.rs
new file mode 100644
index 00000000..5658c21f
--- /dev/null
+++ b/src/geom/shape.rs
@@ -0,0 +1,35 @@
+use super::*;
+
+/// A geometric shape with optional fill and stroke.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct Shape {
+ /// The shape's geometry.
+ pub geometry: Geometry,
+ /// The shape's background fill.
+ pub fill: Option<Paint>,
+ /// The shape's border stroke.
+ pub stroke: Option<Stroke>,
+}
+
+/// A shape's geometry.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub enum Geometry {
+ /// A line to a point (relative to its position).
+ Line(Point),
+ /// A rectangle with its origin in the topleft corner.
+ Rect(Size),
+ /// A bezier path.
+ Path(Path),
+}
+
+impl Geometry {
+ /// Fill the geometry without a stroke.
+ pub fn filled(self, fill: Paint) -> Shape {
+ Shape { geometry: self, fill: Some(fill), stroke: None }
+ }
+
+ /// Stroke the geometry without a fill.
+ pub fn stroked(self, stroke: Stroke) -> Shape {
+ Shape { geometry: self, fill: None, stroke: Some(stroke) }
+ }
+}
diff --git a/src/geom/sides.rs b/src/geom/sides.rs
index 40327a42..247d9a98 100644
--- a/src/geom/sides.rs
+++ b/src/geom/sides.rs
@@ -177,3 +177,88 @@ impl Side {
}
}
}
+
+impl<T> Cast for Sides<Option<T>>
+where
+ T: Default + Cast + Copy,
+{
+ fn is(value: &Value) -> bool {
+ matches!(value, Value::Dict(_)) || T::is(value)
+ }
+
+ fn cast(mut value: Value) -> StrResult<Self> {
+ if let Value::Dict(dict) = &mut value {
+ let mut take = |key| dict.take(key).ok().map(T::cast).transpose();
+
+ let rest = take("rest")?;
+ let x = take("x")?.or(rest);
+ let y = take("y")?.or(rest);
+ let sides = Sides {
+ left: take("left")?.or(x),
+ top: take("top")?.or(y),
+ right: take("right")?.or(x),
+ bottom: take("bottom")?.or(y),
+ };
+
+ dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?;
+
+ Ok(sides)
+ } else if T::is(&value) {
+ Ok(Self::splat(Some(T::cast(value)?)))
+ } else {
+ <Self as Cast>::error(value)
+ }
+ }
+
+ fn describe() -> CastInfo {
+ T::describe() + CastInfo::Type("dictionary")
+ }
+}
+
+impl<T> From<Sides<Option<T>>> for Value
+where
+ T: PartialEq + Into<Value>,
+{
+ fn from(sides: Sides<Option<T>>) -> Self {
+ if sides.is_uniform() {
+ if let Some(value) = sides.left {
+ return value.into();
+ }
+ }
+
+ let mut dict = Dict::new();
+ if let Some(left) = sides.left {
+ dict.insert("left".into(), left.into());
+ }
+ if let Some(top) = sides.top {
+ dict.insert("top".into(), top.into());
+ }
+ if let Some(right) = sides.right {
+ dict.insert("right".into(), right.into());
+ }
+ if let Some(bottom) = sides.bottom {
+ dict.insert("bottom".into(), bottom.into());
+ }
+
+ Value::Dict(dict)
+ }
+}
+
+impl<T: Resolve> Resolve for Sides<T> {
+ type Output = Sides<T::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|v| v.resolve(styles))
+ }
+}
+
+impl<T: Fold> Fold for Sides<Option<T>> {
+ type Output = Sides<T::Output>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.zip(outer).map(|(inner, outer)| match inner {
+ Some(value) => value.fold(outer),
+ None => outer,
+ })
+ }
+}
diff --git a/src/geom/smart.rs b/src/geom/smart.rs
index e115e99d..c977d651 100644
--- a/src/geom/smart.rs
+++ b/src/geom/smart.rs
@@ -31,6 +31,18 @@ impl<T> Smart<T> {
}
}
+ /// Map the contained custom value with `f` if it contains a custom value,
+ /// otherwise returns `default`.
+ pub fn map_or<F, U>(self, default: U, f: F) -> U
+ where
+ F: FnOnce(T) -> U,
+ {
+ match self {
+ Self::Auto => default,
+ Self::Custom(x) => f(x),
+ }
+ }
+
/// Keeps `self` if it contains a custom value, otherwise returns `other`.
pub fn or(self, other: Smart<T>) -> Self {
match self {
@@ -72,3 +84,50 @@ impl<T> Default for Smart<T> {
Self::Auto
}
}
+
+impl<T: Cast> Cast for Smart<T> {
+ fn is(value: &Value) -> bool {
+ matches!(value, Value::Auto) || T::is(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() + CastInfo::Type("auto")
+ }
+}
+
+impl<T: Resolve> Resolve for Smart<T> {
+ type Output = Smart<T::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|v| v.resolve(styles))
+ }
+}
+
+impl<T> Fold for Smart<T>
+where
+ T: Fold,
+ T::Output: Default,
+{
+ type Output = Smart<T::Output>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ 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 86191d33..500a4c10 100644
--- a/src/geom/stroke.rs
+++ b/src/geom/stroke.rs
@@ -58,3 +58,37 @@ impl<T: Debug> Debug for PartialStroke<T> {
}
}
}
+
+cast_from_value! {
+ PartialStroke: "stroke",
+ thickness: Length => Self {
+ paint: Smart::Auto,
+ thickness: Smart::Custom(thickness),
+ },
+ color: Color => Self {
+ paint: Smart::Custom(color.into()),
+ thickness: Smart::Auto,
+ },
+}
+
+impl Resolve for PartialStroke {
+ type Output = PartialStroke<Abs>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ PartialStroke {
+ paint: self.paint,
+ thickness: self.thickness.resolve(styles),
+ }
+ }
+}
+
+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),
+ }
+ }
+}
diff --git a/src/ide/complete.rs b/src/ide/complete.rs
index 06ab53a1..f5eece93 100644
--- a/src/ide/complete.rs
+++ b/src/ide/complete.rs
@@ -338,6 +338,11 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) {
}
}
}
+ Value::Content(content) => {
+ for (name, value) in content.fields() {
+ ctx.value_completion(Some(name.clone()), value, false, None);
+ }
+ }
Value::Dict(dict) => {
for (name, value) in dict.iter() {
ctx.value_completion(Some(name.clone().into()), value, false, None);
diff --git a/src/lib.rs b/src/lib.rs
index d73055d1..7cfed897 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -39,14 +39,13 @@ extern crate self as typst;
#[macro_use]
pub mod util;
#[macro_use]
-pub mod geom;
-#[macro_use]
pub mod diag;
#[macro_use]
pub mod eval;
pub mod doc;
pub mod export;
pub mod font;
+pub mod geom;
pub mod ide;
pub mod image;
pub mod model;
diff --git a/src/model/content.rs b/src/model/content.rs
index b10a3409..2af4ae72 100644
--- a/src/model/content.rs
+++ b/src/model/content.rs
@@ -1,27 +1,24 @@
-use std::any::{Any, TypeId};
-use std::fmt::{self, Debug, Formatter};
+use std::any::TypeId;
+use std::fmt::{self, Debug, Formatter, Write};
use std::hash::{Hash, Hasher};
use std::iter::{self, Sum};
use std::ops::{Add, AddAssign};
-use std::sync::Arc;
use comemo::Tracked;
use ecow::{EcoString, EcoVec};
-use siphasher::sip128::{Hasher128, SipHasher};
-use typst_macros::node;
-use super::{capability, capable, Guard, Key, Property, Recipe, Style, StyleMap};
+use super::{node, Guard, Key, Property, Recipe, Style, StyleMap};
use crate::diag::{SourceResult, StrResult};
-use crate::eval::{Args, ParamInfo, Value, Vm};
+use crate::eval::{cast_from_value, Args, Cast, ParamInfo, Value, Vm};
use crate::syntax::Span;
-use crate::util::ReadableTypeId;
use crate::World;
/// Composable representation of styled content.
#[derive(Clone, Hash)]
pub struct Content {
- obj: Arc<dyn Bounds>,
+ id: NodeId,
span: Option<Span>,
+ fields: EcoVec<(EcoString, Value)>,
modifiers: EcoVec<Modifier>,
}
@@ -30,55 +27,43 @@ pub struct Content {
enum Modifier {
Prepared,
Guard(Guard),
- Label(Label),
- Field(EcoString, Value),
}
impl Content {
+ pub fn new<T: Node>() -> Self {
+ Self {
+ id: T::id(),
+ span: None,
+ fields: EcoVec::new(),
+ modifiers: EcoVec::new(),
+ }
+ }
+
/// Create empty content.
pub fn empty() -> Self {
- SequenceNode(vec![]).pack()
+ SequenceNode::new(vec![]).pack()
}
/// Create a new sequence node from multiples nodes.
pub fn sequence(seq: Vec<Self>) -> Self {
match seq.as_slice() {
[_] => seq.into_iter().next().unwrap(),
- _ => SequenceNode(seq).pack(),
+ _ => SequenceNode::new(seq).pack(),
}
}
/// Attach a span to the content.
pub fn spanned(mut self, span: Span) -> Self {
- if let Some(styled) = self.to_mut::<StyledNode>() {
- styled.sub.span = Some(span);
- } else if let Some(styled) = self.to::<StyledNode>() {
- self = StyledNode {
- sub: styled.sub.clone().spanned(span),
- map: styled.map.clone(),
- }
- .pack();
+ if let Some(styled) = self.to::<StyledNode>() {
+ self = StyledNode::new(styled.sub().spanned(span), styled.map()).pack();
}
self.span = Some(span);
self
}
/// Attach a label to the content.
- pub fn labelled(mut self, label: Label) -> Self {
- for (i, modifier) in self.modifiers.iter().enumerate() {
- if matches!(modifier, Modifier::Label(_)) {
- self.modifiers.make_mut()[i] = Modifier::Label(label);
- return self;
- }
- }
-
- self.modifiers.push(Modifier::Label(label));
- self
- }
-
- /// Attach a field to the content.
- pub fn push_field(&mut self, name: impl Into<EcoString>, value: Value) {
- self.modifiers.push(Modifier::Field(name.into(), value));
+ pub fn labelled(self, label: Label) -> Self {
+ self.with_field("label", label)
}
/// Style this content with a single style property.
@@ -87,31 +72,21 @@ impl Content {
}
/// Style this content with a style entry.
- pub fn styled_with_entry(mut self, style: Style) -> Self {
- if let Some(styled) = self.to_mut::<StyledNode>() {
- styled.map.apply_one(style);
- self
- } else if let Some(styled) = self.to::<StyledNode>() {
- let mut map = styled.map.clone();
- map.apply_one(style);
- StyledNode { sub: styled.sub.clone(), map }.pack()
- } else {
- StyledNode { sub: self, map: style.into() }.pack()
- }
+ pub fn styled_with_entry(self, style: Style) -> Self {
+ self.styled_with_map(style.into())
}
/// Style this content with a full style map.
- pub fn styled_with_map(mut self, styles: StyleMap) -> Self {
+ pub fn styled_with_map(self, styles: StyleMap) -> Self {
if styles.is_empty() {
- return self;
- }
-
- if let Some(styled) = self.to_mut::<StyledNode>() {
- styled.map.apply(styles);
- return self;
+ self
+ } else if let Some(styled) = self.to::<StyledNode>() {
+ let mut map = styled.map();
+ map.apply(styles);
+ StyledNode::new(styled.sub(), map).pack()
+ } else {
+ StyledNode::new(self, styles).pack()
}
-
- StyledNode { sub: self, map: styles }.pack()
}
/// Style this content with a recipe, eagerly applying it if possible.
@@ -139,12 +114,12 @@ impl Content {
impl Content {
/// The id of the contained node.
pub fn id(&self) -> NodeId {
- (*self.obj).id()
+ self.id
}
/// The node's human-readable name.
pub fn name(&self) -> &'static str {
- (*self.obj).name()
+ self.id.name()
}
/// The node's span.
@@ -154,72 +129,86 @@ impl Content {
/// The content's label.
pub fn label(&self) -> Option<&Label> {
- self.modifiers.iter().find_map(|modifier| match modifier {
- Modifier::Label(label) => Some(label),
+ match self.field("label")? {
+ Value::Label(label) => Some(label),
_ => None,
- })
+ }
}
- /// Access a field on this content.
- pub fn field(&self, name: &str) -> Option<Value> {
- if name == "label" {
- return Some(match self.label() {
- Some(label) => Value::Label(label.clone()),
- None => Value::None,
- });
- }
+ pub fn with_field(
+ mut self,
+ name: impl Into<EcoString>,
+ value: impl Into<Value>,
+ ) -> Self {
+ self.push_field(name, value);
+ self
+ }
- for modifier in &self.modifiers {
- if let Modifier::Field(other, value) = modifier {
- if name == other {
- return Some(value.clone());
- }
- }
+ /// Attach a field to the content.
+ pub fn push_field(&mut self, name: impl Into<EcoString>, value: impl Into<Value>) {
+ let name = name.into();
+ if let Some(i) = self.fields.iter().position(|(field, _)| *field == name) {
+ self.fields.make_mut()[i] = (name, value.into());
+ } else {
+ self.fields.push((name, value.into()));
}
+ }
- self.obj.field(name)
+ pub fn field(&self, name: &str) -> Option<&Value> {
+ static NONE: Value = Value::None;
+ self.fields
+ .iter()
+ .find(|(field, _)| field == name)
+ .map(|(_, value)| value)
+ .or_else(|| (name == "label").then(|| &NONE))
+ }
+
+ pub fn fields(&self) -> &[(EcoString, Value)] {
+ &self.fields
+ }
+
+ #[track_caller]
+ pub fn cast_field<T: Cast>(&self, name: &str) -> T {
+ match self.field(name) {
+ Some(value) => value.clone().cast().unwrap(),
+ None => field_is_missing(name),
+ }
}
/// Whether the contained node is of type `T`.
pub fn is<T>(&self) -> bool
where
- T: Capable + 'static,
+ T: Node + 'static,
{
- (*self.obj).as_any().is::<T>()
+ self.id == NodeId::of::<T>()
}
/// Cast to `T` if the contained node is of type `T`.
pub fn to<T>(&self) -> Option<&T>
where
- T: Capable + 'static,
+ T: Node + 'static,
{
- (*self.obj).as_any().downcast_ref::<T>()
+ self.is::<T>().then(|| unsafe { std::mem::transmute(self) })
}
/// Whether this content has the given capability.
pub fn has<C>(&self) -> bool
where
- C: Capability + ?Sized,
+ C: ?Sized + 'static,
{
- self.obj.vtable(TypeId::of::<C>()).is_some()
+ (self.id.0.vtable)(TypeId::of::<C>()).is_some()
}
/// Cast to a trait object if this content has the given capability.
pub fn with<C>(&self) -> Option<&C>
where
- C: Capability + ?Sized,
+ C: ?Sized + 'static,
{
- let node: &dyn Bounds = &*self.obj;
- let vtable = node.vtable(TypeId::of::<C>())?;
- let data = node as *const dyn Bounds as *const ();
+ let vtable = (self.id.0.vtable)(TypeId::of::<C>())?;
+ let data = self as *const Self as *const ();
Some(unsafe { &*crate::util::fat::from_raw_parts(data, vtable) })
}
- /// Try to cast to a mutable instance of `T`.
- fn to_mut<T: 'static>(&mut self) -> Option<&mut T> {
- Arc::get_mut(&mut self.obj)?.as_any_mut().downcast_mut::<T>()
- }
-
/// Disable a show rule recipe.
#[doc(hidden)]
pub fn guarded(mut self, id: Guard) -> Self {
@@ -262,12 +251,40 @@ impl Content {
pub(super) fn copy_modifiers(&mut self, from: &Content) {
self.span = from.span;
self.modifiers = from.modifiers.clone();
+ if let Some(label) = from.label() {
+ self.push_field("label", label.clone())
+ }
}
}
impl Debug for Content {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.obj.fmt(f)
+ struct Pad<'a>(&'a str);
+ impl Debug for Pad<'_> {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad(self.0)
+ }
+ }
+
+ if let Some(styled) = self.to::<StyledNode>() {
+ styled.map().fmt(f)?;
+ styled.sub().fmt(f)
+ } else if let Some(seq) = self.to::<SequenceNode>() {
+ f.debug_list().entries(&seq.children()).finish()
+ } else if self.id.name() == "space" {
+ ' '.fmt(f)
+ } else if self.id.name() == "text" {
+ self.field("text").unwrap().fmt(f)
+ } else {
+ f.write_str(self.name())?;
+ if self.fields.is_empty() {
+ return Ok(());
+ }
+ f.write_char(' ')?;
+ f.debug_map()
+ .entries(self.fields.iter().map(|(name, value)| (Pad(name), value)))
+ .finish()
+ }
}
}
@@ -280,27 +297,19 @@ impl Default for Content {
impl Add for Content {
type Output = Self;
- fn add(self, mut rhs: Self) -> Self::Output {
- let mut lhs = self;
- if let Some(lhs_mut) = lhs.to_mut::<SequenceNode>() {
- if let Some(rhs_mut) = rhs.to_mut::<SequenceNode>() {
- lhs_mut.0.append(&mut rhs_mut.0);
- } else if let Some(rhs) = rhs.to::<SequenceNode>() {
- lhs_mut.0.extend(rhs.0.iter().cloned());
- } else {
- lhs_mut.0.push(rhs);
- }
- return lhs;
- }
-
+ fn add(self, rhs: Self) -> Self::Output {
+ let lhs = self;
let seq = match (lhs.to::<SequenceNode>(), rhs.to::<SequenceNode>()) {
- (Some(lhs), Some(rhs)) => lhs.0.iter().chain(&rhs.0).cloned().collect(),
- (Some(lhs), None) => lhs.0.iter().cloned().chain(iter::once(rhs)).collect(),
- (None, Some(rhs)) => iter::once(lhs).chain(rhs.0.iter().cloned()).collect(),
+ (Some(lhs), Some(rhs)) => {
+ lhs.children().into_iter().chain(rhs.children()).collect()
+ }
+ (Some(lhs), None) => {
+ lhs.children().into_iter().chain(iter::once(rhs)).collect()
+ }
+ (None, Some(rhs)) => iter::once(lhs).chain(rhs.children()).collect(),
(None, None) => vec![lhs, rhs],
};
-
- SequenceNode(seq).pack()
+ SequenceNode::new(seq).pack()
}
}
@@ -316,73 +325,33 @@ impl Sum for Content {
}
}
-trait Bounds: Node + Debug + Sync + Send + 'static {
- fn as_any(&self) -> &dyn Any;
- fn as_any_mut(&mut self) -> &mut dyn Any;
- fn hash128(&self) -> u128;
-}
-
-impl<T> Bounds for T
-where
- T: Node + Debug + Hash + Sync + Send + 'static,
-{
- fn as_any(&self) -> &dyn Any {
- self
- }
-
- fn as_any_mut(&mut self) -> &mut dyn Any {
- self
- }
-
- fn hash128(&self) -> u128 {
- let mut state = SipHasher::new();
- self.type_id().hash(&mut state);
- self.hash(&mut state);
- state.finish128().as_u128()
- }
-}
-
-impl Hash for dyn Bounds {
- fn hash<H: Hasher>(&self, state: &mut H) {
- state.write_u128(self.hash128());
- }
-}
-
/// A node with applied styles.
-#[capable]
-#[derive(Clone, Hash)]
+#[node]
pub struct StyledNode {
/// The styled content.
+ #[positional]
+ #[required]
pub sub: Content,
+
/// The styles.
+ #[positional]
+ #[required]
pub map: StyleMap,
}
-#[node]
-impl StyledNode {}
-
-impl Debug for StyledNode {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.map.fmt(f)?;
- self.sub.fmt(f)
- }
+cast_from_value! {
+ StyleMap: "style map",
}
/// A sequence of nodes.
///
/// Combines other arbitrary content. So, when you write `[Hi] + [you]` in
/// Typst, the two text nodes are combined into a single sequence node.
-#[capable]
-#[derive(Clone, Hash)]
-pub struct SequenceNode(pub Vec<Content>);
-
#[node]
-impl SequenceNode {}
-
-impl Debug for SequenceNode {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.debug_list().entries(self.0.iter()).finish()
- }
+pub struct SequenceNode {
+ #[variadic]
+ #[required]
+ pub children: Vec<Content>,
}
/// A label for a node.
@@ -396,80 +365,83 @@ impl Debug for Label {
}
/// A constructable, stylable content node.
-pub trait Node: 'static + Capable {
+pub trait Node: Construct + Set + Sized + 'static {
+ /// The node's ID.
+ fn id() -> NodeId;
+
/// Pack a node into type-erased content.
- fn pack(self) -> Content
- where
- Self: Node + Debug + Hash + Sync + Send + Sized + 'static,
- {
- Content {
- obj: Arc::new(self),
- span: None,
- modifiers: EcoVec::new(),
- }
- }
+ fn pack(self) -> Content;
+}
- /// A unique identifier of the node type.
- fn id(&self) -> NodeId;
+/// A unique identifier for a node.
+#[derive(Copy, Clone)]
+pub struct NodeId(&'static NodeMeta);
- /// The node's name.
- fn name(&self) -> &'static str;
+impl NodeId {
+ pub fn of<T: Node>() -> Self {
+ T::id()
+ }
- /// Construct a node from the arguments.
- ///
- /// This is passed only the arguments that remain after execution of the
- /// node's set rule.
- fn construct(vm: &Vm, args: &mut Args) -> SourceResult<Content>
- where
- Self: Sized;
+ pub fn from_meta(meta: &'static NodeMeta) -> Self {
+ Self(meta)
+ }
- /// Parse relevant arguments into style properties for this node.
- ///
- /// When `constructor` is true, [`construct`](Self::construct) will run
- /// after this invocation of `set` with the remaining arguments.
- fn set(args: &mut Args, constructor: bool) -> SourceResult<StyleMap>
- where
- Self: Sized;
+ /// The name of the identified node.
+ pub fn name(self) -> &'static str {
+ self.0.name
+ }
+}
- /// List the settable properties.
- fn properties() -> Vec<ParamInfo>
- where
- Self: Sized;
+impl Debug for NodeId {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad(self.name())
+ }
+}
- /// Access a field on this node.
- fn field(&self, name: &str) -> Option<Value>;
+impl Hash for NodeId {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ state.write_usize(self.0 as *const _ as usize);
+ }
}
-/// A unique identifier for a node type.
-#[derive(Copy, Clone, Eq, PartialEq, Hash)]
-pub struct NodeId(ReadableTypeId);
+impl Eq for NodeId {}
-impl NodeId {
- /// The id of the given node type.
- pub fn of<T: 'static>() -> Self {
- Self(ReadableTypeId::of::<T>())
+impl PartialEq for NodeId {
+ fn eq(&self, other: &Self) -> bool {
+ std::ptr::eq(self.0, other.0)
}
}
-impl Debug for NodeId {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.0.fmt(f)
- }
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct NodeMeta {
+ pub name: &'static str,
+ pub vtable: fn(of: TypeId) -> Option<*const ()>,
}
-/// A capability a node can have.
-///
-/// Should be implemented by trait objects that are accessible through
-/// [`Capable`].
-pub trait Capability: 'static {}
-
-/// Dynamically access a trait implementation at runtime.
-pub unsafe trait Capable {
- /// Return the vtable pointer of the trait object with given type `id`
- /// if `self` implements the trait.
- fn vtable(&self, of: TypeId) -> Option<*const ()>;
+pub trait Construct {
+ /// Construct a node from the arguments.
+ ///
+ /// This is passed only the arguments that remain after execution of the
+ /// node's set rule.
+ fn construct(vm: &Vm, args: &mut Args) -> SourceResult<Content>;
+}
+
+pub trait Set {
+ /// Parse relevant arguments into style properties for this node.
+ ///
+ /// When `constructor` is true, [`construct`](Construct::construct) will run
+ /// after this invocation of `set` with the remaining arguments.
+ fn set(args: &mut Args, constructor: bool) -> SourceResult<StyleMap>;
+
+ /// List the settable properties.
+ fn properties() -> Vec<ParamInfo>;
}
/// Indicates that a node cannot be labelled.
-#[capability]
pub trait Unlabellable {}
+
+#[cold]
+#[track_caller]
+fn field_is_missing(name: &str) -> ! {
+ panic!("required field `{name}` is missing")
+}
diff --git a/src/model/mod.rs b/src/model/mod.rs
index 692d18d5..07329e3f 100644
--- a/src/model/mod.rs
+++ b/src/model/mod.rs
@@ -13,4 +13,4 @@ pub use self::typeset::*;
#[doc(hidden)]
pub use once_cell;
-pub use typst_macros::{capability, capable, node};
+pub use typst_macros::node;
diff --git a/src/model/realize.rs b/src/model/realize.rs
index b33cc0bb..2f38df51 100644
--- a/src/model/realize.rs
+++ b/src/model/realize.rs
@@ -1,4 +1,4 @@
-use super::{capability, Content, NodeId, Recipe, Selector, StyleChain, Vt};
+use super::{Content, NodeId, Recipe, Selector, StyleChain, Vt};
use crate::diag::SourceResult;
/// Whether the target is affected by show rules in the given style chain.
@@ -105,7 +105,7 @@ fn try_apply(
let mut result = vec![];
let mut cursor = 0;
- for m in regex.find_iter(text) {
+ for m in regex.find_iter(&text) {
let start = m.start();
if cursor < start {
result.push(make(text[cursor..start].into()));
@@ -133,7 +133,6 @@ fn try_apply(
}
/// Preparations before execution of any show rule.
-#[capability]
pub trait Prepare {
/// Prepare the node for show rule application.
fn prepare(
@@ -145,7 +144,6 @@ pub trait Prepare {
}
/// The base recipe for a node.
-#[capability]
pub trait Show {
/// Execute the base recipe for this node.
fn show(
@@ -157,7 +155,6 @@ pub trait Show {
}
/// Post-process a node after it was realized.
-#[capability]
pub trait Finalize {
/// Finalize the fully realized form of the node. Use this for effects that
/// should work even in the face of a user-defined show rule, for example
diff --git a/src/model/styles.rs b/src/model/styles.rs
index 18507491..cbf4cfb2 100644
--- a/src/model/styles.rs
+++ b/src/model/styles.rs
@@ -1,21 +1,16 @@
use std::any::Any;
use std::fmt::{self, Debug, Formatter};
-use std::hash::Hash;
+use std::hash::{Hash, Hasher};
use std::iter;
use std::marker::PhantomData;
-use std::sync::Arc;
-use comemo::{Prehashed, Tracked};
+use comemo::Tracked;
+use ecow::EcoString;
-use super::{Content, Label, NodeId};
+use super::{Content, Label, Node, NodeId};
use crate::diag::{SourceResult, Trace, Tracepoint};
-use crate::eval::{Args, Dict, Func, Regex, Value};
-use crate::geom::{
- Abs, Align, Axes, Corners, Em, GenAlign, Length, Numeric, PartialStroke, Rel, Sides,
- Smart,
-};
+use crate::eval::{cast_from_value, Args, Cast, Dict, Func, Regex, Value};
use crate::syntax::Span;
-use crate::util::ReadableTypeId;
use crate::World;
/// A map of style properties.
@@ -76,7 +71,7 @@ impl StyleMap {
/// Mark all contained properties as _scoped_. This means that they only
/// apply to the first descendant node (of their type) in the hierarchy and
/// not its children, too. This is used by
- /// [constructors](super::Node::construct).
+ /// [constructors](super::Construct::construct).
pub fn scoped(mut self) -> Self {
for entry in &mut self.0 {
if let Style::Property(property) = entry {
@@ -98,7 +93,7 @@ impl StyleMap {
/// Returns `Some(_)` with an optional span if this map contains styles for
/// the given `node`.
- pub fn interruption<T: 'static>(&self) -> Option<Option<Span>> {
+ pub fn interruption<T: Node>(&self) -> Option<Option<Span>> {
let node = NodeId::of::<T>();
self.0.iter().find_map(|entry| match entry {
Style::Property(property) => property.is_of(node).then(|| property.origin),
@@ -114,6 +109,12 @@ impl From<Style> for StyleMap {
}
}
+impl PartialEq for StyleMap {
+ fn eq(&self, other: &Self) -> bool {
+ crate::util::hash128(self) == crate::util::hash128(other)
+ }
+}
+
impl Debug for StyleMap {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
for entry in self.0.iter() {
@@ -154,13 +155,11 @@ impl Style {
impl Debug for Style {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.write_str("#[")?;
match self {
- Self::Property(property) => property.fmt(f)?,
- Self::Recipe(recipe) => recipe.fmt(f)?,
- Self::Barrier(id) => write!(f, "Barrier for {id:?}")?,
+ Self::Property(property) => property.fmt(f),
+ Self::Recipe(recipe) => recipe.fmt(f),
+ Self::Barrier(id) => write!(f, "#[Barrier for {id:?}]"),
}
- f.write_str("]")
}
}
@@ -175,12 +174,9 @@ pub struct Property {
/// hierarchy. Used by constructors.
scoped: bool,
/// The property's value.
- value: Arc<Prehashed<dyn Bounds>>,
+ value: Value,
/// The span of the set rule the property stems from.
origin: Option<Span>,
- /// The name of the property.
- #[cfg(debug_assertions)]
- name: &'static str,
}
impl Property {
@@ -189,11 +185,9 @@ impl Property {
Self {
key: KeyId::of::<K>(),
node: K::node(),
- value: Arc::new(Prehashed::new(value)),
+ value: value.into(),
scoped: false,
origin: None,
- #[cfg(debug_assertions)]
- name: K::NAME,
}
}
@@ -208,9 +202,12 @@ impl Property {
}
/// Access the property's value if it is of the given key.
- pub fn downcast<K: Key>(&self) -> Option<&K::Value> {
+ #[track_caller]
+ pub fn cast<K: Key>(&self) -> Option<K::Value> {
if self.key == KeyId::of::<K>() {
- (**self.value).as_any().downcast_ref()
+ Some(self.value.clone().cast().unwrap_or_else(|err| {
+ panic!("{} (for {} with value {:?})", err, self.key.name(), self.value)
+ }))
} else {
None
}
@@ -234,9 +231,7 @@ impl Property {
impl Debug for Property {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- #[cfg(debug_assertions)]
- write!(f, "{} = ", self.name)?;
- write!(f, "{:?}", self.value)?;
+ write!(f, "#set {}({}: {:?})", self.node.name(), self.key.name(), self.value)?;
if self.scoped {
write!(f, " [scoped]")?;
}
@@ -267,47 +262,69 @@ where
/// A style property key.
///
-/// This trait is not intended to be implemented manually, but rather through
-/// the `#[node]` proc-macro.
+/// This trait is not intended to be implemented manually.
pub trait Key: Copy + 'static {
/// The unfolded type which this property is stored as in a style map.
- type Value: Debug + Clone + Hash + Sync + Send + 'static;
+ type Value: Cast + Into<Value>;
/// The folded type of value that is returned when reading this property
/// from a style chain.
- type Output<'a>;
+ type Output;
- /// The name of the property, used for debug printing.
- const NAME: &'static str;
+ /// The id of the property.
+ fn id() -> KeyId;
/// The id of the node the key belongs to.
fn node() -> NodeId;
/// Compute an output value from a sequence of values belonging to this key,
/// folding if necessary.
- fn get<'a>(
- chain: StyleChain<'a>,
- values: impl Iterator<Item = &'a Self::Value>,
- ) -> Self::Output<'a>;
+ fn get(chain: StyleChain, values: impl Iterator<Item = Self::Value>) -> Self::Output;
}
-/// A unique identifier for a property key.
-#[derive(Copy, Clone, Eq, PartialEq, Hash)]
-struct KeyId(ReadableTypeId);
+/// A unique identifier for a style key.
+#[derive(Copy, Clone)]
+pub struct KeyId(&'static KeyMeta);
impl KeyId {
- /// The id of the given key.
pub fn of<T: Key>() -> Self {
- Self(ReadableTypeId::of::<T>())
+ T::id()
+ }
+
+ pub fn from_meta(meta: &'static KeyMeta) -> Self {
+ Self(meta)
+ }
+
+ pub fn name(self) -> &'static str {
+ self.0.name
}
}
impl Debug for KeyId {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.0.fmt(f)
+ f.pad(self.name())
+ }
+}
+
+impl Hash for KeyId {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ state.write_usize(self.0 as *const _ as usize);
+ }
+}
+
+impl Eq for KeyId {}
+
+impl PartialEq for KeyId {
+ fn eq(&self, other: &Self) -> bool {
+ std::ptr::eq(self.0, other.0)
}
}
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct KeyMeta {
+ pub name: &'static str,
+}
+
/// A show rule recipe.
#[derive(Clone, Hash)]
pub struct Recipe {
@@ -362,7 +379,7 @@ impl Recipe {
impl Debug for Recipe {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "Recipe matching {:?}", self.selector)
+ write!(f, "#show {:?}: {:?}", self.selector, self.transform)
}
}
@@ -382,7 +399,7 @@ pub enum Selector {
impl Selector {
/// Define a simple node selector.
- pub fn node<T: 'static>() -> Self {
+ pub fn node<T: Node>() -> Self {
Self::Node(NodeId::of::<T>(), None)
}
@@ -399,17 +416,25 @@ impl Selector {
&& dict
.iter()
.flat_map(|dict| dict.iter())
- .all(|(name, value)| target.field(name).as_ref() == Some(value))
+ .all(|(name, value)| target.field(name) == Some(value))
}
Self::Label(label) => target.label() == Some(label),
Self::Regex(regex) => {
target.id() == item!(text_id)
- && item!(text_str)(target).map_or(false, |text| regex.is_match(text))
+ && item!(text_str)(target).map_or(false, |text| regex.is_match(&text))
}
}
}
}
+cast_from_value! {
+ Selector: "selector",
+ text: EcoString => Self::text(&text),
+ label: Label => Self::Label(label),
+ func: Func => func.select(None)?,
+ regex: Regex => Self::Regex(regex),
+}
+
/// A show rule transformation that can be applied to a match.
#[derive(Debug, Clone, Hash)]
pub enum Transform {
@@ -421,6 +446,17 @@ pub enum Transform {
Style(StyleMap),
}
+cast_from_value! {
+ Transform,
+ content: Content => Self::Content(content),
+ func: Func => {
+ if func.argc().map_or(false, |count| count != 1) {
+ Err("function must have exactly one parameter")?
+ }
+ Self::Func(func)
+ },
+}
+
/// A chain of style maps, similar to a linked list.
///
/// A style chain allows to combine properties from multiple style maps in a
@@ -478,7 +514,7 @@ impl<'a> StyleChain<'a> {
/// Returns the property's default value if no map in the chain contains an
/// entry for it. Also takes care of resolving and folding and returns
/// references where applicable.
- pub fn get<K: Key>(self, key: K) -> K::Output<'a> {
+ pub fn get<K: Key>(self, key: K) -> K::Output {
K::get(self, self.values(key))
}
@@ -534,10 +570,7 @@ impl Debug for StyleChain<'_> {
impl PartialEq for StyleChain<'_> {
fn eq(&self, other: &Self) -> bool {
- let as_ptr = |s| s as *const _;
- self.head.as_ptr() == other.head.as_ptr()
- && self.head.len() == other.head.len()
- && self.tail.map(as_ptr) == other.tail.map(as_ptr)
+ crate::util::hash128(self) == crate::util::hash128(other)
}
}
@@ -585,13 +618,14 @@ struct Values<'a, K> {
}
impl<'a, K: Key> Iterator for Values<'a, K> {
- type Item = &'a K::Value;
+ type Item = K::Value;
+ #[track_caller]
fn next(&mut self) -> Option<Self::Item> {
for entry in &mut self.entries {
match entry {
Style::Property(property) => {
- if let Some(value) = property.downcast::<K>() {
+ if let Some(value) = property.cast::<K>() {
if !property.scoped() || self.barriers <= 1 {
return Some(value);
}
@@ -672,6 +706,20 @@ impl<T> StyleVec<T> {
}
}
+impl StyleVec<Content> {
+ pub fn to_vec(self) -> Vec<Content> {
+ self.items
+ .into_iter()
+ .zip(
+ self.maps
+ .iter()
+ .flat_map(|(map, count)| iter::repeat(map).take(*count)),
+ )
+ .map(|(content, map)| content.styled_with_map(map.clone()))
+ .collect()
+ }
+}
+
impl<T> Default for StyleVec<T> {
fn default() -> Self {
Self { items: vec![], maps: vec![] }
@@ -791,26 +839,6 @@ pub trait Resolve {
fn resolve(self, styles: StyleChain) -> Self::Output;
}
-impl Resolve for Em {
- type Output = Abs;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- if self.is_zero() {
- Abs::zero()
- } else {
- self.at(item!(em)(styles))
- }
- }
-}
-
-impl Resolve for Length {
- type Output = Abs;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.abs + self.em.resolve(styles)
- }
-}
-
impl<T: Resolve> Resolve for Option<T> {
type Output = Option<T::Output>;
@@ -819,74 +847,6 @@ impl<T: Resolve> Resolve for Option<T> {
}
}
-impl<T: Resolve> Resolve for Smart<T> {
- type Output = Smart<T::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|v| v.resolve(styles))
- }
-}
-
-impl<T: Resolve> Resolve for Axes<T> {
- type Output = Axes<T::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|v| v.resolve(styles))
- }
-}
-
-impl<T: Resolve> Resolve for Sides<T> {
- type Output = Sides<T::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|v| v.resolve(styles))
- }
-}
-
-impl<T: Resolve> Resolve for Corners<T> {
- type Output = Corners<T::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|v| v.resolve(styles))
- }
-}
-
-impl<T> Resolve for Rel<T>
-where
- T: Resolve + Numeric,
- <T as Resolve>::Output: Numeric,
-{
- type Output = Rel<<T as Resolve>::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|abs| abs.resolve(styles))
- }
-}
-
-impl Resolve for GenAlign {
- type Output = Align;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- let dir = item!(dir)(styles);
- match self {
- Self::Start => dir.start().into(),
- Self::End => dir.end().into(),
- Self::Specific(align) => align,
- }
- }
-}
-
-impl Resolve for PartialStroke {
- type Output = PartialStroke<Abs>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- PartialStroke {
- paint: self.paint,
- thickness: self.thickness.resolve(styles),
- }
- }
-}
-
/// A property that is folded to determine its final value.
pub trait Fold {
/// The type of the folded output.
@@ -907,92 +867,3 @@ where
self.map(|inner| inner.fold(outer.unwrap_or_default()))
}
}
-
-impl<T> Fold for Smart<T>
-where
- T: Fold,
- T::Output: Default,
-{
- type Output = Smart<T::Output>;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- self.map(|inner| inner.fold(outer.unwrap_or_default()))
- }
-}
-
-impl<T> Fold for Axes<Option<T>>
-where
- T: Fold,
-{
- type Output = Axes<T::Output>;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- self.zip(outer).map(|(inner, outer)| match inner {
- Some(value) => value.fold(outer),
- None => outer,
- })
- }
-}
-
-impl<T> Fold for Sides<Option<T>>
-where
- T: Fold,
-{
- type Output = Sides<T::Output>;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- self.zip(outer).map(|(inner, outer)| match inner {
- Some(value) => value.fold(outer),
- None => outer,
- })
- }
-}
-
-impl<T> Fold for Corners<Option<T>>
-where
- T: Fold,
-{
- type Output = Corners<T::Output>;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- self.zip(outer).map(|(inner, outer)| match inner {
- Some(value) => value.fold(outer),
- None => outer,
- })
- }
-}
-
-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),
- }
- }
-}
-
-impl Fold for Rel<Length> {
- type Output = Self;
-
- fn fold(self, _: Self::Output) -> Self::Output {
- self
- }
-}
-
-impl Fold for Rel<Abs> {
- type Output = Self;
-
- fn fold(self, _: Self::Output) -> Self::Output {
- self
- }
-}
-
-impl Fold for GenAlign {
- type Output = Self;
-
- fn fold(self, _: Self::Output) -> Self::Output {
- self
- }
-}
diff --git a/src/model/typeset.rs b/src/model/typeset.rs
index f8b5e012..6361e6ce 100644
--- a/src/model/typeset.rs
+++ b/src/model/typeset.rs
@@ -8,7 +8,6 @@ use comemo::{Track, Tracked, TrackedMut};
use super::{Content, Selector, StyleChain};
use crate::diag::SourceResult;
use crate::doc::{Document, Element, Frame, Location, Meta};
-use crate::eval::Value;
use crate::geom::Transform;
use crate::util::hash128;
use crate::World;
@@ -162,7 +161,7 @@ impl Introspector {
let pos = pos.transform(ts);
let mut node = content.clone();
let loc = Location { page, pos };
- node.push_field("loc", Value::Dict(loc.encode()));
+ node.push_field("loc", loc);
self.nodes.push((id, node));
}
}
diff --git a/src/util/mod.rs b/src/util/mod.rs
index ce0ed1aa..54b9fe27 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -60,10 +60,7 @@ pub trait ArcExt<T> {
fn take(self) -> T;
}
-impl<T> ArcExt<T> for Arc<T>
-where
- T: Clone,
-{
+impl<T: Clone> ArcExt<T> for Arc<T> {
fn take(self) -> T {
match Arc::try_unwrap(self) {
Ok(v) => v,