Unmodeled Markdown Highlighting
Note
Status: implemented. Extends Compiler-Projected Editor Semantics so the editor shows what a construct's fate is — ignored, or dialogue — instead of muting the blockquotes that carry control blocks and coloring ignored material as if it played.
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
A DialogueDown script is Markdown, but not all of it becomes dialogue. A table is ignored — left out of the dialogue entirely — while the blockquote beside it may be a control block that branches the story. The editor colors both as ordinary Markdown, and mutes the blockquote, so the liveliest construct in the language reads as the most inert and the absent one reads as if it plays.
This component makes the editor show that difference, layered on Markdown highlighting rather than replacing it.
In scope: one semantic-token kind for Markdown the policy ignores; styling it as visibly absent; removing the blockquote muting; and styling Markdown comments, which the editor's own parser already recognizes.
Out of scope: changing what is kept or ignored — the policy is unchanged and
this component only shows it. dialogue.toml handling overrides are applied;
because the projection follows DLG1114, an override changes the highlighting without a
second client-side policy. Front matter is a separate invariant metadata region,
designed in Front Matter Source Highlighting.
Functionality checklist
- [x] Add a token kind for Markdown the policy ignores.
- [x] Derive it from the policy's own decision, not a second classification.
- [x] Cover block and inline constructs alike.
- [x] Reach constructs nested in a list item or blockquote.
- [x] Style ignored material as present in the file but absent from the dialogue, in both Source and Preview.
- [x] Stop muting blockquotes, so a control block reads as live dialogue.
- [x] Mark a conditional blockquote with a question sticker, from its compiler-projected control keyword.
- [x] Style Markdown comments as the writer-only notes they are.
- [x] Leave dialogue constructs' existing tokens untouched.
Ubiquitous language
This component adds no vocabulary of its own. UnmodeledNodeKind and UnmodeledNodeHandling
live in DialogueDown.Configuration — they are the words a project writes in dialogue.toml,
and configuration is a foundation layer that must not depend on Markdown. The editor names each
fate with the same word the author configures it with, so a color on screen and a line in
dialogue.toml mean the same thing.
| Term | Meaning |
|---|---|
| Unmodeled construct | Markdown DialogueDown does not model as dialogue, classified as an UnmodeledNodeKind. |
| Ignore | The construct is left out of the dialogue entirely, like a comment. Reported as DLG1114. |
| Keep | The construct's source text becomes dialogue text, exactly as written — its text, not its structure. |
| Comment | An HTML comment. Always left out, unconditionally, before the policy is consulted — so it is never reported. |
Writer-facing behavior
Given a script:
# The Tavern
<!-- reminder: rewrite this scene -->
| Rumor | Source |
| --- | --- |
| The bridge is out | The miller |
> `if` `Thirsty?`
>
> Innkeeper: You look parched.
<div class="portrait"></div>
| Construct | Fate | How it reads |
|---|---|---|
| The comment | Never compiled | Light gray and italic — a note to the writer |
| The table | Ignored | Dark gray inside an eye-marked region; the region glows on hover |
The if quote |
Dialogue | Fully colored inside a question-marked region — it plays |
The <div> |
Kept | Ordinary dialogue text, because that is exactly what it becomes |
The blockquote matters most: a muted quote would be the dimmest thing on screen, though it is the only one of the four that branches the dialogue.
Architecture
The projection already reads both trees — the Dialogue AST for dialogue constructs and the
Markdown tree for block-control keywords. Ignored constructs are a third source, and the
compiler already located every one of them while reporting DLG1114.
flowchart LR
C["CompilationVisualizer:<br/>compiles and projects"] --> P["SemanticTokenProjection"]
P --> D["Dialogue AST:<br/>speakers, tags, jumps"]
P --> K["Markdown tree:<br/>control keywords"]
P --> I["DLG1114 diagnostics:<br/>what the policy ignored"]
D --> T["SemanticToken list:<br/>carried in the report"]
K --> T
I --> T
T --> E["Source editor:<br/>one CSS class per kind"]
T --> V["Markdown Preview:<br/>semantic regions + stickers"]
E --> M["Markdown highlighting:<br/>comments, quotes"]
Interfaces and responsibilities
| Type | Responsibility | Collaborators |
|---|---|---|
TokenKind |
Gains IgnoredMarkdown. |
— |
SemanticTokenProjection |
Emits a token for each ignored construct, from the reported spans. | LocatedDiagnostic, DiagnosticCatalog |
IUnmodeledNodeHandlingPolicy |
Unchanged: still the single authority on a kind's fate. | UnmodeledNodeKind |
semantic-tokens.ts |
Maps the new kind to its CSS class. | styles.css |
text.ts |
Marks rendered Markdown from ignored and control-keyword source spans. | marked |
source-view.ts |
Maps tokens through edits, mirrors ignored regions in Preview, stops muting blockquotes, and styles comments. | the Markdown highlight style |
Key design decisions
D1 — Project what the policy decides; style natively what Markdown already knows
The editor projects a construct's fate from the compiler only when that fate can vary.
What the handling policy decides is variable: a project can configure it, so the editor cannot know a table's fate without asking the compiler. That is projected.
A Markdown comment, by contrast, is always left out, unconditionally — there is nothing to
learn from the compiler. It is also ordinary CommonMark, which the editor's own parser already
recognizes: @lezer/markdown maps Comment and CommentBlock to the comment highlight tag.
Projecting it would duplicate work the client already does and spend payload bytes stating an
invariant.
This sharpens the "compiler is the single source of truth" principle inherited from Compiler-Projected Editor Semantics: the compiler is the authority on DialogueDown's grammar and its configuration, not on plain Markdown the client can already parse correctly.
D2 — Recover ignored spans from the diagnostic the front end already reports
An ignored construct leaves nothing behind in the Markdown tree — being left out is what
Ignore means — so it cannot be found by inspecting the tree. It can be found by inspecting
what the compiler said: every ignored construct is reported as DLG1114 with its exact span,
the note added for it.
The projection reads those diagnostics. This keeps one authority — the handler decides and reports; the editor draws what was reported — and a project that configures the policy colors correctly with no change here.
The tradeoff: the highlighting depends on that diagnostic being produced, so demoting or
suppressing DLG1114 would silently take the coloring with it. A test pins the pair together so
it cannot happen unnoticed.
D2a — The diagnostics reach the projection as an optional argument
Project takes the compile's located diagnostics as a trailing optional parameter, so a caller
that only wants dialogue tokens — the projection's existing tests among them — is unchanged, and
a caller that has diagnostics passes them. The compile that renders a report always has them.
The coupling to DLG1114 is made in code rather than by a literal: the
projection compares against DiagnosticCatalog.IgnoredUnmodeledMarkdown.Code,
so the code cannot drift from the catalog silently.
D3 — Kept material is styled as dialogue, because that is what it becomes
Keep means the construct's source text becomes dialogue text, exactly as written. Text that
will be said should therefore look like text that will be said. A distinct tint would assert
"this is special" precisely where the compiler's position is that it is now ordinary — the
color would contradict the semantics rather than reveal them.
This also means the design needs no way to recognize kept constructs, which is fortunate: a flattened construct is byte-for-byte indistinguishable from ordinary text in the Markdown tree, so recognizing it would have required a new record threaded out of the parse for no reader benefit.
D4 — Stop muting blockquotes; mark control regions from existing tokens
The fix is to delete { tag: tags.quote, color: "var(--md-muted)" }, not to add a token that
re-colors quotes. In DialogueDown a blockquote is always live: a marker-headed quote is a
control block, and any other quote is a transparent wrapper whose contents are dialogue. Its
contents already carry their own tokens — speaker, tags, control keyword — and the mute was
the only thing overriding them. Removing it lets the existing projection show through, with no
new token and no new span arithmetic.
The Preview adds a question sticker only when an existing ControlKeyword token lands inside
the rendered blockquote. It does not recognize if strings in TypeScript. This preserves the
same boundary as D1: the compiler decides DialogueDown grammar; the client renders it.
D5 — Use semantic regions and a two-level gray hierarchy
Ignored material is dark gray; a comment is lighter gray and italic. Both survive a colorblind reader and both themes, where new hues in an already thirteen-color legend would not. The difference in opacity carries intent: comments are expected writer-only notes, while ignored Markdown stays more legible because it may be an accidental omission the writer needs to inspect. A strikethrough was rejected after preview because it added visual noise across multi-line tables and code blocks.
Opacity alone was also rejected for Preview: it was too easy to overlook, and an adjacent badge did not say which content it covered. The final design wraps each ignored rendered construct in a region with a persistent closed-eye sticker and left rail. Hovering brightens the rail and adds the same soft tint and glow blockquotes already use. A conditional blockquote gets a question sticker in the same corner. The stickers add no separate row, so the document's vertical flow is unchanged.
The Preview regions are still policy-driven. renderDocument receives the same
IgnoredMarkdown spans as Source and matches them to Marked's source tokens; it does not
dim every table, pre, or hr. When configuration changes a kind from Ignore to Keep, its
token disappears and both panes return it to full strength.
Error and boundary cases
| Case | Behavior |
|---|---|
| An ignored table, code block, or divider | One ignored token over the construct's span; Source and Preview show the same opacity, and Preview encloses it in an eye-marked region. |
| Raw HTML kept as dialogue text | No token — it reads as the dialogue text it becomes. |
| An HTML comment, block or inline | Styled by the editor's Markdown parser; never projected. |
| An ignored construct inside a list item or blockquote | Tokenized — the diagnostic carries its span wherever it sat. |
| A conditional blockquote | Its contents keep their own colors; its existing control-keyword token adds the question sticker in Preview. |
| A non-conditional blockquote | No question sticker; its contents keep their own colors. |
| A project that configures the policy | Follows the configured fate, with no change here. |
| An ignored construct in a script that fails to compile | Reported and colored as far as the front end ran; later stages add nothing here. |
Integration
- Report payload. One more
TokenKindvalue; the payload shape is unchanged. - LSP. The kind rides the same legend a language server would publish, so the projection is reused unchanged when it arrives.
- Source editor. One CSS class following the existing
dd-tok-*convention, plus two lines in the Markdown highlight style. - Markdown Preview. The same semantic spans add ignored/control region classes and codicon stickers. Spans map through unsaved edits until the next compile.
- Diagnostics overlay. An ignored construct also carries its
DLG1114note; the two agree because they come from the same report.
Testability
- Unit — projection: an ignored construct emits a token over the reported span; a script with no ignored construct emits none; nesting is covered.
- Unit — the D2 coupling: a test asserts an ignored construct produces both the diagnostic and the token, so demoting one cannot silently break the other.
- Unit — client: the new kind maps to its class, alongside every existing kind.
- Unit — Markdown layer:
markdownHighlightStyleis asserted to leave a blockquote unstyled and to style a comment, so neither decision can be undone by reflex. - Unit — Preview renderer: ignored tables, code blocks, dividers, raw HTML, and autolinks are wrapped only when their projected spans are supplied; kept Markdown stays full-strength.
- Integration — Source view: setting and clearing semantic tokens adds and removes ignored Preview regions, a control-keyword token marks its enclosing blockquote, and ranges map through unsaved edits.
Alternatives not chosen
| Alternative | Why not |
|---|---|
| Re-run the policy inside the projection | Two authorities on one question, and an ignored node is no longer in the tree to classify (D2). |
| Thread a record of every fate out of the parse | A second transport for something the diagnostic already carries, to distinguish kept text that should not be distinguished (D3). |
| A projected token kind for comments | Duplicates what the editor's Markdown parser already recognizes, for a fate that never varies (D1). |
| Re-color blockquotes with a new token | The mute was the problem; the contents already have their own colors (D4). |
| Opacity alone in Preview | Too easy to overlook; a semantic region preserves flow while making coverage explicit (D5). |
| A badge on its own row | Breaks the Markdown's vertical rhythm and does not clearly enclose the content it labels. |