Cross-references

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:

  1. Pass 1 — every cross-reference renders as ?. While the engine paginates, anchor positions, captured texts and counter snapshots are collected.
  2. Aux file — the collected {id → page, text, counters} map is written to <output>-aux.json alongside the per-document _aux table.
  3. 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 ::before becomes its first inline content, ::after its last). On <li> the ::before content feeds the list marker instead, and table-structural elements (table, tr, tbody, …) do not render generated content — cells (td, th) and captions do.