Custom counters¶
Technical documents number things that LaTeX knows nothing about: findings,
requirements, bugs, risks, test cases. Declare such a series under
press.declare.counters, define each item in the body, and refer to it with
the ordinary @ sigil — the numbers are computed by TeXSmith rather than by
the backend, so the LaTeX build, the Typst build and the MkDocs site all show
the same values.
---
press:
declare:
counters:
n: {name: Requirement, format: "N-{n:02d}"}
---
| Id | Requirement |
| --- | --- |
| {counter}(n:joy) | Everyone shall be happy |
| {counter}(n:respect) | Everyone shall respect the others |
Smile in every circumstance (@n:joy) and do no harm to others (@n:respect).
renders as
| Id | Requirement |
|---|---|
| N-01 | Everyone shall be happy |
| N-02 | Everyone shall respect the others |
Smile in every circumstance (N-01) and do no harm to others (N-02).
Both N-01 occurrences are the same PDF anchor: the table cell is the target,
the parenthesised one is a clickable link.
One registry for every series¶
Every referenceable series is an entry of the counter registry, keyed by its prefix. The built-in prefixes are simply predeclared entries; there is no second mechanism for "reserved prefixes".
| Prefix | Name | Scope | Numbered by | Applies to |
|---|---|---|---|---|
part chap sec app |
Part, Chapter, Section, Appendix | document | backend | headings |
fig |
Figure | chapter | backend | images, sub-figures |
tbl |
Table | chapter | backend | tables |
lst |
Listing | chapter | backend | code blocks |
eq |
Equation | chapter | backend | display math |
thm |
Theorem | chapter | backend | theorem-type callouts |
note |
Note | document | backend | footnotes |
gls |
— | — | — | glossary entries |
doi |
— | — | — | DOIs cited in place |
A user-declared entry adds a series the backend knows nothing about:
press:
declare:
counters:
fw: {name: Finding, format: "FW-{n:02d}", start: 1, scope: document}
name- The label word, used in references and diagnostics.
format- A Python format string over the fields
n(the value),prefixandkey; it defaults to"{n}", so"FW-{n:02d}"pads to two digits. An invalid format ({oops}, unbalanced braces) is an error at parse time. start- The first value; defaults to 1.
scopedocument | chapter | section. It is a hint to whoever numbers the series: under site-wide numbering every series isdocumentand continuous, so the same document may print "Figure 3.2" and show "Figure 12".ref- The template a reference renders, with the fields
{name}and{number}. It defaults to"{number}"when aformatis given — a formatted number such asFW-01is self-identifying — and to"{name} {number}"otherwise, which is what the predeclared entries use.
A prefix matches [A-Za-z][A-Za-z0-9_-]* and is matched case-insensitively, so
@Fw:boot-loop capitalises the label word at the start of a sentence. You may
override the fields of a predeclared entry (fig: {scope: document}) but not
add a prefix that shadows a role name.
declare: may sit at the root
press is an optional namespace: every key TMark reads may sit at the root
of the front matter or under press:, and press wins when both are
present. The namespace exists so that a file shared with a static site
generator keeps the root free. A top-level counters: key is the 0.6
spelling, accepted with a deprecation warning; tmark lint --fix moves it
under press.declare. See Migrating to TMark.
Defining an item¶
{counter}(prefix:key) — define and print¶
The canonical form is a role, and its key is an argument, hence the parentheses: nothing inside them is Markdown.
---
press:
declare:
counters:
fw: {name: Finding, format: "FW-{n:02d}"}
---
{counter}(fw:boot-loop) The firmware reboots when the watchdog fires.
It is a plain inline construct: it works in a paragraph, a table cell, a list item, a callout title, a heading. Numbers are allocated in document order.
Shorthand: #(fw:boot-loop) — the # sigil with the same argument
bracketing, one character shorter and the same node. \# forces a literal
#, and nothing is ever substituted inside a code span or a fenced block.
#{prefix:key} is deprecated
The 0.6 marker is still recognised, and only when its prefix is
declared — so a Ruby or CoffeeScript interpolation such as
#{user.name} in prose stays the literal text it is. tmark lint --fix
rewrites it to #(prefix:key).
{#prefix:key} — define silently¶
Any element carrying an id whose prefix is declared is numbered too, without printing anything. This is the ordinary attribute syntax, so it attaches to headings, figures and tables:
---
press:
declare:
counters:
fw: {name: Finding, format: "FW-{n:02d}"}
---
## Boot loop {#fw:boot-loop}
{#fw:trace}
The watchdog issue (@fw:boot-loop) is fixed in 1.4.2.
@fw:boot-loop resolves to FW-01 even though the number appears nowhere in
the heading.
An attribute needs a host
An attribute list is consumed at the end of a heading, on an image, or on a
line of its own after a block. In the middle of a sentence, or in a table
cell, it has nothing to attach to — that is exactly the case
{counter}(…) exists for.
Referring to an item¶
@prefix:key, or @[prefix:key] when the reference contains a space. This is
the regular reference sigil; a declared prefix simply routes
it to the counter registry.
---
press:
declare:
counters:
fw: {name: Finding, format: "FW-{n:02d}"}
---
{counter}(fw:boot-loop) The firmware reboots.
The watchdog issue (@fw:boot-loop) is fixed in 1.4.2.
The reference renders the ref template. With a format declared that
defaults to the number alone — FW-01, not Finding FW-01 — so write the noun
yourself, as you would for a section, or declare ref: "{name} {number}".
Diagnostics¶
| Situation | Code |
|---|---|
@n:missing — no such item |
ref-unresolved, and a visible [?n:missing] |
| the same key defined twice | label-duplicate |
{counter}(x:joy) with x undeclared |
prefix-unknown |
{#tbl:x} on a figure |
prefix-host-mismatch |
an invalid format or prefix |
an error at parse time |
Scope and stability¶
Numbers are allocated in document order and shared across all the documents of
a multi-document build — a.md, b.md and c.md continue a single series
rather than restarting.
Numbers are positional
Inserting an item renumbers every item after it. When the identifiers leave
the document — a finding quoted in an audit report, a requirement cited in a
test plan — that renumbering breaks the external traceability silently.
Pin the values with a dedicated start: per counter and stable ordering, or
keep the volatile numbering for internal documents only. Explicit pinning is
planned but not implemented yet.
Backend mapping¶
| Definition | Reference | |
|---|---|---|
| LaTeX | \phantomsection\label{n:joy}N-01 |
\hyperref[n:joy]{N-01} |
| Typst | N-01<n:joy> |
#link(<n:joy>)[N-01] |
| HTML | <span class="ts-counter" data-counter="n" data-key="joy" id="n:joy">N-01</span> |
<a href="#n:joy">N-01</a> |
\phantomsection is what makes the hyperref anchor land on the item rather
than on the enclosing section, and it lets \pageref{n:joy} work.
On a MkDocs site¶
The texsmith MkDocs plugin (package mkdocs_texsmith)
renders counters on the site. One plugin, no Markdown extension to wire:
plugins:
- texsmith:
declare:
counters: # optional site-wide declarations
req:
name: Requirement
format: "REQ-{n:03d}"
start: 100
texsmith.counters and texsmith.index are gone
Those two standalone plugins are the 0.6 spelling. They still load — each
logs a deprecation warning and does nothing — but no longer number
anything; the single texsmith plugin above does both jobs (counters and
search-index entries) site-wide. Remove them from plugins: in
mkdocs.yml and move any counters: they declared under
declare.counters. The entry points disappear in 0.8.
Counters declared under declare.counters apply to the whole site; a page may
declare its own under press.declare.counters in its front matter, and the
page's declaration wins for the same prefix. Every item is visible from every
page — a series defined in findings.md is referenceable from index.md.
Numbering is site-wide, in navigation order: a pre-pass parses and resolves
every page before any of them is converted, chaining each page's counters after
the previous page's, so a reference on the first page resolves to an item
defined on the last one. Cross-page references are rewritten to point at the
page that defines the item (<a href="../findings/#fw:watchdog">FW-01</a>);
same-page ones keep a local anchor (<a href="#fw:watchdog">FW-01</a>).
The pre-pass parses the page rather than scanning it, so a marker inside a
fence or a code span is never counted, and #{user.name} in prose stays the
literal text it is.
A user-declared series such as fw: above is always numbered by tmark itself
— no backend knows about it, on the site or in a standalone build. This
differs from the predeclared series of the registry
(fig, tbl, lst, eq, sec, …): a standalone texsmith build lets LaTeX
or Typst allocate those (--numbering backend, the CLI default) or has tmark
allocate them instead so both backends agree (--numbering tmark). The MkDocs
plugin always resolves every page with tmark numbering every series —
equivalent to --numbering tmark — because a number allocated by the LaTeX or
Typst backend of one page would mean nothing site-wide.
Citing an item from another document¶
A number allocated here means nothing in a sister document, and a renumbering
breaks every hard-coded FW-10 it contains. Publish an inventory and cite it
explicitly — see Cross-document references.
Not implemented yet¶
Auto-generated listings (a "List of requirements" table), extra per-item
attributes (status, priority, source), back-references ("cited on pages 4,
12"), hierarchical numbering (FW-3.2) and per-chapter resets are deliberately
out of the first iteration.