Is there a mechanism to delay evaluation, and in particular to delay the evaluation of counters?
In examining how Typst might handle endnotes (using code based on #link[Typst] I noticed that counters are incremented at the time in which they are first referenced, and not the order in which they are finally typeset.
I have put together a demonstration using footnotes instead of endnotes. It shows the problem as well.
#import "@preview/marginalia:0.1.4" as marginalia: note
#let config = ()
#marginalia.configure(..config)
#set page(..marginalia.page-setup(..config))
#let qm = marginalia.note.with(numbered: false)
#let qi(label) = counter("qi").step() + qm(context counter("qi").display())
#quote[#qi(<third>)To be is to do#footnote[
#qi(<first>)Do be do be do]]
#quote[#qi(<second>)To be or not to be]
(What I would expect to see is the first two quotations displayed on the page get the first two counter values, and the footnote get the next.)
#import "@preview/marginalia:0.1.4" as marginalia: note
#marginalia.configure()
#set page(..marginalia.page-setup())
#let qm = note.with(numbered: false)
#let qi(label) = counter("qi").step() + qm(context counter("qi").display())
#quote[#qi(<third>)To be is to do#footnote[#qi(<first>)Do be do be do]]
#quote[#qi(<second>)To be or not to be]
I think you don’t need configure if you don’t have anything custom.
What does that mean (to split a footnote that way)?
In fact, it is needed. Without it, the index is placed a line below where it oughtta be.
Here is an updated example incorporating the referenced endnote implementation. It also demonstrates that the problem exists with equation numbering as well, so perhaps it is a little more urgent.
#import "@preview/marginalia:0.1.4" as marginalia: note
#set page("a5", ..marginalia.page-setup(..()))
#set footnote(numbering: "*")
#set math.equation(numbering: "(1)")
#let llap(Text) = {
show: box.with(width: 0pt)
show: align.with(right)
text[#Text]
}
#let allendotes = state("endnotes", ())
#let endnote(cnt) = {
allendotes.update(x => {
x.push(cnt + parbreak())
return x
})
context super[#allendotes.get().len()]
}
#let showendnote() = context {
v(2em)
align(center, heading(level: 1)[Endnotes])
v(2em)
set par(first-line-indent: 0pt)
for (idx, cnt) in allendotes.get().enumerate() {
llap([#(idx+1))~~~]) + cnt
}
}
#let qm = note.with(numbered: false)
#let qi(label) = counter("qi").step() + qm(context counter("qi").display())
#qi(<first>)Expected to be index \#1. #lorem(10)#endnote[#qi(<seventh>)Index \#7 as expected. #lorem(10)]#parbreak()#lorem(20)
#qi(<second>)Expected to be index \#2. #lorem(10)
#qi(<eighth>)Expected to be index \#3. #lorem(10)#endnote[#qi(<eighth>)Index \#8 as expected. #lorem(12)]
#qi(<fourth>)Expected to be index \#4. #lorem(10)#footnote[#qi(<sixth>)Should be index \#6, is index \#5.]
#qi(<fifth>)Expected to be index \#5, is index \#6. #lorem(10)
Here are some silly equations:
$ a - a = 0 $
Also note the sum.#footnote[Some text.$ a + a = 2a $]
$ e^(i pi) = 0 $
#pagebreak()
#showendnote()
The endnote routine stores all endnotes in an array and replays them on command. Thus it should be usable for Chapter, Part, or full endnotes.
Since the process of placing a footnote is already an iterative process based on the location of the footnote in the text and the volume of text on the page, it would seem that the right way would be to save the invocation of the counter, and not the value of the counter or the fully formatted footnote, and deal with additional iterations if setting that changes the size of the footnote.
In some sense you can say you can delay it. It’s clear that things happen in a specific “document order”, so here’s one way to think about how to subvert that.
Save some data when the delayed action is invoked
When it’s safe to do so, realize the saved action
We don’t have many points in typst where we can periodically or in fixed points generate certain content, but the page footer is a good place and it happens on every page. So you can do something like this, and it fits the bill at least in some sense for delayed invocation.
To do the plan here we use
labelled metadata - we can store anything in these
query for this page’s delayed metadata
And then for the really tricky part, in the footer we do this:
Step counter
Drop label <anchor1>
Step counter
Drop label <anchor2>
… and so on
This way our “delayed qi” can just do this: Display the value of the qi counter at its own anchor (which is delayed, it’s in the footer).
#import "@preview/marginalia:0.1.4" as marginalia: note
#let config = (top: 1cm, bottom: 1cm)
#marginalia.configure(..config)
#set page(width: 10cm, height: 5cm, ..marginalia.page-setup(..config))
#let delayid = counter("__delaqi_id")
#let anchor(id) = label("__delayqi_anchor:" + str(id))
// Get qi counter value at anchor `id`
#let qi-at(id) = {
let ids = query(anchor(id))
if ids.len() > 0 {
numbering("1", ..counter("qi").at(ids.first().location()))
}
}
#let qm = marginalia.note.with(numbered: false)
#let qi(label) = counter("qi").step() + qm(context counter("qi").display())
#let delayqi(label) = {
delayid.step()
context [#metadata((label: label, id: delayid.get().first()))<__delayqi>]
qm(context qi-at(delayid.get().first()))
}
#set page(footer: context {
// find all delayed qi on this page
let delayed = query(selector.and(metadata, <__delayqi>)).filter(elt => elt.location().page() == here().page())
// step delayed qi counters and put down anchors for their id
for del in delayed {
counter("qi").step()
[#metadata(none)#anchor(del.value.id)]
}
})
#qi(<first>) This is the first#footnote[This is delayed #delayqi(<later>)]
#footnote[And this too #delayqi(<later>)]
#qi(<second>) This is the second
Excuse the second post, alternative version doing the same thing in a completely different way.
Method: Assume you want counter values in page display order (not “document order”). Then… we could find all “qi”'s on the page and just calculate which one in the page display order we are… (However, we need to think further about what happens if there are duplicate locations.)
#let config = (top: 1cm, bottom: 1cm)
#set page(width: 10cm, height: 5cm, margin: 1cm)
#let qic = counter("qi")
#qic.step()
#set page(header: {
[#metadata(none)<__qi_header_anchor>]
})
#let qi(label) = {
qic.step()
[#metadata(none)<__qi_label>]
context {
let thispos = here().position()
// get counter value for this page
let page-value = qic.at(query(selector(<__qi_header_anchor>).before(here())).last().location()).first()
// find out which one we are on this page
let qipos = (query(<__qi_label>)
.filter(elt => elt.location().page() == here().page())
.map(elt => elt.location().position()))
.sorted(key: elt => (elt.y, elt.x))
let pos = qipos.position(elt => elt == thispos)
let counter-value = page-value + pos
[*(#counter-value)*]
}
}
#qi(<first>) This is the first#footnote[This is delayed #qi(<later>)]
#footnote[And this too #qi(<later>)]
#qi(<second>) This is the second
#pagebreak()
#qi(<more>) This is the next page. #footnote[Footnote though #qi(<footy>)]
#qi(<more2>) It continues..
Nothing is split here. The footnote flag/callout/marker (in this case, an asterisk) is raised inline following the content to which it applies. A corresponding asterisk, also raised, introduces the content of the footnote at the bottom of the page, below a separator line.
The content of the footnote includes as content the value of a counter, and therein lies the problem. My expectation is that this counter will not be updated until the footnote is typeset.
This is equivalent to the issue, also demonstrated, of the equation number for an equation in a footnote. Equation numbers should proceed vertically and sequentially down the page. Typst as it currently exists does not do that.
Two solution outlines have been presented to address the case of an explicit counter in a footnote. Neither appears to be directly appropriate to the equation numbering problem. I would suspect that a good solution for one would also handle the other.