summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-03-17 14:39:30 +0100
committerLaurenz <laurmaedje@gmail.com>2023-03-17 16:04:12 +0100
commitc47e4cb4969836e7fb8955361728105555b6d722 (patch)
tree08b613938b7bc7b82ae358aa8f117af11c1db6a8
parent312197b276748e1a17258ad21837850f582a467c (diff)
State
-rw-r--r--library/src/layout/page.rs11
-rw-r--r--library/src/lib.rs13
-rw-r--r--library/src/meta/counter.rs183
-rw-r--r--library/src/meta/figure.rs2
-rw-r--r--library/src/meta/heading.rs14
-rw-r--r--library/src/meta/mod.rs2
-rw-r--r--library/src/meta/outline.rs14
-rw-r--r--library/src/meta/reference.rs7
-rw-r--r--library/src/meta/state.rs209
-rw-r--r--macros/src/node.rs6
-rw-r--r--src/eval/library.rs5
-rw-r--r--src/eval/methods.rs7
-rw-r--r--src/model/content.rs7
-rw-r--r--tests/ref/meta/state.pngbin0 -> 47353 bytes
-rw-r--r--tests/typ/meta/counter.typ1
-rw-r--r--tests/typ/meta/state.typ24
16 files changed, 392 insertions, 113 deletions
diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs
index 8ad76387..962e8a16 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 b397bfb4..83dbe17a 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 ab089d5e..b8f719ac 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::<PageNode>() {
+ return Ok(Self::Page);
+ }
+
+ if !Content::new(id).can::<dyn Locatable>() {
+ 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<Value> {
- let counter = dynamic.downcast::<Counter>().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<Content> {
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<StableId>,
numbering: &Numbering,
) -> SourceResult<Content> {
+ 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<CounterSequence> {
let mut search = Selector::Node(
NodeId::of::<CounterNode>(),
- 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::<PageNode>() {
- return Ok(Self::Page);
- }
-
- if !Content::new_of(id).can::<dyn Locatable>() {
- 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<StableId>) -> CounterState {
+ fn at(&self, stop: Option<StableId>) -> Option<CounterState> {
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<StableId>) -> NonZeroUsize {
- self.at(stop).0.first().copied().unwrap_or(NonZeroUsize::ONE)
+ fn single(&self, stop: Option<StableId>) -> Option<NonZeroUsize> {
+ 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 3c3f6361..a92ff5e7 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::<Self>()),
+ Counter::of(Self::id()),
CounterAction::Get(numbering),
)
.pack()
diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs
index 614200b8..83342155 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<Content> {
let mut realized = self.body();
if let Some(numbering) = self.numbering(styles) {
- realized = CounterNode::new(
- Counter::Selector(Selector::node::<Self>()),
- 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 a7de2dad..1d774058 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 933119ec..97611036 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::<HeadingNode>())
- .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::<HeadingNode>())
- .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 095a846c..f05692dd 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 00000000..8b0a0aa6
--- /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<Value> {
+ 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<Content> {
+ 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<Func>),
+ /// Displays the final state.
+ Final(Option<Func>),
+ /// 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<StableId>,
+ func: Option<Func>,
+ ) -> SourceResult<Content> {
+ 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<dyn World>,
+ introspector: Tracked<Introspector>,
+ ) -> SourceResult<StateSequence> {
+ let search = Selector::Node(
+ NodeId::of::<StateNode>(),
+ 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::<StateNode>().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<StableId>) -> Option<Value> {
+ 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 1d05b9a6..68d43d9c 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>())
+ Self(::typst::model::Content::new(<Self as ::typst::model::Node>::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::<Self>());
+ let mut node = Self(::typst::model::Content::new(<Self as ::typst::model::Node>::id()));
#(#handlers)*
Ok(node.0)
}
diff --git a/src/eval/library.rs b/src/eval/library.rs
index 45c23d17..0e0b38aa 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 a449ac16..036f7ba2 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 11ad635f..bd24829d 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<T: Node>() -> 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 00000000..d48e1dc3
--- /dev/null
+++ b/tests/ref/meta/state.png
Binary files differ
diff --git a/tests/typ/meta/counter.typ b/tests/typ/meta/counter.typ
index 9f6f4c8a..539af6b9 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 00000000..dd34deac
--- /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]