Skip to main content

Assemble Markdown slide sources into a deck and render it through a pluggable framework adapter.

Project description

Lectern

CI PyPI Python License: MIT

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-slidespip 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:

  1. CLI flags--theme, --renderer, --asset-base, --aspect, --out, --remark-compat, --partial (repeatable), --max-include-depth, the PDF flags, …
  2. the deck's deck.toml (or lectern.toml),
  3. 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 waylectern 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 the theme_paths dirs first (see below), then the CSS shipped inside the package at src/lectern/themes/<name>.css. Drop a .css in the package dir to ship it by name with the tool. (The top-level themes/ directory and the sample deck's themes/ 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 pptxtry 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 (![alt](src) 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 -->

![Instruction and data lanes](images/instructions-vs-data-lanes.svg)

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

![alt](logo.svg)
<!-- .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-citedfile: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" -->
    

    label lowers to aria-label on the reveal <section> (aria-label="…" works too). This is the right tool for a full-bleed data-background-image slide, which has no alt slot of its own.

  • Images carry alt text via standard Markdown — ![A bar chart of token probabilities](chart.png). An empty alt (![](divider.svg)) is the correct, intentional way to mark an image decorative, so it isn't flagged. A raw <img> (HTML passthrough) with no alt attribute at all is flagged — add alt="…" (or alt="" to mark it decorative).

  • <iframe> embeds need a title= (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/accDescr in 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/--bg and --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-dark forces light, legible text (and links, plus a soft shadow) over a dark backdrop, independent of a theme's .inverse treatment — 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 ARIA region) from its heading (or its explicit label), so screen-reader users can navigate slide-to-slide; interactive content shows a visible :focus-visible outline for keyboard users. On navigation the current slide ("Slide 3 of 18: …") is announced via a polite aria-live region.

  • Outline export-f outline writes a linear, heading-structured Markdown transcript (outline.md): each slide's heading (or its label), prose body with directives stripped, and the speaker notes as prose; Mermaid diagrams collapse to their accDescr. A screen-reader transcript / no-JS, crawlable fallback, independent of any render framework.

  • Document language — set lang (a BCP 47 tag like en, fr, de) in the config so assistive tech pronounces the content correctly; it becomes <html lang> (and the marp/quarto front-matter lang). Defaults to en.

  • Tagged PDF-f pdf --layout 1up emits 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 the 1up master — N-up imposition flattens it. [pdf].tagged = false to 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 system Highlight color, 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 .md stays 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 Source protocol, adapter registry, theme token contract, and [serve].coi headers are seams kept clean so later phases (components, CMS backend) stay cheap.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lectern_slides-0.1.0.tar.gz (1.8 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lectern_slides-0.1.0-py3-none-any.whl (135.5 kB view details)

Uploaded Python 3

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

Hashes for lectern_slides-0.1.0.tar.gz
Algorithm Hash digest
SHA256 cd9db461be390c70a57800b4eed3a13e7e7dec90fa6065c13ca99cb32e2f640a
MD5 af557b7a6f0cbd16db4a25f68de7dc02
BLAKE2b-256 9c62f8d5158756aab721a8aa39cd07b9962173ed6c64918b96d34ee4831ce5f5

See more details on using hashes here.

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

Hashes for lectern_slides-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 236a177af7e76431959335a1d0982757e1b81aef1e3d4f7b316e7d952abf8112
MD5 4e7f167026b24265df8d3be0e45f46d6
BLAKE2b-256 6028850a52b5967a2c1a213846dcf7175c5c1f6eb419c05977bf34370ab8dad8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page