summaryrefslogtreecommitdiff
path: root/src/model
diff options
context:
space:
mode:
Diffstat (limited to 'src/model')
-rw-r--r--src/model/cast.rs11
-rw-r--r--src/model/content.rs33
-rw-r--r--src/model/eval.rs69
-rw-r--r--src/model/func.rs16
-rw-r--r--src/model/items.rs16
-rw-r--r--src/model/ops.rs8
-rw-r--r--src/model/styles.rs107
-rw-r--r--src/model/value.rs2
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 }