Record deep equality, the text break, empty keys, -0, the typed carry fence and per-node code fences; file items 52 and 53
This commit is contained in:
+90
-16
@@ -14,7 +14,7 @@ a backslash reach them intact; what the flavour cannot spell reduces ADF→ADF a
|
||||
2026-08-23, real payloads 2026-09-15, the maintainer. Goal 1. Valid while a consumer saves back
|
||||
through the lossless pair.
|
||||
|
||||
`markdownToAdf(adfToMarkdown(doc))` and `htmlToAdf(adfToHtml(doc))` must equal `doc` — anything
|
||||
`markdownToAdf(adfToMarkdown(doc))` and `htmlToAdf(adfToHtml(doc))` must deep-equal `doc` — anything
|
||||
less silently destroys content an editor could not represent, in a document it did not author.
|
||||
When losslessness and readability conflict, losslessness wins. Round-trip equality is a property
|
||||
tested over a checked-in corpus (`corpus/README.md`), not a claim made in prose. Its real payloads
|
||||
@@ -31,27 +31,100 @@ where there is a way back. CommonMark spells some things the flavour has no esca
|
||||
paragraph opening with a code span whose backticks read back as a fence — so a parse succeeding
|
||||
does not imply a spellable document; `corpus/commonmark-spec/exceptions.json` names those.
|
||||
|
||||
## Equality is editor-normal
|
||||
## Equality is deep
|
||||
|
||||
2026-08-24, the maintainer. Goal 1. Valid while markdown cannot tell apart the ADF shapes this
|
||||
merges.
|
||||
2026-08-24, deep 2026-10-03, the maintainer. Goal 1. Valid while a pipeline or a bot can build a
|
||||
shape the editor would not.
|
||||
|
||||
"Equals" is structural equality over editor-normal ADF — adjacent text nodes with identical marks
|
||||
and no attributes merged, JSON number semantics, an empty attrs object, marks array or content
|
||||
array the absent key — the only domain markdown can restore.
|
||||
`todo.md` item 40 replaces this with deep equality (2026-09-28, the maintainer).
|
||||
"Equals" is `assert.deepStrictEqual`: every key and value `doc` holds, two adjacent text nodes, an
|
||||
empty `attrs`, `content` or `marks` and `-0` included, with no normalization on either side.
|
||||
CommonMark's spelling stays wherever a document holds none of those shapes. The plain flavour is
|
||||
lossy and stays editor-normal: its reduction reads and writes ADF with adjacent text nodes of
|
||||
identical marks and no attributes merged, `-0` read as `0`, and an empty `attrs`, `content` or
|
||||
`marks` the absent key.
|
||||
|
||||
## `!adf:textBreak{}` parts text CommonMark would join
|
||||
|
||||
2026-10-03, the maintainer. Goal 1. Valid while CommonMark reads adjacent text as one run.
|
||||
|
||||
Two adjacent text nodes a reader would build back as one — neither carried, neither holding
|
||||
`attrs` or an empty key, their marks identical, attributes included — are parted by the reserved
|
||||
inline leaf `!adf:textBreak{}`, mirroring `!adf:listBreak`: it builds no node, and anywhere but
|
||||
between two such nodes, or given `[content]` or `{attrs}`, it is `unsupported-node-shape`. It sits
|
||||
inside every mark spelling the pair shares, `**Hello, !adf:textBreak{}world**`; a code span holds
|
||||
no directive, so it closes and reopens, `` `a`!adf:textBreak{}`b` ``. A carried node never joins
|
||||
its neighbour, so a carry needs no break. A mark run breaks on any difference in the mark, `attrs:
|
||||
{}` against no `attrs` included.
|
||||
|
||||
## An empty key spells `empty`
|
||||
|
||||
2026-10-03, a writer panel and the maintainer. Goals 1 and 5. Valid while no attribute value
|
||||
spells an empty object or array.
|
||||
|
||||
An `attrs`, `content` or `marks` key holding an empty object or array is the reserved key with the
|
||||
bare value `empty` on every directive — block, inline node and mark: `{attrs=empty}`,
|
||||
`{content=empty}`, `{marks=empty}`. A writer panel chose the spelling, 5 of 7. A container
|
||||
spelling `content=empty` closes with no body, so an empty pair stays the node holding no content
|
||||
key; a leaf spells it too, `!adf:rule {content=empty}`. A node CommonMark spells takes its
|
||||
directive form to hold an empty key, and a text node holding one rides the inline carry, as does a
|
||||
node under an `em`, `strong`, `strike`, `code` or `link` mark whose `attrs` is empty, since none of
|
||||
those spellings holds attributes. Any other value of a reserved key is `unsupported-node-shape`,
|
||||
except on a block's `marks`, which reads it as the marks array in JSON.
|
||||
|
||||
## `-0` is spelled `-0`
|
||||
|
||||
2026-10-03, the maintainer. Goal 1. Valid while JSON's own serialization writes `-0` as `0`.
|
||||
|
||||
Wherever the flavour writes a number or a JSON value — an attribute, a `json` value, the carry —
|
||||
`-0` is `-0`, which JSON's grammar reads back as `-0`. An `orderedList` whose `order` is `-0` takes
|
||||
the directive form, since no list marker spells the sign.
|
||||
|
||||
## Empty markdown is a document of no blocks
|
||||
|
||||
2026-10-03, a panel and the maintainer. Goals 1 and 5. Valid while ADF's schema requires
|
||||
`content` on `doc`.
|
||||
|
||||
Markdown holding no block reads as `{ content: [], type: 'doc', version: 1 }`, the document
|
||||
`spec/adf-schema/full.json` requires. A document holding no `content` key is
|
||||
`!adf:doc {content=none}` standing alone as its only block, and a named error anywhere else. The
|
||||
panel split 4 for `none` and 3 for `absent`, and the maintainer chose `none`; all seven rejected a
|
||||
bare `!adf:doc`.
|
||||
|
||||
## Unknown nodes ride the carry
|
||||
|
||||
2026-08-23, extended to misplaced known nodes 2026-08-26, the maintainer. Goal 1. Valid while ADF
|
||||
holds nodes, or node positions, this library does not spell.
|
||||
2026-08-23, extended to misplaced known nodes 2026-08-26 and to code block children 2026-10-03, the
|
||||
maintainer. Goal 1. Valid while ADF holds nodes, or node positions, this library does not spell.
|
||||
|
||||
An unknown ADF node is carried opaquely — raw JSON rides a dedicated syntax in both formats and
|
||||
restores to a deep-equal node. The round-trip holds for documents newer than the library. So does
|
||||
a known node no section spells where it stands: a markdown serializer spells a node by type without
|
||||
checking its position, and refusing loses a document ADF itself keeps in an `unsupportedBlock`.
|
||||
Where a container's own spelling cannot hold the child it has — a `bulletList` holding other than
|
||||
`listItem`, a `codeBlock` other than text — the error result names that instead.
|
||||
checking its position, and refusing loses a document ADF itself keeps in an `unsupportedBlock`. A
|
||||
`codeBlock` holding a child no fence holds — anything but a text node carrying no marks, `attrs` or
|
||||
`content` — rides the carry whole.
|
||||
|
||||
## The carry fence names the node type
|
||||
|
||||
2026-10-03, the maintainer. Goals 1 and 5. Valid while a code fence's info string reads back
|
||||
verbatim.
|
||||
|
||||
The block carry is a code fence whose info string `adf:<type>` names the node's type, its body the
|
||||
node's JSON without `type`: ```` ```adf:blockCard ````. A type no info string carries back — by the
|
||||
rule a code language follows — leaves the info string `adf:` and keeps `type` in the body. Every
|
||||
info string opening `adf:` is reserved, so a `codeBlock` whose language opens so takes the
|
||||
`language` attribute, and `carry` is an ordinary language. A body holding `type` under a named
|
||||
type, or an `adf:` fence whose type an info string carries, is `unsupported-node-shape`.
|
||||
|
||||
## A code block is a fence per text node
|
||||
|
||||
2026-10-03, the maintainer. Goal 1. Valid while ADF holds a code block's text in more than one
|
||||
node.
|
||||
|
||||
A `codeBlock` holding several text nodes is the `!adf:codeBlock` container holding one fence per
|
||||
node, each fence's info string the language; its other attributes sit on the opener, and a
|
||||
language no info string carries stays the opener's `language` with bare fences. A `codeBlock`
|
||||
spelling `content=empty` has no fence to carry the language, so the opener does. Fences with
|
||||
differing info strings are `unsupported-node-shape` — ADF holds one language — and so is an empty
|
||||
fence beside another, since a text node holds text.
|
||||
|
||||
## Foreign HTML sorts three ways
|
||||
|
||||
@@ -295,10 +368,11 @@ handles one cause alike whichever node, attribute or direction raised it.
|
||||
- A directive whose name reads back to no node is `unknown-directive-name` rather than a claim
|
||||
code — the spelling is well formed, and telling that apart from a typo is what a consumer
|
||||
switches on when a later MINOR gives the name meaning. A reserved name is a known name, so never
|
||||
that code, and the two the flavour reserves part on form: a form the grammar does not have is a
|
||||
claim code — `!adf:carry`, whose carry is the fence — and a well-formed form in the wrong place
|
||||
that code, and the names the flavour reserves part on form: a form the grammar does not have is
|
||||
a claim code — `!adf:carry`, whose carry is the fence — and a well-formed form in the wrong place
|
||||
is `unsupported-node-shape`, `!adf:listBreak` parting anything but two adjacent lists of one
|
||||
type (2026-09-01).
|
||||
type, `!adf:textBreak{}` anything but two text nodes a reader joins, `!adf:doc` standing beside
|
||||
another block (2026-09-01, the text break and `doc` 2026-10-03).
|
||||
- A well-formed directive the node tables refuse — an attribute a node does not hold or spells
|
||||
elsewhere, a value outside its kind or its canonical spelling, an argument, or a body of a shape
|
||||
its content model does not take — is `unsupported-node-shape`, the emitter's code for the same
|
||||
|
||||
Reference in New Issue
Block a user