# Internals Notes for contributors: the contracts that hold the editing stack together. File paths are relative to the repository root. ## The provenance contract The WYSIWYG layer works because **dev builds know where every element came from**. When `build.py` runs with `dev=True` (as the dev server does), it annotates the generated HTML: - `data-rv-src="N"` — the 1-based `.pres` line that produced the element; - `data-rv-src-end="M"` — the last line of a multi-line construct; - `data-rv-implicit` — marks wrappers with no own source line (e.g. the implicit first `> col` of a row); - two `` tags carry the source file name and the SHA-256 of the exact bytes the build read. Two invariants are enforced by tests: 1. **Prod purity**: a production build contains none of these annotations, and stripping them from a dev build yields the prod build **byte for byte** (`tests/helpers.py: strip_dev`). The editor can never leak into an exported deck. 2. **SHA precondition**: line numbers are only meaningful against the exact bytes that were built. Every edit request carries the sha; if the file changed since, the edit is refused (see below) — provenance is never trusted across a stale build. ## The edit protocol The dev server (`src/revealer/serve.py`) exposes a small JSON API under `/__rv__/` (localhost-only, token-guarded, dev builds only): - `GET /__rv__/schema` — the construct grammar as JSON (see [the grammar registry](#the-grammar-registry)). - `GET /__rv__/src?from=N&to=M` — the current source span + file sha. - `POST /__rv__/edit` — `{sha256, edits: [...]}`: a batch of **semantic operations** referencing `.pres` line numbers. - `POST /__rv__/undo` / `redo`, `POST /__rv__/export`, `PUT /__rv__/upload`, and the `history/*` endpoints below. `src/revealer/edit.py` compiles the semantic ops into line-level text primitives and applies them surgically — the rest of the file is never touched, so a GUI session diffs like hand edits. The operations are: `set_col_size`, `set_block_width`, `set_row_height`, `set_stack_height`, `set_pin`, `set_media_size`, `set_row_gap`, `set_grid_gap`, `set_fragment_index`, `move_block`, `delete_block`, `replace_lines`, `insert_lines`, `insert_media`, `reorder_fragments`. Failure semantics: - **409** — the sha precondition failed (the file changed on disk). The client refetches and resyncs; nothing was written. - **422** — the target line no longer matches the construct's own syntax. A provenance bug surfaces as a rejected edit, never as silent corruption. - Batches are all-or-nothing; overlapping edits are rejected; writes are atomic (temp file + `os.replace`) and preserve the file's EOL flavour and trailing-newline state. ## The history model Save history is a **shadow git repository** at `/.rv-history/` (`git --git-dir`, work-tree = the deck folder — invisible to any real repository around it). `serve.py` owns it: - every successful rebuild with changed bytes auto-commits (`auto: HH:MM:SS`), staging the `.pres`, the deck's `.bib` files and any `> include:`d files inside the deck; - manual snapshots from the editor commit with a `save:` prefix; - **undo/redo is a cursor** walking the first-parent chain of this history. Undo moves the cursor to the parent commit and writes that blob back; redo walks forward. The cursor is revalidated against the working bytes on every use (`_resolve_position`) — it is never trusted blindly; - committing while the cursor is detached first appends a **rewind commit** (the cursor's tree), so new work parents onto what the user was actually looking at, and redo history is preserved rather than destroyed. Restores from the time machine use the same mechanism, which is why undo works across restores. Endpoints: `GET /__rv__/history` (list), `GET /__rv__/history/diff`, `POST /__rv__/history/commit|restore|preview`. Without git installed, the server degrades to a single before-image undo slot. ## The grammar registry `src/revealer/grammar.py` is the **single source of truth** for the `.pres` construct grammar. Each `ConstructSpec` carries the opener pattern, head-token grammar, end token, nesting/terminator behaviour, CSS classes and editor metadata. From this one table derive: - the parser dispatch and block/paragraph atomicity rules in `build.py`; - the anchor and token grammars of the semantic edit ops in `edit.py`; - the JSON schema served at `GET /__rv__/schema`, which drives the browser editor's construct model, panel fields and cheatsheet; - the generated [Constructs](reference/constructs.md) and [Directives](reference/directives.md) reference pages (`Documentation/gen_reference.py`, run at every docs build). Behaviour (body rendering, emission templates, provenance attachment) stays in `build.py`; the registry holds patterns and metadata only. ## The fit engine `fitSlide` (in `src/revealer/data/js/revealer.js`) positions each slide's body between the fixed header and footer, then shrinks every block's `--rv-fontscale` with a binary search until its content fits its box. The engine rests on three invariants; break one and slides render with collapsed (floor-scale) or overflowing text: - **Measurements must respond synchronously.** A probe sets `--rv-fontscale` and immediately reads `scrollHeight`, so no CSS transition or animation may delay layout changes on slide content. reveal.css transitions *all* properties on `.fragment`s; the base stylesheet restricts that to paint-only properties (opacity, visibility, transform, colours), and while fitting, an `html.rv-measuring` guard class enforces the same restriction on every element, whatever the stylesheet. `rv_fitBlock` additionally verifies that its first probe moved the measurement before trusting a search, and keeps the previous scale (retrying next frame, bounded) when it did not. - **Every layout-changing event re-fits.** All triggers — `ready`, `slidechanged`, `fragmentshown`/`fragmenthidden`, `resize`, media loads, `document.fonts.ready` — funnel through one scheduler (`rv_queueFit`): a synchronous pass so the new state paints fitted, a next-frame pass, and a 300 ms pass for async renderers (web fonts, KaTeX). Re-arming cancels the pending deferred passes, so a timer armed for one slide state can never fire in the middle of another. - **Fits are idempotent.** `rv_fitBlock` re-measures from scale 1 each time, so repeated passes converge to the same value regardless of what was applied before — arrival timing must never change the final layout. The regression suite for all three lives in `src/revealer/data/js/test/suite-fit.js`. ## Editor architecture The browser editor is thirteen small JavaScript IIFE modules under `src/revealer/data/js/editor/`, sharing a single `window.RV` namespace (state bus, cross-module function table, UI chrome helpers). They are injected into dev builds only — the wheel and exported decks never ship them. The module list, load order and state-bus conventions are documented in `src/revealer/data/js/editor/README.md`. ## Test harness - **Python unit tests** — `tests/`, run with `pytest -q` from that folder. Hermetic: a deck fixture builds fully offline against an empty `reveal.js/` scaffold. Committed goldens freeze the prod build output byte-for-byte (`UPDATE_GOLDEN=1` to regenerate deliberately). - **In-browser JS suites** — `src/revealer/data/js/test/` (wheel-excluded) holds a runner (`rvt.js`) and `suite-*.js` files exercising the editor against a real served deck. The dev server exposes the runner at `/__rv__/test` (test mode only); `tests/test_editor_js.py` drives it with headless Chrome and collects results. Requires Chrome and a reveal.js checkout at `Demo/reveal.js`. - **Docs** — `sphinx -W` (warning-free is enforced); the reference pages are regenerated at every build and committed.