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:
@@ -10,8 +10,14 @@ In `docs/decisions.md`:
|
||||
- Plain markdown is a flavour of the grammar
|
||||
- The round-trip is the product
|
||||
- Markdown in is a canonical fixpoint
|
||||
- Equality is editor-normal
|
||||
- Equality is deep
|
||||
- `!adf:textBreak{}` parts text CommonMark would join
|
||||
- An empty key spells `empty`
|
||||
- `-0` is spelled `-0`
|
||||
- Empty markdown is a document of no blocks
|
||||
- Unknown nodes ride the carry
|
||||
- The carry fence names the node type
|
||||
- A code block is a fence per text node
|
||||
- Foreign HTML sorts three ways
|
||||
- Names stay text
|
||||
- Directives under `!adf:`
|
||||
|
||||
+14
-4
@@ -2,10 +2,20 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- **Breaking:** directives, the opaque carry among them (now `carry`), are spelled under an `!adf:`
|
||||
prefix (`!adf:name … !adf:/name`, `!adf:name[content]{attrs}`, `!adf:name arg {attrs}`) in place
|
||||
of the `:::`/`::`/`:name` forms: text holding an unescaped `!adf:` is claimed, and `adf` is an
|
||||
ordinary code block language. Convert stored markdown per `MIGRATION.md`.
|
||||
- **Breaking:** directives, the inline opaque carry among them (now `!adf:carry{json="…"}`), are
|
||||
spelled under an `!adf:` prefix (`!adf:name … !adf:/name`, `!adf:name[content]{attrs}`,
|
||||
`!adf:name arg {attrs}`) in place of the `:::`/`::`/`:name` forms, and 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`: text holding an unescaped `!adf:` and a code fence whose info string opens `adf:` are
|
||||
claimed, and `adf` is an ordinary code block language. Convert stored markdown per `MIGRATION.md`.
|
||||
- **Breaking:** `markdownToAdf` reads markdown holding no block as a document whose `content` is
|
||||
empty, as Atlassian's schema requires; `!adf:doc {content=none}` spells a document holding no
|
||||
`content` key.
|
||||
- `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc`: two adjacent text nodes a reader would
|
||||
join are parted by `!adf:textBreak{}`, an empty `attrs`, `content` or `marks` is spelled
|
||||
`{attrs=empty}`, `{content=empty}` or `{marks=empty}`, `-0` is spelled `-0`, and a `codeBlock`
|
||||
of several text nodes is a fence per node. A `codeBlock` holding other than plain text nodes
|
||||
rides the block carry, where it was refused.
|
||||
- **Breaking:** `unspellable-link` leaves `ConvertErrorCode`; a link whose `href` or `title` no
|
||||
CommonMark escape spells is written as `!adf:link[text]{attrs}`.
|
||||
- **Breaking:** some directive refusals carry `malformed-directive` where they carried
|
||||
|
||||
+4
-2
@@ -40,12 +40,12 @@ function migrateMarkdown(stored: string) {
|
||||
| `::media {id=a type=file}` | `!adf:media {id=a type=file}` |
|
||||
| `::taskItem TODO {localId=i}`: an empty `caption`, `decisionItem`, `paragraph` or `taskItem`, or an empty `heading` carrying `localId` | `!adf:taskItem TODO {localId=i}` then `!adf:/taskItem` |
|
||||
| `:mention[@Mikael]{id=5b10a2}` | `!adf:mention[@Mikael]{id=5b10a2}` |
|
||||
| the `adf` code fence and `:adf{json="…"}` | the `carry` code fence and `!adf:carry{json="…"}` |
|
||||
| the `adf` code fence and `:adf{json="…"}` | the `adf:<type>` code fence, its JSON without `type`, and `!adf:carry{json="…"}` |
|
||||
| `\:` keeps a directive literal | `\!adf:` keeps a directive literal |
|
||||
| `:adf{json="…"}` carrying a link for its `collection`, `id` or `occurrenceKey` | `!adf:link[text]{attrs}` |
|
||||
|
||||
A colon run and `:name[` are plain text now, and `adf` an ordinary code block language; text
|
||||
holding an unescaped `!adf:` and a `carry` fence are claimed instead.
|
||||
holding an unescaped `!adf:` and a code fence whose info string opens `adf:` are claimed instead.
|
||||
|
||||
### Readings
|
||||
|
||||
@@ -54,6 +54,8 @@ Markdown the spelling table leaves alone, which `0.2.0` reads as a different doc
|
||||
| Input | `0.1.0` | `0.2.0` |
|
||||
| --- | --- | --- |
|
||||
| a link whose text already holds one (`[a<https://example.com/>b](/v)`) | marks every node the inner link does not, splitting the outer link around it | leaves the outer brackets literal text; write the pieces as separate links to keep them |
|
||||
| markdown holding no block (`markdownToAdf("")`) | `{ type: 'doc', version: 1 }` | `{ content: [], type: 'doc', version: 1 }`; `!adf:doc {content=none}` reads as the former |
|
||||
| a code fence whose info string opens `adf:` (```` ```adf:x ````) | a `codeBlock` with that language | the block carry; write `!adf:codeBlock {language="adf:x"}` around a bare fence to keep the code block |
|
||||
|
||||
### Error codes
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ Parsing — `markdownToAdf` and `plainMarkdownToAdf`, and `htmlToAdf` at `0.2.0`
|
||||
|
||||
| Code | Fires when | What you can do |
|
||||
| --- | --- | --- |
|
||||
| `malformed-directive` | an `!adf:` the grammar cannot read — a prefix completing no directive, an unclosed container, `[content]` or `{attrs}`, a closer with no container of its name open, a leaf given a body, `{attrs}` out of order or duplicated, invalid JSON in a `carry` | write the spelling the message names, or escape the prefix — `\!adf:`, block and inline alike — to keep it literal text |
|
||||
| `malformed-directive` | an `!adf:` the grammar cannot read — a prefix completing no directive, an unclosed container, `[content]` or `{attrs}`, a closer with no container of its name open, a leaf given a body, `{attrs}` out of order or duplicated, invalid JSON in an opaque carry | write the spelling the message names, or escape the prefix — `\!adf:`, block and inline alike — to keep it literal text |
|
||||
| `malformed-pipe-table` | a pipe row that is no pipe table — a missing or ragged `---` delimiter row, an alignment colon in it, or a row not opening with a pipe | open every row with a pipe and give the delimiter row the header's cell count; to keep the lines literal text instead, escape the leading pipe of every one — escaping a single row leaves the next to open a fresh table and fail the same way |
|
||||
| `unknown-directive-name` | a directive whose name is no node or mark this version spells | check the name in `spec/flavour.md`, or escape the prefix as `\!adf:`; the spelling itself is well formed, so a later minor may give the name meaning |
|
||||
| `unmappable-html` | the input holds an HTML construct the documented element set does not map, a comment and a processing instruction among them — at this version that is every raw HTML construct in markdown, the element set landing at `0.2.0` | remove the construct, or write what it holds in the lossless flavour |
|
||||
@@ -204,7 +204,9 @@ emit refuses:
|
||||
|
||||
Serves Goals 1, 3 and 4.
|
||||
|
||||
- `markdownToAdf(adfToMarkdown(doc))` equals `doc` — unknown node types included, carried opaquely
|
||||
- `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc` — every key and value as `doc` holds it,
|
||||
adjacent text nodes, an empty `attrs`, `content` or `marks` and `-0` included, and unknown node
|
||||
types carried opaquely
|
||||
([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)).
|
||||
- Markdown this library reads, and markdown it writes, means what the CommonMark spec says; from
|
||||
`0.2.0`, well-formed HTML means what the HTML standard says, read or written. The bullets below
|
||||
@@ -239,7 +241,7 @@ Serves Goals 1, 3 and 4.
|
||||
makes a call loop forever.
|
||||
- The emitted formats are semver surface
|
||||
([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#the-formats-are-api)).
|
||||
- **`0.2.0`** — `htmlToAdf(adfToHtml(doc))` equals `doc`; fidelity HTML cannot express rides
|
||||
- **`0.2.0`** — `htmlToAdf(adfToHtml(doc))` deep-equals `doc`; fidelity HTML cannot express rides
|
||||
`data-*` attributes. Foreign HTML maps a documented element set, which markdown's raw HTML reads
|
||||
through as well, and a construct outside it is an error; well-formed HTML only — no tag-soup
|
||||
recovery.
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@ One directory per contract kind:
|
||||
`reason`. `kind` is `mark-model` (the permanent count divergence from ADF's mark-per-text-node
|
||||
model), `unspellable` (parses but the flavour has no spelling) or `pending` (a parser gap).
|
||||
|
||||
JSON is editor-normal (`docs/decisions.md` §Equality is editor-normal), two-space indent, keys
|
||||
sorted. `spec.json` is the vendored, upstream machine-readable suite, byte-exact from
|
||||
JSON is two-space indent, keys sorted, and a document read back must deep-equal the fixture's
|
||||
(`docs/decisions.md` §Equality is deep). `spec.json` is the vendored, upstream machine-readable suite, byte-exact from
|
||||
[spec.commonmark.org](https://spec.commonmark.org/0.31.2/spec.json) (CommonMark 0.31.2, © John
|
||||
MacFarlane, [CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/)), and is not
|
||||
re-serialized by the corpus gate.
|
||||
|
||||
+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
|
||||
|
||||
+66
-36
@@ -44,7 +44,8 @@ normalizes to it through the round-trip.
|
||||
CommonMark admits no spelling — the end of a block, inside an ATX heading — or where the node
|
||||
carries an attribute, it is the inline directive.
|
||||
- An empty paragraph — real payloads carry them — is an `!adf:paragraph` … `!adf:/paragraph` pair
|
||||
holding nothing.
|
||||
holding nothing, and one whose `content` is an empty array the pair
|
||||
`!adf:paragraph {content=empty}` … `!adf:/paragraph` (Attributes).
|
||||
- Links `[text](url)`; `<…>` around a destination containing spaces, `<>` an empty one beside a
|
||||
title; title in double quotes. A backslash escapes a parenthesis the destination leaves
|
||||
unbalanced, and a quote inside the title; a balanced pair stays bare. `<url>` autolink form only
|
||||
@@ -67,7 +68,10 @@ normalizes to it through the round-trip.
|
||||
matching below, which is what lets the emitter decide its own pairings.
|
||||
- Blocks separated by one blank line at document level, inside a blockquote and between CommonMark
|
||||
blocks; inside a directive container a pair holding a directive block takes none. No trailing
|
||||
whitespace outside a code block's content, single trailing newline; a document with no blocks is the empty string.
|
||||
whitespace outside a code block's content, single trailing newline. A document whose `content` is
|
||||
an empty array is the empty string, and markdown holding no block reads back to it; a document
|
||||
holding no `content` key is the leaf `!adf:doc {content=none}` as its only block, which is a
|
||||
named error anywhere else or spelled any other way.
|
||||
|
||||
## Directives
|
||||
|
||||
@@ -79,9 +83,11 @@ result naming it at the opener, whatever follows it — so output an old emitter
|
||||
escaped, and erroring input gaining meaning later is MINOR, never a reparse (`docs/decisions.md`
|
||||
§The formats are API). Each name belongs to one position, and a name the other one spells — a mark
|
||||
or an inline node written as a block directive, a block node written inline — is a different error,
|
||||
naming the spelling it takes. Two reserved names read back to no node: `carry` for the opaque carry,
|
||||
as both directive name and fence info string, and `listBreak` for the leaf that parts two adjacent
|
||||
lists (Canonical form).
|
||||
naming the spelling it takes. Four reserved names read back to no node: `carry` for the inline
|
||||
opaque carry, `listBreak` for the leaf that parts two adjacent lists and `doc` for a document
|
||||
holding no `content` key (Canonical form), and `textBreak` for the leaf that parts two text nodes
|
||||
(Inline nodes). Every fence info string opening `adf:` is reserved for the block carry (The opaque
|
||||
carry).
|
||||
|
||||
**Claiming**: an unescaped `!adf:` claims wherever it stands. What follows picks the form: `/name`
|
||||
closes a container, and a name picks by what follows it in turn — a space or the line's end a block
|
||||
@@ -142,6 +148,13 @@ ends the name (`!adf:hardBreak{}`). Input reads that spelling alone: keys out of
|
||||
quoted where bare carries it, an escape longer than it need be, an empty `{attrs}` on a block line
|
||||
or after a `[content]`, and a number or `json` value outside its canonical JSON spelling are each a
|
||||
named error naming the spelling to write instead.
|
||||
`attrs`, `content` and `marks` are reserved keys on every directive — block, inline node and mark —
|
||||
whose bare value `empty` spells the node's or mark's key holding an empty object or array:
|
||||
`!adf:underline[a]{attrs=empty}`, `!adf:date{content=empty}`, `!adf:hardBreak{marks=empty}`. A
|
||||
container spelling `content=empty` closes with no body; `attrs=empty` stands beside no other
|
||||
attribute, argument or content slot; and an inline node spelling `marks=empty` stands inside no
|
||||
mark spelling. Any other value of a reserved key is a named error, except on a block's `marks`
|
||||
(Block nodes).
|
||||
|
||||
**Escaping**: the emitter backslash-escapes whatever literal text would otherwise parse as
|
||||
directive syntax — every literal `!adf:`, `]` inside content, a bracket a link's destination and
|
||||
@@ -163,15 +176,18 @@ and restores to a deep-equal node. A carry may hold a node the emitter spells na
|
||||
unreinterpreted, and the next emit spells it canonically (`docs/decisions.md` §The round-trip is the
|
||||
product). Block and inline positions canonicalize differently, each fitting where it sits:
|
||||
|
||||
- **Block position**: a fenced code block with info string `carry`, body = the node's JSON —
|
||||
two-space indent, object keys sorted.
|
||||
- **Block position**: a fenced code block with info string `adf:` and the node's type, body = the
|
||||
node's JSON without its `type` — two-space indent, object keys sorted: ```` ```adf:blockCard ````.
|
||||
A type no info string carries back, by the rule a `codeBlock`'s language follows, leaves the info
|
||||
string `adf:` and keeps `type` in the body. A body holding `type` under a named type, or an
|
||||
`adf:` fence whose type an info string carries, is a named error.
|
||||
- **Inline position**: `!adf:carry{json="…"}` — compact serialization (keys sorted, no whitespace),
|
||||
JSON-string-escaped into the attribute.
|
||||
|
||||
The info string `carry` is reserved: a genuine `codeBlock` whose `language` is exactly `carry` takes
|
||||
Every info string opening `adf:` is reserved: a genuine `codeBlock` whose `language` opens so takes
|
||||
the attribute the section below keeps for a language no info string holds, so the reservation
|
||||
stays absolute.
|
||||
In block-directive position `!adf:carry` is a named error — the carry's block form is the fence.
|
||||
stays absolute. In block-directive position `!adf:carry` is a named error — the carry's block form
|
||||
is the fence.
|
||||
|
||||
## Raw HTML in input
|
||||
|
||||
@@ -193,13 +209,14 @@ Each section lists attributes as `name (type)`. A parenthesized value set docume
|
||||
payloads hold; the type stays string and any value round-trips verbatim. Values map to attrs by
|
||||
type: strings verbatim, numbers and booleans in canonical JSON spelling — quoted where not bare
|
||||
(`width="33.33"`) — and `json` values as the inline carry's serialization (compact, keys sorted),
|
||||
quoted. `markdownToAdf` emits `attrs`, `content` and `marks` keys only when non-empty; editor-normal
|
||||
ADF reads an empty attrs object, marks array or content array as the absent key (`docs/decisions.md`
|
||||
§Equality is editor-normal) — the grammar's empty-`{attrs}` omission already collapses the two
|
||||
spellings.
|
||||
quoted, `-0` spelled `-0`. `markdownToAdf` builds an `attrs`, `content` or `marks` key only where the
|
||||
markdown spells one, an empty one through its reserved key (Attributes), so a document reads back
|
||||
deep-equal (`docs/decisions.md` §Equality is deep). A node CommonMark spells takes the directive
|
||||
form to hold an empty key.
|
||||
|
||||
Marks on a block node ride the reserved attribute key `marks` — the node's marks array as a
|
||||
`json` value: `!adf:layoutSection {marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"}`.
|
||||
`json` value, `marks=empty` where it is empty:
|
||||
`!adf:layoutSection {marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"}`.
|
||||
A section saying its body is inline takes at most one paragraph, whose inline content becomes
|
||||
the node's `content`; any other body is a named error, and an empty pair is a node holding none.
|
||||
|
||||
@@ -215,14 +232,17 @@ cannot — `localId` (string) on any of them, marks, and the values below — ta
|
||||
form.
|
||||
|
||||
- `blockquote`, `bulletList`, `listItem` — containers, block body. Attributes: `localId` (string).
|
||||
- `codeBlock` — container, body one fenced code block whose info string is the language and whose
|
||||
content is the node's. Attributes: `hideLineNumbers` (boolean), `language` (string), `localId`
|
||||
(string), `uniqueId` (string), `wrap` (boolean). A language no info string carries back — empty,
|
||||
the reserved `carry`, or holding a backtick, a backslash, a control character, edge whitespace or
|
||||
an entity reference — rides the `language` attribute instead and the fence carries no info
|
||||
string; writing it in the slot that rule leaves empty, or in both, is a named error. The body is
|
||||
one ordinary code block, and a fence's info string decodes escapes and entity references as any
|
||||
other does.
|
||||
- `codeBlock` — container, body one fenced code block per text node, each fence's info string the
|
||||
language and its content the node's text; a node holding no `content` key is one empty fence.
|
||||
Attributes: `hideLineNumbers` (boolean), `language` (string), `localId` (string), `uniqueId`
|
||||
(string), `wrap` (boolean). A language no info string carries back — empty, opening the reserved
|
||||
`adf:`, or holding a backtick, a backslash, a control character, edge whitespace or an entity
|
||||
reference — rides the `language` attribute instead and the fences carry no info string, and so
|
||||
does a language beside `content=empty`, which has no fence; writing it in the slot that rule
|
||||
leaves empty, or in both, is a named error, and so are fences whose info strings differ and an
|
||||
empty fence beside another. Each fence is an ordinary code block, and its info string decodes
|
||||
escapes and entity references as any other does. A node holding a child no fence holds — any but
|
||||
a text node carrying no marks, `attrs` or `content` — rides the block carry.
|
||||
- `heading` — container, inline body. Attributes: `level` (number), `localId` (string). `level` is
|
||||
the `#` count, so a heading carrying none, or one that is no whole number from 1 to 6, has no
|
||||
CommonMark spelling.
|
||||
@@ -424,9 +444,8 @@ Right.
|
||||
Attributes and the carry fallback read as in the block sections, the carry in its inline form. Of
|
||||
the nodes below, `emoji`, `mention` and `status` spell their `text` attribute in the content slot as
|
||||
plain text: `[]` is the empty string, absent content is the absent attribute, non-empty content
|
||||
parsing to anything but one text node carrying neither marks, attributes nor content — adjacent text
|
||||
nodes with identical marks and no attributes merged first — is a named error, and so is a `text` key
|
||||
in `{attrs}`. An enclosing mark spelling does not reach into the slot. The rest take no content,
|
||||
parsing to anything but one text node carrying neither marks, attributes nor content is a named
|
||||
error, and so is a `text` key in `{attrs}`. An enclosing mark spelling does not reach into the slot. The rest take no content,
|
||||
`!adf:text` included; content on a node that takes none is a named error.
|
||||
|
||||
- `date` — Attributes: `localId` (string), `timestamp` (string, epoch milliseconds).
|
||||
@@ -453,16 +472,27 @@ Shipped !adf:emoji[🎉]{shortName=":tada:"} on !adf:date{timestamp=175608000000
|
||||
CommonMark strips or refuses one — a block's inline content edges, either side of a line break, an
|
||||
em, strong or strike spelling's inner edges, a pipe cell's edges — is spelled `!adf:text{text="…"}`,
|
||||
the reserved key carrying the node's text, escaped by the attribute grammar and never literal: pipe
|
||||
cells trim and pad. The emitter wraps the whitespace run alone and leaves the rest plain text;
|
||||
`markdownToAdf` merges adjacent text nodes carrying identical marks and no attributes
|
||||
(`docs/decisions.md` §Equality is editor-normal). Input reads that spelling alone: the value is one
|
||||
run of spaces and tabs, or one run of newlines, and anything else — a mixed run, or text CommonMark
|
||||
carries plainly — is a named error.
|
||||
cells trim and pad. The emitter wraps the whitespace run alone and leaves the rest plain text, which
|
||||
the spelled run joins on reading. Input reads that spelling alone: the value is one run of spaces
|
||||
and tabs, or one run of newlines, and anything else — a mixed run, or text CommonMark carries
|
||||
plainly — is a named error.
|
||||
|
||||
```
|
||||
!adf:text{text=" "}Two leading spaces held, and one text node split!adf:text{text="\n"}over two lines.
|
||||
```
|
||||
|
||||
**Adjacent text nodes.** CommonMark reads two adjacent text nodes back as one where neither is
|
||||
carried, neither holds `attrs` or an empty key, and their marks are identical, attributes included.
|
||||
The reserved leaf `!adf:textBreak{}` parts such a pair, inside every mark spelling the two share; a
|
||||
code span holds no directive, so it closes and reopens. It builds no node and reads only between
|
||||
two such nodes: elsewhere, or with `[content]` or `{attrs}`, it is a named error (`docs/decisions.md`
|
||||
§`!adf:textBreak{}` parts text CommonMark would join). A text node holding `attrs` or an empty key
|
||||
rides the inline carry.
|
||||
|
||||
```
|
||||
Hello, !adf:textBreak{}world — **Hello, !adf:textBreak{}world** — `a`!adf:textBreak{}`b`
|
||||
```
|
||||
|
||||
## Marks
|
||||
|
||||
An inline node's marks ride the spelling wrapped around them, never the block sections' reserved
|
||||
@@ -490,14 +520,14 @@ the directive form, open to no literal reading, is a named error.
|
||||
A spelling adds its mark to every inline node it wraps, and nesting is the marks array in order,
|
||||
outermost first: `_!adf:underline[x]_` gives marks `[em, underline]`, `!adf:underline[_x_]` the
|
||||
reverse. `adfToMarkdown` nests in the order the array holds rather than sorting it —
|
||||
`docs/decisions.md` §Equality is editor-normal restores the array, not a set — and opens each
|
||||
spelling once over the longest run of adjacent inline nodes carrying an identical mark, attributes
|
||||
included, at that depth. A run breaks at every node the emitter carries, so no emitted carry sits
|
||||
inside a mark spelling.
|
||||
`docs/decisions.md` §Equality is deep restores the array, not a set — and opens each spelling once
|
||||
over the longest run of adjacent inline nodes carrying an identical mark, attributes included, at
|
||||
that depth: `attrs: {}` differs from no `attrs`, and a directive spells it `{attrs=empty}`. A run
|
||||
breaks at every node the emitter carries, so no emitted carry sits inside a mark spelling.
|
||||
|
||||
An inline node whose marks no nesting spells — a mark type not listed here, an attrs key its
|
||||
spelling does not list, a value that is not the spelling's type, an attribute the spelling needs
|
||||
and the mark lacks, an order putting a code span outside another mark, `code` over anything but a
|
||||
and the mark lacks, an empty `attrs` on a mark CommonMark spells, an order putting a code span outside another mark, `code` over anything but a
|
||||
text node or over text holding a newline, or a spelling CommonMark's flanking rules cannot open or
|
||||
close where the run sits (`un**-real**istic`), or one CommonMark's matching pairs elsewhere — the
|
||||
intra-word `*` runs together with a neighbouring `**`, and the multiple-of-3 rule can leave the
|
||||
|
||||
@@ -51,7 +51,7 @@ export function reduceToPlain(document: AdfDocument): Result<AdfDocument> {
|
||||
const fault = adfDocumentFault(document)
|
||||
if (fault !== undefined) return faulted(fault, [])
|
||||
if (document.version !== 1) return failure('unsupported-document-version', `no markdown spelling carries ADF version ${document.version}`, [])
|
||||
// The plain flavour is lossy: it reads and writes editor-normal ADF, whose shapes CommonMark spells.
|
||||
// docs/decisions.md §Equality is deep: the plain flavour reads and writes editor-normal ADF.
|
||||
const blocks = reduceBlocks(nodeContent(toEditorNormal(document)), { depth: 0, memo: new Map(), path: [] })
|
||||
return blocks.ok ? success({ content: nodeContent(toEditorNormal({ content: blocks.value, type: 'doc', version: 1 })).slice(), type: 'doc', version: 1 }) : blocks
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
`Bar = 9`
|
||||
|
||||
`Next ID = 52`
|
||||
`Next ID = 54`
|
||||
|
||||
| Goal | W |
|
||||
|---|---|
|
||||
@@ -24,13 +24,14 @@
|
||||
|
||||
| ID | Release | Exempt | Item | R | S | A | G | Goals | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 40 | 0.2.0 | decision | **Make `markdownToAdf(adfToMarkdown(doc))` deep-equal `doc` for every document `adfToMarkdown` takes.** | 6 | 7 | 8 | 9 | 1 | 26.2 |
|
||||
| 7 | 0.2.0 | | **Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** | 6 | 9 | 9 | 9 | 2, 3 | 25.8 |
|
||||
| 45 | 0.2.0 | | **Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.** | 2 | 3 | 6 | 8 | 1 | 25.2 |
|
||||
| 6 | 0.2.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 3 | 24.7 |
|
||||
| 43 | 0.2.0 | | **Give each markdown input its own reader, strict to its own standard.** | 6 | 7 | 8 | 9 | 3, 4 | 22.3 |
|
||||
| 49 | 0.2.0 | | **Read a list whose bullet or ordered delimiter changes as two lists in the CommonMark reader.** | 4 | 5 | 5 | 8 | 3, 4 | 17.2 |
|
||||
| 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 5 | 13.0 |
|
||||
| 51 | 0.2.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 3, 4 | 12.4 |
|
||||
| 53 | 0.2.0 | | **Bench a block's `marks` spelling with the writer panel and adopt its pick.** | 4 | 5 | 5 | 6 | 5 | 11.5 |
|
||||
| 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 3, 4, 6 | 10.4 |
|
||||
| 38 | 0.3.0 | | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 2 | 2 | 4 | 7 | 1 | 19.5 |
|
||||
| 47 | 0.3.0 | | **Open the README with what the package is, what it does and for whom.** | 1 | 4 | 7 | 9 | 9 | 14.0 |
|
||||
@@ -45,14 +46,6 @@
|
||||
|
||||
## Details
|
||||
|
||||
### 40. Make `markdownToAdf(adfToMarkdown(doc))` deep-equal `doc` for every document `adfToMarkdown` takes.
|
||||
|
||||
Today it holds for editor-normal documents only: two adjacent text nodes with the same marks merge,
|
||||
an empty `attrs`, `marks` or `content` drops, and `-0` reads back `0` — shapes pipelines and bots
|
||||
build. Spell each so it reads back as written; CommonMark's spelling stays wherever the document
|
||||
holds none of these shapes. The spellings are part of the chunk. `docs/decisions.md` §Equality is
|
||||
editor-normal, `spec/flavour.md` and `corpus/README.md` follow, and the tests drop `toEditorNormal`.
|
||||
|
||||
### 7. Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.
|
||||
|
||||
Lands after item 6. The CommonMark spec suite also runs against `markdownToHtml`. The README
|
||||
@@ -90,6 +83,11 @@ Readings table gains its row, and its Spellings table one if `!adf:listBreak` re
|
||||
and 302 lose their `pending` exceptions, and the spelling leaves the README's "Four CommonMark
|
||||
spellings" bullet, which counts one fewer.
|
||||
|
||||
### 52. Spell `colwidth` as a comma list, `colwidth="340,420"`.
|
||||
|
||||
A writer panel chose it on 2026-10-03, 5 of 7, over today's `colwidth="[340,420]"`. Breaking:
|
||||
`MIGRATION.md`'s Spellings table gains its row.
|
||||
|
||||
### 51. Match a reference label to its definition under Unicode case folding.
|
||||
|
||||
`link-syntax.ts` normalizes a label with `toLowerCase`, so `[ẞ]` misses its `[SS]` definition (spec
|
||||
@@ -97,6 +95,12 @@ example 540); lowercasing and then uppercasing folds it. Breaking, so it ships b
|
||||
`MIGRATION.md`'s Readings table gains its row. Its `pending` exceptions go, and its spelling leaves
|
||||
the README's "Four CommonMark spellings" bullet, which counts one fewer.
|
||||
|
||||
### 53. Bench a block's `marks` spelling with the writer panel and adopt its pick.
|
||||
|
||||
Today `marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"`, the marks array as
|
||||
escaped JSON. Breaking where the panel picks another spelling: `MIGRATION.md`'s Spellings table
|
||||
gains its row.
|
||||
|
||||
### 50. Read `[](/url)` and `[]()` as CommonMark's empty link.
|
||||
|
||||
Both stay literal text today (spec examples 484 and 487). ADF holds no empty text node to carry a
|
||||
|
||||
Reference in New Issue
Block a user