Three companion Pandoc Lua filters:
container-writer.lua— translates genericDivandSpancontainers into format-native environments for LaTeX, ConTeXt, Typst, and passthrough for HTML/EPUB.container-strip.lua— removesDivandSpanelements by class, content and all, for stripping editorial annotations in production builds.container-unwrap.lua— removesDivandSpancontainer elements while preserving their content. Useful as a post-processing step aftercontainer-writer.luato neutralise elements that were not in the whitelist.
Copyright 2026 Pedro Luis Barrio under GPL-3.0-or-later, see LICENSE file for details.
Maintained by plbarrio.
Pandoc >= 2.19.1 · Quarto >= 1.4.0 (for Quarto usage)
pandoc --lua-filter=container-writer.lua input.md -o output.pdfDeclare in _quarto.yml:
filters:
- container-writer.luaPandoc renders Div and Span elements with CSS classes natively in
HTML/EPUB. In other formats they are invisible — content is emitted but
without any wrapping. This filter bridges that gap by wrapping whitelisted
containers in the appropriate format command.
| Element | LaTeX | ConTeXt | Typst | HTML/EPUB |
|---|---|---|---|---|
Div |
\begin{name}...\end{name} |
\startname...\stopname |
#block[...] <name> |
unchanged |
Span |
\name{...} |
\name{...} |
#[...] <n> |
unchanged |
---
config:
theme: 'base'
themeVariables:
primaryColor: '#FEFEFE'
primaryTextColor: '#555'
primaryBorderColor: '#AAA'
lineColor: '#555'
secondaryColor: '#666'
tertiaryColor: '#AAA'
---
flowchart TD
A[Div]
A --> B[Whitelist?]
B --> L["**LaTeX:**<br> \begin{name}<br>\end{name}"]
B --> M["**ConTeXt:**<br> \startname<br>\stopname"]
B --> N["**Typst:**<br> #block[...] <name>"]
---
config:
theme: 'base'
themeVariables:
primaryColor: '#FEFEFE'
primaryTextColor: '#555'
primaryBorderColor: '#AAA'
lineColor: '#555'
secondaryColor: '#666'
tertiaryColor: '#AAA'
---
flowchart TD
A[Span]
A --> B[Whitelist?]
B --> L["**LaTeX:**<br>\name{...}"]
B --> M["**ConTeXt:**<br>\name{...}"]
B --> N["**Typst:**<br> #[...] <name>"]
The effective whitelist for a given format is common + the FORMAT-specific
list. Containers not in the whitelist are left untouched.
Both common and format-specific keys accept a scalar for a single entry
or a list for multiple entries:
container-writer:
common: epigraph
container-writer:
common:
- epigraph
- noteOnly whitelisted names are processed — unknown containers never cause errors in the output format.
Compound entries (parent.child) control how nested elements are wrapped.
When a Div or Span with class parent is visited, its children matching
class child are wrapped using the child's own class name as environment —
mirroring the AST directly:
container-writer:
common:
- note
- note.title # Div.title inside Div.note → \begin{title}In HTML/EPUB children are left as-is — rendered natively by Pandoc, styled
via CSS descendant selectors (.note .title { ... }).
In Typst and ConTeXt the child style can be scoped inside the parent rule.
In LaTeX \begin{title} is global — use the remap syntax to give it a
per-context name.
A compound entry can remap the child's environment name for specific formats:
container-writer:
common:
- note
- note.title # HTML/EPUB: uses class name 'title'
latex:
- note.title: notetitle # LaTeX: uses 'notetitle' instead
context:
- note.title: notetitle # ConTeXt: uses 'notetitle' insteadThis lets CSS use .note .title naturally while LaTeX/ConTeXt use
\begin{notetitle} / \startnotetitle — fully per-context, no global
namespace collision.
Remap is also useful to avoid conflicts with existing LaTeX environments. If a class name collides with an environment already defined by your document class or a package, remap it to a different name without changing your source:
container-writer:
latex:
- dedication: mydedication # avoids collision with existing \dedicationThen define mydedication in your preamble instead of dedication.
If the parent is not in the whitelist but parent.child is, the parent
passes through unwrapped while its matching children are still processed.
Chains are supported: note.title.icon — each level uses its own class name.
::: epigraph
Content of the epigraph block.
:::
A paragraph with [an inline span]{.sidebar} inside.Use the env or environment attribute to override the environment name
for a specific block, provided the name is in the whitelist:
::: {.epigraph env=myepigraph}
Content.
:::\begin{epigraph}
Content of the epigraph block.
\end{epigraph}
A paragraph with \sidebar{an inline span} inside.\startepigraph
Content of the epigraph block.
\stopepigraph
A paragraph with \sidebar{an inline span} inside.
#block[
Content of the epigraph block.
] <epigraph>
A paragraph with #[an inline span] <sidebar> inside.<div class="epigraph">
<p>Content of the epigraph block.</p>
</div>
<p>A paragraph with <span class="sidebar">an inline span</span> inside.</p>A practical case for review workflows: annotations visible in draft builds,
removed entirely in production by container-strip.lua — no changes to
source files needed.
[this scene needs more tension]{.marginnoteopen}
[checked against sources]{.marginnoteclosed}
::: marginnoteopenblock
Longer note spanning multiple lines.
:::Review build:
pandoc --lua-filter=container-writer.lua input.md -o draft.pdfProduction build:
pandoc --lua-filter=container-strip.lua \
--lua-filter=container-writer.lua \
input.md -o final.pdfcontainer-strip:
- marginnoteopen
- marginnoteclosed
- marginnoteopenblock
- marginnoteclosedblockStyle files: notes.tex, notes.ctx, notes.typ, notes.css.
Labels allow #show rules to target the containers:
#show <epigraph>: it => block(
inset: (left: 2em, right: 2em),
above: 1em,
below: 1em,
text(style: "italic", it)
)Define the environments in your preamble or template:
\usepackage{epigraph}
% or define manually:
\newenvironment{epigraph}{\begin{quote}\itshape}{\end{quote}}\definestartstop[epigraph]
[before={\blank\startnarrow},
after={\stopnarrow\blank}]
Removes Div and Span elements by class — content and all. Configure
with a separate YAML key. Accepts a scalar for a single class or a list:
container-strip: marginnoteopen
container-strip:
- marginnoteopen
- marginnoteclosed
- marginnoteopenblock
- marginnoteclosedblockpandoc --lua-filter=container-strip.lua \
--lua-filter=container-writer.lua \
input.md -o output.pdfNote: container-strip must run before container-writer — if writer
runs first it converts spans to raw format commands that strip never sees.
Removes Div and Span container elements while preserving their content.
Useful after container-writer.lua to neutralise elements not in the
whitelist that would otherwise render differently across Pandoc versions
(e.g. #block[] in Typst 3.9 vs nothing in 3.1).
Must run after container-writer.lua — the writer converts whitelisted
elements to raw format commands that unwrap never sees.
pandoc --lua-filter=container-writer.lua \
--lua-filter=container-unwrap.lua \
input.md -t typstAccepts a scalar or a list. Two reserved keywords control bulk behaviour:
all— unwrap every remainingDiv/Spanregardless of classvoid— unwrap elements that carry no class at all
container-unwrap: all
container-unwrap: void
container-unwrap: sidebar
container-unwrap:
- void
- sidebar
- note- A container name present in both
DivandSpancontexts uses the same environment name. If your format requires different names for block and inline, use theenvattribute to override per block. LineBlock(|) inside a whitelistedDivis not processed by this filter.- Container names must be valid LaTeX/ConTeXt command names — letters only,
no hyphens, no leading digits. A name like
marginnote-openproduces\marginnote-open{...}in LaTeX where-is interpreted as subtraction, silently breaking the output. Usemarginnoteopenor a short prefix likemnopeninstead. CSS classes and Typst labels accept hyphens freely. container-stripdoes not support compound entries — blacklist entries are plain class names only. To stripDiv.titleinsideDiv.note, listtitleexplicitly (strips all.titleelements) or list the specific classes you want removed.
Issues and PRs welcome at the project repository.
GPL-3.0-or-later. See LICENSE.
- Pandoc — universal document converter
- Quarto — open-source scientific publishing system
- Lua — lightweight embeddable scripting language
- Pandoc Lua filters — official documentation
- Quarto extensions — official documentation