I’ve slightly adapted this snippet that replicates endnotes in Typst but I would like to tweak it further so that the endnotes are grouped by heading, and ideally restarted numbering after each new heading.
It’s a bit beyond my skill level with Typst, but I guess the approach would be to capture the heading at the same time the footnote is captured by the footnote(cnt) function and then use this later via showfootnote().
Any advice on how to approach this would be very much appreciated. Thanks!
When I think of coordinating order of elements in the document, then I think of query. If you query for headings and endnotes together, then you get an array of them correctly interspersed in document order.
I did this, but still ended up relying on heading numbering to handle naming labels for cross linking, just like your current solution, so I think that’s the trickiest part of it. (Fixed in later version.)
The following supports resetting the counter on chapters, or not, as desired.
// endnote counter
#let encnt = counter("endnote")
#let encnt-monotonic = counter("endnote-unique")
/// Create an endnote
#let endnote(note, numbering: "1") = {
encnt.step()
encnt-monotonic.step()
context {
let number-monotonic = str(encnt-monotonic.get().first())
let number = std.numbering(numbering, ..encnt.get())
let labelname = "_endnote:monotonic:" + number-monotonic
let link = x => x
if query(label(labelname)).len() > 0 {
link = std.link.with(label(labelname))
}
link(super(number))
[#metadata((note: note, number: number, labelname: labelname))<_endnote>]
}
}
#show heading.where(level: 1): it => it + encnt.update(0)
#set heading(numbering: "1.1")
#let split-array-by(arr, func) = {
let chunks = ()
let chunk = ()
for elt in arr {
if func(elt) {
if chunk != () { chunks.push(chunk) }
chunk = ()
}
chunk.push(elt)
}
if chunk != () { chunks.push(chunk) }
chunks
}
#let show-endnotes(title: [Endnotes], level: 1) = {
if title != none {
heading(level: level, title)
}
context {
// Find headings and endnotes in a sequence
let headings-and-endnotes = query(selector.or(heading.where(level: 1), <_endnote>))
// split the result by heading groups
let chunks = split-array-by(headings-and-endnotes, elt => elt.func() == heading)
for chunk in chunks {
if chunk.len() <= 1 { continue }
// heading first and notes later
let head = chunk.at(0)
let notes = chunk.slice(1)
// format the list of endnotes
set list(marker: none, body-indent: 0pt)
list.item({
let num = none
if head.numbering != none {
num = std.numbering(head.numbering, ..counter(heading).at(head.location()))
}
strong([#num #head.body])
linebreak()
notes.map(note => {
let link = link.with(note.location())
list.item(
link[#note.value.number;. #note.value.note#label(note.value.labelname)]
)
}).join()
})
}
}
}
#show link: set text(purple) // Just for the example
= A
Yes#endnote[Yep] and unsure#endnote[Not really]
= B
No#endnote[Nope]
#show-endnotes()
I do prefer query over state when it’s possible, but state could have the benefit of being easy to scope to parts of the document? Let’s imagine you want to show endnotes per part and reset all states at the start of each part; the query approach would have to be updated to be able to support something like that.