Assemble Markdown slide sources into a deck and render it through a pluggable framework adapter.
Project description
Lectern
Lectern is a Python CLI that assembles Markdown slide sources — transclusion,
#1-3 slide ranges, partial search paths — into one deck and renders it via
reveal.js (with remark / marp / quarto as alternates), with a live-reload preview
server and vector PDF export. It's the Markdown front-end to a larger
resource-oriented slide system; the Source seam is designed so a semantic-CMS
backend can replace the filesystem later.
Jump to Install and Usage to get going.
Names (locked)
- Product / tool: Lectern
- CLI command:
lectern - PyPI distribution + GitHub repo:
lectern-slides→pip install lectern-slides - Python import package:
lectern - PDF extra:
lectern-slides[pdf] - License: MIT (see
LICENSE); all dependencies are permissive (MIT/BSD/Apache)
Install
pip install lectern-slides # core: assemble + reveal/remark HTML + watch
# PDF export (vector master, imposition, B&W) — pulls Playwright, pypdf, reportlab:
pip install 'lectern-slides[pdf]'
playwright install chromium # one-time: the headless browser for PDF
# Optional external tools, only for their adapters (each is availability-guarded):
# marp-cli (npm i -g @marp-team/marp-cli) → marp adapter: html / pdf / pptx
# quarto (quarto.org) → quarto adapter: high-quality html
# ghostscript (brew install ghostscript) → full-document B&W engine
Usage
SOURCE is a deck directory (containing deck.toml/lectern.toml, else
directory order), a .toml manifest, or a single .md file.
# New: scaffold a deck (deck.toml + a couple of slides) in a dir — default: cwd
lectern new # scaffold in the current dir
lectern new ./talks/ai-sec # ...or in a dir (created if missing)
# Assemble: expand includes / #1-3 ranges / partials into one Markdown deck
lectern assemble ./talks/ai-sec -o assembled.md # omit -o to write to stdout
# Check: validate includes/ranges/partials (and surface warnings), no render
lectern check ./talks/ai-sec
# Config: show the effective merged config and where each value came from
lectern config ./talks/ai-sec
# Build: render to the deck's out_dir (default: reveal HTML)
lectern build ./talks/ai-sec # -> dist/index.html
lectern build ./talks/ai-sec -t ./themes/mine.css -o site
lectern build ./talks/ai-sec -f outline # -> dist/outline.md (text transcript)
lectern build ./talks/ai-sec --all-themes # -> dist/index-<theme>.html, one per theme
lectern build ./talks/ai-sec --all-themes -f pdf # same, as index-<theme>.pdf
# Live preview: rebuilds on change with SSE reload + build-error overlay
lectern watch ./talks/ai-sec # serves http://127.0.0.1:8080
lectern watch ./talks/ai-sec --browser chrome # open a specific browser (or [serve].browser)
# Clean: remove generated output (the out_dir). --all also drops the PDF cache
lectern clean ./talks/ai-sec # removes dist/ (keeps build/ cache)
lectern clean ./talks/ai-sec --all --yes # also removes build/ (-y skips prompt)
out_dir (dist/) is disposable rendered output; build_dir (build/) holds
the reusable PDF master cache. clean removes out_dir and keeps the cache
so the next PDF export is fast; clean --all removes both. It only ever deletes
the deck's own out_dir/build_dir (deck-root-relative) — never a source,
partials, theme, or asset directory — and prompts first unless you pass -y
(preview with --dry-run).
Configuration
A deck is external to this tool. SOURCE's directory (the manifest's folder)
is the deck root, resolved to an absolute path, and every relative path in
the config — slides, partials, asset_base, theme, out_dir, build_dir
— resolves against it, never your CWD or where Lectern is installed. Absolute
paths and ~ pass through; URLs pass through. So out_dir/build_dir default to
dist/build inside the deck's own repo.
Config is merged from three layers, highest precedence first, over the built-in defaults:
- CLI flags —
--theme,--renderer,--asset-base,--aspect,--out,--remark-compat,--partial(repeatable),--max-include-depth, the PDF flags, … - the deck's
deck.toml(orlectern.toml), - a user config at
$XDG_CONFIG_HOME/lectern/config.toml(fallback~/.config/lectern/config.toml).
The merge is a deep, per-key merge, so a user config can set a house theme and a
shared partials library once and every separate deck repo inherits them (use
absolute/~ paths there, since relative paths resolve against each deck's root):
# ~/.config/lectern/config.toml
author = "Your Name" # inherited by every deck — never committed per-deck
theme = "house" # a bundled name, or an absolute/~ path
partials = ["~/talks/_lib"]
Putting author here is the recommended way to set your name: lectern new
reads it and leaves it out of the generated deck.toml (a comment notes it's
inherited), so your name is never committed into a deck repo — it's filled in at
build time from this file. With no user-config author, lectern new writes an
author = "Deck Author" placeholder instead (override with --author). theme
works the same way — lectern new inherits a user-config theme rather than
baking one into the deck (a baked-in value would shadow the user config, since
deck.toml outranks it); with none set it falls back to base (override with
--theme).
Inspect the effective, merged config and where each value came from with:
lectern config ./talks/ai-sec # value · (cli | deck.toml | user | default)
lectern config ./talks/ai-sec --theme grove # preview an override before building
lectern config ./talks/ai-sec --list-themes # themes usable by name (bundled + theme_paths + your theme's folder)
The full key reference (top-level + [serve]/[reveal]/[marp]/[quarto]/[pdf])
is in SPECIFICATION.md. Note: not every key has a flag — partials,
remark_compat, max_include_depth, and aspect are exposed on
build/watch/assemble/check; the rest are config-only.
Themes
theme = is either a bundled name or a path:
- bundled name (e.g.
theme = "base","cartesian","grove","soft-editorial") → resolved from thetheme_pathsdirs first (see below), then the CSS shipped inside the package atsrc/lectern/themes/<name>.css. Drop a.cssin the package dir to ship it by name with the tool. (The top-levelthemes/directory and the sample deck'sthemes/are not search paths — the former is unshipped design source, the latter is deck-local.) - path (
./themes/mine.css,~/talks/house.css, or absolute) → loaded directly; a relative path resolves against the deck root.
theme_paths — a reusable theme library without bundling. Like partials,
it's a list of directories where a bare theme name resolves (in order, before
the package themes). Set it once in your user config to point at a themes folder
you control, and every deck can use those themes by name — no ~/absolute path
per deck, no editing the package:
# ~/.config/lectern/config.toml
theme_paths = ["~/talks/_themes"] # contains house.css, dark.css, …
# any deck.toml, on any machine that shares that folder:
theme = "house"
Relative theme_paths entries resolve against the deck root; use absolute/~
for a global library. lectern config SOURCE shows the resolved theme_paths
and which layer set theme.
Themes are CSS driven by design tokens (--bg, --accent, --font-display, the
size scale, …) and the Remark-parity classes; the structural layout layer
(.slide anchor grid, .place boxes) is theme-independent, so a theme swap never
moves content.
Renderers (-r/--renderer, or renderer = in the manifest)
| Renderer | Engine | Formats |
|---|---|---|
reveal (default) |
native reveal.js | html, pdf |
remark |
native remark.js (legacy-deck parity) | html |
marp |
shells out to marp-cli |
html, pdf, pptx |
quarto |
shells out to quarto render |
html |
The format is gated by the adapter's capabilities; asking for one it can't make
prints a hint toward an adapter that can (e.g. -f pptx → try renderer: marp).
Diagrams (Mermaid)
A fenced ```mermaid block renders to a diagram — it stays valid CommonMark
(degrades to a code block anywhere) and is lowered to a <pre class="mermaid">
that the reveal and remark adapters render client-side:
```mermaid
flowchart LR
A[Source] --> B[Assemble] --> C{Render}
```
Mermaid loads (from a CDN) only when a diagram is present — auto-detected,
or forced with [reveal].mermaid = true|false. Diagrams are themed from the
deck's design tokens, and the PDF master waits for the async render, so they
appear in the vector PDF too. (For marp/quarto, or for full control, the
isolated-iframe embed pattern still works in every renderer.)
Icons (Font Awesome)
Set font_awesome in the manifest (or user config) and drop glyphs in with raw
HTML — <i class="fa-solid fa-rocket"></i> — anywhere in a slide; they inherit
the theme's text color and embed as vector in the PDF.
font_awesome = true # free kit, pinned CDN
# font_awesome = "~/decks/_assets/font-awesome" # a local kit (e.g. Pro), self-hosted
true links the free kit from a pinned CDN (a <i class="fa-brands fa-…">
gets brand icons too). A path self-hosts a local kit: the directory is copied
verbatim into dist/font-awesome/ (preserving css/ + webfonts/, which the
content-hashing asset pipeline would otherwise break) and linked locally — so a
paid/Pro kit set once in your user config is inherited by every deck, served
same-origin (no CDN, COI-friendly). Mark decorative icons aria-hidden="true",
or give meaningful ones an aria-label. Pro-only styles (fa-thin,
fa-duotone, …) won't render under the free kit, so keep shareable decks to
fa-solid/fa-regular/fa-brands.
Images
A standalone Markdown image ( on its own line) fills the space the
slide's heading/body leave — generous on an image-only slide, smaller when
there's also text — scaled to fit (up or down) with no distortion or overflow,
under any theme. So a diagram slide is usually just:
<!-- slide: .center .middle -->

To size one image differently, either add a reveal element comment right
after it, or use a raw <img> (which bypasses the fill rule):

<!-- .element: style="max-width: 40%;" -->
<img src="diagram.svg" alt="…" style="width: 1040px;"> <!-- ~px in the 1280×720 grid -->
(Inline images — not alone on a line — are left at their natural size.)
Speaker notes
Attach speaker notes to a slide with an HTML-comment block (or the equivalent fenced div). They never appear on the slide itself — adapters route them to the presenter view (reveal's speaker window, marp/quarto/remark equivalents):
# Threat model
<!-- notes -->
Walk the diagram left to right; the trust boundary is the dashed line.
<!-- /notes -->
::: {.notes}
Fenced-div form — handy when a note itself contains HTML comments.
:::
Add the presenter category for notes that should stay on stage but not be
printed in the PDF handout — pacing reminders, asides, "skip if running long":
<!-- notes:presenter -->
Eight minutes per section to land on time.
<!-- /notes -->
::: {.notes .presenter}
Same, fenced-div form.
:::
Ordinary notes show in the presenter view and the *-notes PDF layouts;
presenter notes show in the presenter view only. A mistyped category is
flagged by lectern check and treated as an ordinary note, so it can't silently
leak into print.
PDF export (-f pdf)
lectern build ./talks/ai-sec -f pdf # 2up-notes handout (default)
lectern build ./talks/ai-sec -f pdf --layout 1up # clean projection slides
lectern build ./talks/ai-sec -f pdf --ink-saver --paper letter
A single vector master is printed once (headless Chromium) and then imposed; re-exporting another layout/color reuses the cached master.
| Flag | Effect |
|---|---|
--layout |
1up, 2up, 2up-notes, 4up, 6up, 3up-notes |
--paper |
deck, letter, a4, or WxH (multi-up defaults to letter) |
--bw |
grayscale (vector tokens engine; ghostscript for raster too) |
--no-backgrounds |
drop background fills/images for clean paper |
--light-inverse |
flip dark slides to light for ink economy |
--ink-saver |
--bw + --no-backgrounds + --light-inverse |
All knobs also live under [pdf] in the manifest (CLI flags win). See
PDF-EXPORT.md for the full set and SPECIFICATION.md for the source format and
config reference. Run lectern --help (or lectern build --help) for everything.
Accessibility
lectern check runs an accessibility audit by default (turn it off with
--no-a11y). Like every Lectern diagnostic, each warning is source-cited —
file:line or theme — so you can act on it. It flags the issues that genuinely
break a screen-reader experience, and nothing noisy:
-
Every slide needs an accessible name — a heading or a
label. Give an image- or quotation-only slide a name with the slide directive:<!-- slide: label="Loki and Freyja peering through the deck rails" -->
labellowers toaria-labelon the reveal<section>(aria-label="…"works too). This is the right tool for a full-bleeddata-background-imageslide, which has no alt slot of its own. -
Images carry alt text via standard Markdown —
. An empty alt () is the correct, intentional way to mark an image decorative, so it isn't flagged. A raw<img>(HTML passthrough) with noaltattribute at all is flagged — addalt="…"(oralt=""to mark it decorative). -
<iframe>embeds need atitle=(the D3/WebGL demos) for an accessible name. -
Font Awesome icons need
aria-hidden="true"(decorative) or an accessible name (aria-label/title); a bare<i class="fa-…">is read as nothing or garbage by a screen reader, so it's flagged. -
Mermaid diagrams need an
accTitle/accDescrin the diagram source — Mermaid renders them to the SVG's<title>/<desc>(+aria-labelledby), a text alternative for the graphic:```mermaid flowchart LR accTitle: Request flow accDescr: The client calls the gateway, which routes to the services. Client --> Gateway --> Services ```
-
Themes ship AA contrast. The audit checks each theme's
--fg/--bgand--inverse-*text tokens against WCAG AA (4.5:1), and--accent— a graphical element (rules, list markers, mermaid diagram lines) — against WCAG non-text (3:1). -
.on-darkforces light, legible text (and links, plus a soft shadow) over a dark backdrop, independent of a theme's.inversetreatment — reach for it on a dark background-image slide:<!-- slide: .on-dark data-background-image="night.jpg" -->. -
Region landmarks + focus — each slide
<section>is named (an ARIAregion) from its heading (or its explicitlabel), so screen-reader users can navigate slide-to-slide; interactive content shows a visible:focus-visibleoutline for keyboard users. On navigation the current slide ("Slide 3 of 18: …") is announced via a politearia-liveregion. -
Outline export —
-f outlinewrites a linear, heading-structured Markdown transcript (outline.md): each slide's heading (or itslabel), prose body with directives stripped, and the speaker notes as prose; Mermaid diagrams collapse to theiraccDescr. A screen-reader transcript / no-JS, crawlable fallback, independent of any render framework. -
Document language — set
lang(a BCP 47 tag likeen,fr,de) in the config so assistive tech pronounces the content correctly; it becomes<html lang>(and the marp/quarto front-matterlang). Defaults toen. -
Tagged PDF —
-f pdf --layout 1upemits a tagged (structured) PDF: headings, paragraphs, lists, and tables carry structure tags, and<html lang>becomes the PDF's/Lang, so screen readers can read the export. (Tagging is on by default; the structure survives the1upmaster — N-up imposition flattens it.[pdf].tagged = falseto disable.) -
Forced colors — under Windows High Contrast /
forced-colors: active, links are underlined (not distinguished by color alone) and the keyboard-focus ring uses the systemHighlightcolor, so cues survive the OS palette override. -
Reduced motion — the live deck honors
prefers-reduced-motion: reduce: reveal's slide/fragment/background transitions are zeroed, so slides change instantly (fragments still step, they just don't animate). The PDF master already renders with reduced motion.
The bundled examples/sample-deck passes the audit, and a test keeps it that way
as slides are added.
What's here
src/lectern/ ← the implementation: assemble · render adapters · pdf · serve · theming
src/lectern/themes/ ← bundled themes (base, cartesian, grove, soft-editorial); add one here to ship it by name
tests/ ← unit + golden-file + render/PDF tests
SPECIFICATION.md ← the full functional + technical spec (the substance)
PDF-EXPORT.md ← the PDF strategy (vector master → 2-up-with-notes, B&W, posters)
CLAUDE.md ← the operating manual: build order, conventions, milestones
ROADMAP.md ← phases: assemble → render/watch → adapters → components → CMS
deck.toml ← a minimal starter manifest (root example)
examples/sample-deck/ ← a complete reference deck that exercises every feature
deck.toml · slides/ · _partials/ · assets/ · themes/ (paper · midnight)
README.md ← how the sample maps to features
Development
uv sync --extra pdf # runtime + dev + PDF deps
uv run pytest # unit + golden + render/PDF tests
uv run ruff check . && uv run ruff format --check .
The PDF/render tests that need a browser are skipped unless Chromium is present
(uv run playwright install chromium to enable them). Lectern was built milestone
by milestone (M1 assemble → M6 PDF finishing); CLAUDE.md is the operating manual
and remains the guide for further work with Claude Code, and ROADMAP.md covers
what's beyond v1 (component embeds, then a graph/CMS Source backend).
Design decisions
- Default renderer: reveal (native, no external binary); remark is the legacy-parity adapter; marp/quarto are opt-in subprocess adapters, availability-guarded. No mandatory Pandoc.
- Source directives live in HTML comments so raw
.mdstays valid CommonMark. - Core stays dependency-light; Playwright + pypdf + reportlab are the
lectern-slides[pdf]extra, imported lazily. - Manifests are TOML; the PDF default is 2up-notes (one vector master, imposed two-up with the real speaker notes beside each slide).
- The
Sourceprotocol, adapter registry, theme token contract, and[serve].coiheaders are seams kept clean so later phases (components, CMS backend) stay cheap.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lectern_slides-0.1.0.tar.gz.
File metadata
- Download URL: lectern_slides-0.1.0.tar.gz
- Upload date:
- Size: 1.8 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cd9db461be390c70a57800b4eed3a13e7e7dec90fa6065c13ca99cb32e2f640a
|
|
| MD5 |
af557b7a6f0cbd16db4a25f68de7dc02
|
|
| BLAKE2b-256 |
9c62f8d5158756aab721a8aa39cd07b9962173ed6c64918b96d34ee4831ce5f5
|
File details
Details for the file lectern_slides-0.1.0-py3-none-any.whl.
File metadata
- Download URL: lectern_slides-0.1.0-py3-none-any.whl
- Upload date:
- Size: 135.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
236a177af7e76431959335a1d0982757e1b81aef1e3d4f7b316e7d952abf8112
|
|
| MD5 |
4e7f167026b24265df8d3be0e45f46d6
|
|
| BLAKE2b-256 |
6028850a52b5967a2c1a213846dcf7175c5c1f6eb419c05977bf34370ab8dad8
|