typst_library/introspection/
counter.rs

1use std::num::NonZeroUsize;
2use std::str::FromStr;
3
4use comemo::{Track, Tracked, TrackedMut};
5use ecow::{eco_format, eco_vec, EcoString, EcoVec};
6use smallvec::{smallvec, SmallVec};
7use typst_syntax::Span;
8use typst_utils::NonZeroExt;
9
10use crate::diag::{bail, At, HintedStrResult, SourceResult};
11use crate::engine::{Engine, Route, Sink, Traced};
12use crate::foundations::{
13    cast, elem, func, scope, select_where, ty, Args, Array, Construct, Content, Context,
14    Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr,
15    Selector, Show, Smart, Str, StyleChain, Value,
16};
17use crate::introspection::{Introspector, Locatable, Location, Tag};
18use crate::layout::{Frame, FrameItem, PageElem};
19use crate::math::EquationElem;
20use crate::model::{FigureElem, FootnoteElem, HeadingElem, Numbering, NumberingPattern};
21use crate::routines::Routines;
22use crate::World;
23
24/// Counts through pages, elements, and more.
25///
26/// With the counter function, you can access and modify counters for pages,
27/// headings, figures, and more. Moreover, you can define custom counters for
28/// other things you want to count.
29///
30/// Since counters change throughout the course of the document, their current
31/// value is _contextual._ It is recommended to read the chapter on [context]
32/// before continuing here.
33///
34/// # Accessing a counter { #accessing }
35/// To access the raw value of a counter, we can use the [`get`]($counter.get)
36/// function. This function returns an [array]: Counters can have multiple
37/// levels (in the case of headings for sections, subsections, and so on), and
38/// each item in the array corresponds to one level.
39///
40/// ```example
41/// #set heading(numbering: "1.")
42///
43/// = Introduction
44/// Raw value of heading counter is
45/// #context counter(heading).get()
46/// ```
47///
48/// # Displaying a counter { #displaying }
49/// Often, we want to display the value of a counter in a more human-readable
50/// way. To do that, we can call the [`display`]($counter.display) function on
51/// the counter. This function retrieves the current counter value and formats
52/// it either with a provided or with an automatically inferred [numbering].
53///
54/// ```example
55/// #set heading(numbering: "1.")
56///
57/// = Introduction
58/// Some text here.
59///
60/// = Background
61/// The current value is: #context {
62///   counter(heading).display()
63/// }
64///
65/// Or in roman numerals: #context {
66///   counter(heading).display("I")
67/// }
68/// ```
69///
70/// # Modifying a counter { #modifying }
71/// To modify a counter, you can use the `step` and `update` methods:
72///
73/// - The `step` method increases the value of the counter by one. Because
74///   counters can have multiple levels , it optionally takes a `level`
75///   argument. If given, the counter steps at the given depth.
76///
77/// - The `update` method allows you to arbitrarily modify the counter. In its
78///   basic form, you give it an integer (or an array for multiple levels). For
79///   more flexibility, you can instead also give it a function that receives
80///   the current value and returns a new value.
81///
82/// The heading counter is stepped before the heading is displayed, so
83/// `Analysis` gets the number seven even though the counter is at six after the
84/// second update.
85///
86/// ```example
87/// #set heading(numbering: "1.")
88///
89/// = Introduction
90/// #counter(heading).step()
91///
92/// = Background
93/// #counter(heading).update(3)
94/// #counter(heading).update(n => n * 2)
95///
96/// = Analysis
97/// Let's skip 7.1.
98/// #counter(heading).step(level: 2)
99///
100/// == Analysis
101/// Still at #context {
102///   counter(heading).display()
103/// }
104/// ```
105///
106/// # Page counter
107/// The page counter is special. It is automatically stepped at each pagebreak.
108/// But like other counters, you can also step it manually. For example, you
109/// could have Roman page numbers for your preface, then switch to Arabic page
110/// numbers for your main content and reset the page counter to one.
111///
112/// ```example
113/// >>> #set page(
114/// >>>   height: 100pt,
115/// >>>   margin: (bottom: 24pt, rest: 16pt),
116/// >>> )
117/// #set page(numbering: "(i)")
118///
119/// = Preface
120/// The preface is numbered with
121/// roman numerals.
122///
123/// #set page(numbering: "1 / 1")
124/// #counter(page).update(1)
125///
126/// = Main text
127/// Here, the counter is reset to one.
128/// We also display both the current
129/// page and total number of pages in
130/// Arabic numbers.
131/// ```
132///
133/// # Custom counters
134/// To define your own counter, call the `counter` function with a string as a
135/// key. This key identifies the counter globally.
136///
137/// ```example
138/// #let mine = counter("mycounter")
139/// #context mine.display() \
140/// #mine.step()
141/// #context mine.display() \
142/// #mine.update(c => c * 3)
143/// #context mine.display()
144/// ```
145///
146/// # How to step
147/// When you define and use a custom counter, in general, you should first step
148/// the counter and then display it. This way, the stepping behaviour of a
149/// counter can depend on the element it is stepped for. If you were writing a
150/// counter for, let's say, theorems, your theorem's definition would thus first
151/// include the counter step and only then display the counter and the theorem's
152/// contents.
153///
154/// ```example
155/// #let c = counter("theorem")
156/// #let theorem(it) = block[
157///   #c.step()
158///   *Theorem #context c.display():*
159///   #it
160/// ]
161///
162/// #theorem[$1 = 1$]
163/// #theorem[$2 < 3$]
164/// ```
165///
166/// The rationale behind this is best explained on the example of the heading
167/// counter: An update to the heading counter depends on the heading's level. By
168/// stepping directly before the heading, we can correctly step from `1` to
169/// `1.1` when encountering a level 2 heading. If we were to step after the
170/// heading, we wouldn't know what to step to.
171///
172/// Because counters should always be stepped before the elements they count,
173/// they always start at zero. This way, they are at one for the first display
174/// (which happens after the first step).
175///
176/// # Time travel
177/// Counters can travel through time! You can find out the final value of the
178/// counter before it is reached and even determine what the value was at any
179/// particular location in the document.
180///
181/// ```example
182/// #let mine = counter("mycounter")
183///
184/// = Values
185/// #context [
186///   Value here: #mine.get() \
187///   At intro: #mine.at(<intro>) \
188///   Final value: #mine.final()
189/// ]
190///
191/// #mine.update(n => n + 3)
192///
193/// = Introduction <intro>
194/// #lorem(10)
195///
196/// #mine.step()
197/// #mine.step()
198/// ```
199///
200/// # Other kinds of state { #other-state }
201/// The `counter` type is closely related to [state] type. Read its
202/// documentation for more details on state management in Typst and why it
203/// doesn't just use normal variables for counters.
204#[ty(scope)]
205#[derive(Debug, Clone, PartialEq, Hash)]
206pub struct Counter(CounterKey);
207
208impl Counter {
209    /// Create a new counter identified by a key.
210    pub fn new(key: CounterKey) -> Counter {
211        Self(key)
212    }
213
214    /// The counter for the given element.
215    pub fn of(func: Element) -> Self {
216        Self::new(CounterKey::Selector(Selector::Elem(func, None)))
217    }
218
219    /// Gets the current and final value of the state combined in one state.
220    pub fn both(
221        &self,
222        engine: &mut Engine,
223        location: Location,
224    ) -> SourceResult<CounterState> {
225        let sequence = self.sequence(engine)?;
226        let offset = engine.introspector.query_count_before(&self.selector(), location);
227        let (mut at_state, at_page) = sequence[offset].clone();
228        let (mut final_state, final_page) = sequence.last().unwrap().clone();
229        if self.is_page() {
230            let at_delta =
231                engine.introspector.page(location).get().saturating_sub(at_page.get());
232            at_state.step(NonZeroUsize::ONE, at_delta);
233            let final_delta =
234                engine.introspector.pages().get().saturating_sub(final_page.get());
235            final_state.step(NonZeroUsize::ONE, final_delta);
236        }
237        Ok(CounterState(smallvec![at_state.first(), final_state.first()]))
238    }
239
240    /// Gets the value of the counter at the given location. Always returns an
241    /// array of integers, even if the counter has just one number.
242    pub fn at_loc(
243        &self,
244        engine: &mut Engine,
245        location: Location,
246    ) -> SourceResult<CounterState> {
247        let sequence = self.sequence(engine)?;
248        let offset = engine.introspector.query_count_before(&self.selector(), location);
249        let (mut state, page) = sequence[offset].clone();
250        if self.is_page() {
251            let delta =
252                engine.introspector.page(location).get().saturating_sub(page.get());
253            state.step(NonZeroUsize::ONE, delta);
254        }
255        Ok(state)
256    }
257
258    /// Displays the value of the counter at the given location.
259    pub fn display_at_loc(
260        &self,
261        engine: &mut Engine,
262        loc: Location,
263        styles: StyleChain,
264        numbering: &Numbering,
265    ) -> SourceResult<Content> {
266        let context = Context::new(Some(loc), Some(styles));
267        Ok(self
268            .at_loc(engine, loc)?
269            .display(engine, context.track(), numbering)?
270            .display())
271    }
272
273    /// Produce the whole sequence of counter states.
274    ///
275    /// This has to happen just once for all counters, cutting down the number
276    /// of counter updates from quadratic to linear.
277    fn sequence(
278        &self,
279        engine: &mut Engine,
280    ) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> {
281        self.sequence_impl(
282            engine.routines,
283            engine.world,
284            engine.introspector,
285            engine.traced,
286            TrackedMut::reborrow_mut(&mut engine.sink),
287            engine.route.track(),
288        )
289    }
290
291    /// Memoized implementation of `sequence`.
292    #[comemo::memoize]
293    fn sequence_impl(
294        &self,
295        routines: &Routines,
296        world: Tracked<dyn World + '_>,
297        introspector: Tracked<Introspector>,
298        traced: Tracked<Traced>,
299        sink: TrackedMut<Sink>,
300        route: Tracked<Route>,
301    ) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> {
302        let mut engine = Engine {
303            routines,
304            world,
305            introspector,
306            traced,
307            sink,
308            route: Route::extend(route).unnested(),
309        };
310
311        let mut state = CounterState::init(matches!(self.0, CounterKey::Page));
312        let mut page = NonZeroUsize::ONE;
313        let mut stops = eco_vec![(state.clone(), page)];
314
315        for elem in introspector.query(&self.selector()) {
316            if self.is_page() {
317                let prev = page;
318                page = introspector.page(elem.location().unwrap());
319
320                let delta = page.get() - prev.get();
321                if delta > 0 {
322                    state.step(NonZeroUsize::ONE, delta);
323                }
324            }
325
326            if let Some(update) = match elem.with::<dyn Count>() {
327                Some(countable) => countable.update(),
328                None => Some(CounterUpdate::Step(NonZeroUsize::ONE)),
329            } {
330                state.update(&mut engine, update)?;
331            }
332
333            stops.push((state.clone(), page));
334        }
335
336        Ok(stops)
337    }
338
339    /// The selector relevant for this counter's updates.
340    fn selector(&self) -> Selector {
341        let mut selector = select_where!(CounterUpdateElem, Key => self.0.clone());
342
343        if let CounterKey::Selector(key) = &self.0 {
344            selector = Selector::Or(eco_vec![selector, key.clone()]);
345        }
346
347        selector
348    }
349
350    /// Whether this is the page counter.
351    fn is_page(&self) -> bool {
352        self.0 == CounterKey::Page
353    }
354
355    /// Shared implementation of displaying between `counter.display` and
356    /// `CounterDisplayElem`.
357    fn display_impl(
358        &self,
359        engine: &mut Engine,
360        location: Location,
361        numbering: Smart<Numbering>,
362        both: bool,
363        styles: Option<StyleChain>,
364    ) -> SourceResult<Value> {
365        let numbering = numbering
366            .custom()
367            .or_else(|| {
368                let styles = styles?;
369                match self.0 {
370                    CounterKey::Page => PageElem::numbering_in(styles).clone(),
371                    CounterKey::Selector(Selector::Elem(func, _)) => {
372                        if func == HeadingElem::elem() {
373                            HeadingElem::numbering_in(styles).clone()
374                        } else if func == FigureElem::elem() {
375                            FigureElem::numbering_in(styles).clone()
376                        } else if func == EquationElem::elem() {
377                            EquationElem::numbering_in(styles).clone()
378                        } else if func == FootnoteElem::elem() {
379                            Some(FootnoteElem::numbering_in(styles).clone())
380                        } else {
381                            None
382                        }
383                    }
384                    _ => None,
385                }
386            })
387            .unwrap_or_else(|| NumberingPattern::from_str("1.1").unwrap().into());
388
389        let state = if both {
390            self.both(engine, location)?
391        } else {
392            self.at_loc(engine, location)?
393        };
394
395        let context = Context::new(Some(location), styles);
396        state.display(engine, context.track(), &numbering)
397    }
398
399    /// Selects all state updates.
400    pub fn select_any() -> Selector {
401        CounterUpdateElem::elem().select()
402    }
403}
404
405#[scope]
406impl Counter {
407    /// Create a new counter identified by a key.
408    #[func(constructor)]
409    pub fn construct(
410        /// The key that identifies this counter.
411        ///
412        /// - If it is a string, creates a custom counter that is only affected
413        ///   by manual updates,
414        /// - If it is the [`page`] function, counts through pages,
415        /// - If it is a [selector], counts through elements that matches with the
416        ///   selector. For example,
417        ///   - provide an element function: counts elements of that type,
418        ///   - provide a [`{<label>}`]($label): counts elements with that label.
419        key: CounterKey,
420    ) -> Counter {
421        Self::new(key)
422    }
423
424    /// Retrieves the value of the counter at the current location. Always
425    /// returns an array of integers, even if the counter has just one number.
426    ///
427    /// This is equivalent to `{counter.at(here())}`.
428    #[func(contextual)]
429    pub fn get(
430        &self,
431        engine: &mut Engine,
432        context: Tracked<Context>,
433        span: Span,
434    ) -> SourceResult<CounterState> {
435        let loc = context.location().at(span)?;
436        self.at_loc(engine, loc)
437    }
438
439    /// Displays the current value of the counter with a numbering and returns
440    /// the formatted output.
441    #[func(contextual)]
442    pub fn display(
443        self,
444        engine: &mut Engine,
445        context: Tracked<Context>,
446        span: Span,
447        /// A [numbering pattern or a function]($numbering), which specifies how
448        /// to display the counter. If given a function, that function receives
449        /// each number of the counter as a separate argument. If the amount of
450        /// numbers varies, e.g. for the heading argument, you can use an
451        /// [argument sink]($arguments).
452        ///
453        /// If this is omitted or set to `{auto}`, displays the counter with the
454        /// numbering style for the counted element or with the pattern
455        /// `{"1.1"}` if no such style exists.
456        #[default]
457        numbering: Smart<Numbering>,
458        /// If enabled, displays the current and final top-level count together.
459        /// Both can be styled through a single numbering pattern. This is used
460        /// by the page numbering property to display the current and total
461        /// number of pages when a pattern like `{"1 / 1"}` is given.
462        #[named]
463        #[default(false)]
464        both: bool,
465    ) -> SourceResult<Value> {
466        let loc = context.location().at(span)?;
467        self.display_impl(engine, loc, numbering, both, context.styles().ok())
468    }
469
470    /// Retrieves the value of the counter at the given location. Always returns
471    /// an array of integers, even if the counter has just one number.
472    ///
473    /// The `selector` must match exactly one element in the document. The most
474    /// useful kinds of selectors for this are [labels]($label) and
475    /// [locations]($location).
476    #[func(contextual)]
477    pub fn at(
478        &self,
479        engine: &mut Engine,
480        context: Tracked<Context>,
481        span: Span,
482        /// The place at which the counter's value should be retrieved.
483        selector: LocatableSelector,
484    ) -> SourceResult<CounterState> {
485        let loc = selector.resolve_unique(engine.introspector, context).at(span)?;
486        self.at_loc(engine, loc)
487    }
488
489    /// Retrieves the value of the counter at the end of the document. Always
490    /// returns an array of integers, even if the counter has just one number.
491    #[func(contextual)]
492    pub fn final_(
493        &self,
494        engine: &mut Engine,
495        context: Tracked<Context>,
496        span: Span,
497    ) -> SourceResult<CounterState> {
498        context.introspect().at(span)?;
499        let sequence = self.sequence(engine)?;
500        let (mut state, page) = sequence.last().unwrap().clone();
501        if self.is_page() {
502            let delta = engine.introspector.pages().get().saturating_sub(page.get());
503            state.step(NonZeroUsize::ONE, delta);
504        }
505        Ok(state)
506    }
507
508    /// Increases the value of the counter by one.
509    ///
510    /// The update will be in effect at the position where the returned content
511    /// is inserted into the document. If you don't put the output into the
512    /// document, nothing happens! This would be the case, for example, if you
513    /// write `{let _ = counter(page).step()}`. Counter updates are always
514    /// applied in layout order and in that case, Typst wouldn't know when to
515    /// step the counter.
516    #[func]
517    pub fn step(
518        self,
519        span: Span,
520        /// The depth at which to step the counter. Defaults to `{1}`.
521        #[named]
522        #[default(NonZeroUsize::ONE)]
523        level: NonZeroUsize,
524    ) -> Content {
525        self.update(span, CounterUpdate::Step(level))
526    }
527
528    /// Updates the value of the counter.
529    ///
530    /// Just like with `step`, the update only occurs if you put the resulting
531    /// content into the document.
532    #[func]
533    pub fn update(
534        self,
535        span: Span,
536        /// If given an integer or array of integers, sets the counter to that
537        /// value. If given a function, that function receives the previous
538        /// counter value (with each number as a separate argument) and has to
539        /// return the new value (integer or array).
540        update: CounterUpdate,
541    ) -> Content {
542        CounterUpdateElem::new(self.0, update).pack().spanned(span)
543    }
544}
545
546impl Repr for Counter {
547    fn repr(&self) -> EcoString {
548        eco_format!("counter({})", self.0.repr())
549    }
550}
551
552/// Identifies a counter.
553#[derive(Debug, Clone, PartialEq, Hash)]
554pub enum CounterKey {
555    /// The page counter.
556    Page,
557    /// Counts elements matching the given selectors. Only works for
558    /// [locatable]($location/#locatable)
559    /// elements or labels.
560    Selector(Selector),
561    /// Counts through manual counters with the same key.
562    Str(Str),
563}
564
565cast! {
566    CounterKey,
567    self => match self {
568        Self::Page => PageElem::elem().into_value(),
569        Self::Selector(v) => v.into_value(),
570        Self::Str(v) => v.into_value(),
571    },
572    v: Str => Self::Str(v),
573    v: Label => Self::Selector(Selector::Label(v)),
574    v: Element => {
575        if v == PageElem::elem() {
576            Self::Page
577        } else {
578            Self::Selector(LocatableSelector::from_value(v.into_value())?.0)
579        }
580    },
581    v: LocatableSelector => Self::Selector(v.0),
582}
583
584impl Repr for CounterKey {
585    fn repr(&self) -> EcoString {
586        match self {
587            Self::Page => "page".into(),
588            Self::Selector(selector) => selector.repr(),
589            Self::Str(str) => str.repr(),
590        }
591    }
592}
593
594/// An update to perform on a counter.
595#[derive(Debug, Clone, PartialEq, Hash)]
596pub enum CounterUpdate {
597    /// Set the counter to the specified state.
598    Set(CounterState),
599    /// Increase the number for the given level by one.
600    Step(NonZeroUsize),
601    /// Apply the given function to the counter's state.
602    Func(Func),
603}
604
605cast! {
606    CounterUpdate,
607    v: CounterState => Self::Set(v),
608    v: Func => Self::Func(v),
609}
610
611/// Elements that have special counting behaviour.
612pub trait Count {
613    /// Get the counter update for this element.
614    fn update(&self) -> Option<CounterUpdate>;
615}
616
617/// Counts through elements with different levels.
618#[derive(Debug, Clone, PartialEq, Hash)]
619pub struct CounterState(pub SmallVec<[usize; 3]>);
620
621impl CounterState {
622    /// Get the initial counter state for the key.
623    pub fn init(page: bool) -> Self {
624        // Special case, because pages always start at one.
625        Self(smallvec![usize::from(page)])
626    }
627
628    /// Advance the counter and return the numbers for the given heading.
629    pub fn update(
630        &mut self,
631        engine: &mut Engine,
632        update: CounterUpdate,
633    ) -> SourceResult<()> {
634        match update {
635            CounterUpdate::Set(state) => *self = state,
636            CounterUpdate::Step(level) => self.step(level, 1),
637            CounterUpdate::Func(func) => {
638                *self = func
639                    .call(engine, Context::none().track(), self.0.iter().copied())?
640                    .cast()
641                    .at(func.span())?
642            }
643        }
644        Ok(())
645    }
646
647    /// Advance the number of the given level by the specified amount.
648    pub fn step(&mut self, level: NonZeroUsize, by: usize) {
649        let level = level.get();
650
651        while self.0.len() < level {
652            self.0.push(0);
653        }
654
655        self.0[level - 1] = self.0[level - 1].saturating_add(by);
656        self.0.truncate(level);
657    }
658
659    /// Get the first number of the state.
660    pub fn first(&self) -> usize {
661        self.0.first().copied().unwrap_or(1)
662    }
663
664    /// Display the counter state with a numbering.
665    pub fn display(
666        &self,
667        engine: &mut Engine,
668        context: Tracked<Context>,
669        numbering: &Numbering,
670    ) -> SourceResult<Value> {
671        numbering.apply(engine, context, &self.0)
672    }
673}
674
675cast! {
676    CounterState,
677    self => Value::Array(self.0.into_iter().map(IntoValue::into_value).collect()),
678    num: usize => Self(smallvec![num]),
679    array: Array => Self(array
680        .into_iter()
681        .map(Value::cast)
682        .collect::<HintedStrResult<_>>()?),
683}
684
685/// Executes an update of a counter.
686#[elem(Construct, Locatable, Show, Count)]
687struct CounterUpdateElem {
688    /// The key that identifies the counter.
689    #[required]
690    key: CounterKey,
691
692    /// The update to perform on the counter.
693    #[required]
694    #[internal]
695    update: CounterUpdate,
696}
697
698impl Construct for CounterUpdateElem {
699    fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
700        bail!(args.span, "cannot be constructed manually");
701    }
702}
703
704impl Show for Packed<CounterUpdateElem> {
705    fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
706        Ok(Content::empty())
707    }
708}
709
710impl Count for Packed<CounterUpdateElem> {
711    fn update(&self) -> Option<CounterUpdate> {
712        Some(self.update.clone())
713    }
714}
715
716/// Executes a display of a counter.
717#[elem(Construct, Locatable, Show)]
718pub struct CounterDisplayElem {
719    /// The counter.
720    #[required]
721    #[internal]
722    counter: Counter,
723
724    /// The numbering to display the counter with.
725    #[required]
726    #[internal]
727    numbering: Smart<Numbering>,
728
729    /// Whether to display both the current and final value.
730    #[required]
731    #[internal]
732    both: bool,
733}
734
735impl Construct for CounterDisplayElem {
736    fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
737        bail!(args.span, "cannot be constructed manually");
738    }
739}
740
741impl Show for Packed<CounterDisplayElem> {
742    fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
743        Ok(self
744            .counter
745            .display_impl(
746                engine,
747                self.location().unwrap(),
748                self.numbering.clone(),
749                self.both,
750                Some(styles),
751            )?
752            .display())
753    }
754}
755
756/// An specialized handler of the page counter that tracks both the physical
757/// and the logical page counter.
758#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
759pub struct ManualPageCounter {
760    physical: NonZeroUsize,
761    logical: usize,
762}
763
764impl ManualPageCounter {
765    /// Create a new fast page counter, starting at 1.
766    pub fn new() -> Self {
767        Self { physical: NonZeroUsize::ONE, logical: 1 }
768    }
769
770    /// Get the current physical page counter state.
771    pub fn physical(&self) -> NonZeroUsize {
772        self.physical
773    }
774
775    /// Get the current logical page counter state.
776    pub fn logical(&self) -> usize {
777        self.logical
778    }
779
780    /// Advance past a page.
781    pub fn visit(&mut self, engine: &mut Engine, page: &Frame) -> SourceResult<()> {
782        for (_, item) in page.items() {
783            match item {
784                FrameItem::Group(group) => self.visit(engine, &group.frame)?,
785                FrameItem::Tag(Tag::Start(elem)) => {
786                    let Some(elem) = elem.to_packed::<CounterUpdateElem>() else {
787                        continue;
788                    };
789                    if elem.key == CounterKey::Page {
790                        let mut state = CounterState(smallvec![self.logical]);
791                        state.update(engine, elem.update.clone())?;
792                        self.logical = state.first();
793                    }
794                }
795                _ => {}
796            }
797        }
798
799        Ok(())
800    }
801
802    /// Step past a page _boundary._
803    pub fn step(&mut self) {
804        self.physical = self.physical.saturating_add(1);
805        self.logical += 1;
806    }
807}
808
809impl Default for ManualPageCounter {
810    fn default() -> Self {
811        Self::new()
812    }
813}