Flavour spec 1a: the directive grammar, canonical form, opaque carry #3

Merged
lilleman merged 7 commits from flavour-grammar into main 2026-08-24 00:49:26 +02:00
4 changed files with 35 additions and 15 deletions
Showing only changes of commit f0a3db3004 - Show all commits
+5 -1
View File
@@ -18,6 +18,9 @@ When losslessness and readability conflict, losslessness wins.
The other direction is a canonical fixpoint, not byte-identity: human markdown normalizes, the way The other direction is a canonical fixpoint, not byte-identity: human markdown normalizes, the way
back yields the library's canonical spelling, and that spelling round-trips byte-identically. back yields the library's canonical spelling, and that spelling round-trips byte-identically.
"Equals" is structural equality over editor-normal ADF — adjacent text nodes with identical marks
merged, JSON number semantics — the only domain markdown can restore.
Round-trip equality is a property tested over a corpus, not a claim made in prose. Round-trip equality is a property tested over a corpus, not a claim made in prose.
## 3. Unknown input policy ## 3. Unknown input policy
@@ -32,7 +35,8 @@ Round-trip equality is a property tested over a corpus, not a claim made in pros
- Directives, one grammar for everything markdown lacks: `:::panel info``:::` blocks, - Directives, one grammar for everything markdown lacks: `:::panel info``:::` blocks,
`:mention[@Mikael]{id=5b10a2}` inline. Prior art: CommonMark's generic-directives proposal. `:mention[@Mikael]{id=5b10a2}` inline. Prior art: CommonMark's generic-directives proposal.
- Plain CommonMark is a subset: the flavour adds syntax, never changes CommonMark meaning. - Plain CommonMark is a subset, with one carve-out (`spec/flavour.md`): directive-shaped literal
text is claimed.
- Tables: one header row plus plain inline cells → pipe table; anything richer → directive form. - Tables: one header row plus plain inline cells → pipe table; anything richer → directive form.
- Identity-bearing nodes carry their ids in attributes; a document is only portable within its - Identity-bearing nodes carry their ids in attributes; a document is only portable within its
site — accepted. site — accepted.
+3 -2
View File
@@ -38,8 +38,9 @@ isAdfDocument(v: unknown): v is AdfDocument
(AGENTS.md §3). (AGENTS.md §3).
- `htmlToAdf(adfToHtml(doc))` equals `doc` — fidelity HTML cannot express rides `data-*` - `htmlToAdf(adfToHtml(doc))` equals `doc` — fidelity HTML cannot express rides `data-*`
attributes. attributes.
- Plain CommonMark is valid input to `markdownToAdf`; converting back yields the library's - Plain CommonMark is valid input to `markdownToAdf`, with one carve-out: literal text matching
canonical spelling, which round-trips byte-identically. directive syntax is claimed (escapable — `spec/flavour.md`). Converting back yields the
library's canonical spelling, which round-trips byte-identically.
- Foreign HTML maps a documented element set; an unmappable element is an error, never a silent - Foreign HTML maps a documented element set; an unmappable element is an error, never a silent
drop. Well-formed HTML only — no tag-soup recovery. drop. Well-formed HTML only — no tag-soup recovery.
- The emitted formats are semver surface (AGENTS.md §8). - The emitted formats are semver surface (AGENTS.md §8).
+23 -10
View File
@@ -14,18 +14,20 @@ normalizes to it through the round-trip.
(intra-word). (intra-word).
- Bullet lists `- `; ordered lists incrementing `1.` `2.` `3.`, the first number taken from the - Bullet lists `- `; ordered lists incrementing `1.` `2.` `3.`, the first number taken from the
node's `order` attribute. Continuation lines align with the first character after the marker node's `order` attribute. Continuation lines align with the first character after the marker
(two spaces for `- `, three for `1. `). Lists are tight — blank lines between items normalize (two spaces for `- `, three for `1. `); blank lines inside an item are empty lines. Lists are
away; ADF does not record tightness. tight — blank lines between items normalize away; ADF does not record tightness.
- Blockquotes prefix every line with `> `. - Blockquotes prefix lines with `> `; a blank line inside a blockquote is a bare `>`.
- ATX headings (`#``######`); setext input normalizes to ATX. - ATX headings (`#``######`); setext input normalizes to ATX.
- Code fences ``` with the node's language as info string, the fence lengthened past any backtick - Code fences ``` with the node's language as info string, the fence lengthened past any backtick
run in the content; indented-code input normalizes to fences. run in the content; indented-code input normalizes to fences.
- Thematic break `---`. - Thematic break `---`.
- Hard break: backslash at end of line (survives editors that trim trailing spaces). - Hard break: backslash at end of line (survives editors that trim trailing spaces). Where
CommonMark admits no spelling — the end of a block, inside a heading — it is `:hardBreak{}`.
- An empty paragraph — real payloads carry them — is `::paragraph`.
- Links `[text](url)`; `<…>` around a destination containing spaces; title in double quotes. - Links `[text](url)`; `<…>` around a destination containing spaces; title in double quotes.
`<url>` autolink form only when the text equals the destination and the destination is a valid `<url>` autolink form only when the text equals the destination and the destination is a valid
CommonMark autolink (absolute URI). CommonMark autolink (absolute URI).
- Paragraphs on one line — no soft wrapping; soft line breaks in input collapse per CommonMark. - Paragraphs on one line — no soft wrapping; a soft line break in input becomes a single space.
- Entity references in input decode to their characters; output backslash-escapes only where text - Entity references in input decode to their characters; output backslash-escapes only where text
would otherwise parse as syntax. would otherwise parse as syntax.
- Blocks separated by one blank line, no trailing whitespace, single trailing newline. - Blocks separated by one blank line, no trailing whitespace, single trailing newline.
@@ -59,11 +61,19 @@ defines (e.g. the panel type). The body is block markdown. The closing fence is
least the opening's length, and a container's fence is longer than every directive fence line in least the opening's length, and a container's fence is longer than every directive fence line in
its body — counting only lines that parse as directive fences in the body's block structure; a its body — counting only lines that parse as directive fences in the body's block structure; a
colon run inside a code fence or opaque carry is content. Canonical form uses minimal lengths. colon run inside a code fence or opaque carry is content. Canonical form uses minimal lengths.
Directive fence lines follow code-fence indentation (up to three spaces relative to their
container); trailing whitespace on a fence line is tolerated in input, never emitted.
**Leaf block**: `::name {attrs}` — a block-position node with no body. **Leaf block**: `::name {attrs}` — a block-position node with no body.
**Attributes**: `{key=value key2="two words"}`. A bare value matches `[A-Za-z0-9_-]+`; any other **Claiming at block level**, symmetric with inline: a line whose leading run of two or more
value is double-quoted with JSON string escaping (`\"` `\\` `\n` `\t` `\uXXXX`, …) — total over colons is followed immediately by a name character is claimed and must parse fully as a container
opening or a leaf, else it is a named error. A bare colon-run line is a closing fence while a
container is open, a named error otherwise.
**Attributes**: `{key=value key2="two words"}`. `{attrs}` is optional in every form, and `{}` is
valid — no attributes. A bare value matches `[A-Za-z0-9_-]+`; any other value is double-quoted
with JSON string escaping (`\"` `\\` `\n` `\t` `\uXXXX`, …) — total over
Unicode, and raw newlines never appear inside quotes. All values are strings at the grammar Unicode, and raw newlines never appear inside quotes. All values are strings at the grammar
level; each node's section assigns types. Canonical form orders keys alphabetically, spells level; each node's section assigns types. Canonical form orders keys alphabetically, spells
values bare wherever allowed, and inside quotes escapes only what it must, using the shortest values bare wherever allowed, and inside quotes escapes only what it must, using the shortest
@@ -74,9 +84,10 @@ directive syntax — the leading `:` of a would-be directive, `]` inside content
before `:` in input always yields a literal colon. before `:` in input always yields a literal colon.
**Malformed directives are error results**, named: an unclosed container at end of input, a body **Malformed directives are error results**, named: an unclosed container at end of input, a body
fence line of the container's length or longer, unparseable or duplicate-keyed attrs, invalid fence line of the container's length or longer, a bare colon-run line outside any container, an
JSON in an opaque carry. Never a silent literal-text fallback — a typo that reparses as prose is inline `[content]` or `{attrs}` left unclosed at end of line, unparseable or duplicate-keyed
the silent loss §2 refuses. attrs, invalid JSON in an opaque carry. Never a silent literal-text fallback — a typo that
reparses as prose is the silent loss §2 refuses.
## The opaque carry (AGENTS.md §3) ## The opaque carry (AGENTS.md §3)
@@ -90,6 +101,8 @@ Block and inline positions canonicalize differently, each fitting where it sits:
The info string `adf` is reserved: a genuine `codeBlock` whose `language` is exactly `adf` is The info string `adf` is reserved: a genuine `codeBlock` whose `language` is exactly `adf` is
itself emitted through the opaque carry, so the reservation stays absolute and stays lossless. itself emitted through the opaque carry, so the reservation stays absolute and stays lossless.
In block-directive positions (`::adf`, `:::adf`) the reserved name is a named error — the
carry's block form is the fence.
## Raw HTML in input ## Raw HTML in input
+4 -2
View File
@@ -15,14 +15,16 @@ detail is settled at its own milestone.
family, the pipe-vs-directive table rule and the directive table form, task and decision family, the pipe-vs-directive table rule and the directive table form, task and decision
lists, layout, extensions, syncBlock. lists, layout, extensions, syncBlock.
- [ ] **1c — Inline node syntaxes and marks** in `spec/flavour.md`: mention, emoji, status, date, - [ ] **1c — Inline node syntaxes and marks** in `spec/flavour.md`: mention, emoji, status, date,
inlineCard, mediaInline; underline, subsup, textColor, border. inlineCard, mediaInline; underline, subsup, textColor, border; the spelling for text nodes
whose whitespace CommonMark cannot hold (literal newlines, leading or trailing spaces).
- [ ] **1d — Corpus start** (§10): checked-in ADF ↔ canonical-markdown fixture pairs per spec'd - [ ] **1d — Corpus start** (§10): checked-in ADF ↔ canonical-markdown fixture pairs per spec'd
node. node.
- [ ] **2 — `adfToMarkdown`.** First real code — decide here where §10's coverage check lives. - [ ] **2 — `adfToMarkdown`.** First real code — decide here where §10's coverage check lives.
- [ ] **3 — `markdownToAdf`.** The CommonMark parser is the largest single component. The raw-HTML - [ ] **3 — `markdownToAdf`.** The CommonMark parser is the largest single component. The raw-HTML
element mapping is empty until milestone 6, so at `0.1.0` every raw-HTML construct in input element mapping is empty until milestone 6, so at `0.1.0` every raw-HTML construct in input
is an error result. is an error result.
- [ ] **4 — Round-trip property tests** over the corpus, both ways — the thing that proves 2 and 3. - [ ] **4 — Round-trip property tests** over the corpus, both ways — the thing that proves 2 and
3. Generators emit editor-normal ADF (§2).
- [ ] **5 — Release pipeline, ship `0.1.0`.** Publish-on-version-change (§9), `NPM_TOKEN` secret, - [ ] **5 — Release pipeline, ship `0.1.0`.** Publish-on-version-change (§9), `NPM_TOKEN` secret,
the repo made public first (§6). `0.1.0` is the markdown round-trip: both markdown the repo made public first (§6). `0.1.0` is the markdown round-trip: both markdown
directions, the types, `isAdfDocument`. The build lands here: a build tsconfig emitting JS directions, the types, `isAdfDocument`. The build lands here: a build tsconfig emitting JS