Metadata-Version: 2.5
Name: Deckoction-md
Version: 0.3.0
Summary: Markdown + YAML frontmatter to static HTML slide deck generator
Project-URL: Homepage, https://github.com/julianholland/slides_md
Project-URL: Repository, https://github.com/julianholland/slides_md
Project-URL: Documentation, https://slides-md.readthedocs.io
Author-email: Julian Holland <holland@fhi.mpg.de>
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Graphics :: Presentation
Requires-Python: >=3.10
Requires-Dist: jinja2>=3.1
Requires-Dist: markdown-it-py>=3.0
Requires-Dist: pyyaml>=6.0
Provides-Extra: docs
Requires-Dist: myst-parser>=0.18; extra == 'docs'
Requires-Dist: sphinx-rtd-theme>=1.2; extra == 'docs'
Requires-Dist: sphinx>=6.0; extra == 'docs'
Provides-Extra: pdf
Requires-Dist: playwright>=1.40; extra == 'pdf'
Provides-Extra: pdf-images
Requires-Dist: pymupdf>=1.23; extra == 'pdf-images'
Description-Content-Type: text/markdown

# deckoction

[![CI](https://github.com/julianholland/slides_md/actions/workflows/ci.yml/badge.svg)](https://github.com/julianholland/slides_md/actions/workflows/ci.yml)
[![Documentation Status](https://readthedocs.org/projects/slides-md/badge/?version=latest)](https://slides-md.readthedocs.io/en/latest/?badge=latest)

**Full documentation: [slides-md.readthedocs.io](https://slides-md.readthedocs.io/)**

Turn a single Markdown file into a static HTML slide deck. No client-side framework —
just the same vanilla HTML/CSS/JS approach as the hand-built `sulfur_slides`
deck it's based on: a dark theme, keyboard/click navigation, click-to-zoom images, and
vendored KaTeX for math, with no build step or CDN dependency in the output.

## Quickstart

```bash
uv tool install deckoction-md                  # or 'deckoction-md[pdf]' for --pdf/--thumbnail
deckoction slides.md                       # build next to slides.md and serve at :8000
```

From a source checkout, `uv sync` then `uv run deckoction examples/demo/slides.md` (add
`--force` to rebuild over a previous build) builds `examples/demo/build/index.html` (plus
`assets/`, `vendor/katex/`, and `slide_images/`) and serves it at `http://localhost:8000`.
`examples/demo/slides.md` exercises every layout and feature in one deck — a good starting
point to copy from.

## Authoring format

Write one Markdown file (e.g. `slides.md`). Slides are separated by a line containing
exactly `+++`. Each slide has YAML frontmatter (fenced by `---`/`---`) followed by a
Markdown body:

```markdown
---
layout: content
kicker: Overview
title: Why a Markdown Slide Tool
image: images/diagram.png
image_alt: Diagram of the pipeline
---

- Author once in a single Markdown file
  - YAML frontmatter per slide controls layout and media
- Generator assembles static HTML/CSS/JS

+++

---
layout: title
title: My Presentation
subtitle: A subtitle
author: Jane Doe
date: today
background: images/hero.jpg
---
```

The body supports paragraphs, up to two levels of bullet lists, inline `**bold**` /
`*em*` / inline code, inline KaTeX `$...$` (rendered client-side, so any valid LaTeX
passes through untouched), and `> ` blockquotes (rendered as an italic grey callout box,
matching the KaTeX formula box style — a quote inside a bullet list, either as its own
item (`- > quote`, shown without a bullet marker) or indented under a bullet, keeps the
same look, offset to the bullet's indent), and GFM pipe tables (`| a | b |` rows with a
`|---|:-:|` separator line; column alignment and inline math in cells both work). Don't use Markdown headings or `![]()` images in
the body — use the `title`/`kicker`/`image` fields instead (the generator warns if it
sees either).

Anything wrapped in `<!-- ... -->` (or `<!--- ... --->`) is stripped before the file is
even split into slides, so it's never parsed or rendered — comment out a note-to-self
inside a slide's body, a YAML field, or a whole slide (fences included) and it's simply
gone from the build.

### Layouts

| `layout`  | What it renders |
|---|---|
| `title`   | Centered title over a full-bleed background, in a translucent panel, with optional `subtitle`, `author`, `date` |
| `content` | `title`/`kicker` + bullets on the left, a single `image`, two `images`, `video`, or `panels` comparison on the right (default layout) |
| `stacked` | `title`/`kicker` + full-width bullets, no media column — also where block math (`formula`) usually goes |
| `split`   | `title`/`kicker` + a full-width row of 2-3 `panels` (image + caption), no bullets |
| `image`   | A single `image` fills the entire slide edge-to-edge, sized via `fit` (`contain`, default — fit to screen, no cropping; or `cover` — fill the screen, cropping if needed); `title`/`kicker` are optional, shown as a small overlay panel — omit both for a bare full-slide image |
| `references` | Renders the full numbered bibliography (see [Citations & references](#citations--references)) — no fields of its own to set |

### Per-slide fields

| field | applies to | notes |
|---|---|---|
| `layout` | all | `title` / `content` / `stacked` / `split` / `image` / `references`; default `content` |
| `title` | all | required for `title` layout; optional caption for `image` |
| `kicker` | content/stacked/split/image | small uppercase label above the title |
| `subtitle` | all | a line under the title (on `image`, inside the caption overlay) |
| `author` | title | falls back to `deck.yaml`'s `default_author` |
| `date` | title | literal string, or `today` to fill in the build date |
| `image` / `image_alt` / `image_label` / `image_reference` | content, image | on `content`, a boxed image with an optional caption `image_label` (mutually exclusive with `video`/`panels`/`images`/`phase_in`); on `image`, the full-bleed slide image (required unless `phase_in` is set, mutually exclusive with `background`/`phase_images`). Accepts PNG/JPEG/GIF/SVG, a placeholder name, or a `.pdf` file. `image_reference` cites a bibliography key, numbering it in the image's top-right corner |
| `images` | content | 2 or more `{image, label?, alt?, reference?}`; auto-arranged into rows in reading order from their aspect ratios (mutually exclusive with `image`/`video`/`panels`/`phase_in`) |
| `video` | content | mutually exclusive with `image`/`panels`/`images`/`phase_in` |
| `equation` | content | LaTeX (surrounding `$...$`/`$$...$$` optional) shown as display math on a highlight box (accent-tinted; a theme can override `equation-bg`/`equation-fg`), scaled to fill the media column, in place of an image; mutually exclusive with `image`/`video`/`panels`/`images`/`mermaid`/`phase_in` |
| `mermaid` | content | a [Mermaid](https://mermaid.js.org) diagram definition (flowchart, sequence, class, state, gantt, pie, ...) drawn in the media column in place of an image, colored to match the deck theme; usually written as a `|` block. Mutually exclusive with `image`/`video`/`panels`/`images`/`equation`/`phase_in` |
| `panels` | content, split | list of `{image, label, alt?, reference?}`; required for `split` |
| `background` | all except `image` | image path (or `.pdf`); layers behind the slide content |
| `background_opacity` | all | `0.0`-`1.0`, default `1.0` |
| `fit` | image | `contain` (default, fit to screen) or `cover` (fill the screen) |
| `formula` | content/stacked | raw LaTeX (no `$$`), rendered in a `.formula-box` |
| `formula_note` | content/stacked | small caption under the formula |
| `notes` | all | speaker notes, emitted as an HTML comment (not visible) |
| `id` / `classes` | all | override the slide's `id`, or add extra CSS classes |
| `phase_in` | content, stacked, split, image | `true` to reveal the slide step by step — bullets on `content`/`stacked`, panels on `split`, `phase_images` on `image`; see [Phase-in reveal](#phase-in-reveal). On `content`, mutually exclusive with `image`/`video`/`images`/`panels` |
| `phase_level` | content, stacked | with `phase_in: true`: which bullet-indent level drives the reveal (`1` = top-level, default; `2` = first sub-level; ...) |
| `phase_images` | content, image | with `phase_in: true`: list of `{image, alt?, reference?}` shown one per reveal step. On `content`, it stays on the last image once the list runs out, and an entry can instead be `{images: [{image, label?, alt?, reference?}, ...]}` (2 or more) to show a group on that step (arranged like `images`), `{equation: ...}` to show an equation, or `{mermaid: ...}` to show a diagram on that step; on `image`, it's required and each entry is one full-bleed step |

Any slide can carry a `background` + `background_opacity` — this is independent of the
title slide's translucent panel, so a dimmed background works on content slides too.

A `content` slide's `images` (2 or more) are arranged into rows from each image's actual
pixel dimensions (read at build time, no dependency needed). Images keep their order,
filling rows left to right and then top to bottom; within a row they share one height and
span the full width. Of every possible way to break the list into rows, the build picks
the one that leaves the least blank space in the media column while keeping the images
roughly the same size — e.g. 3 squares sit in one row, 4 form a 2x2 grid, two wide plots
stack, two portraits sit side by side.

Every rendered `<img>` is click-to-zoom — clicking it opens a full-screen lightbox fit to
the screen. Inside the lightbox, click the image (or scroll the mouse wheel over it) to
zoom in further, centered on the cursor; click again to zoom back out. Click the dark
backdrop or press `Escape` to close.

### Phase-in reveal

Setting `phase_in: true` on a `content` slide turns its body bullets into a build/reveal
sequence: the deck steps through them one at a time (each step is its own slide for
navigation and `--pdf` purposes, but they all share one number in the `current / total`
counter). Write the bullets exactly as usual — no special syntax:

```markdown
---
layout: content
title: Rollout Plan
phase_in: true
phase_images:
  - image: images/step1.png
  - image: images/step2.png
---

- First we do X
  - a supporting detail
- Then Y
- Finally Z
```

This produces 4 physical steps: bullet 1 revealed → bullet 2 revealed (bullet 1 dims) →
bullet 3 revealed (bullets 1-2 dim) → everything undimmed (the settled final view).
`phase_images` is optional and cycles one image per step, freezing on the last entry
once the list runs out — a single-entry list just keeps one static image on screen
throughout. On `content`, an entry can be `- equation: $a^2+b^2=c^2$` instead of an
image, to show that equation in the media column on that step. Sub-bullets always reveal together with their parent bullet.

`phase_level` changes which indent level drives the reveal (default `1`, top-level
bullets). `phase_level: 2` reveals at the first sub-bullet level instead — a top-level
bullet with sub-bullets appears alongside its first sub-bullet, and a top-level bullet
with no sub-bullets at all just becomes its own step.

`phase_in` works on other layouts too:

- **`stacked`** — reveals the body's bullets exactly as above (`phase_level` included).
  The `formula` box stays visible on every step.
- **`split`** — reveals one panel per step. Earlier panels dim, and panels not shown yet
  keep their space so nothing shifts. A final step shows every panel at full brightness:
  3 panels → 4 steps.
- **`image`** — cycles through `phase_images` full-bleed, one per step, in place of the
  single `image` field. There's no extra final step: 3 images → 3 steps. `title`/`kicker`
  stay as the caption throughout.

```markdown
---
layout: image
title: Time Series
phase_in: true
phase_images:
  - image: images/t0.png
    alt: Frame at t = 0
  - image: images/t1.png
    alt: Frame at t = 1
---
```

### Citations & references

Set `bibliography:` in `deck.yaml` to a file of numbered references — a `.bib` file
(BibTeX, formatted per the American Chemical Society/ACS style — the only style
implemented so far) or a plain file of `"key": "citation text"` lines:

```yaml
bibliography: refs.bib   # or refs.md -- see below
```

Cite from a slide's body with `[@key]`, or several at once with `[@key1; @key2]`:

```markdown
The reaction proceeds via a radical mechanism [@smith2020].
```

References are numbered in the order they're first cited across the whole deck (not
per-slide), rendered inline as a small superscript `[1]` (linking to the paper's DOI
when the bibliography entry has one), collected as a small footnote at the bottom-left
of whichever slide cited them, and listed in full wherever you place a `layout:
references` slide, typically the last one.

Cite from an image instead of body text with `reference: key` (`images[]`/`panels[]`
entries) or `image_reference: key` (the single boxed `image`, or the full-bleed `image`
layout's image) — its assigned number appears as a small badge in the image's top-right
corner.

**BibTeX (`.bib`) files**: a deliberately partial parser, not the full BibTeX spec —
`article`, `book`, `incollection`/`inbook`, and `inproceedings` entries are recognized
and ACS-formatted; anything else falls back to whatever of author/title/year is present.
Not supported: `@string` macros, cross-references, and LaTeX accent/escape sequences
(`{\'e}` stays literal) — write accented characters directly as UTF-8 instead. Journal
names are used exactly as given, with no abbreviation lookup.

**Plain (`.md`) files**: one entry per line, the citation text used exactly as given
(including any HTML you write yourself, e.g. `<i>...</i>`) with an optional DOI as a
third field:

```text
"smith2020": "Smith, J. A.; Doe, J. B. A Study of DNA Nanotechnology. J. Chem. 2020, 12, 34-56." : "10.1021/xyz123"
"jones2019": "Jones, A. Principles of Organic Synthesis. Academic Press: New York, 2019."
```

Citing a key that isn't in the bibliography, or citing anything at all with no
`bibliography:` configured, is a build error — without `bibliography:` set, `[@...]`-
shaped text is otherwise left completely untouched.

On a `phase_in` slide, the footnote list only shows references for whichever bullet,
panel or image is *currently* revealed at each step — not the whole slide's accumulated
citations — with the final "everything undimmed" step (if the layout has one) showing
the full combined list.

### Mermaid diagrams

A `content` slide's `mermaid` field draws a [Mermaid](https://mermaid.js.org) diagram in
the media column, in place of an image:

```yaml
---
layout: content
title: Build pipeline
mermaid: |
  flowchart LR
    A[slides.md] --> B{parse} --> C[index.html]
---
```

Any Mermaid diagram type works (flowchart, sequence, class, state, ER, gantt, pie,
mindmap, ...). A `phase_images` entry can also be `- mermaid: |` to show a diagram on one
reveal step. Click a diagram to zoom it, like an image.

How it works: as with KaTeX, the diagram is drawn in the browser by a bundled copy of
Mermaid (no internet connection needed), which is copied into the output only when a
deck actually uses a diagram. Mermaid can't read CSS variables, so the deck's script
resolves the theme's colors and passes them to Mermaid's `base` theme: node fill
`diagram-node-bg` (default `bg-panel`), node text `diagram-node-fg` (default
`panel-fg`), node border `diagram-border` (default `accent`) and lines/arrows
`diagram-line` (default `fg-muted`); edge labels and other text use `fg`, and the
theme's body font is used throughout. A theme (or a `theme:` override dict) can set any
of those four keys if a diagram needs different colors. A diagram with a syntax error
shows Mermaid's error message in its place instead of breaking the deck. `--pdf` and
`--thumbnail` wait for diagrams to finish drawing.

### Image formats

Image fields take PNG, JPEG, GIF or SVG files (plus `.pdf`, below). An SVG is copied
as-is and stays vector — sharp at any zoom and in `--pdf` output. For the `images`
arrangement its aspect ratio comes from the root `<svg>` tag's `width`/`height` (absolute
units like `px`/`pt`/`in`), or else its `viewBox`; an SVG with neither is a build error
there.

### PDFs as images

Any image-path field (`image`, `background`, `images[].image`, `panels[].image`,
`phase_images[].image`) accepts a `.pdf` file too — no different syntax, just point at
a PDF the way you'd point at a PNG. Its first page is rasterized to a PNG at build time
(200 DPI, not configurable) and flows through the rest of the pipeline exactly like any
other image — same styling, same click-to-zoom, same aspect-ratio-based
`images` arrangement, same citation reference badges. This needs the `pdf-images` extra:

```bash
uv sync --extra pdf-images
```

A PDF referenced on multiple slides is only rasterized once. Only the first page is
used; there's currently no way to pick a different page.

### Placeholder images

Any `image` (on `content` or `image` layouts), `background`, or `images`/`panels` entry
accepts a LaTeX-`mwe`-style placeholder name instead of a real file path — no image asset
needed while drafting: `example-image` (a plain outlined box) or `example-image-a`
through `example-image-z` (the same box with that letter in it, e.g. `example-image-c`).
These are bundled with the package and copied into the build like any other referenced
image. Omitted `alt` text is auto-filled (e.g. "Placeholder image C"), so no warning
fires for a placeholder left without one.

### Deck-wide config (optional)

A `deck.yaml` file next to your `.md` file (or passed via `--config`) sets page-wide
defaults:

```yaml
title: My Presentation      # <title> tag
default_author: Jane Doe    # used when a title slide omits `author`
theme: alomancy              # named preset — see below
bibliography: refs.bib       # numbered citations — see Citations & references above
```

`theme:` accepts either of two forms:

- **A named preset** (a single string) — swaps colors, the body font, and an optional
  watermark image in one line. Shipped presets:

  | Preset | Look |
  |---|---|
  | `default` | The original dark/gold theme (used automatically if `theme:` is omitted — no visual change either way) |
  | `alomancy` | Dark, with ALomancy's brand violet (`#6C2FBE`) as the accent, the "Inter" font, and a subtle ALomancy logo watermark in the bottom-left corner of every slide |
  | `neuefische` | Dark violet background with Neuefische's brand orange (`#f44717`) as the accent, the "Poppins" font, and a subtle Neuefische logo watermark |
  | `fhi` | Light background with dark teal body text/panels, an olive-green (`#a5b631`) accent, the "Poppins" font, and a subtle FHI logo watermark — the one light-background preset, so its title slide overrides `title-fg` (see below) to stay legible |

  New presets are added by registering a `ThemeDefinition` in `deckoction/themes.py` — see
  that module for the fields (`colors`, `font_body`, `watermark`).

- **A raw dict of CSS custom-property overrides** (the original form, still fully
  supported) — layers only the keys you set on top of `assets/style.css`'s own defaults,
  independent of any preset:
  ```yaml
  theme:
    accent: "#4cc9f0"
  ```
  Overridable keys: `bg`, `bg-panel`, `fg`, `fg-muted`, `accent`, `accent-dim`, `border`,
  and `title-fg` — the title slide's `h1`/`subtitle`/author-date text color, which
  defaults to `fg` and only needs setting if `fg` doesn't contrast against the title
  slide's translucent dark panel (a fixed color, independent of the theme) — e.g. a
  light-background theme with a dark `fg`, like `fhi` above, which sets it to white.

## CLI

```
deckoction <input.md>         # build into build/ next to input, then serve it
  [--force]                   # overwrite a non-empty output directory (needed to rebuild)
  [-o DIR]                    # output directory instead of build/ next to input
  [--port N]                  # default: 8000
```

If a preview server from an earlier run is still on the port, it's stopped first; any
other process on the port is an error (use `--port`).

This quick preview shortcut is also available as `slides_md <input.md>` (identical
options). For every other option use the `build` subcommand:

```
python -m deckoction build <input.md> -o <output_dir>
  [--config deck.yaml]        # default: deck.yaml next to input, if present
  [--images-dir DIR]          # default: input file's directory
  [--strict]                  # promote warnings (missing image_alt, unknown fields, etc.) to errors
  [--force]                   # overwrite a non-empty output directory
  [--serve]                   # serve the output with `python3 -m http.server` after building
  [--pdf PATH]                # also export a PDF (one page per slide) to PATH, via headless Chromium
  [--thumbnail PATH]          # also export a PNG screenshot of the title slide to PATH
```

Only images actually referenced by a slide are copied into `<output>/slide_images/`.
A missing referenced image is always a build error, `--strict` or not. A `.pdf` file
can be used anywhere a PNG/JPEG can — its first page is rasterized (200 DPI) and
treated identically from there on — with the `pdf-images` extra (`uv sync --extra
pdf-images`).

`--pdf` and `--thumbnail` both require the `pdf` extra (`uv sync --extra pdf`) plus
a one-time `uv run playwright install chromium` to download the browser.

## Development

```bash
uv sync
uv run pytest
uv run ruff check .
```

CI (`.github/workflows/ci.yml`) runs tests (Python 3.10-3.12), `ruff check`, and a docs
build with warnings promoted to errors on every push/PR — see `docs/contributing.md` for
details.

`examples/demo/slides.md` exercises all six layouts, background alpha, citations, and
KaTeX — useful as both a smoke test and a syntax reference.

`docs/_static/cheatsheet.pdf` (the downloadable cheatsheet linked from the docs site) is
generated from `examples/cheatsheet/slides.md` by `scripts/build_cheatsheet.py` — a
dev-only script needing the `pdf` extra, same as `--pdf`/`--thumbnail`. Re-run it and
commit the result whenever that source deck changes:

```bash
python scripts/build_cheatsheet.py
```
