diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-11-07 12:21:12 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-11-07 12:46:05 +0100 |
| commit | efd1853d069fbd1476e82d015da4d0d04cfaccc0 (patch) | |
| tree | 842b745c134306539d10c61be9485794fe8dc7dc /src/model | |
| parent | eb951c008beea502042db4a3a0e8d1f8b51f6f52 (diff) | |
Show it!
- New show rule syntax
- Set if syntax
- Removed wrap syntax
Diffstat (limited to 'src/model')
| -rw-r--r-- | src/model/cast.rs | 11 | ||||
| -rw-r--r-- | src/model/content.rs | 33 | ||||
| -rw-r--r-- | src/model/eval.rs | 69 | ||||
| -rw-r--r-- | src/model/func.rs | 16 | ||||
| -rw-r--r-- | src/model/items.rs | 16 | ||||
| -rw-r--r-- | src/model/ops.rs | 8 | ||||
| -rw-r--r-- | src/model/styles.rs | 107 | ||||
| -rw-r--r-- | src/model/value.rs | 2 |
8 files changed, 134 insertions, 128 deletions
diff --git a/src/model/cast.rs b/src/model/cast.rs index cbb2952d..7a466b72 100644 --- a/src/model/cast.rs +++ b/src/model/cast.rs @@ -1,7 +1,7 @@ use std::num::NonZeroUsize; use std::str::FromStr; -use super::{Pattern, Regex, Value}; +use super::{Content, Pattern, Regex, Transform, Value}; use crate::diag::{with_alternative, StrResult}; use crate::font::{FontStretch, FontStyle, FontWeight}; use crate::frame::{Destination, Lang, Location, Region}; @@ -189,6 +189,15 @@ castable! { @regex: Regex => Self::Regex(regex.clone()), } +castable! { + Transform, + Expected: "content or function", + Value::None => Self::Content(Content::empty()), + Value::Str(text) => Self::Content(item!(text)(text.into())), + Value::Content(content) => Self::Content(content), + Value::Func(func) => Self::Func(func), +} + dynamic! { Dir: "direction", } diff --git a/src/model/content.rs b/src/model/content.rs index 7b09c697..0257f4da 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -5,13 +5,14 @@ use std::iter::{self, Sum}; use std::ops::{Add, AddAssign}; use std::sync::Arc; +use comemo::Tracked; use siphasher::sip128::{Hasher128, SipHasher}; use typst_macros::node; -use super::{Args, Key, Property, Selector, StyleEntry, StyleMap, Vm}; -use crate as typst; +use super::{Args, Key, Property, Recipe, Selector, StyleEntry, StyleMap, Value, Vm}; use crate::diag::{SourceResult, StrResult}; -use crate::util::{EcoString, ReadableTypeId}; +use crate::util::ReadableTypeId; +use crate::World; /// Composable representation of styled content. /// @@ -27,11 +28,6 @@ impl Content { SequenceNode(vec![]).pack() } - /// Create content from a string of text. - pub fn text(text: impl Into<EcoString>) -> Self { - item!(text)(text.into()) - } - /// Create a new sequence node from multiples nodes. pub fn sequence(seq: Vec<Self>) -> Self { match seq.as_slice() { @@ -65,6 +61,11 @@ impl Content { Arc::get_mut(&mut self.0)?.as_any_mut().downcast_mut::<T>() } + /// Access a field on this content. + pub fn field(&self, name: &str) -> Option<Value> { + self.0.field(name) + } + /// Whether this content has the given capability. pub fn has<C>(&self) -> bool where @@ -97,6 +98,19 @@ impl Content { self.styled_with_entry(StyleEntry::Property(Property::new(key, value))) } + /// Style this content with a recipe, eagerly applying it if possible. + pub fn styled_with_recipe( + self, + world: Tracked<dyn World>, + recipe: Recipe, + ) -> SourceResult<Self> { + if recipe.pattern.is_none() { + recipe.transform.apply(world, recipe.span, || Value::Content(self)) + } else { + Ok(self.styled_with_entry(StyleEntry::Recipe(recipe))) + } + } + /// Style this content with a style entry. pub fn styled_with_entry(mut self, entry: StyleEntry) -> Self { if let Some(styled) = self.try_downcast_mut::<StyledNode>() { @@ -242,6 +256,9 @@ pub trait Node: 'static { where Self: Sized; + /// Access a field on this node. + fn field(&self, name: &str) -> Option<Value>; + /// A unique identifier of the node type. fn id(&self) -> NodeId; diff --git a/src/model/eval.rs b/src/model/eval.rs index 16e66818..fd43c4c3 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -7,7 +7,7 @@ use unicode_segmentation::UnicodeSegmentation; use super::{ methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Flow, Func, - Pattern, Recipe, Scope, Scopes, Show, StyleEntry, StyleMap, Value, Vm, + Pattern, Recipe, Scope, Scopes, StyleMap, Transform, Value, Vm, }; use crate::diag::{bail, error, At, SourceResult, StrResult, Trace, Tracepoint}; use crate::geom::{Abs, Angle, Em, Fr, Ratio}; @@ -133,12 +133,8 @@ fn eval_markup( break; } - eval_markup(vm, nodes)?.styled_with_entry(StyleEntry::Recipe(recipe)) - } - ast::MarkupNode::Expr(ast::Expr::Wrap(wrap)) => { let tail = eval_markup(vm, nodes)?; - vm.scopes.top.define(wrap.binding().take(), tail); - wrap.body().eval(vm)?.display(vm.world) + tail.styled_with_recipe(vm.world, recipe)? } _ => node.eval(vm)?, }); @@ -408,7 +404,6 @@ impl Eval for ast::Expr { Self::Let(v) => v.eval(vm), Self::Set(_) => bail!(forbidden("set")), Self::Show(_) => bail!(forbidden("show")), - Self::Wrap(_) => bail!(forbidden("wrap")), Self::Conditional(v) => v.eval(vm), Self::While(v) => v.eval(vm), Self::For(v) => v.eval(vm), @@ -484,18 +479,12 @@ fn eval_code( } ast::Expr::Show(show) => { let recipe = show.eval(vm)?; - let entry = StyleEntry::Recipe(recipe); if vm.flow.is_some() { break; } let tail = eval_code(vm, exprs)?.display(vm.world); - Value::Content(tail.styled_with_entry(entry)) - } - ast::Expr::Wrap(wrap) => { - let tail = eval_code(vm, exprs)?; - vm.scopes.top.define(wrap.binding().take(), tail); - wrap.body().eval(vm)? + Value::Content(tail.styled_with_recipe(vm.world, recipe)?) } _ => expr.eval(vm)?, }; @@ -671,9 +660,8 @@ impl Eval for ast::FieldAccess { Ok(match object { Value::Dict(dict) => dict.get(&field).at(span)?.clone(), - Value::Content(node) => node - .to::<dyn Show>() - .and_then(|node| node.field(&field)) + Value::Content(content) => content + .field(&field) .ok_or_else(|| format!("unknown field {field:?}")) .at(span)?, v => bail!(self.target().span(), "cannot access field on {}", v.type_name()), @@ -838,6 +826,11 @@ impl Eval for ast::SetRule { type Output = StyleMap; fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { + if let Some(condition) = self.condition() { + if !condition.eval(vm)?.cast::<bool>().at(condition.span())? { + return Ok(StyleMap::new()); + } + } let target = self.target(); let target = target.eval(vm)?.cast::<Func>().at(target.span())?; let args = self.args().eval(vm)?; @@ -849,36 +842,16 @@ impl Eval for ast::ShowRule { type Output = Recipe; fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { - // Evaluate the target function. - let pattern = self.pattern(); - let pattern = pattern.eval(vm)?.cast::<Pattern>().at(pattern.span())?; - - // Collect captured variables. - let captured = { - let mut visitor = CapturesVisitor::new(&vm.scopes); - visitor.visit(self.as_untyped()); - visitor.finish() - }; - - // Define parameters. - let mut params = vec![]; - if let Some(binding) = self.binding() { - params.push((binding.take(), None)); - } + let pattern = self + .pattern() + .map(|pattern| pattern.eval(vm)?.cast::<Pattern>().at(pattern.span())) + .transpose()?; - // Define the recipe function. - let body = self.body(); - let span = body.span(); - let func = Func::from_closure(Closure { - location: vm.location, - name: None, - captured, - params, - sink: None, - body, - }); + let transform = self.transform(); + let span = transform.span(); + let transform = transform.eval(vm)?.cast::<Transform>().at(span)?; - Ok(Recipe { pattern, func: Spanned::new(func, span) }) + Ok(Recipe { span, pattern, transform }) } } @@ -1066,7 +1039,7 @@ fn import(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> { Ok(module) } -impl Eval for ast::BreakStmt { +impl Eval for ast::LoopBreak { type Output = Value; fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { @@ -1077,7 +1050,7 @@ impl Eval for ast::BreakStmt { } } -impl Eval for ast::ContinueStmt { +impl Eval for ast::LoopContinue { type Output = Value; fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { @@ -1088,7 +1061,7 @@ impl Eval for ast::ContinueStmt { } } -impl Eval for ast::ReturnStmt { +impl Eval for ast::FuncReturn { type Output = Value; fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { diff --git a/src/model/func.rs b/src/model/func.rs index 456b6aa6..8cedb158 100644 --- a/src/model/func.rs +++ b/src/model/func.rs @@ -310,16 +310,6 @@ impl<'a> CapturesVisitor<'a> { self.bind(expr.binding()); } - // A show rule contains a binding, but that binding is only active - // after the target has been evaluated. - Some(ast::Expr::Show(show)) => { - self.visit(show.pattern().as_untyped()); - if let Some(binding) = show.binding() { - self.bind(binding); - } - self.visit(show.body().as_untyped()); - } - // A for loop contains one or two bindings in its pattern. These are // active after the iterable is evaluated but before the body is // evaluated. @@ -391,9 +381,9 @@ mod tests { test("{(x, y: x + z) => x + y}", &["x", "z"]); // Show rule. - test("#show x: y as x", &["y"]); - test("#show x: y as x + z", &["y", "z"]); - test("#show x: x as x", &["x"]); + test("#show y: x => x", &["y"]); + test("#show y: x => x + z", &["y", "z"]); + test("#show x: x => x", &["x"]); // For loop. test("#for x in y { x + z }", &["y", "z"]); diff --git a/src/model/items.rs b/src/model/items.rs index 164d9602..e9c23c26 100644 --- a/src/model/items.rs +++ b/src/model/items.rs @@ -48,11 +48,11 @@ pub struct LangItems { pub em: fn(StyleChain) -> Abs, /// Access the text direction. pub dir: fn(StyleChain) -> Dir, - /// A space. + /// Whitespace. pub space: fn() -> Content, - /// A forced line break. + /// A forced line break: `\`. pub linebreak: fn(justify: bool) -> Content, - /// Plain text. + /// Plain text without markup. pub text: fn(text: EcoString) -> Content, /// A smart quote: `'` or `"`. pub smart_quote: fn(double: bool) -> Content, @@ -72,18 +72,18 @@ pub struct LangItems { pub heading: fn(level: NonZeroUsize, body: Content) -> Content, /// An item in an unordered list: `- ...`. pub list_item: fn(body: Content) -> Content, - /// An item in an enumeration (ordered list): `1. ...`. + /// An item in an enumeration (ordered list): `+ ...` or `1. ...`. pub enum_item: fn(number: Option<usize>, body: Content) -> Content, /// An item in a description list: `/ Term: Details`. pub desc_item: fn(term: Content, body: Content) -> Content, - /// A math formula: `$x$`, `$ x^2 $`. + /// A mathematical formula: `$x$`, `$ x^2 $`. pub math: fn(children: Vec<Content>, display: bool) -> Content, - /// A atom in a formula: `x`, `+`, `12`. + /// An atom in a formula: `x`, `+`, `12`. pub math_atom: fn(atom: EcoString) -> Content, - /// A base with an optional sub- and superscript in a formula: `a_1^2`. + /// A base with optional sub- and superscripts in a formula: `a_1^2`. pub math_script: fn(base: Content, sub: Option<Content>, sup: Option<Content>) -> Content, - /// A fraction in a formula: `x/2` + /// A fraction in a formula: `x/2`. pub math_frac: fn(num: Content, denom: Content) -> Content, /// An alignment indicator in a formula: `&`, `&&`. pub math_align: fn(count: usize) -> Content, diff --git a/src/model/ops.rs b/src/model/ops.rs index 9d55fa63..0110fb96 100644 --- a/src/model/ops.rs +++ b/src/model/ops.rs @@ -19,8 +19,8 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> { (a, None) => a, (None, b) => b, (Str(a), Str(b)) => Str(a + b), - (Str(a), Content(b)) => Content(super::Content::text(a) + b), - (Content(a), Str(b)) => Content(a + super::Content::text(b)), + (Str(a), Content(b)) => Content(item!(text)(a.into()) + b), + (Content(a), Str(b)) => Content(a + item!(text)(b.into())), (Content(a), Content(b)) => Content(a + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), @@ -85,8 +85,8 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> { (Str(a), Str(b)) => Str(a + b), (Content(a), Content(b)) => Content(a + b), - (Content(a), Str(b)) => Content(a + super::Content::text(b)), - (Str(a), Content(b)) => Content(super::Content::text(a) + b), + (Content(a), Str(b)) => Content(a + item!(text)(b.into())), + (Str(a), Content(b)) => Content(item!(text)(a.into()) + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), diff --git a/src/model/styles.rs b/src/model/styles.rs index 9463e55e..3800490b 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -12,7 +12,7 @@ use crate::diag::SourceResult; use crate::geom::{ Abs, Align, Axes, Corners, Em, GenAlign, Length, Numeric, PartialStroke, Rel, Sides, }; -use crate::syntax::Spanned; +use crate::syntax::Span; use crate::util::ReadableTypeId; use crate::World; @@ -283,17 +283,17 @@ impl<'a> StyleChain<'a> { .unguard_parts(sel) .to::<dyn Show>() .unwrap() - .realize(world, self)?; + .show(world, 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.to::<dyn Show>().unwrap().finalize(world, self, content)?, - ); + if let Some(node) = node.to::<dyn Finalize>() { + if let Some(content) = realized { + realized = Some(node.finalize(world, self, content)?); + } } } } @@ -974,18 +974,20 @@ impl Fold for PartialStroke<Abs> { /// A show rule recipe. #[derive(Clone, PartialEq, Hash)] pub struct Recipe { - /// The patterns to customize. - pub pattern: Pattern, - /// The function that defines the recipe. - pub func: Spanned<Func>, + /// The span errors are reported with. + pub span: Span, + /// The pattern that the rule applies to. + pub pattern: Option<Pattern>, + /// The transformation to perform on the match. + pub transform: Transform, } impl Recipe { /// 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(), - (Pattern::Regex(_), Target::Text(_)) => true, + (Some(Pattern::Node(id)), Target::Node(node)) => *id == node.id(), + (Some(Pattern::Regex(_)), Target::Text(_)) => true, _ => false, } } @@ -998,12 +1000,13 @@ impl Recipe { target: Target, ) -> SourceResult<Option<Content>> { let content = match (target, &self.pattern) { - (Target::Node(node), &Pattern::Node(id)) if node.id() == id => { - let node = node.to::<dyn Show>().unwrap().unguard_parts(sel); - self.call(world, || Value::Content(node))? + (Target::Node(node), Some(Pattern::Node(id))) if node.id() == *id => { + self.transform.apply(world, self.span, || { + Value::Content(node.to::<dyn Show>().unwrap().unguard_parts(sel)) + })? } - (Target::Text(text), Pattern::Regex(regex)) => { + (Target::Text(text), Some(Pattern::Regex(regex))) => { let make = world.config().items.text; let mut result = vec![]; let mut cursor = 0; @@ -1014,7 +1017,10 @@ impl Recipe { result.push(make(text[cursor..start].into())); } - result.push(self.call(world, || Value::Str(mat.as_str().into()))?); + let transformed = self + .transform + .apply(world, self.span, || Value::Str(mat.as_str().into()))?; + result.push(transformed); cursor = mat.end(); } @@ -1035,24 +1041,10 @@ impl Recipe { Ok(Some(content.styled_with_entry(StyleEntry::Guard(sel)))) } - /// Call the recipe function, with the argument if desired. - fn call<F>(&self, world: Tracked<dyn World>, arg: F) -> SourceResult<Content> - where - F: FnOnce() -> Value, - { - let args = if self.func.v.argc() == Some(0) { - Args::new(self.func.span, []) - } else { - Args::new(self.func.span, [arg()]) - }; - - Ok(self.func.v.call_detached(world, args)?.display(world)) - } - /// Whether this recipe is for the given node. pub fn is_of(&self, node: NodeId) -> bool { match self.pattern { - Pattern::Node(id) => id == node, + Some(Pattern::Node(id)) => id == node, _ => false, } } @@ -1060,7 +1052,7 @@ impl Recipe { impl Debug for Recipe { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Recipe matching {:?} from {:?}", self.pattern, self.func.span) + write!(f, "Recipe matching {:?}", self.pattern) } } @@ -1080,6 +1072,36 @@ impl Pattern { } } +/// A show rule transformation that can be applied to a match. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Transform { + /// Replacement content. + Content(Content), + /// A function to apply to the match. + Func(Func), +} + +impl Transform { + /// Apply the transform. + pub fn apply<F>( + &self, + world: Tracked<dyn World>, + span: Span, + arg: F, + ) -> SourceResult<Content> + where + F: FnOnce() -> Value, + { + match self { + Transform::Content(content) => Ok(content.clone()), + Transform::Func(func) => { + let args = Args::new(span, [arg()]); + Ok(func.call_detached(world, args)?.display(world)) + } + } + } +} + /// A target for a show rule recipe. #[derive(Debug, Copy, Clone, PartialEq)] pub enum Target<'a> { @@ -1104,30 +1126,25 @@ pub trait Show: 'static + Sync + Send { /// Unguard nested content against recursive show rules. fn unguard_parts(&self, sel: Selector) -> Content; - /// Access a field on this node. - fn field(&self, name: &str) -> Option<Value>; - /// The base recipe for this node that is executed if there is no /// user-defined show rule. - fn realize( + fn show( &self, world: Tracked<dyn World>, styles: StyleChain, ) -> SourceResult<Content>; +} +/// Post-process a node after it was realized. +#[capability] +pub trait Finalize: 'static + Sync + Send { /// Finalize this node given the realization of a base or user recipe. Use /// this for effects that should work even in the face of a user-defined - /// show rule, for example: - /// - Application of general settable properties - /// - /// Defaults to just the realized content. - #[allow(unused_variables)] + /// show rule, for example the linking behaviour of a link node. fn finalize( &self, world: Tracked<dyn World>, styles: StyleChain, realized: Content, - ) -> SourceResult<Content> { - Ok(realized) - } + ) -> SourceResult<Content>; } diff --git a/src/model/value.rs b/src/model/value.rs index 825e6f55..9e6968fa 100644 --- a/src/model/value.rs +++ b/src/model/value.rs @@ -381,7 +381,7 @@ primitive! { Str: "string", Str } primitive! { Content: "content", Content, None => Content::empty(), - Str(text) => Content::text(text) + Str(text) => item!(text)(text.into()) } primitive! { Array: "array", Array } primitive! { Dict: "dictionary", Dict } |
