Close the stability round's contract holes: block claiming, unspellable states, carve-out qualifiers, deep-equal domain
CI / gate (push) Successful in 7s
CI / gate (push) Successful in 7s
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user