What follows is what is planned, what was decided against and why, and what has been built.
A remap gives one name to a container whatever it is: - note: mynote applies
to Div.note and Span.note alike. That is fine where the two forms differ —
\begin{mynote} and \mynote{...} in LaTeX — but in Typst both are now a
call, so one name has to serve both positions. A function holding a block
breaks a paragraph when used inline, and one built only from text works
anywhere: which of the two you need depends on where the container appears,
and the author knows, while the filter does not.
Written out, it would attach the name to the flag:
typst:
- poemtitle: [/divcall=ptblock, /spancall=ptinline]which adds a second separator to a grammar whose first one took some finding.
Not urgent, because the case has a plainer answer: use two classes. If a poem title looks different standing alone than it does inside a line, it is not the same container, and saying so in the source is more honest than remapping it away. The feature is worth having when a real document needs one name in two positions and two classes would be a lie.
A class like marginnote-open is fine in CSS and Typst but breaks the TeX
formats. The filter could derive a safe name — dropping hyphens, so
marginnoteopen — while HTML and Typst keep the original. It was considered
and will not be done.
The failure it would fix is already loud: ConTeXt stops the build, LaTeX reports an undefined control sequence. The user learns immediately that the name is not usable. Deriving silently would trade that for a worse failure — the document compiles, an environment that was never defined is called, and the name in the error message appears nowhere in the source, because the filter invented it.
It would also let two classes collapse onto one name: note-title and
notetitle both derive to notetitle, quietly merging two environments.
Remap entries already solve the case properly, per format and in the open:
container-writer:
latex:
- marginnote-open: marginnoteopenChild processing runs in two passes: mark_children annotates matching
descendants with _cw_env and _cw_acc, and wrap_marked substitutes them
afterwards. The marks exist because since Pandoc 3.9 a walk handler that
returns a replacement stops descending, so wrapping note.title on sight
would never reach note.title.icon. Marking first breaks that dependency.
Iterating over el.content directly would remove the need for marks
altogether: controlling the order means substituting from the inside out,
leaving nothing behind in the AST. It will not be done for now.
el:walk handles what is easiest to get wrong — descending through blocks
and inlines under different rules, into table cells, footnotes, block
quotes. Reimplementing that is more code than the two functions it would
replace, and every branch left out is a container that silently stops being
processed. It would also merge two short functions with one job each into a
single larger one, since controlling the order means doing both at once.
The gain would be dropping the attributes from the AST. They did leak into
the output once — fixed in f6c7d77 — but the passthrough golden files now
cover exactly that, which is most of what the rewrite would buy.
Nothing in the Pandoc API helps here: pandoc.walk_block and
walk_inline behave the same way on replacement, and there is no
bottom-up mode.
Done. Six flags choose the wrapping form per entry, overriding the default of
block form for a Div and inline form for a Span:
/divblock /divinline /divcall /spanblock /spaninline /spancall
latex:
- poemtitle: [/divcall, /spanblock, ptcmd]The leading slash tells a flag from a remap target, so both fit in one entry.
It also had to survive two parsers: a backslash, as an earlier draft of this
entry proposed, does not — Pandoc reads metadata values as Markdown and eats
it as an escape — and @ * % # break the YAML unless quoted, while ! and
& are a tag and an anchor and arrive empty.
Two things turned up while building it. A call takes its content as an
argument, and a paragraph break inside a TeX argument ends it: emitting a Div
as a block made \poemtitle{...} fail even with \long, because any
non-long command further in stops at the \par. Single-paragraph Divs are
now emitted as their inlines. And a form a format does not have — call in
JATS, anywhere in DOCX — falls back to the default with a warning rather than
silently.
Verified against the real verse package: a Div.poemtitle now compiles as
\poemtitle{Hamlet} with the title centred, which is what the package
provides as a command and no environment could give.
The if FORMAT == 'latex' ... elseif FORMAT == 'context' chain is now a
FORMATS table. Each entry declares what the format can do — block/inline
templates, an attr to set, or nothing at all for passthrough — and a single
engine acts on it.
It went further than planned in two ways: entries also cover formats that set
an attribute instead of wrapping (DOCX, ODT) and formats that deliberately do
nothing (HTML, EPUB, and the JSON/native AST passes), and writer variants
resolve to their base entry, so html5 and jats_publishing need no entry of
their own.
The premise held: adding JATS was four lines of data, variants included, without touching the engine.
The fields were originally named env and cmd, after LaTeX, where a Div
happens to want an environment and a Span a command. That coincidence does
not hold elsewhere: Typst's inline form is an anonymous group with a label,
JATS's is a named-content element, and neither is a "command". They are now
block and inline, which describe what the engine actually decides — which
context the node is in — and that question does have the same answer in every
format.
The call form it left pending is done — see above.