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.
1. Conformance levels
Section titled “1. Conformance levels”LFM defines three levels so a tool can advertise precisely what it supports.
| Level | Name | What it means |
|---|---|---|
| L0 | Markdown-faithful | The document is valid CommonMark + GFM. An LFM-unaware renderer produces a readable result. Every conformant LFM document is L0. |
| L1 | Lattice-structured | The renderer understands the slide and block extensions (§2–§3): _class directives, front-matter directives, the card grammar, and state markers. It produces Lattice layouts. |
| L2 | Lattice-diagnostic | The 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.
2. Slide-level extensions
Section titled “2. Slide-level extensions”2.1 The _class directive
Section titled “2.1 The _class directive”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.
2.2 Slide separators
Section titled “2.2 Slide separators”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.
2.3 Front-matter directives
Section titled “2.3 Front-matter directives”A leading YAML front-matter block carries deck-wide directives. LFM recognizes,
in addition to Marpit’s own (theme, paginate, _class, …):
| Key | Value | Effect |
|---|---|---|
mode: | boardroom | sketch | sketch-clean | The deck’s rendering mode. boardroom is the default. |
finish: | a finish name, or none | The 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: | path | Deck 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).
3. Block-level extensions
Section titled “3. Block-level extensions”3.1 The card grammar (nested-list slots)
Section titled “3.1 The card grammar (nested-list slots)”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 textThe 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.
3.2 State markers
Section titled “3.2 State markers”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.
| Marker | Answer | Notes |
|---|---|---|
[x] | yes / done / met | GFM task-list syntax. |
[-] | partly / in progress | Not GFM syntax (see §5.1). |
[!] | no / failed / not met | Not GFM syntax (see §5.1). |
[?] | unknown — looked at, cannot be settled | Not GFM syntax (see §5.1). |
[ ] | open — not started, not assessed | GFM task-list syntax. |
[/] | does not apply / out of scope | Not 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 string | Sub-language | Degrades to |
|---|---|---|
functionplot | A 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. |
mermaid | Mermaid (passthrough), used by the diagram component. | A code block. L0-clean (and Mermaid-aware hosts render the diagram). |
math | A TeX display equation, used by the math component, written as a fence instead of $$…$$. | A code block showing the TeX. L0-clean. |
anima | An 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
functionplotfence was previously namedlatticeplot. That name implied a Lattice-owned grammar that does not exist (the body is function-plot’s, verbatim), so it was renamed for honesty.```latticeplotis accepted as a deprecated alias for one release and will be removed in a future major version. See the2026-06-13-lfm-standarddecision note.
3.4 Other inline conventions
Section titled “3.4 Other inline conventions”- 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 ingrammar.json, and are L0-clean by construction.
3.5 Speaker notes
Section titled “3.5 Speaker notes”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], andlint disable|enable|ignore …(the editor/formatter control comments). The recognized set is Marpit’smagicCommentMatchers, mirrored in the reference implementationlib/authoring/notes-core.jsso 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.
4. The component & modifier vocabulary
Section titled “4. The component & modifier vocabulary”_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.bucketsand the per-component entries) and the human catalogdist/docs/components.md. - Per-component grammar —
dist/docs/grammar.json: for each component, its_classtoken, 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 isMODIFIER_PREFIXESinlib/authoring/lint-core.js; the enumerated variant tiers are cataloged indesign/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.
5.1 Known non-GFM-clean constructs
Section titled “5.1 Known non-GFM-clean constructs”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.
6. Relationship to other standards
Section titled “6. Relationship to other standards”- 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-teamruntime dependency — seeengineering/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.
7. Versioning
Section titled “7. Versioning”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.
8. Conformance testing
Section titled “8. Conformance testing”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
_classtoken, the card-grammar reads, the state-marker semantics. The per-component grammar indist/docs/grammar.jsonis 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.
9. Security considerations
Section titled “9. Security considerations”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
mermaidblock. - 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-filesflag.
10. Adding to the vocabulary
Section titled “10. Adding to the vocabulary”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).
11. Non-goals (LFM 1.0)
Section titled “11. Non-goals (LFM 1.0)”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.jsonis 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.
12. Governance & license
Section titled “12. Governance & license”- License. This specification — the prose under
spec/— is published under CC-BY-4.0. Anyone may implement LFM (a renderer, a linter, aremarkplugin) 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 inCHANGELOG.md. Any change to a normative MUST/MAY requires a version bump per §7.
Document history
Section titled “Document history”| Version | Date | Change |
|---|---|---|
| 1.0-draft | 2026-06-13 | Initial draft. Formalises the existing extension set, conformance levels, degradation table, and the companion diagnostic protocol. |
| 1.0-draft | 2026-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-draft | 2026-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.0 | 2026-10-08 | Ratified 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: indacomode: boardroompaginate: 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
_classcomments 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
titlecomponent: 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.