The anchor primitive
anchor(kind, payload)- marks the current location with a namespaced, queryable piece of data.
kindnamespaces the anchor so two unrelated pieces of code picking the sameidscheme never collide — prefix it with something specific to what you’re building ("glossary-term","figure-list-entry").payloadis whatever you need back later — entirely opaque tocontextureitself. Deliberately renders nothing on its own beyond the metadata: emitting it and deciding how (or whether) to render content around it are two different jobs, left to the caller, exactly asterm()(the quickstart) does both explicitly. anchors(kind)- every anchor of that kind, in document order, from anywhere in the bundle — including a document other than the one this is called from, which is the entire point. Must be called from within a
context.
Re-emitting stored content: strip-labels
A term’s body can be arbitrary content, including a labelled figure — and re-emitting that figure verbatim into the glossary would otherwise plant a second copy of its label, which Typst rejects outright as a duplicate. strip-labels(node) reconstructs node with every label removed, and — specifically for a figure, a labelled block equation, or a heading — pins the real, already-resolved number of the true original onto the copy’s own numbering, read directly via a fresh query() of that label. The copy shows the same number as the original; only the original stays a real, referenceable target.
#import "../../lib.typ": *
// Same idea as bundle-glossary-basics.typ, but one term's body is itself
// a labelled figure --- re-emitting it verbatim in the glossary would
// otherwise plant a second copy of <tab-doses>, which Typst rejects as a
// duplicate label. strip-labels() drops the label from the copy while
// pinning the real, original "Figure 1" number onto it, read directly
// off the true instance via a query --- so both copies show the same
// number, and only one of them is a real, referenceable target.
#let term(id, body) = {
anchor("demo-term", (id: id, body: body))
body
}
#let render-glossary() = context {
for h in anchors("demo-term") [
*#h.value.id* (p. #h.location().page()):
#strip-labels(h.value.body)
#v(0.5em)
]
}
#let glossary = satellite("glossary", render: () => render-glossary())
#show: bundle.with(
template: body => {
set page(width: 16.6cm, height: auto, margin: 12pt)
set text(size: 10.5pt)
body
},
documents: (glossary,),
)
= Methods
#term("doses")[
#figure(
table(columns: 2, [*Arm*], [*Dose*], [A], [10mg], [B], [Placebo]),
caption: [Study drug doses.],
) <tab-doses>
]
As shown in @tab-doses, two arms were compared.
Both the manuscript and the glossary show “Table 1” — the genuine, resolved number, not a guess:
manuscript.pdf | glossary.pdf |
Structural utilities
collect-metadata(body, tag) and collect-labels(body) are the structural building blocks strip-labels and anchor/anchors themselves are built from — a plain walk of an in-memory content tree, no context or layout involved, exposed directly for code that needs to inspect content it’s holding but hasn’t (or may never) placed into any document this compile. is-blank(body) answers “does this contain any real text at all” (handy for flagging an accidentally empty call to something like term()); is-textual(body) answers “is this safe to wrap in literal quotation marks”, i.e. free of any figure, table, or block equation that a stray quote mark would otherwise float above.
| ← Quickstart | List of figures → |