diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs index 8ad76387a..962e8a168 100644 --- a/library/src/layout/page.rs +++ b/library/src/layout/page.rs @@ -2,7 +2,7 @@ use std::ptr; use std::str::FromStr; use super::{AlignNode, ColumnsNode}; -use crate::meta::{Counter, CounterAction, CounterNode, Numbering}; +use crate::meta::{Counter, CounterAction, CounterKey, CounterNode, Numbering}; use crate::prelude::*; /// Layouts its child onto one or multiple pages. @@ -311,9 +311,12 @@ impl PageNode { let header_ascent = self.header_ascent(styles); let footer = self.footer(styles).or_else(|| { self.numbering(styles).map(|numbering| { - CounterNode::new(Counter::Page, CounterAction::Both(numbering)) - .pack() - .aligned(self.number_align(styles)) + CounterNode::new( + Counter::new(CounterKey::Page), + CounterAction::Both(numbering), + ) + .pack() + .aligned(self.number_align(styles)) }) }); let footer_descent = self.footer_descent(styles); diff --git a/library/src/lib.rs b/library/src/lib.rs index b397bfb45..83dbe17aa 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -10,6 +10,7 @@ pub mod symbols; pub mod text; pub mod visualize; +use typst::diag::At; use typst::eval::{LangItems, Library, Module, Scope}; use typst::geom::{Align, Color, Dir, GenAlign, Smart}; use typst::model::{Node, NodeId, StyleMap}; @@ -93,6 +94,7 @@ fn global(math: Module, calc: Module) -> Module { global.define("bibliography", meta::BibliographyNode::id()); global.define("counter", meta::counter); global.define("numbering", meta::numbering); + global.define("state", meta::state); // Symbols. global.define("sym", symbols::sym()); @@ -225,6 +227,15 @@ fn items() -> LangItems { math::AccentNode::new(base, math::Accent::new(accent)).pack() }, math_frac: |num, denom| math::FracNode::new(num, denom).pack(), - counter_method: meta::counter_method, + library_method: |dynamic, method, args, span| { + if let Some(counter) = dynamic.downcast().cloned() { + meta::counter_method(counter, method, args, span) + } else if let Some(state) = dynamic.downcast().cloned() { + meta::state_method(state, method, args, span) + } else { + Err(format!("type {} has no method `{method}`", dynamic.type_name())) + .at(span) + } + }, } } diff --git a/library/src/meta/counter.rs b/library/src/meta/counter.rs index ab089d5e4..b8f719ac8 100644 --- a/library/src/meta/counter.rs +++ b/library/src/meta/counter.rs @@ -3,7 +3,6 @@ use std::str::FromStr; use ecow::{eco_vec, EcoVec}; use smallvec::{smallvec, SmallVec}; -use typst::eval::Dynamic; use super::{Numbering, NumberingPattern}; use crate::layout::PageNode; @@ -13,20 +12,65 @@ use crate::prelude::*; /// /// Display: Counter /// Category: meta -/// Returns: content +/// Returns: counter #[func] -pub fn counter(key: Counter) -> Value { - Value::dynamic(key) +pub fn counter( + /// The key that identifies this counter. + key: CounterKey, +) -> Value { + Value::dynamic(Counter::new(key)) +} + +/// Identifies a counter. +#[derive(Clone, PartialEq, Hash)] +pub enum CounterKey { + /// The page counter. + Page, + /// Counts elements matching the given selectors. Only works for locatable + /// elements or labels. + Selector(Selector), + /// Counts through manual counters with the same key. + Str(Str), +} + +cast_from_value! { + CounterKey, + v: Str => Self::Str(v), + label: Label => Self::Selector(Selector::Label(label)), + func: Func => { + let Some(id) = func.id() else { + return Err("this function is not selectable".into()); + }; + + if id == NodeId::of::() { + return Ok(Self::Page); + } + + if !Content::new(id).can::() { + Err(eco_format!("cannot count through {}s", id.name))?; + } + + Self::Selector(Selector::Node(id, None)) + } +} + +impl Debug for CounterKey { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Page => f.pad("page"), + Self::Selector(selector) => selector.fmt(f), + Self::Str(str) => str.fmt(f), + } + } } /// Call a method on counter. pub fn counter_method( - dynamic: &Dynamic, + counter: Counter, method: &str, mut args: Args, span: Span, ) -> SourceResult { - let counter = dynamic.downcast::().unwrap(); let pattern = |s| NumberingPattern::from_str(s).unwrap().into(); let action = match method { "get" => CounterAction::Get(args.eat()?.unwrap_or_else(|| pattern("1.1"))), @@ -41,7 +85,7 @@ pub fn counter_method( args.finish()?; - let content = CounterNode::new(counter.clone(), action).pack(); + let content = CounterNode::new(counter, action).pack(); Ok(Value::Content(content)) } @@ -53,7 +97,7 @@ pub fn counter_method( pub struct CounterNode { /// The counter key. #[required] - pub key: Counter, + pub counter: Counter, /// The action. #[required] @@ -64,24 +108,30 @@ impl Show for CounterNode { fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult { match self.action() { CounterAction::Get(numbering) => { - self.key().resolve(vt, self.0.stable_id(), &numbering) + self.counter().resolve(vt, self.0.stable_id(), &numbering) + } + CounterAction::Final(numbering) => { + self.counter().resolve(vt, None, &numbering) } - CounterAction::Final(numbering) => self.key().resolve(vt, None, &numbering), CounterAction::Both(numbering) => { let both = match &numbering { Numbering::Pattern(pattern) => pattern.pieces() >= 2, _ => false, }; - let key = self.key(); + let counter = self.counter(); let id = self.0.stable_id(); if !both { - return key.resolve(vt, id, &numbering); + return counter.resolve(vt, id, &numbering); } - let sequence = key.sequence(vt.world, vt.introspector)?; - let numbers = [sequence.single(id), sequence.single(None)]; - Ok(numbering.apply(vt.world, &numbers)?.display()) + let sequence = counter.sequence(vt.world, vt.introspector)?; + Ok(match (sequence.single(id), sequence.single(None)) { + (Some(current), Some(total)) => { + numbering.apply(vt.world, &[current, total])?.display() + } + _ => Content::empty(), + }) } CounterAction::Update(_) => Ok(Content::empty()), } @@ -109,7 +159,12 @@ cast_from_value! { impl Debug for CounterAction { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad("..") + match self { + Self::Get(_) => f.pad("get(..)"), + Self::Final(_) => f.pad("final(..)"), + Self::Both(_) => f.pad("both(..)"), + Self::Update(_) => f.pad("update(..)"), + } } } @@ -138,17 +193,22 @@ pub trait Count { /// Counts through pages, elements, and more. #[derive(Clone, PartialEq, Hash)] -pub enum Counter { - /// The page counter. - Page, - /// Counts elements matching the given selectors. Only works for locatable - /// elements or labels. - Selector(Selector), - /// Counts through manual counters with the same key. - Str(Str), +pub struct Counter { + /// The key that identifies the counter. + pub key: CounterKey, } impl Counter { + /// Create a new counter from a key. + pub fn new(key: CounterKey) -> Self { + Self { key } + } + + /// The counter for the given node. + pub fn of(id: NodeId) -> Self { + Self::new(CounterKey::Selector(Selector::Node(id, None))) + } + /// Display the value of the counter at the postition of the given stable /// id. pub fn resolve( @@ -157,9 +217,15 @@ impl Counter { stop: Option, numbering: &Numbering, ) -> SourceResult { + if !vt.introspector.init() { + return Ok(Content::empty()); + } + let sequence = self.sequence(vt.world, vt.introspector)?; - let numbers = sequence.at(stop).0; - Ok(numbering.apply(vt.world, &numbers)?.display()) + Ok(match sequence.at(stop) { + Some(state) => numbering.apply(vt.world, &state.0)?.display(), + None => Content::empty(), + }) } /// Produce the whole sequence of counter states. @@ -174,21 +240,21 @@ impl Counter { ) -> SourceResult { let mut search = Selector::Node( NodeId::of::(), - Some(dict! { "key" => self.clone() }), + Some(dict! { "counter" => self.clone() }), ); - if let Counter::Selector(selector) = self { + if let CounterKey::Selector(selector) = &self.key { search = Selector::Any(eco_vec![search, selector.clone()]); } - let mut state = CounterState::new(); let mut stops = EcoVec::new(); + let mut state = CounterState(match &self.key { + CounterKey::Selector(_) => smallvec![], + _ => smallvec![NonZeroUsize::ONE], + }); + let is_page = self.key == CounterKey::Page; let mut prev_page = NonZeroUsize::ONE; - let is_page = *self == Self::Page; - if is_page { - state.0.push(prev_page); - } for node in introspector.query(search) { let id = node.stable_id().unwrap(); @@ -221,40 +287,18 @@ impl Counter { } } -cast_from_value! { - Counter: "counter", - v: Str => Self::Str(v), - v: Selector => { - match v { - Selector::Node(id, _) => { - if id == NodeId::of::() { - return Ok(Self::Page); - } - - if !Content::new_of(id).can::() { - Err(eco_format!("cannot count through {}s", id.name))?; - } - } - Selector::Label(_) => {} - Selector::Regex(_) => Err("cannot count through text")?, - Selector::Any(_) => {} - } - Self::Selector(v) - } -} - impl Debug for Counter { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.write_str("counter(")?; - match self { - Self::Page => f.pad("page")?, - Self::Selector(selector) => selector.fmt(f)?, - Self::Str(str) => str.fmt(f)?, - } + self.key.fmt(f)?; f.write_char(')') } } +cast_from_value! { + Counter: "counter", +} + /// A sequence of counter values. #[derive(Debug, Clone)] struct CounterSequence { @@ -263,38 +307,33 @@ struct CounterSequence { } impl CounterSequence { - fn at(&self, stop: Option) -> CounterState { + fn at(&self, stop: Option) -> Option { let entry = match stop { Some(stop) => self.stops.iter().find(|&&(id, _)| id == stop), None => self.stops.last(), }; if let Some((_, state)) = entry { - return state.clone(); + return Some(state.clone()); } if self.is_page { - return CounterState(smallvec![NonZeroUsize::ONE]); + return Some(CounterState(smallvec![NonZeroUsize::ONE])); } - CounterState::default() + None } - fn single(&self, stop: Option) -> NonZeroUsize { - self.at(stop).0.first().copied().unwrap_or(NonZeroUsize::ONE) + fn single(&self, stop: Option) -> Option { + Some(*self.at(stop)?.0.first()?) } } /// Counts through elements with different levels. -#[derive(Debug, Default, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq, Hash)] pub struct CounterState(pub SmallVec<[NonZeroUsize; 3]>); impl CounterState { - /// Create a new levelled counter. - pub fn new() -> Self { - Self::default() - } - /// Advance the counter and return the numbers for the given heading. pub fn update( &mut self, diff --git a/library/src/meta/figure.rs b/library/src/meta/figure.rs index 3c3f6361b..a92ff5e71 100644 --- a/library/src/meta/figure.rs +++ b/library/src/meta/figure.rs @@ -60,7 +60,7 @@ impl Show for FigureNode { let name = self.local_name(TextNode::lang_in(styles)); caption = TextNode::packed(eco_format!("{name}\u{a0}")) + CounterNode::new( - Counter::Selector(Selector::node::()), + Counter::of(Self::id()), CounterAction::Get(numbering), ) .pack() diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs index 614200b83..833421557 100644 --- a/library/src/meta/heading.rs +++ b/library/src/meta/heading.rs @@ -91,14 +91,12 @@ impl Show for HeadingNode { fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult { let mut realized = self.body(); if let Some(numbering) = self.numbering(styles) { - realized = CounterNode::new( - Counter::Selector(Selector::node::()), - CounterAction::Get(numbering), - ) - .pack() - .spanned(self.span()) - + HNode::new(Em::new(0.3).into()).with_weak(true).pack() - + realized; + realized = + CounterNode::new(Counter::of(Self::id()), CounterAction::Get(numbering)) + .pack() + .spanned(self.span()) + + HNode::new(Em::new(0.3).into()).with_weak(true).pack() + + realized; } Ok(BlockNode::new().with_body(Some(realized)).pack()) } diff --git a/library/src/meta/mod.rs b/library/src/meta/mod.rs index a7de2dad6..1d7740580 100644 --- a/library/src/meta/mod.rs +++ b/library/src/meta/mod.rs @@ -9,6 +9,7 @@ mod link; mod numbering; mod outline; mod reference; +mod state; pub use self::bibliography::*; pub use self::counter::*; @@ -19,6 +20,7 @@ pub use self::link::*; pub use self::numbering::*; pub use self::outline::*; pub use self::reference::*; +pub use self::state::*; use typst::doc::Lang; diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs index 933119ece..976110360 100644 --- a/library/src/meta/outline.rs +++ b/library/src/meta/outline.rs @@ -118,8 +118,11 @@ impl Show for OutlineNode { let mut hidden = Content::empty(); for ancestor in &ancestors { if let Some(numbering) = ancestor.numbering(StyleChain::default()) { - let numbers = Counter::Selector(Selector::node::()) - .resolve(vt, ancestor.0.stable_id(), &numbering)?; + let numbers = Counter::of(HeadingNode::id()).resolve( + vt, + ancestor.0.stable_id(), + &numbering, + )?; hidden += numbers + SpaceNode::new().pack(); }; } @@ -133,8 +136,11 @@ impl Show for OutlineNode { // Format the numbering. let mut start = heading.body(); if let Some(numbering) = heading.numbering(StyleChain::default()) { - let numbers = Counter::Selector(Selector::node::()) - .resolve(vt, Some(stable_id), &numbering)?; + let numbers = Counter::of(HeadingNode::id()).resolve( + vt, + Some(stable_id), + &numbering, + )?; start = numbers + SpaceNode::new().pack() + start; }; diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs index 095a846c2..f05692dd3 100644 --- a/library/src/meta/reference.rs +++ b/library/src/meta/reference.rs @@ -115,11 +115,8 @@ impl Show for RefNode { bail!(self.span(), "only numbered elements can be referenced"); }; - let numbers = Counter::Selector(Selector::Node(node.id(), None)).resolve( - vt, - node.stable_id(), - &numbering.trimmed(), - )?; + let numbers = + Counter::of(node.id()).resolve(vt, node.stable_id(), &numbering.trimmed())?; Ok((supplement + numbers).linked(Link::Node(node.stable_id().unwrap()))) } diff --git a/library/src/meta/state.rs b/library/src/meta/state.rs new file mode 100644 index 000000000..8b0a0aa64 --- /dev/null +++ b/library/src/meta/state.rs @@ -0,0 +1,209 @@ +use std::fmt::{self, Debug, Formatter, Write}; + +use ecow::EcoVec; + +use crate::prelude::*; + +/// Handle stateful tasks. +/// +/// Display: State +/// Category: meta +/// Returns: state +#[func] +pub fn state( + /// The key that identifies this state. + key: Str, + /// The initial value of the state. + #[default] + init: Value, +) -> Value { + Value::dynamic(State { key, init }) +} + +/// Call a method on a state. +pub fn state_method( + state: State, + method: &str, + mut args: Args, + span: Span, +) -> SourceResult { + let action = match method { + "get" => StateAction::Get(args.eat()?), + "final" => StateAction::Final(args.eat()?), + "update" => StateAction::Update(args.expect("value or function")?), + _ => bail!(span, "type state has no method `{}`", method), + }; + + args.finish()?; + + let content = StateNode::new(state, action).pack(); + Ok(Value::Content(content)) +} + +/// Executes an action on a state. +/// +/// Display: State +/// Category: special +#[node(Locatable, Show)] +pub struct StateNode { + /// The state. + #[required] + pub state: State, + + /// The action. + #[required] + pub action: StateAction, +} + +impl Show for StateNode { + fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult { + match self.action() { + StateAction::Get(func) => self.state().resolve(vt, self.0.stable_id(), func), + StateAction::Final(func) => self.state().resolve(vt, None, func), + StateAction::Update(_) => Ok(Content::empty()), + } + } +} + +/// The action to perform on the state. +#[derive(Clone, PartialEq, Hash)] +pub enum StateAction { + /// Displays the current state. + Get(Option), + /// Displays the final state. + Final(Option), + /// Updates the state, possibly based on the previous one. + Update(StateUpdate), +} + +cast_from_value! { + StateAction: "state action", +} + +impl Debug for StateAction { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Get(_) => f.pad("get(..)"), + Self::Final(_) => f.pad("final(..)"), + Self::Update(_) => f.pad("update(..)"), + } + } +} + +/// An update to perform on a state. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum StateUpdate { + /// Set the state to the specified value. + Set(Value), + /// Apply the given function to the state. + Func(Func), +} + +cast_from_value! { + StateUpdate, + v: Func => Self::Func(v), + v: Value => Self::Set(v), +} + +/// A state. +#[derive(Clone, PartialEq, Hash)] +pub struct State { + /// The key that identifies the state. + key: Str, + /// The initial value of the state. + init: Value, +} + +impl State { + /// Display the state at the postition of the given stable id. + fn resolve( + &self, + vt: &Vt, + stop: Option, + func: Option, + ) -> SourceResult { + if !vt.introspector.init() { + return Ok(Content::empty()); + } + + let sequence = self.sequence(vt.world, vt.introspector)?; + Ok(match sequence.at(stop) { + Some(value) => { + if let Some(func) = func { + let args = Args::new(func.span(), [value]); + func.call_detached(vt.world, args)?.display() + } else { + value.display() + } + } + None => Content::empty(), + }) + } + + /// Produce the whole sequence of states. + /// + /// This has to happen just once for all states, cutting down the number + /// of state updates from quadratic to linear. + #[comemo::memoize] + fn sequence( + &self, + world: Tracked, + introspector: Tracked, + ) -> SourceResult { + let search = Selector::Node( + NodeId::of::(), + Some(dict! { "state" => self.clone() }), + ); + + let mut stops = EcoVec::new(); + let mut state = self.init.clone(); + + for node in introspector.query(search) { + let id = node.stable_id().unwrap(); + let node = node.to::().unwrap(); + + if let StateAction::Update(update) = node.action() { + match update { + StateUpdate::Set(value) => state = value, + StateUpdate::Func(func) => { + let args = Args::new(func.span(), [state]); + state = func.call_detached(world, args)?; + } + } + } + + stops.push((id, state.clone())); + } + + Ok(StateSequence(stops)) + } +} + +impl Debug for State { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("state(")?; + self.key.fmt(f)?; + f.write_str(", ")?; + self.init.fmt(f)?; + f.write_char(')') + } +} + +cast_from_value! { + State: "state", +} + +/// A sequence of state values. +#[derive(Debug, Clone)] +struct StateSequence(EcoVec<(StableId, Value)>); + +impl StateSequence { + fn at(&self, stop: Option) -> Option { + let entry = match stop { + Some(stop) => self.0.iter().find(|&&(id, _)| id == stop), + None => self.0.last(), + }; + + entry.map(|(_, value)| value.clone()) + } +} diff --git a/macros/src/node.rs b/macros/src/node.rs index 1d05b9a6d..68d43d9c6 100644 --- a/macros/src/node.rs +++ b/macros/src/node.rs @@ -234,7 +234,7 @@ fn create_new_func(node: &Node) -> TokenStream { quote! { /// Create a new node. pub fn new(#(#params),*) -> Self { - Self(::typst::model::Content::new::()) + Self(::typst::model::Content::new(::id())) #(#builder_calls)* } } @@ -388,7 +388,7 @@ fn create_vtable_func(node: &Node) -> TokenStream { quote! { |id| { - let null = Self(::typst::model::Content::new::<#ident>()); + let null = Self(::typst::model::Content::new(<#ident as ::typst::model::Node>::id())); #(#checks)* None } @@ -456,7 +456,7 @@ fn create_construct_impl(node: &Node) -> TokenStream { vm: &::typst::eval::Vm, args: &mut ::typst::eval::Args, ) -> ::typst::diag::SourceResult<::typst::model::Content> { - let mut node = Self(::typst::model::Content::new::()); + let mut node = Self(::typst::model::Content::new(::id())); #(#handlers)* Ok(node.0) } diff --git a/src/eval/library.rs b/src/eval/library.rs index 45c23d170..0e0b38aa5 100644 --- a/src/eval/library.rs +++ b/src/eval/library.rs @@ -90,9 +90,8 @@ pub struct LangItems { pub math_accent: fn(base: Content, accent: char) -> Content, /// A fraction in a formula: `x/2`. pub math_frac: fn(num: Content, denom: Content) -> Content, - /// Dispatch a method on a counter. This is hacky and should be superseded - /// by more dynamic method dispatch. - pub counter_method: fn( + /// Dispatch a method on a library value. + pub library_method: fn( dynamic: &Dynamic, method: &str, args: Args, diff --git a/src/eval/methods.rs b/src/eval/methods.rs index a449ac166..036f7ba22 100644 --- a/src/eval/methods.rs +++ b/src/eval/methods.rs @@ -135,11 +135,7 @@ pub fn call( }, Value::Dyn(dynamic) => { - if dynamic.type_name() == "counter" { - return (vm.items.counter_method)(&dynamic, method, args, span); - } - - return missing(); + return (vm.items.library_method)(&dynamic, method, args, span); } _ => return missing(), @@ -296,6 +292,7 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] { ("step", true), ("update", true), ], + "state" => &[("get", true), ("final", true), ("update", true)], _ => &[], } } diff --git a/src/model/content.rs b/src/model/content.rs index 11ad635fc..bd24829dc 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -40,12 +40,7 @@ enum Modifier { impl Content { /// Create a content of the given node kind. - pub fn new() -> Self { - Self::new_of(T::id()) - } - - /// Create a content of the given node kind. - pub fn new_of(id: NodeId) -> Self { + pub fn new(id: NodeId) -> Self { Self { id, span: Span::detached(), diff --git a/tests/ref/meta/state.png b/tests/ref/meta/state.png new file mode 100644 index 000000000..d48e1dc3c Binary files /dev/null and b/tests/ref/meta/state.png differ diff --git a/tests/typ/meta/counter.typ b/tests/typ/meta/counter.typ index 9f6f4c8a0..539af6b90 100644 --- a/tests/typ/meta/counter.typ +++ b/tests/typ/meta/counter.typ @@ -6,7 +6,6 @@ Final: #mine.final() \ #mine.step() -#mine.step() First: #mine.get() \ #mine.update(7) #mine.both("1 of 1") \ diff --git a/tests/typ/meta/state.typ b/tests/typ/meta/state.typ new file mode 100644 index 000000000..dd34deac2 --- /dev/null +++ b/tests/typ/meta/state.typ @@ -0,0 +1,24 @@ +// Test state. + +--- +#set page(width: 200pt) +#set text(8pt) + +#let ls = state("lorem", lorem(1000).split(".")) +#let loremum(count) = { + ls.get(list => list.slice(0, count).join(".").trim() + ".") + ls.update(list => list.slice(count)) +} + +#let fs = state("fader", red) +#let trait(title) = block[ + #fs.get(color => text(fill: color)[ + *#title:* #loremum(1) + ]) + #fs.update(color => color.lighten(30%)) +] + +#trait[Boldness] +#trait[Adventure] +#trait[Fear] +#trait[Anger]