summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-05-03 15:58:15 +0200
committerLaurenz <laurmaedje@gmail.com>2022-05-03 15:58:15 +0200
commitd59109e8fffa1d0b03234329e12f5d3e578804e8 (patch)
treefe7453da6f2ae327993e5ca6436ddc6a448a2c41 /src
parentf77f1f61bf05ae506689be3c40252c5807276280 (diff)
Support recursive show rules
Diffstat (limited to 'src')
-rw-r--r--src/eval/args.rs25
-rw-r--r--src/eval/capture.rs6
-rw-r--r--src/eval/dict.rs12
-rw-r--r--src/eval/func.rs42
-rw-r--r--src/eval/methods.rs2
-rw-r--r--src/eval/mod.rs41
-rw-r--r--src/eval/value.rs18
-rw-r--r--src/geom/relative.rs6
-rw-r--r--src/library/math/mod.rs4
-rw-r--r--src/library/prelude.rs4
-rw-r--r--src/library/structure/heading.rs4
-rw-r--r--src/library/structure/list.rs13
-rw-r--r--src/library/structure/table.rs9
-rw-r--r--src/library/text/deco.rs6
-rw-r--r--src/library/text/link.rs8
-rw-r--r--src/library/text/mod.rs8
-rw-r--r--src/library/text/par.rs5
-rw-r--r--src/library/text/raw.rs4
-rw-r--r--src/model/content.rs61
-rw-r--r--src/model/recipe.rs92
-rw-r--r--src/model/show.rs9
-rw-r--r--src/model/styles.rs206
-rw-r--r--src/parse/mod.rs12
-rw-r--r--src/syntax/ast.rs18
24 files changed, 433 insertions, 182 deletions
diff --git a/src/eval/args.rs b/src/eval/args.rs
index 40454ff5..f507e714 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<Item = Value>) -> 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 8e54122f..24fc7abc 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 b630fc63..bda433e1 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 b2ecfe93..7e4040b6 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::<T>(recipe, span);
- styles
- })
- } else {
- None
- },
+ node: T::SHOWABLE.then(|| NodeId::of::<T>()),
})))
}
@@ -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<usize> {
+ 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<Value> {
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<StyleMap> {
+ /// The id of the node to customize with this function's show rule.
+ pub fn node(&self) -> StrResult<NodeId> {
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<Value>,
/// The set rule.
pub set: Option<fn(&mut Args) -> TypResult<StyleMap>>,
- /// The show rule.
- pub show: Option<fn(Func, Span) -> StyleMap>,
+ /// The id of the node to customize with this function's show rule.
+ pub node: Option<NodeId>,
}
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 017591b4..b3674dff 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 5542de89..e9d62a69 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<Self::Output> {
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<Self::Output> {
// Evaluate the target function.
- let target = self.target();
- let target_span = target.span();
- let target = target.eval(ctx, scp)?.cast::<Func>().at(target_span)?;
+ let pattern = self.pattern();
+ let pattern = pattern.eval(ctx, scp)?.cast::<Pattern>().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 352906aa..ba30348f 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 f213ae52..42e60d39 100644
--- a/src/geom/relative.rs
+++ b/src/geom/relative.rs
@@ -52,7 +52,11 @@ impl<T: Numeric> Relative<T> {
impl<T: Numeric> Debug for Relative<T> {
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 7b4998ca..9709c00c 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 584b2ab2..99bff51a 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 4f6c54f3..468757ad 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 4356ffb4..fca7d5ce 100644
--- a/src/library/structure/list.rs
+++ b/src/library/structure/list.rs
@@ -75,6 +75,17 @@ impl<const L: ListKind> ListNode<L> {
}
impl<const L: ListKind> Show for ListNode<L> {
+ 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<const L: ListKind> Show for ListNode<L> {
"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 7b3f2ac5..304a8681 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 70040f9c..34c70720 100644
--- a/src/library/text/deco.rs
+++ b/src/library/text/deco.rs
@@ -36,11 +36,15 @@ impl<const L: DecoLine> DecoNode<L> {
pub const EVADE: bool = true;
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
- Ok(Content::show(Self(args.expect::<Content>("body")?)))
+ Ok(Content::show(Self(args.expect("body")?)))
}
}
impl<const L: DecoLine> Show for DecoNode<L> {
+ 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 710fbc47..423bcbfb 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 ecc0c546..df00d87a 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 669d07ba..6c274e7e 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<ParChild>,
key: K,
) -> Option<K::Output> {
- 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 ee6c6356..c711aa16 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 31255a29..70205acc 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<Dict>),
/// 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>) -> 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::<UNDERLINE>(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 2f5f7c09..f6adf4a5 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<T: Node>(func: Func, span: Span) -> Self {
- Self { node: NodeId::of::<T>(), 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<Option<Content>> {
+ 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<F>(&self, ctx: &mut Context, arg: F) -> TypResult<Content>
+ 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<Interruption> {
+ if let Pattern::Node(id) = self.pattern {
+ if id == NodeId::of::<ListNode>() || id == NodeId::of::<EnumNode>() {
+ 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 16374deb..af87d930 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 b3020c88..3c25971c 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<T: Node>(&mut self, func: Func, span: Span) {
- self.push(StyleEntry::Recipe(Recipe::new::<T>(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<Interruption> {
- self.0
- .iter()
- .filter_map(|entry| entry.property())
- .filter_map(|property| property.interruption())
- .max()
+ self.0.iter().filter_map(|entry| entry.interruption()).max()
}
}
@@ -182,13 +167,35 @@ 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 {
+ /// 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 {
+ if !tail
+ .entries()
+ .filter_map(StyleEntry::property)
+ .any(|p| p.scoped && barrier.is_for(p.node))
+ {
+ return *tail;
+ }
+ }
+
+ StyleChain {
+ head: std::slice::from_ref(self),
+ tail: Some(tail),
+ }
+ }
+
/// If this is a property, return it.
pub fn property(&self) -> Option<&Property> {
match self {
@@ -205,21 +212,12 @@ impl StyleEntry {
}
}
- /// 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 {
- if !tail
- .entries()
- .filter_map(StyleEntry::property)
- .any(|p| p.scoped && barrier.is_for(p.node))
- {
- return *tail;
- }
- }
-
- StyleChain {
- head: std::slice::from_ref(self),
- tail: Some(tail),
+ /// The highest-level kind of of structure the entry interrupts.
+ pub fn interruption(&self) -> Option<Interruption> {
+ match self {
+ Self::Property(property) => property.interruption(),
+ Self::Recipe(recipe) => recipe.interruption(),
+ _ => None,
}
}
}
@@ -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<K: Key<'a>>(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<Option<Content>> {
- 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<K: Key<'a>>(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<Option<Content>> {
+ // 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,20 +504,31 @@ impl<T> StyleVec<T> {
}
}
+ /// Map the contained items.
+ pub fn map<F, U>(&self, f: F) -> StyleVec<U>
+ where
+ F: FnMut(&T) -> U,
+ {
+ StyleVec {
+ items: self.items.iter().map(f).collect(),
+ maps: self.maps.clone(),
+ }
+ }
+
+ /// Iterate over the contained items.
+ pub fn items(&self) -> std::slice::Iter<'_, T> {
+ 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 maps(&self) -> impl Iterator<Item = &StyleMap> {
+ pub fn styles(&self) -> impl Iterator<Item = &StyleMap> {
self.maps.iter().map(|(map, _)| map)
}
- /// Iterate over the contained items.
- pub fn items(&self) -> std::slice::Iter<'_, T> {
- self.items.iter()
- }
-
/// Iterate over references to the contained items and associated style maps.
pub fn iter(&self) -> impl Iterator<Item = (&T, &StyleMap)> + '_ {
self.items().zip(
diff --git a/src/parse/mod.rs b/src/parse/mod.rs
index 32d61885..9f4860e7 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 42a4235d..fa00fe4b 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<Ident> {
+ 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.