diff --git a/src/eval/args.rs b/src/eval/args.rs index 40454ff58..f507e7145 100644 --- a/src/eval/args.rs +++ b/src/eval/args.rs @@ -26,19 +26,22 @@ pub struct Arg { } impl Args { + /// Create empty arguments from a span. + pub fn new(span: Span) -> Self { + Self { span, items: vec![] } + } + /// Create positional arguments from a span and values. pub fn from_values(span: Span, values: impl IntoIterator) -> Self { - Self { - span, - items: values - .into_iter() - .map(|value| Arg { - span, - name: None, - value: Spanned::new(value, span), - }) - .collect(), - } + let items = values + .into_iter() + .map(|value| Arg { + span, + name: None, + value: Spanned::new(value, span), + }) + .collect(); + Self { span, items } } /// Consume and cast the first positional argument. diff --git a/src/eval/capture.rs b/src/eval/capture.rs index 8e54122fa..24fc7abcd 100644 --- a/src/eval/capture.rs +++ b/src/eval/capture.rs @@ -91,8 +91,10 @@ impl<'a> CapturesVisitor<'a> { // A show rule contains a binding, but that binding is only active // after the target has been evaluated. Some(Expr::Show(show)) => { - self.visit(show.target().as_red()); - self.bind(show.binding()); + self.visit(show.pattern().as_red()); + if let Some(binding) = show.binding() { + self.bind(binding); + } self.visit(show.body().as_red()); } diff --git a/src/eval/dict.rs b/src/eval/dict.rs index b630fc63c..bda433e15 100644 --- a/src/eval/dict.rs +++ b/src/eval/dict.rs @@ -46,8 +46,8 @@ impl Dict { } /// Borrow the value the given `key` maps to. - pub fn get(&self, key: EcoString) -> StrResult<&Value> { - self.0.get(&key).ok_or_else(|| missing_key(&key)) + pub fn get(&self, key: &EcoString) -> StrResult<&Value> { + self.0.get(key).ok_or_else(|| missing_key(key)) } /// Mutably borrow the value the given `key` maps to. @@ -59,7 +59,7 @@ impl Dict { } /// Whether the dictionary contains a specific key. - pub fn contains(&self, key: &str) -> bool { + pub fn contains(&self, key: &EcoString) -> bool { self.0.contains_key(key) } @@ -69,10 +69,10 @@ impl Dict { } /// Remove a mapping by `key`. - pub fn remove(&mut self, key: EcoString) -> StrResult<()> { - match Arc::make_mut(&mut self.0).remove(&key) { + pub fn remove(&mut self, key: &EcoString) -> StrResult<()> { + match Arc::make_mut(&mut self.0).remove(key) { Some(_) => Ok(()), - None => Err(missing_key(&key)), + None => Err(missing_key(key)), } } diff --git a/src/eval/func.rs b/src/eval/func.rs index b2ecfe939..7e4040b6a 100644 --- a/src/eval/func.rs +++ b/src/eval/func.rs @@ -4,9 +4,8 @@ use std::sync::Arc; use super::{Args, Control, Eval, Scope, Scopes, Value}; use crate::diag::{StrResult, TypResult}; -use crate::model::{Content, StyleMap}; +use crate::model::{Content, NodeId, StyleMap}; use crate::syntax::ast::Expr; -use crate::syntax::Span; use crate::util::EcoString; use crate::Context; @@ -35,7 +34,7 @@ impl Func { name, func, set: None, - show: None, + node: None, }))) } @@ -49,15 +48,7 @@ impl Func { Ok(Value::Content(content.styled_with_map(styles.scoped()))) }, set: Some(T::set), - show: if T::SHOWABLE { - Some(|recipe, span| { - let mut styles = StyleMap::new(); - styles.set_recipe::(recipe, span); - styles - }) - } else { - None - }, + node: T::SHOWABLE.then(|| NodeId::of::()), }))) } @@ -80,7 +71,20 @@ impl Func { } } - /// Call the function with a virtual machine and arguments. + /// The number of positional arguments this function takes, if known. + pub fn argc(&self) -> Option { + match self.0.as_ref() { + Repr::Closure(closure) => Some( + closure.params.iter().filter(|(_, default)| default.is_none()).count(), + ), + Repr::With(wrapped, applied) => Some(wrapped.argc()?.saturating_sub( + applied.items.iter().filter(|arg| arg.name.is_none()).count(), + )), + _ => None, + } + } + + /// Call the function with the given arguments. pub fn call(&self, ctx: &mut Context, mut args: Args) -> TypResult { let value = match self.0.as_ref() { Repr::Native(native) => (native.func)(ctx, &mut args)?, @@ -104,10 +108,10 @@ impl Func { Ok(styles) } - /// Execute the function's show rule. - pub fn show(&self, recipe: Func, span: Span) -> StrResult { + /// The id of the node to customize with this function's show rule. + pub fn node(&self) -> StrResult { match self.0.as_ref() { - Repr::Native(Native { show: Some(show), .. }) => Ok(show(recipe, span)), + Repr::Native(Native { node: Some(id), .. }) => Ok(*id), _ => Err("this function cannot be customized with show")?, } } @@ -138,8 +142,8 @@ struct Native { pub func: fn(&mut Context, &mut Args) -> TypResult, /// The set rule. pub set: Option TypResult>, - /// The show rule. - pub show: Option StyleMap>, + /// The id of the node to customize with this function's show rule. + pub node: Option, } impl Hash for Native { @@ -147,7 +151,7 @@ impl Hash for Native { self.name.hash(state); (self.func as usize).hash(state); self.set.map(|set| set as usize).hash(state); - self.show.map(|show| show as usize).hash(state); + self.node.hash(state); } } diff --git a/src/eval/methods.rs b/src/eval/methods.rs index 017591b44..b3674dff8 100644 --- a/src/eval/methods.rs +++ b/src/eval/methods.rs @@ -96,7 +96,7 @@ pub fn call_mut( }, Value::Dict(dict) => match method { - "remove" => dict.remove(args.expect("key")?).at(span)?, + "remove" => dict.remove(&args.expect("key")?).at(span)?, _ => missing()?, }, diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 5542de891..e9d62a69c 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -38,7 +38,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::diag::{At, StrResult, Trace, Tracepoint, TypResult}; use crate::geom::{Angle, Em, Fraction, Length, Ratio}; use crate::library; -use crate::model::{Content, StyleMap}; +use crate::model::{Content, Pattern, Recipe, StyleEntry, StyleMap}; use crate::syntax::ast::*; use crate::syntax::{Span, Spanned}; use crate::util::EcoString; @@ -79,8 +79,9 @@ fn eval_markup( eval_markup(ctx, scp, nodes)?.styled_with_map(styles) } MarkupNode::Expr(Expr::Show(show)) => { - let styles = show.eval(ctx, scp)?; - eval_markup(ctx, scp, nodes)?.styled_with_map(styles) + let recipe = show.eval(ctx, scp)?; + eval_markup(ctx, scp, nodes)? + .styled_with_entry(StyleEntry::Recipe(recipe).into()) } MarkupNode::Expr(Expr::Wrap(wrap)) => { let tail = eval_markup(ctx, scp, nodes)?; @@ -434,8 +435,17 @@ impl Eval for FieldAccess { fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult { let object = self.object().eval(ctx, scp)?; + let span = self.field().span(); + let field = self.field().take(); + Ok(match object { - Value::Dict(dict) => dict.get(self.field().take()).at(self.span())?.clone(), + Value::Dict(dict) => dict.get(&field).at(span)?.clone(), + + Value::Content(Content::Show(_, Some(dict))) => dict + .get(&field) + .map_err(|_| format!("unknown field {field:?}")) + .at(span)? + .clone(), v => bail!( self.object().span(), @@ -455,7 +465,7 @@ impl Eval for FuncCall { Ok(match callee { Value::Array(array) => array.get(args.into_index()?).at(self.span())?.clone(), - Value::Dict(dict) => dict.get(args.into_key()?).at(self.span())?.clone(), + Value::Dict(dict) => dict.get(&args.into_key()?).at(self.span())?.clone(), Value::Func(func) => { let point = || Tracepoint::Call(func.name().map(ToString::to_string)); func.call(ctx, args).trace(point, self.span())? @@ -615,13 +625,12 @@ impl Eval for SetExpr { } impl Eval for ShowExpr { - type Output = StyleMap; + type Output = Recipe; fn eval(&self, ctx: &mut Context, scp: &mut Scopes) -> EvalResult { // Evaluate the target function. - let target = self.target(); - let target_span = target.span(); - let target = target.eval(ctx, scp)?.cast::().at(target_span)?; + let pattern = self.pattern(); + let pattern = pattern.eval(ctx, scp)?.cast::().at(pattern.span())?; // Collect captured variables. let captured = { @@ -630,18 +639,24 @@ impl Eval for ShowExpr { visitor.finish() }; + // Define parameters. + let mut params = vec![]; + if let Some(binding) = self.binding() { + params.push((binding.take(), None)); + } + // Define the recipe function. let body = self.body(); - let body_span = body.span(); - let recipe = Func::from_closure(Closure { + let span = body.span(); + let func = Func::from_closure(Closure { name: None, captured, - params: vec![(self.binding().take(), None)], + params, sink: None, body, }); - Ok(target.show(recipe, body_span).at(target_span)?) + Ok(Recipe { pattern, func, span }) } } diff --git a/src/eval/value.rs b/src/eval/value.rs index 352906aab..ba30348fe 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -11,7 +11,7 @@ use crate::geom::{ Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor, Sides, }; use crate::library::text::RawNode; -use crate::model::{Content, Layout, LayoutNode}; +use crate::model::{Content, Layout, LayoutNode, Pattern}; use crate::syntax::Spanned; use crate::util::EcoString; @@ -617,13 +617,13 @@ where } let sides = Sides { - left: dict.get("left".into()).or_else(|_| dict.get("x".into())), - top: dict.get("top".into()).or_else(|_| dict.get("y".into())), - right: dict.get("right".into()).or_else(|_| dict.get("x".into())), - bottom: dict.get("bottom".into()).or_else(|_| dict.get("y".into())), + left: dict.get(&"left".into()).or_else(|_| dict.get(&"x".into())), + top: dict.get(&"top".into()).or_else(|_| dict.get(&"y".into())), + right: dict.get(&"right".into()).or_else(|_| dict.get(&"x".into())), + bottom: dict.get(&"bottom".into()).or_else(|_| dict.get(&"y".into())), } .map(|side| { - side.or_else(|_| dict.get("rest".into())) + side.or_else(|_| dict.get(&"rest".into())) .and_then(|v| T::cast(v.clone())) .unwrap_or_default() }); @@ -684,6 +684,12 @@ castable! { Value::Content(content) => content.pack(), } +castable! { + Pattern, + Expected: "function", + Value::Func(func) => Pattern::Node(func.node()?), +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/geom/relative.rs b/src/geom/relative.rs index f213ae522..42e60d39f 100644 --- a/src/geom/relative.rs +++ b/src/geom/relative.rs @@ -52,7 +52,11 @@ impl Relative { impl Debug for Relative { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{:?} + {:?}", self.rel, self.abs) + match (self.rel.is_zero(), self.abs.is_zero()) { + (false, false) => write!(f, "{:?} + {:?}", self.rel, self.abs), + (false, true) => self.rel.fmt(f), + (true, _) => self.abs.fmt(f), + } } } diff --git a/src/library/math/mod.rs b/src/library/math/mod.rs index 7b4998ca8..9709c00c7 100644 --- a/src/library/math/mod.rs +++ b/src/library/math/mod.rs @@ -36,6 +36,10 @@ impl MathNode { } impl Show for MathNode { + fn unguard(&self, _: Selector) -> ShowNode { + Self { formula: self.formula.clone(), ..*self }.pack() + } + fn encode(&self) -> Dict { dict! { "formula" => Value::Str(self.formula.clone()), diff --git a/src/library/prelude.rs b/src/library/prelude.rs index 584b2ab20..99bff51a9 100644 --- a/src/library/prelude.rs +++ b/src/library/prelude.rs @@ -15,8 +15,8 @@ pub use crate::eval::{ pub use crate::frame::*; pub use crate::geom::*; pub use crate::model::{ - Content, Fold, Key, Layout, LayoutNode, Regions, Resolve, Show, ShowNode, StyleChain, - StyleMap, StyleVec, + Content, Fold, Key, Layout, LayoutNode, Regions, Resolve, Selector, Show, ShowNode, + StyleChain, StyleMap, StyleVec, }; pub use crate::syntax::{Span, Spanned}; pub use crate::util::{EcoString, OptionExt}; diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs index 4f6c54f38..468757ad7 100644 --- a/src/library/structure/heading.rs +++ b/src/library/structure/heading.rs @@ -64,6 +64,10 @@ impl HeadingNode { } impl Show for HeadingNode { + fn unguard(&self, sel: Selector) -> ShowNode { + Self { body: self.body.unguard(sel), ..*self }.pack() + } + fn encode(&self) -> Dict { dict! { "level" => Value::Int(self.level.get() as i64), diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs index 4356ffb49..fca7d5ce3 100644 --- a/src/library/structure/list.rs +++ b/src/library/structure/list.rs @@ -75,6 +75,17 @@ impl ListNode { } impl Show for ListNode { + fn unguard(&self, sel: Selector) -> ShowNode { + Self { + items: self.items.map(|item| ListItem { + body: Box::new(item.body.unguard(sel)), + ..*item + }), + ..*self + } + .pack() + } + fn encode(&self) -> Dict { dict! { "start" => Value::Int(self.start as i64), @@ -83,7 +94,7 @@ impl Show for ListNode { "items" => Value::Array( self.items .items() - .map(|item| Value::Content((*item.body).clone())) + .map(|item| Value::Content(item.body.as_ref().clone())) .collect() ), } diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs index 7b3f2ac57..304a86811 100644 --- a/src/library/structure/table.rs +++ b/src/library/structure/table.rs @@ -51,6 +51,15 @@ impl TableNode { } impl Show for TableNode { + fn unguard(&self, sel: Selector) -> ShowNode { + Self { + tracks: self.tracks.clone(), + gutter: self.gutter.clone(), + cells: self.cells.iter().map(|cell| cell.unguard(sel)).collect(), + } + .pack() + } + fn encode(&self) -> Dict { dict! { "cells" => Value::Array( diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs index 70040f9c2..34c707205 100644 --- a/src/library/text/deco.rs +++ b/src/library/text/deco.rs @@ -36,11 +36,15 @@ impl DecoNode { pub const EVADE: bool = true; fn construct(_: &mut Context, args: &mut Args) -> TypResult { - Ok(Content::show(Self(args.expect::("body")?))) + Ok(Content::show(Self(args.expect("body")?))) } } impl Show for DecoNode { + fn unguard(&self, sel: Selector) -> ShowNode { + Self(self.0.unguard(sel)).pack() + } + fn encode(&self) -> Dict { dict! { "body" => Value::Content(self.0.clone()) } } diff --git a/src/library/text/link.rs b/src/library/text/link.rs index 710fbc477..423bcbfb3 100644 --- a/src/library/text/link.rs +++ b/src/library/text/link.rs @@ -28,6 +28,14 @@ impl LinkNode { } impl Show for LinkNode { + fn unguard(&self, sel: Selector) -> ShowNode { + Self { + url: self.url.clone(), + body: self.body.as_ref().map(|body| body.unguard(sel)), + } + .pack() + } + fn encode(&self) -> Dict { dict! { "url" => Value::Str(self.url.clone()), diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs index ecc0c546b..df00d87a9 100644 --- a/src/library/text/mod.rs +++ b/src/library/text/mod.rs @@ -463,6 +463,10 @@ impl StrongNode { } impl Show for StrongNode { + fn unguard(&self, sel: Selector) -> ShowNode { + Self(self.0.unguard(sel)).pack() + } + fn encode(&self) -> Dict { dict! { "body" => Value::Content(self.0.clone()) } } @@ -484,6 +488,10 @@ impl EmphNode { } impl Show for EmphNode { + fn unguard(&self, sel: Selector) -> ShowNode { + Self(self.0.unguard(sel)).pack() + } + fn encode(&self) -> Dict { dict! { "body" => Value::Content(self.0.clone()) } } diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 669d07ba8..6c274e7ea 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -618,7 +618,10 @@ fn shared_get<'a, K: Key<'a>>( children: &StyleVec, key: K, ) -> Option { - children.maps().all(|map| !map.contains(key)).then(|| styles.get(key)) + children + .styles() + .all(|map| !map.contains(key)) + .then(|| styles.get(key)) } /// Find suitable linebreaks. diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs index ee6c6356c..c711aa16d 100644 --- a/src/library/text/raw.rs +++ b/src/library/text/raw.rs @@ -51,6 +51,10 @@ impl RawNode { } impl Show for RawNode { + fn unguard(&self, _: Selector) -> ShowNode { + Self { text: self.text.clone(), ..*self }.pack() + } + fn encode(&self) -> Dict { dict! { "text" => Value::Str(self.text.clone()), diff --git a/src/model/content.rs b/src/model/content.rs index 31255a293..70205acc0 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -7,8 +7,8 @@ use std::ops::{Add, AddAssign}; use typed_arena::Arena; use super::{ - CollapsingBuilder, Interruption, Key, Layout, LayoutNode, Show, ShowNode, StyleMap, - StyleVecBuilder, + CollapsingBuilder, Interruption, Key, Layout, LayoutNode, Property, Show, ShowNode, + StyleEntry, StyleMap, StyleVecBuilder, Target, }; use crate::diag::StrResult; use crate::library::layout::{FlowChild, FlowNode, PageNode, PlaceNode, Spacing}; @@ -38,6 +38,8 @@ use crate::util::EcoString; /// sequences. #[derive(PartialEq, Clone, Hash)] pub enum Content { + /// Empty content. + Empty, /// A word space. Space, /// A forced line break. @@ -68,8 +70,9 @@ pub enum Content { Pagebreak { weak: bool }, /// A page node. Page(PageNode), - /// A node that can be realized with styles. - Show(ShowNode), + /// A node that can be realized with styles, optionally with attached + /// properties. + Show(ShowNode, Option), /// Content with attached styles. Styled(Arc<(Self, StyleMap)>), /// A sequence of multiple nodes. @@ -79,7 +82,7 @@ pub enum Content { impl Content { /// Create empty content. pub fn new() -> Self { - Self::sequence(vec![]) + Self::Empty } /// Create content from an inline-level node. @@ -103,15 +106,15 @@ impl Content { where T: Show + Debug + Hash + Sync + Send + 'static, { - Self::Show(node.pack()) + Self::Show(node.pack(), None) } /// Create a new sequence nodes from multiples nodes. pub fn sequence(seq: Vec) -> Self { - if seq.len() == 1 { - seq.into_iter().next().unwrap() - } else { - Self::Sequence(Arc::new(seq)) + match seq.as_slice() { + [] => Self::Empty, + [_] => seq.into_iter().next().unwrap(), + _ => Self::Sequence(Arc::new(seq)), } } @@ -124,15 +127,20 @@ impl Content { } /// Style this content with a single style property. - pub fn styled<'k, K: Key<'k>>(mut self, key: K, value: K::Value) -> Self { + pub fn styled<'k, K: Key<'k>>(self, key: K, value: K::Value) -> Self { + self.styled_with_entry(StyleEntry::Property(Property::new(key, value))) + } + + /// Style this content with a style entry. + pub fn styled_with_entry(mut self, entry: StyleEntry) -> Self { if let Self::Styled(styled) = &mut self { if let Some((_, map)) = Arc::get_mut(styled) { - map.apply(key, value); + map.apply(entry); return self; } } - Self::Styled(Arc::new((self, StyleMap::with(key, value)))) + Self::Styled(Arc::new((self, entry.into()))) } /// Style this content with a full style map. @@ -151,6 +159,11 @@ impl Content { Self::Styled(Arc::new((self, styles))) } + /// Reenable the show rule identified by the selector. + pub fn unguard(&self, sel: Selector) -> Self { + self.clone().styled_with_entry(StyleEntry::Unguard(sel)) + } + /// Underline this content. pub fn underlined(self) -> Self { Self::show(DecoNode::(self)) @@ -228,6 +241,7 @@ impl Default for Content { impl Debug for Content { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { + Self::Empty => f.pad("Empty"), Self::Space => f.pad("Space"), Self::Linebreak { justified } => write!(f, "Linebreak({justified})"), Self::Horizontal { amount, weak } => { @@ -245,7 +259,7 @@ impl Debug for Content { Self::Item(item) => item.fmt(f), Self::Pagebreak { weak } => write!(f, "Pagebreak({weak})"), Self::Page(page) => page.fmt(f), - Self::Show(node) => node.fmt(f), + Self::Show(node, _) => node.fmt(f), Self::Styled(styled) => { let (sub, map) = styled.as_ref(); map.fmt(f)?; @@ -261,6 +275,8 @@ impl Add for Content { fn add(self, rhs: Self) -> Self::Output { Self::Sequence(match (self, rhs) { + (Self::Empty, rhs) => return rhs, + (lhs, Self::Empty) => return lhs, (Self::Sequence(mut lhs), Self::Sequence(rhs)) => { let mutable = Arc::make_mut(&mut lhs); match Arc::try_unwrap(rhs) { @@ -352,7 +368,8 @@ impl<'a, 'ctx> Builder<'a, 'ctx> { fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> TypResult<()> { // Handle special content kinds. match content { - Content::Show(node) => return self.show(node, styles), + Content::Empty => return Ok(()), + Content::Show(node, _) => return self.show(node, styles), Content::Styled(styled) => return self.styled(styled, styles), Content::Sequence(seq) => return self.sequence(seq, styles), _ => {} @@ -388,15 +405,11 @@ impl<'a, 'ctx> Builder<'a, 'ctx> { } fn show(&mut self, node: &ShowNode, styles: StyleChain<'a>) -> TypResult<()> { - let id = node.id(); - let realized = match styles.realize(self.ctx, node)? { - Some(content) => content, - None => node.realize(self.ctx, styles)?, - }; - - let content = node.finalize(self.ctx, styles, realized)?; - let stored = self.scratch.templates.alloc(content); - self.accept(stored, styles.unscoped(id)) + if let Some(realized) = styles.apply(self.ctx, Target::Node(node))? { + let stored = self.scratch.templates.alloc(realized); + self.accept(stored, styles.unscoped(node.id()))?; + } + Ok(()) } fn styled( diff --git a/src/model/recipe.rs b/src/model/recipe.rs index 2f5f7c097..f6adf4a50 100644 --- a/src/model/recipe.rs +++ b/src/model/recipe.rs @@ -1,14 +1,17 @@ use std::fmt::{self, Debug, Formatter}; -use super::NodeId; -use crate::eval::{Func, Node}; +use super::{Content, Interruption, NodeId, Show, ShowNode, StyleEntry}; +use crate::diag::{At, TypResult}; +use crate::eval::{Args, Func, Value}; +use crate::library::structure::{EnumNode, ListNode}; use crate::syntax::Span; +use crate::Context; /// A show rule recipe. #[derive(Clone, PartialEq, Hash)] pub struct Recipe { - /// The affected node. - pub node: NodeId, + /// The patterns to customize. + pub pattern: Pattern, /// The function that defines the recipe. pub func: Func, /// The span to report all erros with. @@ -16,14 +19,87 @@ pub struct Recipe { } impl Recipe { - /// Create a new recipe for the node `T`. - pub fn new(func: Func, span: Span) -> Self { - Self { node: NodeId::of::(), func, span } + /// Whether the recipe is applicable to the target. + pub fn applicable(&self, target: Target) -> bool { + match (&self.pattern, target) { + (Pattern::Node(id), Target::Node(node)) => *id == node.id(), + _ => false, + } + } + + /// Try to apply the recipe to the target. + pub fn apply( + &self, + ctx: &mut Context, + sel: Selector, + target: Target, + ) -> TypResult> { + let content = match (target, &self.pattern) { + (Target::Node(node), &Pattern::Node(id)) if node.id() == id => { + let node = node.unguard(sel); + self.call(ctx, || { + let dict = node.encode(); + Value::Content(Content::Show(node, Some(dict))) + })? + } + + _ => return Ok(None), + }; + + Ok(Some(content.styled_with_entry(StyleEntry::Guard(sel)))) + } + + /// Call the recipe function, with the argument if desired. + fn call(&self, ctx: &mut Context, arg: F) -> TypResult + where + F: FnOnce() -> Value, + { + let args = if self.func.argc() == Some(0) { + Args::new(self.span) + } else { + Args::from_values(self.span, [arg()]) + }; + + self.func.call(ctx, args)?.cast().at(self.span) + } + + /// What kind of structure the property interrupts. + pub fn interruption(&self) -> Option { + if let Pattern::Node(id) = self.pattern { + if id == NodeId::of::() || id == NodeId::of::() { + return Some(Interruption::List); + } + } + + None } } impl Debug for Recipe { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Recipe for {:?} from {:?}", self.node, self.span) + write!(f, "Recipe matching {:?} from {:?}", self.pattern, self.span) } } + +/// A show rule pattern that may match a target. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Pattern { + /// Defines the appearence of some node. + Node(NodeId), +} + +/// A target for a show rule recipe. +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum Target<'a> { + /// A showable node. + Node(&'a ShowNode), +} + +/// Identifies a show rule recipe. +#[derive(Debug, Copy, Clone, PartialEq, Hash)] +pub enum Selector { + /// The nth recipe from the top of the chain. + Nth(usize), + /// The base recipe for a kind of node. + Base(NodeId), +} diff --git a/src/model/show.rs b/src/model/show.rs index 16374deba..af87d9305 100644 --- a/src/model/show.rs +++ b/src/model/show.rs @@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter, Write}; use std::hash::Hash; use std::sync::Arc; -use super::{Content, NodeId, StyleChain}; +use super::{Content, NodeId, Selector, StyleChain}; use crate::diag::TypResult; use crate::eval::Dict; use crate::util::Prehashed; @@ -10,6 +10,9 @@ use crate::Context; /// A node that can be realized given some styles. pub trait Show: 'static { + /// Unguard nested content against recursive show rules. + fn unguard(&self, sel: Selector) -> ShowNode; + /// Encode this node into a dictionary. fn encode(&self) -> Dict; @@ -63,6 +66,10 @@ impl ShowNode { } impl Show for ShowNode { + fn unguard(&self, sel: Selector) -> ShowNode { + self.0.unguard(sel) + } + fn encode(&self) -> Dict { self.0.encode() } diff --git a/src/model/styles.rs b/src/model/styles.rs index b3020c880..3c25971c8 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -3,11 +3,9 @@ use std::hash::Hash; use std::iter; use std::marker::PhantomData; -use super::{Barrier, Content, Key, Property, Recipe, Show, ShowNode}; -use crate::diag::{At, TypResult}; -use crate::eval::{Args, Func, Node, Value}; +use super::{Barrier, Content, Key, Property, Recipe, Selector, Show, Target}; +use crate::diag::TypResult; use crate::library::text::{FontFamily, TextNode}; -use crate::syntax::Span; use crate::util::ReadableTypeId; use crate::Context; @@ -65,11 +63,6 @@ impl StyleMap { ); } - /// Set a show rule recipe for a node. - pub fn set_recipe(&mut self, func: Func, span: Span) { - self.push(StyleEntry::Recipe(Recipe::new::(func, span))); - } - /// Whether the map contains a style property for the given key. pub fn contains<'a, K: Key<'a>>(&self, _: K) -> bool { self.0 @@ -91,16 +84,12 @@ impl StyleMap { } } - /// Set an outer value for a style property. - /// - /// If the property needs folding and the value is already contained in the - /// style map, `self` contributes the inner values and `value` is the outer - /// one. + /// Set an outer style property. /// /// Like [`chain`](Self::chain) or [`apply_map`](Self::apply_map), but with - /// only a single property. - pub fn apply<'a, K: Key<'a>>(&mut self, key: K, value: K::Value) { - self.0.insert(0, StyleEntry::Property(Property::new(key, value))); + /// only a entry. + pub fn apply(&mut self, entry: StyleEntry) { + self.0.insert(0, entry); } /// Apply styles from `tail` in-place. The resulting style map is equivalent @@ -126,11 +115,7 @@ impl StyleMap { /// The highest-level kind of of structure the map interrupts. pub fn interruption(&self) -> Option { - self.0 - .iter() - .filter_map(|entry| entry.property()) - .filter_map(|property| property.interruption()) - .max() + self.0.iter().filter_map(|entry| entry.interruption()).max() } } @@ -182,29 +167,17 @@ pub enum Interruption { pub enum StyleEntry { /// A style property originating from a set rule or constructor. Property(Property), - /// A barrier for scoped styles. - Barrier(Barrier), /// A show rule recipe. Recipe(Recipe), + /// A barrier for scoped styles. + Barrier(Barrier), + /// Guards against recursive show rules. + Guard(Selector), + /// Allows recursive show rules again. + Unguard(Selector), } impl StyleEntry { - /// If this is a property, return it. - pub fn property(&self) -> Option<&Property> { - match self { - Self::Property(property) => Some(property), - _ => None, - } - } - - /// If this is a recipe, return it. - pub fn recipe(&self) -> Option<&Recipe> { - match self { - Self::Recipe(recipe) => Some(recipe), - _ => None, - } - } - /// Make this style the first link of the `tail` chain. pub fn chain<'a>(&'a self, tail: &'a StyleChain) -> StyleChain<'a> { if let StyleEntry::Barrier(barrier) = self { @@ -222,6 +195,31 @@ impl StyleEntry { tail: Some(tail), } } + + /// If this is a property, return it. + pub fn property(&self) -> Option<&Property> { + match self { + Self::Property(property) => Some(property), + _ => None, + } + } + + /// If this is a recipe, return it. + pub fn recipe(&self) -> Option<&Recipe> { + match self { + Self::Recipe(recipe) => Some(recipe), + _ => None, + } + } + + /// The highest-level kind of of structure the entry interrupts. + pub fn interruption(&self) -> Option { + match self { + Self::Property(property) => property.interruption(), + Self::Recipe(recipe) => recipe.interruption(), + _ => None, + } + } } impl Debug for StyleEntry { @@ -231,6 +229,8 @@ impl Debug for StyleEntry { Self::Property(property) => property.fmt(f)?, Self::Recipe(recipe) => recipe.fmt(f)?, Self::Barrier(barrier) => barrier.fmt(f)?, + Self::Guard(sel) => write!(f, "Guard against {sel:?}")?, + Self::Unguard(sel) => write!(f, "Unguard against {sel:?}")?, } f.write_str("]") } @@ -262,35 +262,6 @@ impl<'a> StyleChain<'a> { Self { head: &root.0, tail: None } } - /// Get the output value of a style property. - /// - /// 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>(self, key: K) -> K::Output { - K::get(self, self.values(key)) - } - - /// Realize a node with a user recipe. - pub fn realize( - self, - ctx: &mut Context, - node: &ShowNode, - ) -> TypResult> { - let id = node.id(); - if let Some(recipe) = self - .entries() - .filter_map(StyleEntry::recipe) - .find(|recipe| recipe.node == id) - { - let dict = node.encode(); - let args = Args::from_values(recipe.span, [Value::Dict(dict)]); - Ok(Some(recipe.func.call(ctx, args)?.cast().at(recipe.span)?)) - } else { - Ok(None) - } - } - /// Return the chain, but without trailing scoped properties for the given /// `node`. pub fn unscoped(mut self, node: NodeId) -> Self { @@ -306,6 +277,80 @@ impl<'a> StyleChain<'a> { self } + /// Get the output value of a style property. + /// + /// 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>(self, key: K) -> K::Output { + K::get(self, self.values(key)) + } + + /// Apply show recipes in this style chain to a target. + pub fn apply(self, ctx: &mut Context, target: Target) -> TypResult> { + // Find out how many recipes there any and whether any of their patterns + // match. + let mut n = 0; + let mut any = true; + for recipe in self.entries().filter_map(StyleEntry::recipe) { + n += 1; + any |= recipe.applicable(target); + } + + // Find an applicable recipe. + let mut realized = None; + let mut guarded = false; + if any { + for recipe in self.entries().filter_map(StyleEntry::recipe) { + if recipe.applicable(target) { + let sel = Selector::Nth(n); + if self.guarded(sel) { + guarded = true; + } else if let Some(content) = recipe.apply(ctx, sel, target)? { + realized = Some(content); + break; + } + } + n -= 1; + } + } + + if let Target::Node(node) = target { + // Realize if there was no matching recipe. + if realized.is_none() { + let sel = Selector::Base(node.id()); + if self.guarded(sel) { + guarded = true; + } else { + let content = node.unguard(sel).realize(ctx, self)?; + realized = Some(content.styled_with_entry(StyleEntry::Guard(sel))); + } + } + + // Finalize only if guarding didn't stop any recipe. + if !guarded { + if let Some(content) = realized { + realized = Some(node.finalize(ctx, self, content)?); + } + } + } + + Ok(realized) + } + + /// Whether the recipe identified by the selector is guarded. + fn guarded(&self, sel: Selector) -> bool { + for entry in self.entries() { + match *entry { + StyleEntry::Guard(s) if s == sel => return true, + StyleEntry::Unguard(s) if s == sel => return false, + _ => {} + } + } + + false + } + /// Remove the last link from the chain. fn pop(&mut self) { *self = self.tail.copied().unwrap_or_default(); @@ -386,7 +431,7 @@ impl<'a, K: Key<'a>> Iterator for Values<'a, K> { StyleEntry::Barrier(barrier) => { self.depth += barrier.is_for(K::node()) as usize; } - StyleEntry::Recipe(_) => {} + _ => {} } } @@ -459,13 +504,15 @@ impl StyleVec { } } - /// Iterate over the contained maps. Note that zipping this with `items()` - /// does not yield the same result as calling `iter()` because this method - /// only returns maps once that are shared by consecutive items. This method - /// is designed for use cases where you want to check, for example, whether - /// any of the maps fulfills a specific property. - pub fn maps(&self) -> impl Iterator { - self.maps.iter().map(|(map, _)| map) + /// Map the contained items. + pub fn map(&self, f: F) -> StyleVec + where + F: FnMut(&T) -> U, + { + StyleVec { + items: self.items.iter().map(f).collect(), + maps: self.maps.clone(), + } } /// Iterate over the contained items. @@ -473,6 +520,15 @@ impl StyleVec { self.items.iter() } + /// Iterate over the contained maps. Note that zipping this with `items()` + /// does not yield the same result as calling `iter()` because this method + /// only returns maps once that are shared by consecutive items. This method + /// is designed for use cases where you want to check, for example, whether + /// any of the maps fulfills a specific property. + pub fn styles(&self) -> impl Iterator { + self.maps.iter().map(|(map, _)| map) + } + /// Iterate over references to the contained items and associated style maps. pub fn iter(&self) -> impl Iterator + '_ { self.items().zip( diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 32d618851..9f4860e71 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -807,9 +807,15 @@ fn set_expr(p: &mut Parser) -> ParseResult { fn show_expr(p: &mut Parser) -> ParseResult { p.perform(NodeKind::ShowExpr, |p| { p.assert(NodeKind::Show); - ident(p)?; - p.expect(NodeKind::Colon)?; - ident(p)?; + let marker = p.marker(); + expr(p)?; + if p.eat_if(NodeKind::Colon) { + marker.filter_children(p, |child| match child.kind() { + NodeKind::Ident(_) | NodeKind::Colon => Ok(()), + _ => Err("expected identifier"), + }); + expr(p)?; + } p.expect(NodeKind::As)?; expr(p) }) diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 42a4235d3..fa00fe4ba 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -1012,17 +1012,21 @@ node! { impl ShowExpr { /// The binding to assign to. - pub fn binding(&self) -> Ident { - self.0.cast_first_child().expect("show rule is missing binding") + pub fn binding(&self) -> Option { + let mut children = self.0.children(); + children + .find_map(RedRef::cast) + .filter(|_| children.any(|child| child.kind() == &NodeKind::Colon)) } - /// The function to customize with this show rule. - pub fn target(&self) -> Ident { + /// The pattern that this rule matches. + pub fn pattern(&self) -> Expr { self.0 .children() - .filter_map(RedRef::cast) - .nth(1) - .expect("show rule is missing target") + .rev() + .skip_while(|child| child.kind() != &NodeKind::As) + .find_map(RedRef::cast) + .expect("show rule is missing pattern") } /// The expression that realizes the node. diff --git a/tests/typ/code/field.typ b/tests/typ/code/field.typ index b30d72e08..ff1dad9ec 100644 --- a/tests/typ/code/field.typ +++ b/tests/typ/code/field.typ @@ -12,7 +12,7 @@ } --- -// Error: 2-13 dictionary does not contain key: "invalid" +// Error: 6-13 dictionary does not contain key: "invalid" {(:).invalid} ---