Skip to content

LFM 1.0 — the specification

Version: 1.0 · Status: Ratified · Date: 2026-10-08 · Owner: @saden1 License: CC-BY-4.0

LFM (Lattice-Flavored Markdown) is the authoring dialect of the Lattice slide engine. Its name follows GitHub-Flavored Markdown: LFM is a profile of Markdown, not a new language.

LFM = CommonMark + GFM task lists + the Lattice extension set.

A document written in LFM is a valid Markdown document. Every LFM extension MUST render readably in a CommonMark viewer that has no knowledge of Lattice — GitHub, GitLab, Bitbucket, and Confluence included. That property is graceful degradation, the governing rule of the standard and the criterion for adopting any future extension (§5).

This document specifies what LFM adds to Markdown, how each addition degrades, and what makes a document conformant. It does not specify a parser: LFM rides CommonMark, and any conformant CommonMark/GFM parser plus the extension handlers below is a conformant LFM implementation.


LFM defines three levels so a tool can advertise precisely what it supports.

LevelNameWhat it means
L0Markdown-faithfulThe document is valid CommonMark + GFM. An LFM-unaware renderer produces a readable result. Every conformant LFM document is L0.
L1Lattice-structuredThe renderer understands the slide and block extensions (§2–§3): _class directives, front-matter directives, the card grammar, and state markers. It produces Lattice layouts.
L2Lattice-diagnosticThe tool additionally implements the Diagnostic Protocol: it validates a document, reports findings with stable rule IDs, and offers machine-applicable fixes.

A conformant LFM document is valid at L0 and follows the grammar in §2–§4 for every Lattice construct it uses. A conformant LFM renderer declares the highest level it implements.

A slide selects a Lattice component with a Marpit class directive — an HTML comment, so it is invisible in every Markdown renderer:

<!-- _class: split-compare -->

The value is a space-separated list of one component name plus zero or more modifier tokens (§4). The complete, machine-readable vocabulary of names, modifiers, slots, and skeletons is published in dist/docs/grammar.json, generated from the component manifests.

  • Degrades to: nothing visible (HTML comment). L0-clean.

Slides are separated by a thematic break on its own line (---). This is CommonMark <hr>; an unaware renderer shows horizontal rules between sections.

A renderer MAY also start a slide at a heading. The reference implementation does by default: within each ----separated section, every # or ## after the section’s first heading starts a new slide. A deck sets split: rule to divide at --- only; LFM 1.1 specifies this. A deck whose every ----separated section holds at most one # or ## heading has the same slides either way, so a 1.0 deck that wants to render the same everywhere is written that way.

  • Degrades to: horizontal rules. L0-clean.

A leading YAML front-matter block carries deck-wide directives. LFM recognizes, in addition to Marpit’s own (theme, paginate, _class, …):

KeyValueEffect
mode:boardroom | sketch | sketch-cleanThe deck’s rendering mode. boardroom is the default.
finish:a finish name, or noneThe backdrop drawn behind every slide. The names are the finishes the engine ships (FINISH_NAMES in lib/core/resolve-finish.js). Unknown values are a diagnostic (§ unknown-finish); so are boardroom, sketch and sketch-clean, which are modes.
logo:pathDeck logo injected into the masthead.

These three keys are the complete LFM-added front-matter surface in 1.0; every other recognized key belongs to Marpit. The many other settings the engine reads (lib/base/base.registers.docs.md) arrive in LFM 1.1.

A delivery register is a third category, and 1.0 does not define one: pace: (2026-08-04) tells a self-presenting player how long to hold on a new slide before speaking, and how long to let a slide’s last sentence land before the next appears. It changes no rendered pixel, so a conforming renderer ignores it exactly as it ignores application metadata — but unlike application metadata it belongs to the DECK and must survive an export. See lib/core/resolve-pace.mjs.

Note that a host application may read its own front-matter keys that LFM does not define and a conforming renderer ignores — the Lattice Studio reads title: to name a deck in its UI and in export filenames, for instance. Such keys are application metadata, not language surface: a conforming implementation may ignore them and still render the deck correctly.

  • Degrades to: front matter is hidden or shown as a metadata table, depending on the host. L0-clean (it is YAML front matter, which every GFM host already special-cases).

Card-style and ledger layouts read a title + body from a nested list (a card-style layout is one whose grammar declares a card slot; the set is published in dist/docs/grammar.json). The canonical shape is:

- Title
- body text

The inline form - **Title.** body is not valid LFM on a card-style layout: the body inherits the title’s weight. This is enforced as a diagnostic (card-style-inline-title, with an autofix). Ledger/numbered layouts use the ordered form:

1. Name
- body text
  • Degrades to: a nested bullet list — exactly what it reads as. L0-clean.

A single-character marker at the start of a list item (or alone in a table cell, or inside single-backtick inline code) encodes an answer. The grammar is shared across checklist, verdict-grid, obligation-matrix, roadmap, pricing, state-cells tables and inline marks, and each marker MUST carry the same answer in every one of them. A component MAY name an answer in its own words (a roadmap’s missed for [!]); it MUST NOT change the answer.

MarkerAnswerNotes
[x]yes / done / metGFM task-list syntax.
[-]partly / in progressNot GFM syntax (see §5.1).
[!]no / failed / not metNot GFM syntax (see §5.1).
[?]unknown — looked at, cannot be settledNot GFM syntax (see §5.1).
[ ]open — not started, not assessedGFM task-list syntax.
[/]does not apply / out of scopeNot GFM syntax (see §5.1).

[X] (capital) is not a marker. GFM reads it as a checked box, the opposite of what a cross suggests, so LFM leaves it literal.

  • [x] / [ ] degrade to: real checkboxes in any GFM host. L0-clean.
  • [-] / [!] / [?] / [/] degrade to: literal text ([-] …). Readable but not a checkbox — LFM 1.0’s only non-GFM-clean construct (§5.1).

3.3 Fenced sub-languages (charts, diagrams & motion)

Section titled “3.3 Fenced sub-languages (charts, diagrams & motion)”

A fenced code block with a recognized info string is a sub-language. Each is its own mini-spec; LFM only requires that it degrades to a code block.

Info stringSub-languageDegrades to
functionplotA function-plot graph spec (JSON body: mathematical functions + axes). Used today by the math component’s canvas variant to draw a curve beside an equation.A code block showing the JSON config. L0-clean.
mermaidMermaid (passthrough), used by the diagram component.A code block. L0-clean (and Mermaid-aware hosts render the diagram).
mathA TeX display equation, used by the math component, written as a fence instead of $$…$$.A code block showing the TeX. L0-clean.
animaAn Anima motion-scene spec (JSON body), used by the scene component to animate its poster still on the HTML and present surfaces. The PDF keeps the poster.A code block showing the JSON spec. L0-clean.

Each fence is named after the library that renders it: functionplot for function-plot, mermaid for Mermaid, anima for Anima, Lattice’s own motion host. This matches how the equation half of the math component uses standard $$…$$ to pass through to KaTeX. LFM does not rebrand these libraries or claim to own their config languages.

What LFM owns is narrow and consistent across every fence: the fence registration, the SVG theming that makes the output inherit the deck’s palette tokens, and the degradation contract. The fence body is the renderer library’s own configuration language, not Markdown, and LFM does not own its schema — including Anima’s, which Lattice writes but which belongs to the Anima host (its scene parser validates it), not to this spec. dist/docs/grammar.json records each fence’s library, component, and deprecated aliases; the config schema stays with that library, so this prose spec stays stable as those options evolve.

Deprecated alias. The functionplot fence was previously named latticeplot. That name implied a Lattice-owned grammar that does not exist (the body is function-plot’s, verbatim), so it was renamed for honesty. ```latticeplot is accepted as a deprecated alias for one release and will be removed in a future major version. See the 2026-06-13-lfm-standard decision note.

  • Eyebrow / subtitle / hero number and similar slot conventions are ordinary Markdown (a leading inline-code span, an <em>-wrapped unit, an <h2> lifted into a panel). They are structural reads over standard Markdown, documented per component in grammar.json, and are L0-clean by construction.

An HTML comment on a slide that is neither a directive nor a tooling pragma is that slide’s speaker note. This is Marp’s own semantics, unchanged:

# The slide title
<!-- Open cold. Pause two seconds before the first word. -->

A slide may carry several note comments; they are concatenated in order. Two comment kinds are not notes, matching Marpit exactly:

  • Directives — <!-- _class: … -->, <!-- paginate: true -->, and any comment that parses as a block of known directive keys (§2.1, §2.3).
  • Tooling pragmas — markdownlint-*, prettier-ignore[-start|-end], and lint disable|enable|ignore … (the editor/formatter control comments). The recognized set is Marpit’s magicCommentMatchers, mirrored in the reference implementation lib/authoring/notes-core.js so every render path agrees on the note/non-note boundary.

A conformant L1 renderer SHOULD surface notes through whatever presenter channel it offers. How a note is presented is implementation-defined; what counts as a note is the contract above, and that boundary is what conformance fixes. Among the reference render paths today, Lattice’s emulator materialises notes — a per-page PDF text annotation plus a hidden aside.lattice-notes element in the HTML sidecar; under marp-cli the same comments are marp-core’s native notes (surfaced by marp --pdf-notes or a PPTX export); the VS Code preview does not surface them yet.

The directive and tooling-pragma exclusions above are the pinned part of the contract: they are identical across implementations (the reference implementation pins them against marp-core’s documented comment-collection behavior — hardcoded expected outputs, not a live marp-core comparison; see test/unit/authoring/notes-core.test.js). A few rare comment-placement cases are explicitly not guaranteed identical, because they depend on how a given parser segments comments rather than on the note boundary: a comment buried inside a raw HTML block, two comments with no blank line between them, and a single comment that mixes a directive with prose. A conformant tool MAY collect these differently; authors should keep a note in its own comment, on its own line.

  • Degrades to: nothing visible — an HTML comment is invisible in every Markdown renderer, and on GitHub/GitLab a comment is already how an author leaves an out-of-band remark. L0-clean.

_class tokens are one component name plus modifier tokens. The full list is generated, not enumerated here, so it cannot drift:

  • Component names — dist/docs/components.json (vocabularies.buckets and the per-component entries) and the human catalog dist/docs/components.md.
  • Per-component grammar — dist/docs/grammar.json: for each component, its _class token, its required and optional slots (CSS selector + description), its skeleton, and the fences/state-markers it reads.
  • Modifiers — recognized by set membership or by these prefix families: tint-, mark-, with-, at-, no-, tone-, treatment-, checks-, plus the universal/semi-universal variant sets. The authoritative prefix list is MODIFIER_PREFIXES in lib/authoring/lint-core.js; the enumerated variant tiers are cataloged in design/design-system.md §6.5.

An unrecognised token is a diagnostic (unknown-class) with a did-you-mean suggestion, never a hard parse error — an LFM document with a typo is still a valid Markdown document.

5. Graceful degradation — the governing rule

Section titled “5. Graceful degradation — the governing rule”

Every LFM extension MUST render as readable Markdown in an LFM-unaware renderer. This is the property that makes LFM safe for a host like GitHub to render, and safe for an author to write without lock-in.

Rule for future extensions: no construct may be added to LFM without a row in the degradation table (§2–§3) stating exactly what an unaware renderer shows. If a construct cannot degrade readably, it is rejected or redesigned.

LFM 1.0 has exactly one: the [-], [!], [?] and [/] state markers (§3.2). GFM task-list syntax covers only [x] and [ ], so the other four render as literal text in a vanilla GFM host. They are retained because the six-answer grammar is load-bearing across every stateful component and the literal-text fallback is readable. A future LFM version MAY introduce GFM-clean aliases. Until then, this is a documented exception, not a conformance failure.

  • CommonMark — LFM is a strict superset. Every CommonMark document is a valid (if Lattice-inert) LFM document.
  • GFM — LFM adopts GFM task lists and tables. The only divergence is §5.1.
  • Marpit / Marp — LFM’s slide model (_class, --- separators, front matter) is Marpit-compatible; Lattice’s own engine natively re-implements that model (zero @marp-team runtime dependency — see engineering/marp-independence.md), it isn’t a Marp wrapper. Marpit gives you slides and a directive syntax; LFM adds what Marpit does not specify — a named, versioned, conformance-levelled contract, the degradation guarantee (§5), a fixed component/modifier vocabulary (§4), and a stable diagnostic protocol. LFM names and constrains the subset that produces boardroom layouts.
  • Mermaid — embedded as a fenced sub-language (§3.3), unchanged.

LFM follows SemVer at the standard level, tracking the engine’s stability contract: layouts and tokens are stable surfaces (see CHANGELOG.md).

  • Major — removing or breaking an extension, or breaking degradation.
  • Minor — adding an extension (with its degradation row) or a component.
  • Patch — clarifications and editorial fixes.

The machine-readable surface (grammar.json, components.json) is regenerated from the manifests on every build, so it always matches the engine that ships alongside this document.

Conformance is verifiable at each level, with a distinct test shape per level:

  • L0 (degradation). A fixture pairs an LFM source fragment with the readable Markdown a Lattice-unaware renderer MUST produce. The degradation rows in §2–§3 are the normative seed for this corpus.
  • L1 (structure). A fixture pairs an LFM document with the components and slots a renderer MUST resolve from it — the _class token, the card-grammar reads, the state-marker semantics. The per-component grammar in dist/docs/grammar.json is the oracle.
  • L2 (diagnostics). A fixture pairs an LFM document with the exact set of findings (diagnostics §1) a tool MUST emit: rule ID, severity, slide, and any autofix result. The reference implementation’s lint fixtures are the seed and its output is the v1 oracle.

The shared test cases live in spec/conformance/lfm/: small decks, each with the L0, L1 and L2 results it must produce, in JSON a second implementation can read. They cover §2 and §3. The reference adapter, tools/lfm-conformance.js, runs them against the Lattice engine in the unit tier. Where a case and this prose disagree, the disagreement is a defect in one of them, and the owner decides which.

LFM itself executes nothing: the prose layer is CommonMark, and every extension degrades to valid Markdown. The security surface is the fenced sub-languages a host renders (§3.3): Mermaid and function-plot both produce SVG from author-supplied configuration. A host that renders untrusted LFM MUST treat them as it treats any embedded renderer.

  • Sanitise the rendered SVG. Strip scripts, foreign objects, and event handlers before inlining the output — exactly as a host already does for a mermaid block.
  • Bound the work. A fence body is attacker-controllable. A host SHOULD cap render time and output size to resist denial-of-service from a pathological config.
  • Resolve asset paths under the host’s policy. The logo: directive (§2.3) and any local-file reference resolve against the host’s asset policy, not LFM’s; a host MUST NOT widen its file-access surface to render LFM. The Lattice reference engine gates local files behind an explicit --allow-local-files flag.

The component, modifier, and fence vocabulary is closed to the manifest set in LFM 1.0: a third party cannot register a new _class component, modifier prefix, or fence into the standard at authoring time. New vocabulary enters LFM the way every current entry did — a component manifest (or a fence registration) is added to the engine, and the build projects it into grammar.json, so the published grammar and the linter vocabulary update together and cannot drift (§4).

An unrecognised token is therefore a diagnostic (unknown-class), never a parse error: an author who reaches for vocabulary that does not exist is warned, not blocked. A registration path for out-of-tree vocabulary — an org-private modifier family, a third-party fence such as vega-lite — is deferred (§11).

LFM 1.0 deliberately does not:

  • Define a parser or a formal grammar (ABNF/EBNF) for the prose layer. LFM rides CommonMark; the parser is CommonMark’s. grammar.json is a structural projection (selectors + skeletons), not a parser grammar.
  • Re-encode the [-] / [!] / [?] / [/] state markers into GFM-clean glyphs (§5.1). The current glyphs are documented, not changed, in 1.0.
  • Open a registration path for out-of-tree components, modifiers, or fences (§10).
  • Ship a Language Server or character-precise diagnostic ranges. The diagnostic protocol is their foundation; see diagnostics §6.
  • License. This specification — the prose under spec/ — is published under CC-BY-4.0. Anyone may implement LFM (a renderer, a linter, a remark plugin) and redistribute the spec, with attribution. The Lattice engine’s code is AGPL-3.0; the spec carries its own license because a normative document is a different artifact from the reference implementation.
  • Owner. @saden1 owns this spec and signs off every change to its meaning.
  • Steward. The Laticent project stewards LFM. The spec is the owned asset; conformant implementations are interchangeable, and a second implementation is an explicit adoption goal.
  • Amendments. Changes follow the SemVer rules in §7 and land through the project’s normal review — a PR against spec/, recorded in the history below and in CHANGELOG.md. Any change to a normative MUST/MAY requires a version bump per §7.
VersionDateChange
1.0-draft2026-06-13Initial draft. Formalises the existing extension set, conformance levels, degradation table, and the companion diagnostic protocol.
1.0-draft2026-09-24§3.3 registers the anima fence (JSON body, read by scene). The engine already rendered it; the table and grammar.json now say so. Additive — a new extension with its degradation row (§7).
1.0-draft2026-09-24§3.2 state markers: six answers, one meaning each in every component. Adds [!] (no) and [?] (unknown); [ ] is “open” everywhere, where verdict-grid used to read it as “not met”. §5.1 lists the new non-GFM markers. A breaking change to a normative meaning, landed in the pre-ratification draft per §7; the engine’s changelog carries the Breaking: line. Record: engineering/decisions/2026-09-24-six-state-marks.md.
1.02026-10-08Ratified as the core it describes, with an owner. §2.3 now matches the engine: boardroom, sketch and sketch-clean are mode: values (they moved off finish: before ratification), and finish: names a backdrop. §2.2 notes that a renderer may also divide at headings, as the reference implementation does by default, and when that changes nothing. §3.3 lists the math fence, which grammar.json already recorded. §8: the shared test cases ship in spec/conformance/lfm/. Owner ruling: engineering/decisions/2026-10-08-spec-audit.md §8.2.

Appendix A — A worked example (informative)

Section titled “Appendix A — A worked example (informative)”

A three-slide LFM deck, and how the same source reads in each environment:

---
theme: indaco
mode: boardroom
paginate: true
---
<!-- _class: title -->
# Q3 Strategy
`Board review · 2026-06-13`
How we get to plan by year-end.
---
<!-- _class: checklist -->
## Readiness
- [x] Revenue plan signed
- [-] Hiring partially staffed
- [ ] Security audit scheduled
---
<!-- _class: cards-grid -->
## Bets
- Expand EU
- Two markets live by Q4.
- Ship the API
- Private beta with six design partners.

In a Lattice-unaware viewer (GitHub, plain CommonMark):

  • The front matter is hidden or shown as a metadata table.
  • The _class comments are invisible.
  • --- renders as horizontal rules between sections.
  • [x] and [ ] render as real checkboxes; [-] renders as the literal text [-] (§5.1).
  • The eyebrow code-span and the “Bets” nested list render as ordinary inline code and a nested bullet list.

A reviewer on GitHub sees a titled, sectioned checklist and a list of bets — the document is completely readable with no Lattice support.

In Lattice (L1+):

  • Slide 1 renders the title component: display headline, eyebrow, standfirst.
  • Slide 2 renders checklist, mapping [x] / [-] / [ ] to pass / partial / todo states.
  • Slide 3 renders cards-grid, lifting each card’s first line to a bold title above its body.

Nothing in the source is lost on the way down, and nothing Lattice-specific leaks up into the degraded view.


This spec is generated-adjacent: tools/build-docs-portal.js builds the vocabulary and per-component grammar it points to (dist/docs/grammar.json, dist/docs/components.json) from the component manifests. This prose is the stable contract; the machine artifacts are the always-current detail.