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. The only counter currently supported is page (the page on which the anchor sits).
target-counters(url, counter, sep) Joined counter stack (for hierarchical numbering). Reserved; resolves to ? until a counter snapshot is available per anchor.
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.

Anchors

Any element with an id attribute is registered as an anchor:

  • 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 and captured texts are collected.
  2. Aux file — the collected {id → page, text} 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 and texts.

The Markdown frontend handles passes automatically: it re-runs the pipeline (up to --max-passes, default 3) until the aux file stops changing. Library callers see this through the regular htmlbag CSSBuilder API and feed the aux content back via SetAnchorPages / SetAnchorTexts.

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 cross-reference-inline example demonstrates inline anchors (<span id>) plus target-text.

Current limitations

  • Only the page counter is resolvable. target-counter(..., section) or any other named counter returns ?.
  • target-counters(...) (the plural-stack form) returns ? — there is no per-anchor counter snapshot yet.
  • target-text(..., before|after|first-letter) (the pseudo-element selector form) returns ? — only the element’s textual content is captured.
  • Block-level ::after is not implemented; the renderer evaluates the inline branch only. ::before works on both inline and block.