Front Matter Source Highlighting
Note
Status: implemented. Teach the Source editor that a script may begin with YAML front matter, so metadata no longer reads as dialogue-shaped Markdown.
Table of contents
- Goal and scope
- Functionality checklist
- Ubiquitous language
- Writer-facing behavior
- Architecture
- Interfaces and responsibilities
- Key design decisions
- Error and boundary cases
- Integration
- Testability
- Alternatives not chosen
Goal and scope
The compiler and Preview already agree that a canonical leading --- … --- block is
front matter: metadata about the script, not dialogue. The Source editor alone configures
plain markdown(), whose parser reads the opening fence as ordinary Markdown and the YAML keys as
paragraph text.
This component gives CodeMirror the same document shape the writer sees: optional YAML front matter followed by a Markdown body. Front matter receives YAML syntax highlighting and muted delimiter lines; the body keeps the existing Markdown and compiler-projected DialogueDown highlighting.
In scope: the canonical front-matter syntax already rendered by Preview; immediate YAML highlighting, indentation, comments, and folding in Source; preservation of Markdown body behavior; focused unit and browser regressions; and the committed report bundle.
Out of scope: changing compiler or Preview behavior; validating YAML as project
configuration; making front matter part of dialogue.toml; supporting every lenient delimiter
Markdig accepts; and projecting front matter from the compiler.
Functionality checklist
- [x] Add the official
@codemirror/lang-yamlpackage as a direct frontend dependency. - [x] Replace plain
markdown()withyamlFrontmatter({ content: markdown() }); do not install Markdown support a second time. - [x] Parse canonical front matter at the start of the document, with the official parser's recoverable behavior while a closing fence is incomplete.
- [x] Highlight front-matter delimiters as metadata and its content as YAML.
- [x] Reuse the report's existing theme variables for YAML keys, values, comments, and punctuation.
- [x] Preserve Markdown headings, links, lists, folding, and compiler-projected semantic tokens in the body.
- [x] Preserve front-matter-aware Preview rendering and source/preview scroll synchronization.
- [x] Leave documents without front matter unchanged.
- [x] Keep malformed or unterminated front matter editable through the YAML parser's recovery.
Ubiquitous language
| Term | Meaning |
|---|---|
| Front matter | An optional YAML metadata region at the start of a dialogue script, enclosed by canonical --- lines. |
| Delimiter | The opening or closing line containing exactly three dashes. |
| Body | The Markdown dialogue source after the optional front matter. |
| Source language | The CodeMirror language support that parses front matter as YAML and the body as Markdown. |
| Canonical syntax | An opening --- at offset 0 and a later closing ---, each alone on its line, using LF or CRLF. |
Writer-facing behavior
Given:
---
title: Scene 1
tags: [intro, tutorial]
draft: true
---
# Arrival
Alice: Hello.
Source reads as two regions:
| Region | Presentation |
|---|---|
--- delimiter lines |
Muted metadata punctuation |
title, tags, draft |
YAML property names |
| strings, arrays, and booleans | YAML values using the existing report palette |
| YAML comments | Muted italic comments |
# Arrival onward |
Existing Markdown plus compiler-projected DialogueDown semantics |
Front matter is metadata, not ignored unmodeled Markdown. It therefore does not receive the
eye-marked ignored-region treatment or DLG1114; the Source language identifies it immediately
and styles its YAML structure.
Architecture
@codemirror/lang-yaml provides the official mixed-language wrapper. Its outer parser recognizes
the optional front-matter region, then mounts the YAML parser inside that region and the existing
Markdown parser inside the body.
flowchart LR
S["Source text"] --> F["yamlFrontmatter wrapper"]
F -->|FrontmatterContent| Y["YAML parser"]
F -->|Body| M["Markdown parser"]
Y --> H["markdownHighlightStyle<br/>metadata palette"]
M --> H
H --> E["CodeMirror Source editor"]
P["Compiler semantic tokens"] --> E
The compiler remains authoritative for DialogueDown grammar and configured policy decisions. CodeMirror parses YAML because it is an established external language, just as it already parses Markdown. No DialogueDown grammar is duplicated in TypeScript.
Interfaces and responsibilities
| Type / seam | Responsibility | Collaborators |
|---|---|---|
yamlFrontmatter |
Build one language support with a YAML front-matter region and Markdown body. | @codemirror/lang-yaml, markdown() |
source-language.ts |
Export sourceLanguage, the mixed language source-view.ts installs. |
CodeMirror extensions |
markdownHighlightStyle |
Style YAML metadata tags alongside the existing Markdown tags. | @lezer/highlight tags, CSS variables |
splitFrontMatter |
Remain the Preview/scroll-sync boundary for canonical front matter. | text.ts, scroll-sync.ts |
MarkdigMarkdownParser |
Remain the compiler boundary; recognize and discard front matter. | Markdig YAML extension |
Key design decisions
D1 — Use CodeMirror's official YAML-front-matter wrapper
@codemirror/lang-yaml exports:
yamlFrontmatter({ content: markdown() })
The wrapper uses mixed parsing: YAML inside FrontmatterContent, Markdown inside Body. It
provides YAML syntax nodes, highlighting, indentation, comments, and folding without a custom
outer parser or a second grammar implementation.
The package is an official CodeMirror module, MIT licensed, approximately 15.7 KB unpacked, and
receives about 4.1 million weekly npm downloads. Its new parser dependency,
@lezer/yaml, is approximately 98 KB unpacked; the other CodeMirror/Lezer dependencies are
already present. It is preferable to hand-writing a Lezer block parser for one standard document
shape.
D2 — Canonical fences define the editor contract
The wrapper recognizes exact --- lines at the start of the document and as the closing
delimiter. This matches splitFrontMatter(), the Preview, the scroll-sync offset, examples, and
the writer-facing contract.
Markdig is deliberately more lenient: it also accepts delimiter lines with trailing whitespace
and a ... closer. Those forms remain compiler tolerance, not syntax the editor promises to
present as front matter. The official parser may recover an initial exact --- as incomplete
front matter through EOF when no exact closing fence exists; ... is YAML content, not a closing
fence. Supporting Markdig's extra forms as first-class editor syntax would require a custom outer
parser and cross-language conformance tests for little writer benefit.
D3 — Front matter is metadata, not ignored Markdown
Front matter is discarded unconditionally before the unmodeled-node policy runs. Calling it
Ignore would incorrectly imply that [markdown.unmodeled] can change its fate. The Source
editor therefore uses YAML/meta highlighting rather than the IgnoredMarkdown token or ignored
Preview region.
The delimiters use tags.meta; YAML syntax reuses the Config editor's CSS-variable palette:
| YAML syntax | Highlight tags | Theme |
|---|---|---|
| property names | tags.definition(tags.propertyName) |
--md-heading |
| strings | tags.string |
--md-code |
| plain scalars (including numbers and booleans) | tags.content |
--md-link |
| comments | tags.comment, tags.lineComment |
--md-muted, italic |
| brackets | tags.bracket, tags.squareBracket |
--md-muted |
The YAML style is scoped to yamlLanguage, so generic tags such as string and content cannot
recolor Markdown link titles or prose. The outer delimiter's tags.meta style remains in the
Markdown-level style. No new theme colors are needed.
D4 — Parse locally rather than project an invariant
A compiler token could mark the front-matter range, but it would arrive only after compilation and would provide no YAML syntax tree, indentation, folding, or comment behavior. The front-matter fate never varies by project, and YAML is not DialogueDown grammar, so immediate local parsing is the simpler and more capable seam.
This follows the boundary in Compiler-Projected Editor Semantics: the compiler projects DialogueDown semantics; the client handles standard Markdown and embedded YAML with established parsers.
Error and boundary cases
| Case | Behavior |
|---|---|
| Canonical closed block at offset 0 | Parsed and highlighted as YAML front matter. |
| CRLF delimiters | Parsed identically to LF. |
Empty front matter (--- then ---) |
Valid empty metadata region; body remains Markdown. |
| YAML comments and nested structures | Highlighted and indented by the YAML language package. |
| Invalid YAML or duplicate keys | CodeMirror recovers and keeps the source editable; this feature adds highlighting, not YAML validation or a compiler diagnostic. |
Opening --- without a closing delimiter |
Recovered as incomplete YAML front matter through EOF; the editor stays usable and shows parser recovery. |
--- after body content |
Remains a thematic break or other ordinary Markdown. |
... closer |
Not a CodeMirror closing fence; parsed as YAML content and recovered through EOF, though Markdig may accept it. |
| Whitespace after a fence | Not part of the canonical editor contract; remains ordinary Markdown, though Markdig may accept it. |
| Document without front matter | Existing Source behavior is unchanged. |
Integration
- Source editor:
source-language.tsexports the wrapper in place of a baremarkdown(). The returnedLanguageSupportalready carries the inner Markdown support, so installing both would be redundant and could produce conflicting language state. Compiler semantic-token decorations, completions, folding services, and the other non-language extensions remain separate. If the Markdown language needs options, they go to themarkdown(...)insidecontent. - Markdown highlighting: add YAML/meta tags to
markdownHighlightStyle; compiler decorations continue to layer above it. - Preview: unchanged.
splitFrontMatter()already presents canonical front matter as a labeled metadata block. - Scroll sync: unchanged. It already subtracts the canonical front-matter prefix and excludes the Preview metadata block from body anchors.
- Dependencies: add
@codemirror/lang-yaml; its CodeMirror/Lezer peers align with packages already used by the frontend.
Testability
- Language unit tests: parse canonical LF/CRLF examples and assert a
Frontmatterregion with YAML nodes plus a MarkdownBody. - Boundary unit tests: no front matter, a post-body
---, and a whitespace-suffixed opener stay outsideFrontmatter; an unterminated opener and...closer pin the official parser's recoverable error shape. - Highlight-style tests:
tags.meta, YAML property names, strings, numbers, booleans, and comments all receive a style; existing Markdown tags remain styled. - Editor integration tests: a real Source editor keeps body headings/folding and compiler-projected tokens after front matter.
- Browser tests: the existing
SAMPLE_SOURCEalready contains canonical front matter, so the report fixture must show it as metadata while the body and Preview continue to render and navigate correctly. - Bundle gate: the client stays under its approved size limit; see Served Client Packaging.
Alternatives not chosen
| Alternative | Why not |
|---|---|
Custom Lezer block parser with yamlLanguage.parser |
Can mirror every Markdig delimiter rule, but hand-rolls the outer grammar and adds cross-language conformance maintenance for noncanonical syntax. |
View decoration over splitFrontMatter() |
Fixes appearance only; the syntax tree, folding, commands, and indentation still treat the region as Markdown. |
| Compiler-projected front-matter token | Duplicates an invariant in the payload, refreshes only after compilation, and supplies no YAML language behavior. |
| Leave ordinary Markdown rendering | Preserves a known mismatch between Source, Preview, and compiler behavior. |