lectern-slides 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lectern_slides-0.1.0/.github/workflows/ci.yml +48 -0
- lectern_slides-0.1.0/.github/workflows/release.yml +46 -0
- lectern_slides-0.1.0/.gitignore +25 -0
- lectern_slides-0.1.0/CHANGELOG.md +70 -0
- lectern_slides-0.1.0/CLAUDE.md +98 -0
- lectern_slides-0.1.0/LICENSE +21 -0
- lectern_slides-0.1.0/PDF-EXPORT.md +179 -0
- lectern_slides-0.1.0/PKG-INFO +457 -0
- lectern_slides-0.1.0/README.md +427 -0
- lectern_slides-0.1.0/ROADMAP.md +85 -0
- lectern_slides-0.1.0/SPECIFICATION.md +326 -0
- lectern_slides-0.1.0/deck.toml +42 -0
- lectern_slides-0.1.0/examples/sample-deck/README.md +109 -0
- lectern_slides-0.1.0/examples/sample-deck/_partials/principles.md +60 -0
- lectern_slides-0.1.0/examples/sample-deck/_partials/qualifications.md +7 -0
- lectern_slides-0.1.0/examples/sample-deck/assets/bg-grid.svg +24 -0
- lectern_slides-0.1.0/examples/sample-deck/assets/d3-bars.html +68 -0
- lectern_slides-0.1.0/examples/sample-deck/assets/pups.jpg +0 -0
- lectern_slides-0.1.0/examples/sample-deck/assets/request-flow.svg +27 -0
- lectern_slides-0.1.0/examples/sample-deck/assets/stars.jpg +0 -0
- lectern_slides-0.1.0/examples/sample-deck/assets/webgl-triangle.html +123 -0
- lectern_slides-0.1.0/examples/sample-deck/deck.toml +71 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/00-title.md +17 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/10-agenda.md +16 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/20-qualifications.md +6 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/22-ranges.md +13 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/25-code.md +27 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/26-diagram.md +15 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/27-icons.md +18 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/28-image-sizing.md +28 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/30-background.md +7 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/32-image.md +6 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/40-incremental.md +22 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/45-layout.md +43 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/50-animation.md +10 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/55-webgl.md +8 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/60-table.md +10 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/70-math.md +16 -0
- lectern_slides-0.1.0/examples/sample-deck/slides/80-closing.md +18 -0
- lectern_slides-0.1.0/examples/sample-deck/themes/midnight.css +94 -0
- lectern_slides-0.1.0/examples/sample-deck/themes/paper.css +97 -0
- lectern_slides-0.1.0/pyproject.toml +64 -0
- lectern_slides-0.1.0/src/lectern/__init__.py +9 -0
- lectern_slides-0.1.0/src/lectern/a11y.py +290 -0
- lectern_slides-0.1.0/src/lectern/assets.py +161 -0
- lectern_slides-0.1.0/src/lectern/cli.py +824 -0
- lectern_slides-0.1.0/src/lectern/config.py +310 -0
- lectern_slides-0.1.0/src/lectern/errors.py +52 -0
- lectern_slides-0.1.0/src/lectern/fontawesome.py +66 -0
- lectern_slides-0.1.0/src/lectern/outline.py +236 -0
- lectern_slides-0.1.0/src/lectern/pdf/__init__.py +13 -0
- lectern_slides-0.1.0/src/lectern/pdf/colors.py +87 -0
- lectern_slides-0.1.0/src/lectern/pdf/geometry.py +185 -0
- lectern_slides-0.1.0/src/lectern/pdf/grayscale.py +47 -0
- lectern_slides-0.1.0/src/lectern/pdf/impose.py +191 -0
- lectern_slides-0.1.0/src/lectern/pdf/master.py +98 -0
- lectern_slides-0.1.0/src/lectern/pdf/options.py +105 -0
- lectern_slides-0.1.0/src/lectern/pdf/pipeline.py +145 -0
- lectern_slides-0.1.0/src/lectern/pdf/posters.py +86 -0
- lectern_slides-0.1.0/src/lectern/pdf/printcss.py +58 -0
- lectern_slides-0.1.0/src/lectern/preprocess.py +333 -0
- lectern_slides-0.1.0/src/lectern/ranges.py +75 -0
- lectern_slides-0.1.0/src/lectern/remark_compat.py +167 -0
- lectern_slides-0.1.0/src/lectern/render/__init__.py +33 -0
- lectern_slides-0.1.0/src/lectern/render/_external.py +41 -0
- lectern_slides-0.1.0/src/lectern/render/base.py +87 -0
- lectern_slides-0.1.0/src/lectern/render/lowering.py +303 -0
- lectern_slides-0.1.0/src/lectern/render/marp.py +160 -0
- lectern_slides-0.1.0/src/lectern/render/quarto.py +179 -0
- lectern_slides-0.1.0/src/lectern/render/remark.py +157 -0
- lectern_slides-0.1.0/src/lectern/render/reveal.py +209 -0
- lectern_slides-0.1.0/src/lectern/serve.py +363 -0
- lectern_slides-0.1.0/src/lectern/slides.py +97 -0
- lectern_slides-0.1.0/src/lectern/source.py +48 -0
- lectern_slides-0.1.0/src/lectern/sourcemap.py +50 -0
- lectern_slides-0.1.0/src/lectern/templates/remark.html.j2 +94 -0
- lectern_slides-0.1.0/src/lectern/templates/reveal.html.j2 +498 -0
- lectern_slides-0.1.0/src/lectern/themes/base.css +136 -0
- lectern_slides-0.1.0/src/lectern/themes/blue-professional.css +148 -0
- lectern_slides-0.1.0/src/lectern/themes/broadside.css +150 -0
- lectern_slides-0.1.0/src/lectern/themes/cartesian.css +137 -0
- lectern_slides-0.1.0/src/lectern/themes/grove.css +149 -0
- lectern_slides-0.1.0/src/lectern/themes/long-table.css +163 -0
- lectern_slides-0.1.0/src/lectern/themes/mat.css +153 -0
- lectern_slides-0.1.0/src/lectern/themes/monochrome.css +145 -0
- lectern_slides-0.1.0/src/lectern/themes/sakura-chroma.css +162 -0
- lectern_slides-0.1.0/src/lectern/themes/signal.css +153 -0
- lectern_slides-0.1.0/src/lectern/themes/soft-editorial.css +146 -0
- lectern_slides-0.1.0/src/lectern/themes/vellum.css +134 -0
- lectern_slides-0.1.0/src/lectern/theming.py +168 -0
- lectern_slides-0.1.0/tests/conftest.py +30 -0
- lectern_slides-0.1.0/tests/fixtures/deck/_partials/lib.md +21 -0
- lectern_slides-0.1.0/tests/fixtures/deck/_partials/note.md +1 -0
- lectern_slides-0.1.0/tests/fixtures/deck/deck.toml +11 -0
- lectern_slides-0.1.0/tests/fixtures/deck/slides/00-intro.md +9 -0
- lectern_slides-0.1.0/tests/fixtures/deck/slides/20-fence.md +12 -0
- lectern_slides-0.1.0/tests/fixtures/deck/slides/30-canvas.md +9 -0
- lectern_slides-0.1.0/tests/fixtures/deck.assembled.golden.md +55 -0
- lectern_slides-0.1.0/tests/fixtures/legacy-remark/builds.md +10 -0
- lectern_slides-0.1.0/tests/fixtures/legacy-remark/deck.toml +10 -0
- lectern_slides-0.1.0/tests/fixtures/legacy-remark/title.md +6 -0
- lectern_slides-0.1.0/tests/fixtures/render-deck/assets/grid.svg +1 -0
- lectern_slides-0.1.0/tests/fixtures/render-deck/assets/logo.png +1 -0
- lectern_slides-0.1.0/tests/fixtures/render-deck/deck.toml +17 -0
- lectern_slides-0.1.0/tests/fixtures/render-deck/slides/anchor.md +9 -0
- lectern_slides-0.1.0/tests/fixtures/render-deck/slides/bg.md +5 -0
- lectern_slides-0.1.0/tests/fixtures/render-deck/slides/incr.md +16 -0
- lectern_slides-0.1.0/tests/test_a11y.py +213 -0
- lectern_slides-0.1.0/tests/test_assets.py +95 -0
- lectern_slides-0.1.0/tests/test_cli.py +345 -0
- lectern_slides-0.1.0/tests/test_config.py +83 -0
- lectern_slides-0.1.0/tests/test_config_layering.py +79 -0
- lectern_slides-0.1.0/tests/test_external_deck.py +90 -0
- lectern_slides-0.1.0/tests/test_fontawesome.py +60 -0
- lectern_slides-0.1.0/tests/test_golden.py +16 -0
- lectern_slides-0.1.0/tests/test_lowering_notes.py +56 -0
- lectern_slides-0.1.0/tests/test_outline.py +108 -0
- lectern_slides-0.1.0/tests/test_pdf.py +384 -0
- lectern_slides-0.1.0/tests/test_preprocess.py +203 -0
- lectern_slides-0.1.0/tests/test_ranges.py +54 -0
- lectern_slides-0.1.0/tests/test_remark_compat.py +112 -0
- lectern_slides-0.1.0/tests/test_render_marp.py +126 -0
- lectern_slides-0.1.0/tests/test_render_quarto.py +104 -0
- lectern_slides-0.1.0/tests/test_render_remark.py +85 -0
- lectern_slides-0.1.0/tests/test_render_reveal.py +350 -0
- lectern_slides-0.1.0/tests/test_serve.py +212 -0
- lectern_slides-0.1.0/tests/test_slides.py +64 -0
- lectern_slides-0.1.0/tests/test_theming.py +134 -0
- lectern_slides-0.1.0/themes/base.css +136 -0
- lectern_slides-0.1.0/uv.lock +1022 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
# Cancel superseded runs on the same ref.
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ci-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
lint:
|
|
16
|
+
name: Lint & format
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v6
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@v7
|
|
22
|
+
with:
|
|
23
|
+
enable-cache: true
|
|
24
|
+
- name: Sync (runtime + dev group)
|
|
25
|
+
run: uv sync
|
|
26
|
+
- name: ruff check
|
|
27
|
+
run: uv run ruff check .
|
|
28
|
+
- name: ruff format --check
|
|
29
|
+
run: uv run ruff format --check .
|
|
30
|
+
|
|
31
|
+
test:
|
|
32
|
+
name: Test (py${{ matrix.python-version }})
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
strategy:
|
|
35
|
+
fail-fast: false
|
|
36
|
+
matrix:
|
|
37
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v6
|
|
40
|
+
- name: Install uv
|
|
41
|
+
uses: astral-sh/setup-uv@v7
|
|
42
|
+
with:
|
|
43
|
+
enable-cache: true
|
|
44
|
+
python-version: ${{ matrix.python-version }}
|
|
45
|
+
- name: Sync (runtime + dev group + pdf extra)
|
|
46
|
+
run: uv sync --extra pdf
|
|
47
|
+
- name: pytest
|
|
48
|
+
run: uv run pytest
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes lectern-slides to PyPI when a GitHub Release is published.
|
|
4
|
+
# Uses PyPI Trusted Publishing (OIDC) — no API token secret required. Configure
|
|
5
|
+
# the trusted publisher once at https://pypi.org/manage/account/publishing/ with:
|
|
6
|
+
# owner=bsletten repo=lectern-slides workflow=release.yml environment=pypi
|
|
7
|
+
on:
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
name: Build sdist + wheel
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v7
|
|
20
|
+
with:
|
|
21
|
+
enable-cache: true
|
|
22
|
+
- name: Build distributions
|
|
23
|
+
run: uv build
|
|
24
|
+
- name: Check metadata
|
|
25
|
+
run: uvx twine check dist/*
|
|
26
|
+
- uses: actions/upload-artifact@v7
|
|
27
|
+
with:
|
|
28
|
+
name: dist
|
|
29
|
+
path: dist/
|
|
30
|
+
|
|
31
|
+
publish:
|
|
32
|
+
name: Publish to PyPI
|
|
33
|
+
needs: build
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
environment: pypi
|
|
36
|
+
permissions:
|
|
37
|
+
id-token: write # required for trusted publishing
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/download-artifact@v8
|
|
40
|
+
with:
|
|
41
|
+
name: dist
|
|
42
|
+
path: dist/
|
|
43
|
+
- name: Install uv
|
|
44
|
+
uses: astral-sh/setup-uv@v7
|
|
45
|
+
- name: Publish
|
|
46
|
+
run: uv publish
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
# Virtualenvs / tooling
|
|
10
|
+
.venv/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
|
|
15
|
+
# Lectern build artifacts (deck out_dir / build_dir defaults)
|
|
16
|
+
/dist/
|
|
17
|
+
/build/
|
|
18
|
+
|
|
19
|
+
# PDF master cache (written under the deck's build_dir on -f pdf)
|
|
20
|
+
.lectern-cache/
|
|
21
|
+
|
|
22
|
+
# Editor backups (Emacs undo-tree, backups, autosaves)
|
|
23
|
+
*.~undo-tree~
|
|
24
|
+
*~
|
|
25
|
+
\#*\#
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **lectern-slides** are documented here. The format
|
|
4
|
+
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
|
|
5
|
+
aims to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] — 2026-06-17
|
|
10
|
+
|
|
11
|
+
First public release. A dependency-light Python CLI (`lectern`) that assembles
|
|
12
|
+
Markdown slide sources into one deck and renders it through a pluggable framework
|
|
13
|
+
adapter, with a live-reload preview server and vector PDF export.
|
|
14
|
+
|
|
15
|
+
### Assembly
|
|
16
|
+
|
|
17
|
+
- Transclusion and slide-range includes with the `#1-3,14` range grammar.
|
|
18
|
+
- Fence-aware slide splitter; relative + `partials` search-path resolution with
|
|
19
|
+
cycle and depth guards.
|
|
20
|
+
- Frontmatter handling and a source-map so every diagnostic cites the
|
|
21
|
+
originating `file:line`/directive, never an internal offset.
|
|
22
|
+
- `lectern assemble` and `lectern check` (validation + warnings, no render).
|
|
23
|
+
|
|
24
|
+
### Rendering
|
|
25
|
+
|
|
26
|
+
- Pluggable adapter registry: **reveal** (default) and **remark** native
|
|
27
|
+
adapters (Python + Jinja2 only), plus **marp** and **quarto** subprocess
|
|
28
|
+
adapters that `available()`-guard their external binary.
|
|
29
|
+
- Remark input-compat normalizer so existing Remark decks assemble and render
|
|
30
|
+
unchanged.
|
|
31
|
+
- Neutral directive syntax in HTML comments / Pandoc fenced divs: per-slide
|
|
32
|
+
classes/id/data-attributes, inline and block classes, content anchors, placed
|
|
33
|
+
boxes, and incremental builds.
|
|
34
|
+
- Theme resolution with a token contract; ships a `base.css` plus a set of
|
|
35
|
+
swappable themes. A deck's own theme folder is discovered automatically.
|
|
36
|
+
- Asset resolver (directory + URL `asset_base`): copies, content-hashes, and
|
|
37
|
+
rewrites references; garbage-collects orphaned hashed assets.
|
|
38
|
+
- Mermaid diagrams, Font Awesome icons, image sizing, and raw HTML/`<canvas>`
|
|
39
|
+
passthrough.
|
|
40
|
+
|
|
41
|
+
### Speaker notes
|
|
42
|
+
|
|
43
|
+
- `<!-- notes -->` comment blocks and `::: {.notes}` fenced divs route to the
|
|
44
|
+
presenter view.
|
|
45
|
+
- `notes:presenter` category for notes that show in the presenter view but are
|
|
46
|
+
**excluded from the printed PDF handout**; a mistyped category is flagged.
|
|
47
|
+
|
|
48
|
+
### Watch + serve
|
|
49
|
+
|
|
50
|
+
- `lectern watch` — Starlette/Uvicorn serving `dist/`, `watchfiles` rebuild, SSE
|
|
51
|
+
live-reload, and a build-error overlay.
|
|
52
|
+
- `[serve].coi` COOP/COEP headers (the on-ramp for WASM-threads / WebGPU embeds).
|
|
53
|
+
|
|
54
|
+
### PDF export
|
|
55
|
+
|
|
56
|
+
- Vector master printed once via headless Chromium, then imposed and cached for
|
|
57
|
+
reuse across layout/color changes.
|
|
58
|
+
- Layouts `1up`, `2up`, `2up-notes`, `4up`/`2x2`, `6up`, `3up-notes`; paper
|
|
59
|
+
presets and `WxH`; B&W (token and ghostscript engines), `--no-backgrounds`,
|
|
60
|
+
`--light-inverse`, and `--ink-saver`.
|
|
61
|
+
- `-f pdf|pptx` output across capable adapters with graceful degradation.
|
|
62
|
+
|
|
63
|
+
### Accessibility
|
|
64
|
+
|
|
65
|
+
- `lectern check` runs a source-cited a11y audit by default: accessible-name and
|
|
66
|
+
alt-text checks, heading-order and contrast lint, tagged PDF output, a document
|
|
67
|
+
outline, and forced-colors support.
|
|
68
|
+
|
|
69
|
+
[Unreleased]: https://github.com/bsletten/lectern-slides/compare/v0.1.0...HEAD
|
|
70
|
+
[0.1.0]: https://github.com/bsletten/lectern-slides/releases/tag/v0.1.0
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# CLAUDE.md — Lectern
|
|
2
|
+
|
|
3
|
+
Guidance for building Lectern with Claude Code. Read `SPECIFICATION.md` for the
|
|
4
|
+
full design and `ROADMAP.md` for sequencing. This file is the operating manual.
|
|
5
|
+
|
|
6
|
+
## What you are building
|
|
7
|
+
|
|
8
|
+
A Python CLI (`lectern`) that assembles Markdown slide sources (transclusion +
|
|
9
|
+
slide-range includes) into one deck and renders it via a pluggable framework
|
|
10
|
+
adapter, with a live `--watch` preview server. It is the Markdown front-end to a
|
|
11
|
+
larger resource-oriented slide system; keep the `Source` seam clean so a future
|
|
12
|
+
CMS/graph backend can replace the filesystem source without changing anything
|
|
13
|
+
downstream.
|
|
14
|
+
|
|
15
|
+
## Prime directives
|
|
16
|
+
|
|
17
|
+
- **Build the smallest thing that works, then grow.** Follow the milestones below
|
|
18
|
+
in order. Do not scaffold future phases speculatively.
|
|
19
|
+
- **Keep the core dependency-light.** `reveal` and `remark` adapters use only
|
|
20
|
+
Python + templates. `marp`/`quarto` adapters shell out and must `available()`-
|
|
21
|
+
guard the external binary. Never make Pandoc a required dependency.
|
|
22
|
+
- **Directives live in HTML comments** so raw `.md` stays valid CommonMark. The
|
|
23
|
+
assemble stage expands them.
|
|
24
|
+
- **Pure where it matters.** `ranges.py`, the fence-aware splitter, and the
|
|
25
|
+
include resolver are pure functions with no I/O — they get the heaviest tests.
|
|
26
|
+
- **Errors map to source.** Carry a source-map; every user-facing error cites the
|
|
27
|
+
originating `file:line`/directive, never an internal offset.
|
|
28
|
+
- **Static-mostly.** Do not model animation timelines. Raw HTML/script passthrough
|
|
29
|
+
plus the asset pipeline is the entire animation story for now.
|
|
30
|
+
|
|
31
|
+
## Tech choices (don't re-litigate)
|
|
32
|
+
|
|
33
|
+
Python ≥3.11 · `typer` CLI · `pydantic` config · `jinja2` templates ·
|
|
34
|
+
`python-frontmatter` · `watchfiles` · `starlette`+`uvicorn` server · `httpx` for
|
|
35
|
+
URL assets · `tomllib` (stdlib). Package with hatchling: distribution name
|
|
36
|
+
**`lectern-slides`** (PyPI/GitHub), import package `lectern`, `console_scripts` →
|
|
37
|
+
`lectern`, PDF extra `lectern-slides[pdf]`, license **MIT** (`LICENSE` file +
|
|
38
|
+
`license = "MIT"` in pyproject). (Import name `lectern` technically
|
|
39
|
+
shares PyPI's Minecraft `lectern` namespace; harmless here — namespace to
|
|
40
|
+
`lectern_slides` only if that ever matters.) Format with `ruff format`; lint with
|
|
41
|
+
`ruff`; type-check with `pyright`/`mypy`.
|
|
42
|
+
|
|
43
|
+
## Milestones (each ends in a working, committed, tested state)
|
|
44
|
+
|
|
45
|
+
**M1 — assemble.** `ranges.py` (the `#1-3,14` grammar), fence-aware slide
|
|
46
|
+
splitter, include resolver (relative + `partials` search paths + cycle/depth
|
|
47
|
+
guards), frontmatter handling, source-map. `lectern assemble SOURCE -o out.md`
|
|
48
|
+
and `lectern check SOURCE`. Unit + golden-file tests. No rendering yet.
|
|
49
|
+
|
|
50
|
+
**M2 — render (reveal).** Config model, theme resolution + token injection,
|
|
51
|
+
`reveal` adapter via Jinja2 template (assembled md → reveal.js HTML), asset
|
|
52
|
+
resolver (dir + URL, copy + rewrite). `lectern build SOURCE`. Ship `themes/base.css`.
|
|
53
|
+
|
|
54
|
+
**M3 — watch + serve.** Starlette/Uvicorn serving `dist/`, `watchfiles` rebuild,
|
|
55
|
+
SSE live-reload, build-error overlay, `[serve].coi` COOP/COEP headers.
|
|
56
|
+
`lectern watch SOURCE`.
|
|
57
|
+
|
|
58
|
+
**M4 — remark adapter + migration.** Native `remark` adapter and the Remark
|
|
59
|
+
input-compat normalizer (`.cls[]`, `class:`/`name:`/`layout:` property lines,
|
|
60
|
+
`--` flatten) so existing decks assemble and render unchanged.
|
|
61
|
+
|
|
62
|
+
**M5 — marp + quarto adapters.** Subprocess adapters with neutral→flavor lowering
|
|
63
|
+
and `available()` guards; capability-based graceful degradation. `-f pdf|pptx`.
|
|
64
|
+
|
|
65
|
+
**M6 — PDF finishing.** Per `PDF-EXPORT.md`: the 1-up vector master via Playwright
|
|
66
|
+
ships earlier (M2); this milestone adds N-up imposition / `3up-notes` (pypdf +
|
|
67
|
+
reportlab overlay),
|
|
68
|
+
the B&W engines, `light_inverse`, auto poster capture, and handout chrome.
|
|
69
|
+
|
|
70
|
+
Stop here for v1. Phases beyond (components/WASM/WebGPU, CMS source backend) are
|
|
71
|
+
in `ROADMAP.md` and are *not* M-scope.
|
|
72
|
+
|
|
73
|
+
## Working habits
|
|
74
|
+
|
|
75
|
+
- Run `pytest` before declaring any milestone done; commit per milestone with a
|
|
76
|
+
short message.
|
|
77
|
+
- When you touch `ranges.py`, the splitter, or the include resolver, add/adjust
|
|
78
|
+
tests in the same change — these are the load-bearing pure functions.
|
|
79
|
+
- Add a `fixtures/` deck early (M1) that exercises includes, ranges, partials,
|
|
80
|
+
assets, classes, notes, and a raw-HTML/`<canvas>` slide; grow it as you go.
|
|
81
|
+
- Prefer one clear way to do a thing over configurable cleverness. This is one
|
|
82
|
+
author's tool.
|
|
83
|
+
|
|
84
|
+
## Seams to respect (so later phases are cheap, without building them now)
|
|
85
|
+
|
|
86
|
+
- `source.py` `Source` protocol — filesystem now, CMS/graph later. Nothing
|
|
87
|
+
downstream may import filesystem paths directly; go through `Source`.
|
|
88
|
+
- `render/base.py` registry — adapters are discovered, not hard-wired.
|
|
89
|
+
- Theme **token contract** — components and adapters read tokens, never hardcode
|
|
90
|
+
colors/sizes.
|
|
91
|
+
- `[serve].coi` headers — the on-ramp for WASM-threads/WebGPU embeds.
|
|
92
|
+
|
|
93
|
+
## Definition of done (v1)
|
|
94
|
+
|
|
95
|
+
`lectern watch ./talks/ai-sec` serves a live, reloading reveal deck assembled
|
|
96
|
+
from a manifest with at least one ranged partial include and a URL `asset_base`;
|
|
97
|
+
`lectern build -f pdf` produces a vector PDF; existing Remark decks render via the
|
|
98
|
+
`remark` adapter unchanged; `pytest` is green.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brian Sletten
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# PDF Export — Strategy
|
|
2
|
+
|
|
3
|
+
How Lectern turns a deck into PDF. Read alongside `SPECIFICATION.md` (§7 reveal
|
|
4
|
+
adapter) and `examples/sample-deck/`.
|
|
5
|
+
|
|
6
|
+
## Principle
|
|
7
|
+
|
|
8
|
+
One **vector master**, many cheap derivations.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
render-time post-process
|
|
12
|
+
assembled deck ──▶ [ Chromium print: reveal ──▶ 1-up master.pdf
|
|
13
|
+
?print-pdf, vector ] │
|
|
14
|
+
• backgrounds on/off ├─▶ impose ──▶ 2-up / N-up / notes
|
|
15
|
+
• light-inverse │ (pypdf places vector pages)
|
|
16
|
+
• poster frames for embeds └─▶ grayscale ──▶ B&W
|
|
17
|
+
• fragments flatten/steps (tokens or Ghostscript)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The master is always 1 slide / page, vector, full color. Everything the user
|
|
21
|
+
asks for — 2-up, handouts with notes, B&W, no-backgrounds — is either a flag that
|
|
22
|
+
changes the *master render* or a transform *applied to the master*. We never fall
|
|
23
|
+
back to rasterizing the deck (that was DeckTape's failure mode).
|
|
24
|
+
|
|
25
|
+
## Engines
|
|
26
|
+
|
|
27
|
+
- **Render + capture:** Playwright for Python driving headless Chromium. One
|
|
28
|
+
dependency, no Node. Gives `page.pdf(...)` (vector, honors `@page`,
|
|
29
|
+
`print_background`) and `page.screenshot(...)` for poster capture, plus
|
|
30
|
+
`emulate_media(media="print", reduced_motion="reduce")`.
|
|
31
|
+
- **Imposition (N-up / handouts):** `pypdf` (BSD-3-Clause, pure Python) places the
|
|
32
|
+
source *vector* pages scaled and translated into the grid via
|
|
33
|
+
`PageObject.add_transformation(Transformation().scale(s).translate(x, y))` +
|
|
34
|
+
`merge_page`. The handout chrome that pypdf can't draw — borders, slide numbers,
|
|
35
|
+
header/footer, and the **notes text** — is rendered as a thin overlay with
|
|
36
|
+
`reportlab` (BSD-3-Clause) and merged in. Both pure-Python and permissive, so the
|
|
37
|
+
whole tool stays MIT-clean. No quality loss (slide pages remain vector).
|
|
38
|
+
- **Grayscale (optional, for full-document B&W incl. raster images):** Ghostscript
|
|
39
|
+
(`-sColorConversionStrategy=Gray`). External binary; only needed for the
|
|
40
|
+
`ghostscript` B&W engine (see Color/B&W).
|
|
41
|
+
|
|
42
|
+
These live behind a `lectern-slides[pdf]` install extra so the core stays light.
|
|
43
|
+
|
|
44
|
+
## Render-time options (change the master)
|
|
45
|
+
|
|
46
|
+
| Option | Effect |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `backgrounds = true\|false` | `print_background` on/off **and** an injected print stylesheet that hides `data-background-*` and `.inverse` fills when off. Default `true`. |
|
|
49
|
+
| `light_inverse = true\|false` | Flip `.inverse` (dark) slides to light for ink economy, using the theme's own tokens (`--inverse-bg`→`--bg`, `--inverse-fg`→`--fg`). Default `false`. |
|
|
50
|
+
| `fragments = "flatten"\|"steps"` | Maps to reveal `pdfSeparateFragments`. `flatten` = one page per slide with all builds shown (handout default); `steps` = one page per build. |
|
|
51
|
+
| `paper = "deck"\|"letter"\|"a4"\|WxH` | `deck` (default) uses the slide aspect via `prefer_css_page_size` for the 1-up master; for a multi-up handout `deck` falls back to `letter` (a deck-shaped sheet leaves tiled slides tiny). Named sizes letterbox the slide for standard paper. |
|
|
52
|
+
| `posters = "auto"\|"explicit"\|"off"` | How live embeds become stills (next section). |
|
|
53
|
+
|
|
54
|
+
Because these change printed pixels, they are set when the master is rendered.
|
|
55
|
+
The pipeline injects a small print stylesheet built on the **theme token
|
|
56
|
+
contract**, so `light_inverse` / `backgrounds=off` work for any theme without the
|
|
57
|
+
theme needing its own print block (a theme MAY still add `@media print`
|
|
58
|
+
refinements; it's optional).
|
|
59
|
+
|
|
60
|
+
## Animations → a default frame
|
|
61
|
+
|
|
62
|
+
Live embeds (the D3 iframe today; WASM/WebGPU components later) can't animate in a
|
|
63
|
+
PDF, so each resolves to one deterministic still. Resolution order:
|
|
64
|
+
|
|
65
|
+
1. **Explicit poster.** `<iframe data-poster="assets/d3.png">` → that image is
|
|
66
|
+
used. Most predictable; recommended for anything you care about exactly.
|
|
67
|
+
2. **Auto-capture** (`posters = "auto"`, the default). The pipeline loads the
|
|
68
|
+
embed in headless Chromium with reduced-motion emulated and `?static=1`
|
|
69
|
+
appended, waits `data-poster-at` ms (default 1200) — or for a
|
|
70
|
+
`window.lecternReady === true` signal if the embed sets one — screenshots the
|
|
71
|
+
embed's box to PNG, and swaps the `<iframe>` for an `<img>` in the print DOM.
|
|
72
|
+
3. **Static-mode passthrough** (`posters = "off"` or capture unavailable). The
|
|
73
|
+
print render loads the embed with reduced-motion + `?static=1`; a well-behaved
|
|
74
|
+
embed paints a single frame, which prints inline. (This already works for the
|
|
75
|
+
sample's `d3-bars.html`, which honors `prefers-reduced-motion`.)
|
|
76
|
+
|
|
77
|
+
**Embed authoring contract** (so an embed produces a clean default frame):
|
|
78
|
+
- Honor `prefers-reduced-motion: reduce` **and** a `?static=1` query by drawing
|
|
79
|
+
one deterministic frame and not looping.
|
|
80
|
+
- Optionally set `window.lecternReady = true` once that frame is painted, so
|
|
81
|
+
auto-capture knows when to shoot instead of guessing with `data-poster-at`.
|
|
82
|
+
|
|
83
|
+
WebGPU/WASM note: headless Chromium needs ANGLE/SwiftShader flags
|
|
84
|
+
(`--enable-unsafe-swiftshader`) for GPU contexts, and late-painting components
|
|
85
|
+
should rely on explicit posters or `lecternReady` rather than a fixed delay.
|
|
86
|
+
|
|
87
|
+
## Layout: 1-up, 2-up, N-up, handouts (imposition)
|
|
88
|
+
|
|
89
|
+
The master is imposed onto sheets with pypdf (+ a reportlab overlay for chrome and
|
|
90
|
+
notes). Presets:
|
|
91
|
+
|
|
92
|
+
| `layout` | Sheet |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `2up-notes` (**default**) | 2 slides stacked on a portrait page, each with its speaker notes beside it |
|
|
95
|
+
| `1up` | the master, unchanged — clean projection slides |
|
|
96
|
+
| `2up` | 2 slides stacked, no notes |
|
|
97
|
+
| `4up` / `2x2` | 4 slides, 2×2 |
|
|
98
|
+
| `6up` | 6 slides, 2×3 |
|
|
99
|
+
| `3up-notes` | 3 slides down the left, **speaker notes beside each** |
|
|
100
|
+
|
|
101
|
+
`2up-notes` is the default delivered layout: a portrait page holds two rows, each
|
|
102
|
+
row a slide thumbnail (~58% width, left) with its notes column (right). Use
|
|
103
|
+
`--layout 1up` when you want bare slides for projection. The notes are the real
|
|
104
|
+
ones Lectern already parses from `<!-- notes -->` / `::: notes`, so handouts carry
|
|
105
|
+
your script, not blank lines.
|
|
106
|
+
|
|
107
|
+
Imposition controls: `paper`, `orientation` (`auto` by default — the sheet is
|
|
108
|
+
turned to match the deck aspect, so wide 16:9 slides tile without big margins),
|
|
109
|
+
`margins`, `gutter`,
|
|
110
|
+
`frame = true|false` (hairline border per thumbnail), `slide_numbers`,
|
|
111
|
+
`header` / `footer` (e.g. title, date, page x/y). All vector; text stays
|
|
112
|
+
selectable.
|
|
113
|
+
|
|
114
|
+
## Color / B&W
|
|
115
|
+
|
|
116
|
+
Two engines, pick per taste:
|
|
117
|
+
|
|
118
|
+
- **`tokens` (default, no dependency).** The pipeline reads the active theme's
|
|
119
|
+
color tokens, maps each to its perceptual-luminance gray, and injects the gray
|
|
120
|
+
token set for the render. Vector, text stays crisp, and contrast is *designed*
|
|
121
|
+
(the indigo and the vermilion seal become distinct grays rather than mud). Does
|
|
122
|
+
**not** recolor embedded raster images / captured posters — those stay as-is.
|
|
123
|
+
- **`ghostscript` (full document).** Post-processes the finished PDF to DeviceGray,
|
|
124
|
+
converting everything including raster posters and images. Needs the `gs`
|
|
125
|
+
binary; use when you want guaranteed end-to-end grayscale.
|
|
126
|
+
|
|
127
|
+
`color = "color" | "bw"`; `bw_engine = "tokens" | "ghostscript"`.
|
|
128
|
+
|
|
129
|
+
Convenience preset: **`ink_saver = true`** = `bw` + `backgrounds = false` +
|
|
130
|
+
`light_inverse = true`. The handout-friendly default for printing.
|
|
131
|
+
|
|
132
|
+
## CLI & config
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
lectern build SOURCE -f pdf [--layout 1up] [--bw] [--no-backgrounds]
|
|
136
|
+
[--light-inverse] [--ink-saver] [--paper a4]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```toml
|
|
140
|
+
[pdf]
|
|
141
|
+
layout = "2up-notes" # 2up-notes (default) | 1up | 2up | 4up | 6up | 3up-notes
|
|
142
|
+
color = "color" # color | bw
|
|
143
|
+
bw_engine = "tokens" # tokens | ghostscript
|
|
144
|
+
backgrounds = true
|
|
145
|
+
light_inverse = false
|
|
146
|
+
fragments = "flatten" # flatten | steps
|
|
147
|
+
paper = "deck" # deck | letter | a4 | 1280x720
|
|
148
|
+
posters = "auto" # auto | explicit | off
|
|
149
|
+
poster_at = 1200 # ms, default capture moment for auto posters
|
|
150
|
+
tagged = true # emit a tagged (structured) PDF for screen readers;
|
|
151
|
+
# preserved by the 1up master, flattened by N-up imposition
|
|
152
|
+
|
|
153
|
+
# handout chrome (used by 2up/4up/6up/3up-notes)
|
|
154
|
+
frame = true
|
|
155
|
+
slide_numbers = true
|
|
156
|
+
gutter = "10mm"
|
|
157
|
+
header = ""
|
|
158
|
+
footer = "{title} · {date} · {page}/{pages}"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
CLI flags override `[pdf]`, which overrides defaults. `--ink-saver` expands to the
|
|
162
|
+
three options above.
|
|
163
|
+
|
|
164
|
+
## Caching & determinism
|
|
165
|
+
|
|
166
|
+
The 1-up master is content-hashed and cached under the deck's `build_dir`
|
|
167
|
+
(`build/.lectern-cache/`). Re-exporting `2up` then `4up` then `--bw` re-runs only
|
|
168
|
+
imposition / conversion, not the (slow) Chromium render. Poster captures are
|
|
169
|
+
cached per embed + `poster_at`. A given deck + options always produces a
|
|
170
|
+
byte-stable master so diffs are meaningful. Because the cache lives in
|
|
171
|
+
`build_dir`, `lectern clean` (which clears `out_dir`) keeps it; `lectern clean
|
|
172
|
+
--all` drops it.
|
|
173
|
+
|
|
174
|
+
## Milestone mapping
|
|
175
|
+
|
|
176
|
+
- **M2** ships the 1-up vector master (`-f pdf`) — backgrounds toggle, fragments
|
|
177
|
+
flatten, poster *passthrough* (reduced-motion static frames).
|
|
178
|
+
- A later milestone (**M6, PDF finishing**) adds imposition (N-up / `3up-notes`),
|
|
179
|
+
the B&W engines, `light_inverse`, auto poster capture, and handout chrome.
|