Single source of truth: the instance's notation/notation-map.json (authored,
committed, evolves with the paper). Everything else — wraps in the PreTeXt
source, far marks, the hover registry, order checking — is derived and
regenerated by paperforge build web.
Common fields per entry (or per sense): definition (MathJax-ready HTML; the
document's macros are available), defsite (xml:id used for order checking and
far-distance), href (best link target when it differs from the defsite —
e.g. defsite = the section where the symbol first appears in the flow, href =
the theorem/definition that formally states it).
A bare Y, H, E, t means different things in different places; no regex
can resolve that. The framework separates the mechanism (deterministic) from
the judgment (LLM):
tex2ptx --disambig notation/disambiguation.jsonwraps an ambiguous match only when a block-grain decision exists:{"Y": {"lem-covertransform": "Ytarget", "sec-quotients": "Ygen", "lem-subdirect": "none"}}. Blocks are theorem-like tags, else the enclosing (sub)section tag — the same stable tags as everything else, so decisions survive restructuring.- Unclassified (key, block) pairs are left unwrapped (safe default) and
written with context snippets to
notation/unclassified.json; the build warns with a count. - An LLM pass (Claude, in session or headless) reads the worklist and writes
decisions into
disambiguation.json— a committed, human-reviewable artifact."none"is an explicit decision (generic use, never wrap). - Draft updates create new blocks -> new unclassified entries -> incremental classification. Decisions for unchanged blocks carry over untouched.
Block grain (not per-occurrence) is deliberate: a statement essentially never mixes two senses of the same letter, decisions stay reviewable (~65 for a 70-page paper), and the cache is robust to intra-block edits.
- Accents bind the next token:
\bar \notnfar{x1}{x_1}is broken LaTeX and wrong semantics (x̄₁ is a different object). Every single-letter pattern needs(?<!\\bar )(?<!\\widetilde )(?<!\\widehat )(?<!\\ol ).... - Font commands: bare-H matches inside
\mathcal H,\mathsf Hwithout(?<!\\mathcal )(?<!\\mathsf )...; bare-t inside\mathrm tsubscripts. - Left-guard letter+subscript patterns:
e_\Gammamatched inside\rangle_\Gamma(the trailing "e" of\rangle!). Use(?<![A-Za-z\\]). - Superscript exclusions where the letter has a standard super/sub meaning:
the H pattern excludes
H^...(cohomology) via lookahead.
notation_order (validator): every non-standard key must have its defsite
before all uses; uses inside <abstract> are exempt (abstracts forward-
reference by design). notation_far.py rewrites \notn -> \notnfar beyond
[notation] far_words from the defsite; standard keys are never far-marked.
The hover UI: near symbols pop after near_hover_delay_ms (400), far symbols
show cursor: help and pop after far_hover_delay_ms (1000) with a
"see definition in context" link resolved from defsite/href.
The prose companion of the math wrap, built for background/terminology
hovers (first used to tie the gq2 paper's usage sites to its Appendix E
background subsections). An authored map (paper.toml [notation] prose_map, conventionally notation/prose-map.json) gives per-key
{match, scope?, first_per?, label, definition, href}; the script runs
after tex2ptx + notation_far (so it sees insertion content and does
not perturb far word counts) and wraps matches in the GENERATED tree as
<termref key="K">...</termref>. The custom XSLs render that as
<span class="ptxnotn-K ptxbg"> in HTML and as bare text in LaTeX.
Semantics: matching runs over a masked logical text per file — every tag
is an opaque barrier (so wraps always nest), <ndash/>/<mdash/>/<nbsp/>
read as their characters, $re$ in a pattern matches a whole inline
<m> atomically, a space matches any whitespace run, and the compiled
pattern gets word-boundary guards. First occurrence per block (nearest
enclosing xml:id) is wrapped by default (first_per: block | division
| all); titles, math displays, xrefs, code, biblio, and any division
whose root id starts with bg- are skipped. Idempotent: existing termrefs
are opaque and count toward the first-per-block bookkeeping.
Registry: notation_registry.py merges prose entries into
window.PAPERFORGE_NOTATION with {html, href, label, more: true};
detail-ui shows label as the popup heading, uses the FAR delay for
.ptxbg spans, and renders the footer link as "more details ↗"
(entry.more). Href targets missing from the numbering database (e.g.
insertion divisions like bg-*) fall back to a tag anchor on the
single-page build.
Following a popup context link ("see definition in context" / "more
details") deliberately bypasses the browser's :target styling (the theme
paints the whole target block for 10s): detail-ui intercepts the click,
jumps there, updates the URL with history.pushState (which does not
engage :target), and paints ONLY the specific referenced text — the
defining occurrence of the term inside the target block when one is
wrapped there, else the target's heading — fading out over five seconds
(.pf-landing-hl / .pf-landing-fade). Modified clicks (new tab) and
cross-page hrefs keep default navigation.