Cross-references
htmlbag resolves CSS Generated Content for Paged Media (GCPM) cross-reference functions. They let generated content pull values from a different element — typically the page number or the text of a referenced anchor — so you can build tables of contents, “see page N” references, and figure / chapter pointers without manual page tracking.
Functions
| Function | Returns |
|---|---|
target-counter(url, counter) |
The named counter’s value at the referenced anchor. page resolves to the page the anchor sits on; any other counter resolves to its innermost value at the anchor’s position (snapshotted during layout). |
target-counters(url, counter, sep) |
The full nesting chain of the named counter at the anchor, joined with sep — the target-side analogue of counters() for hierarchical numbering like 2.1.1. |
target-text(url) |
The textual content of the referenced anchor element (captured up to 200 characters). |
url is typically attr(href) so the function pulls the target from
the element’s own href attribute, but a literal url(#id) works too.
target-counter() and target-counters() take a counter style such
as upper-roman as their last argument, as counter() does.
Anchors
Any element with an id attribute is registered as an anchor and is a
link target: <a href="#id"> jumps to it, whether it is a heading, a
paragraph, a container such as a div, or an inline element.
- Block-level anchors (headings, divs, list items, table rows, …) are picked up on the page they finally land on, after the line breaker finishes paginating.
- Inline anchors (
<span id>,<a id>,<em id>,<strong id>, …) are tracked alongside the enclosing paragraph and resolve to the page where their line of text settles.
Two-pass resolution
Resolution needs two render passes because page numbers depend on the final layout:
- Pass 1 — every cross-reference renders as
?. While the engine paginates, anchor positions, captured texts and counter snapshots are collected. - Aux file — the collected
{id → page, text, counters}map is written to<output>-aux.jsonalongside the per-document_auxtable. - Pass 2 — the aux file is read back; cross-references now resolve to real page numbers, texts and counter values.
The Markdown frontend handles passes automatically: it re-runs the
pipeline (up to --max-passes, default 3) as long as something the
document read from the previous pass changed: the page count if
counter(pages) was used, and the anchors that the target functions
looked up. A pass that runs Lua code, which can read any of the aux
data, compares all of it. Library callers see this through the regular
htmlbag CSSBuilder API and feed the aux content back via
SetAnchorPages / SetAnchorTexts / SetAnchorCounters;
PreviousPassReads tells them what a pass read of that data, and
another pass is needed only when one of those values changed.
Tables of contents
A common shape is “a list of links to headings, each with a dotted leader filling the gap to a right-aligned page number”:
<ol class="toc">
<li><a href="#intro">Introduction</a></li>
<li><a href="#methods">Methods</a></li>
<li><a href="#results">Results</a></li>
</ol>.toc li {
width: 9cm;
}
.toc a::after {
content: leader(".") target-counter(attr(href), page);
}leader(".") becomes a stretchy glue that expands to fill the
available width, repeating the . pattern as needed. Stretch only
takes effect inside a container with a definite width — set width
(or rely on a parent’s fixed width) on the <li>. The example folder
toc-target-counter
shows the full setup.
Inline cross-references with captured text
target-text captures the rendered text of the referenced anchor and
inlines it. Combined with target-counter, this gives “see Chapter 3
on page 17” style references:
<p>See <a href="#methods"></a>.</p>
<h2 id="methods">Methodology</h2>a[href^="#"]::after {
content: " ‘" target-text(attr(href)) "’ on page "
target-counter(attr(href), page);
}The generated content of a link is part of the link: the ::after
above is clickable along with the link’s own text, and so is a
reference whose text comes from target-counter() alone. A leader()
is left out of the link, as its pattern repeats.
The cross-reference-inline
example
demonstrates inline anchors (<span id>) plus target-text.
Section numbers
Named counters resolve against a snapshot taken at the anchor’s
position, so “see section 2.2 on page 7” references work by combining
target-counter calls (or target-counters for counters that nest):
body { counter-reset: h1cnt h2cnt; }
h1 { counter-increment: h1cnt; counter-reset: h2cnt; }
h2 { counter-increment: h2cnt; }
a.secref::before {
content: "section " target-counter(attr(href), h1cnt)
"." target-counter(attr(href), h2cnt);
}
a.secref::after {
content: " on page " target-counter(attr(href), page);
}<p>See <a class="secref" href="#deep"></a>.</p>
...
<h2 id="deep">Details</h2>A counter-increment on the anchor element itself is included in the
snapshot: the h2 above carries counter-increment: h2cnt, and the
reference shows the incremented value, as CSS GCPM requires.
Current limitations
target-text(..., before|after|first-letter)(the pseudo-element selector form) returns?— only the element’s textual content is captured.- Generated content (
::before/::after) renders on inline and block-level elements (a block’s::beforebecomes its first inline content,::afterits last). On<li>the::beforecontent feeds the list marker instead, and table-structural elements (table,tr,tbody, …) do not render generated content — cells (td,th) and captions do.