typst/library/src/meta/counter.rs
2023-03-21 20:23:53 +01:00

636 lines
20 KiB
Rust

use std::fmt::{self, Debug, Formatter, Write};
use std::str::FromStr;
use ecow::{eco_vec, EcoVec};
use smallvec::{smallvec, SmallVec};
use typst::eval::Tracer;
use super::{FigureElem, HeadingElem, Numbering, NumberingPattern};
use crate::layout::PageElem;
use crate::math::EquationElem;
use crate::prelude::*;
/// Count through pages, elements, and more.
///
/// With the counter function, you can access and modify counters for pages,
/// headings, figures, and more. Moreover, you can define custom counters for
/// other things you want to count.
///
/// ## Displaying a counter
/// To display the current value of the heading counter, you call the `counter`
/// function with the `key` set to `heading` and then call the `display` method
/// on the counter. To see any output, you also have to enable heading
/// [numbering]($func/heading.numbering).
///
/// The display function optionally takes an argument telling it how to
/// format the counter. This can be a
/// [numbering pattern or a function]($func/numbering).
///
/// ```example
/// #set heading(numbering: "1.")
///
/// = Introduction
/// Some text here.
///
/// = Background
/// The current value is:
/// #counter(heading).display()
///
/// Or in roman numerals:
/// #counter(heading).display("I")
/// ```
///
/// ## Modifying a counter
/// To modify a counter, you can use the `step` and `update` methods:
///
/// - The `step` method increases the value of the counter by one. Because
/// counters can have multiple levels (in the case of headings for sections,
/// subsections, and so on), the `step` method optionally takes a `level`
/// argument. If given, the counter steps at the given depth.
///
/// - The `update` method allows you to arbitrarily modify the counter. In its
/// basic form, you give it an integer (or multiple for multiple levels). For
/// more flexibility, you can instead also give it a function that gets the
/// current value and returns a new value.
///
/// The heading counter is stepped before the heading is displayed, so
/// `Analysis` gets the number seven even though the counter is at six after the
/// second update.
///
/// ```example
/// #set heading(numbering: "1.")
///
/// = Introduction
/// #counter(heading).step()
///
/// = Background
/// #counter(heading).update(3)
/// #counter(heading).update(n => n * 2)
///
/// = Analysis
/// Let's skip 7.1.
/// #counter(heading).step(level: 2)
///
/// == Analysis
/// Still at #counter(heading).display().
/// ```
///
/// ## Page counter
/// The page counter is special. It is automatically stepped at each pagebreak.
/// But like other counters, you can also step it manually. For example, you
/// could have Roman page numbers for your preface, then switch to Arabic page
/// numbers for your main content and reset the page counter to one.
///
/// ```example
/// >>> #set page(
/// >>> height: 100pt,
/// >>> margin: (bottom: 24pt, rest: 16pt),
/// >>> )
/// #set page(numbering: "(i)")
///
/// = Preface
/// The preface is numbered with
/// roman numerals.
///
/// #set page(numbering: "1 / 1")
/// #counter(page).update(1)
///
/// = Main text
/// Here, the counter is reset to one.
/// We also display both the current
/// page and total number of pages in
/// Arabic numbers.
/// ```
///
/// ## Custom counters
/// To define your own counter, call the `counter` function with a string as a
/// key. This key identifies the counter globally.
///
/// ```example
/// #let mine = counter("mycounter")
/// #mine.display() \
/// #mine.step()
/// #mine.display() \
/// #mine.update(c => c * 3)
/// #mine.display() \
/// ```
///
/// ## Time travel
/// Counters can travel through time! You can find out the final value of the
/// counter before it is reached and even determine what the value was at any
/// particular location in the document.
///
/// ```example
/// #let mine = counter("mycounter")
///
/// = Values
/// #locate(loc => {
/// let start-val = mine.at(loc)
/// let elements = query(<intro>, loc)
/// let intro-val = mine.at(
/// elements.first().location()
/// )
/// let final-val = mine.final(loc)
/// [Starts as: #start-val \
/// Value at intro is: #intro-val \
/// Final value is: #final-val \ ]
/// })
///
/// #mine.update(n => n + 3)
///
/// = Introduction <intro>
/// #lorem(10)
///
/// #mine.step()
/// #mine.step()
/// ```
///
/// Let's dissect what happens in the example above:
///
/// - We call [`locate`]($func/locate) to get access to the current location in
/// the document. We then pass this location to our counter's `at` method to
/// get its value at the current location. The `at` method always returns an
/// array because counters can have multiple levels. As the counter starts at
/// one, the first value is thus `{(1,)}`.
///
/// - We now [`query`]($func/query) the document for all elements with the
/// `{<intro>}` label. The result is an array from which we extract the first
/// (and only) element's [location]($type/content.location). We then look up
/// the value of the counter at that location. The first update to the counter
/// sets it to `{1 + 3 = 4}`. At the introduction heading, the value is thus
/// `{(4,)}`.
///
/// - Last but not least, we call the `final` method on the counter. It tells us
/// what the counter's value will be at the end of the document. We also need
/// to give it a location to prove that we are inside of a `locate` call, but
/// which one doesn't matter. After the heading follow two calls to `step()`,
/// so the final value is `{(6,)}`.
///
/// ## Other kinds of state
/// The `counter` function is closely related to [state]($func/state) function.
/// Read its documentation for more details on state management in Typst and
/// why it doesn't just use normal variables for counters.
///
/// ## Methods
/// ### display()
/// Display the value of the counter.
///
/// - numbering: string or function (positional)
/// A [numbering pattern or a function]($func/numbering), which specifies how
/// to display the counter. If given a function, that function receives each
/// number of the counter as a separate argument. If the amount of numbers
/// varies, e.g. for the heading argument, you can use an
/// [argument sink]($type/arguments).
///
/// If this is omitted, displays the counter with the numbering style for the
/// counted element or with the pattern `{"1.1"}` if no such style exists.
///
/// - returns: content
///
/// ### step()
/// Increase the value of the counter by one.
///
/// The update will be in effect at the position where the returned content is
/// inserted into the document. If you don't put the output into the document,
/// nothing happens! This would be the case, for example, if you write
/// `{let _ = counter(page).step()}`. Counter updates are always applied in
/// layout order and in that case, Typst wouldn't know when to step the counter.
///
/// - level: integer (named)
/// The depth at which to step the counter. Defaults to `{1}`.
///
/// - returns: content
///
/// ### update()
/// Update the value of the counter.
///
/// Just like with `step`, the update only occurs if you put the resulting
/// content into the document.
///
/// - value: integer or array or function (positional, required)
/// If given an integer or array of integers, sets the counter to that value.
/// If given a function, that function receives the previous counter value
/// (with each number as a separate argument) and has to return the new
/// value (integer or array).
///
/// - returns: content
///
/// ### at()
/// Get the value of the counter at the given location. Always returns an
/// array of integers, even if the counter has just one number.
///
/// - location: location (positional, required)
/// The location at which the counter value should be retrieved. A suitable
/// location can be retrieved from [`locate`]($func/locate) or
/// [`query`]($func/query).
///
/// - returns: array
///
/// ### final()
/// Get the value of the counter at the end of the document. Always returns an
/// array of integers, even if the counter has just one number.
///
/// - location: location (positional, required)
/// Can be any location. Why is it required then? Typst has to evaluate parts
/// of your code multiple times to determine all counter values. By only
/// allowing this method within [`locate`]($func/locate) calls, the amount of
/// code that can depend on the method's result is reduced. If you could call
/// `final` directly at the top level of a module, the evaluation of the whole
/// module and its exports could depend on the counter's value.
///
/// - returns: array
///
/// Display: Counter
/// Category: meta
/// Returns: counter
#[func]
pub fn counter(
/// The key that identifies this counter.
///
/// - If this is the [`page`]($func/page) function, counts through pages.
/// - If this is any other element function, counts through its elements.
/// - If it is a string, creates a custom counter that is only affected by
/// manual updates.
key: CounterKey,
) -> Value {
Value::dynamic(Counter::new(key))
}
/// Counts through pages, elements, and more.
#[derive(Clone, PartialEq, Hash)]
pub struct Counter(CounterKey);
impl Counter {
/// Create a new counter from a key.
pub fn new(key: CounterKey) -> Self {
Self(key)
}
/// The counter for the given element.
pub fn of(func: ElemFunc) -> Self {
Self::new(CounterKey::Selector(Selector::Elem(func, None)))
}
/// Call a method on counter.
pub fn call_method(
self,
vm: &mut Vm,
method: &str,
mut args: Args,
span: Span,
) -> SourceResult<Value> {
let value = match method {
"display" => {
self.display(args.eat()?, args.named("both")?.unwrap_or(false)).into()
}
"step" => self
.update(CounterUpdate::Step(
args.named("level")?.unwrap_or(NonZeroUsize::ONE),
))
.into(),
"update" => self.update(args.expect("value or function")?).into(),
"at" => self.at(&mut vm.vt, args.expect("location")?)?.into(),
"final" => self.final_(&mut vm.vt, args.expect("location")?)?.into(),
_ => bail!(span, "type counter has no method `{}`", method),
};
args.finish()?;
Ok(value)
}
/// Display the current value of the counter.
pub fn display(self, numbering: Option<Numbering>, both: bool) -> Content {
DisplayElem::new(self, numbering, both).pack()
}
/// Get the value of the state at the given location.
pub fn at(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> {
let sequence = self.sequence(vt)?;
let offset = vt.introspector.query_before(self.selector(), location).len();
let (mut state, page) = sequence[offset].clone();
if self.is_page() {
let delta = vt.introspector.page(location).get() - page.get();
state.step(NonZeroUsize::ONE, delta);
}
Ok(state)
}
/// Get the value of the state at the final location.
pub fn final_(&self, vt: &mut Vt, _: Location) -> SourceResult<CounterState> {
let sequence = self.sequence(vt)?;
let (mut state, page) = sequence.last().unwrap().clone();
if self.is_page() {
let delta = vt.introspector.pages().get() - page.get();
state.step(NonZeroUsize::ONE, delta);
}
Ok(state)
}
/// Get the current and final value of the state combined in one state.
pub fn both(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> {
let sequence = self.sequence(vt)?;
let offset = vt.introspector.query_before(self.selector(), location).len();
let (mut at_state, at_page) = sequence[offset].clone();
let (mut final_state, final_page) = sequence.last().unwrap().clone();
if self.is_page() {
let at_delta = vt.introspector.page(location).get() - at_page.get();
at_state.step(NonZeroUsize::ONE, at_delta);
let final_delta = vt.introspector.pages().get() - final_page.get();
final_state.step(NonZeroUsize::ONE, final_delta);
}
Ok(CounterState(smallvec![at_state.first(), final_state.first()]))
}
/// Produce content that performs a state update.
pub fn update(self, update: CounterUpdate) -> Content {
UpdateElem::new(self, update).pack()
}
/// Produce the whole sequence of counter states.
///
/// This has to happen just once for all counters, cutting down the number
/// of counter updates from quadratic to linear.
fn sequence(
&self,
vt: &mut Vt,
) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> {
self.sequence_impl(
vt.world,
TrackedMut::reborrow_mut(&mut vt.tracer),
TrackedMut::reborrow_mut(&mut vt.provider),
vt.introspector,
)
}
/// Memoized implementation of `sequence`.
#[comemo::memoize]
fn sequence_impl(
&self,
world: Tracked<dyn World>,
tracer: TrackedMut<Tracer>,
provider: TrackedMut<StabilityProvider>,
introspector: Tracked<Introspector>,
) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> {
let mut vt = Vt { world, tracer, provider, introspector };
let mut state = CounterState(match &self.0 {
CounterKey::Selector(_) => smallvec![],
_ => smallvec![NonZeroUsize::ONE],
});
let mut page = NonZeroUsize::ONE;
let mut stops = eco_vec![(state.clone(), page)];
for elem in introspector.query(self.selector()) {
if self.is_page() {
let location = elem.location().unwrap();
let prev = page;
page = introspector.page(location);
let delta = page.get() - prev.get();
if delta > 0 {
state.step(NonZeroUsize::ONE, delta);
}
}
if let Some(update) = match elem.to::<UpdateElem>() {
Some(elem) => Some(elem.update()),
None => match elem.with::<dyn Count>() {
Some(countable) => countable.update(),
None => Some(CounterUpdate::Step(NonZeroUsize::ONE)),
},
} {
state.update(&mut vt, update)?;
}
stops.push((state.clone(), page));
}
Ok(stops)
}
/// The selector relevant for this counter's updates.
fn selector(&self) -> Selector {
let mut selector =
Selector::Elem(UpdateElem::func(), Some(dict! { "counter" => self.clone() }));
if let CounterKey::Selector(key) = &self.0 {
selector = Selector::Any(eco_vec![selector, key.clone()]);
}
selector
}
/// Whether this is the page counter.
fn is_page(&self) -> bool {
self.0 == CounterKey::Page
}
}
impl Debug for Counter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("counter(")?;
self.0.fmt(f)?;
f.write_char(')')
}
}
cast_from_value! {
Counter: "counter",
}
/// 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)),
element: ElemFunc => {
if element == PageElem::func() {
return Ok(Self::Page);
}
if !Content::new(element).can::<dyn Locatable>() {
Err(eco_format!("cannot count through {}s", element.name()))?;
}
Self::Selector(Selector::Elem(element, 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),
}
}
}
/// An update to perform on a counter.
#[derive(Clone, PartialEq, Hash)]
pub enum CounterUpdate {
/// Set the counter to the specified state.
Set(CounterState),
/// Increase the number for the given level by one.
Step(NonZeroUsize),
/// Apply the given function to the counter's state.
Func(Func),
}
impl Debug for CounterUpdate {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("..")
}
}
cast_from_value! {
CounterUpdate: "counter update",
v: CounterState => Self::Set(v),
v: Func => Self::Func(v),
}
/// Elements that have special counting behaviour.
pub trait Count {
/// Get the counter update for this element.
fn update(&self) -> Option<CounterUpdate>;
}
/// Counts through elements with different levels.
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct CounterState(pub SmallVec<[NonZeroUsize; 3]>);
impl CounterState {
/// Advance the counter and return the numbers for the given heading.
pub fn update(&mut self, vt: &mut Vt, update: CounterUpdate) -> SourceResult<()> {
match update {
CounterUpdate::Set(state) => *self = state,
CounterUpdate::Step(level) => self.step(level, 1),
CounterUpdate::Func(func) => {
*self = func
.call_vt(vt, self.0.iter().copied().map(Into::into))?
.cast()
.at(func.span())?
}
}
Ok(())
}
/// Advance the number of the given level by the specified amount.
pub fn step(&mut self, level: NonZeroUsize, by: usize) {
let level = level.get();
if self.0.len() >= level {
self.0[level - 1] = self.0[level - 1].saturating_add(by);
self.0.truncate(level);
}
while self.0.len() < level {
self.0.push(NonZeroUsize::ONE);
}
}
/// Get the first number of the state.
pub fn first(&self) -> NonZeroUsize {
self.0.first().copied().unwrap_or(NonZeroUsize::ONE)
}
/// Display the counter state with a numbering.
pub fn display(&self, vt: &mut Vt, numbering: &Numbering) -> SourceResult<Content> {
Ok(numbering.apply_vt(vt, &self.0)?.display())
}
}
cast_from_value! {
CounterState,
num: NonZeroUsize => Self(smallvec![num]),
array: Array => Self(array
.into_iter()
.map(Value::cast)
.collect::<StrResult<_>>()?),
}
cast_to_value! {
v: CounterState => Value::Array(v.0.into_iter().map(Into::into).collect())
}
/// Executes a display of a state.
///
/// Display: State
/// Category: special
#[element(Locatable, Show)]
struct DisplayElem {
/// The counter.
#[required]
counter: Counter,
/// The numbering to display the counter with.
#[required]
numbering: Option<Numbering>,
/// Whether to display both the current and final value.
#[required]
both: bool,
}
impl Show for DisplayElem {
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
let location = self.0.location().unwrap();
let counter = self.counter();
let numbering = self
.numbering()
.or_else(|| {
let CounterKey::Selector(Selector::Elem(func, _)) = counter.0 else {
return None;
};
if func == HeadingElem::func() {
HeadingElem::numbering_in(styles)
} else if func == FigureElem::func() {
FigureElem::numbering_in(styles)
} else if func == EquationElem::func() {
EquationElem::numbering_in(styles)
} else {
None
}
})
.unwrap_or_else(|| NumberingPattern::from_str("1.1").unwrap().into());
let state = if self.both() {
counter.both(vt, location)?
} else {
counter.at(vt, location)?
};
state.display(vt, &numbering)
}
}
/// Executes a display of a state.
///
/// Display: State
/// Category: special
#[element(Locatable, Show)]
struct UpdateElem {
/// The counter.
#[required]
counter: Counter,
/// The update to perform on the counter.
#[required]
update: CounterUpdate,
}
impl Show for UpdateElem {
fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
Ok(Content::empty())
}
}