From 61528081943f8653c42eddfc32c1b2fd036c4c10 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 14 Oct 2026 00:07:39 +0200 Subject: [PATCH] =?UTF-8?q?Answer=20prose=20round=201:=200=20=F0=9F=94=B4,?= =?UTF-8?q?=202=20=F0=9F=9F=A1;=20the=20slot's=20rule=20joins=20the=20node?= =?UTF-8?q?-tables=20bullet,=20the=20remedies=20offer=20the=20space=20alik?= =?UTF-8?q?e,=20the=20message=20says=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 16 ++++++++-------- MIGRATION.md | 2 +- README.md | 2 +- docs/decisions.md | 7 +++---- src/markdown/directive-syntax.ts | 4 ++-- src/markdown/parse/markdown-to-adf.test.ts | 2 +- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0161a9d..b037203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,14 +27,14 @@ - **Breaking:** the markdown readers read 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. -- **Breaking:** `unspellable-character`, `unspellable-line-start` and `unspellable-whitespace` leave - `ConvertErrorCode`. A line ending in an `emoji`, `mention` or `status` directive's content slot is - refused as `unsupported-node-shape`. A carriage return in text is - spelled ` ` and a null character `!adf:text{text="\u0000"}`; a paragraph line opening with a - code span is spelled as that code span, its backticks opening no fence; a code block holding - either character, a code span holding a null character, a text node holding no text or holding - `content`, and an `emoji`, `mention` or `status` whose `text` holds a line ending or a null - character ride the carry. See `MIGRATION.md`. +- **Breaking:** `unspellable-character`, `unspellable-line-start` and `unspellable-whitespace` + leave `ConvertErrorCode`. A line ending in an `emoji`, `mention` or `status` directive's content + slot is refused as `unsupported-node-shape`. A carriage return in text is spelled ` ` and a + null character `!adf:text{text="\u0000"}`; a paragraph line opening with a code span is spelled + as that code span, its backticks opening no fence; a code block holding either character, a code + span holding a null character, a text node holding no text or holding `content`, and an `emoji`, + `mention` or `status` whose `text` holds a line ending or a null character ride the carry. See + `MIGRATION.md`. - **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 diff --git a/MIGRATION.md b/MIGRATION.md index 6e29b39..76bb29e 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -106,7 +106,7 @@ the code the table gives. | a paragraph line opening with a code span of three or more backticks, on emit | `unspellable-line-start` | spells the code span | | a block directive whose CommonMark spelling holds a carriage return, a null character or a code span opening a line, on parse | `unspellable-character`, `unspellable-line-start` | `unsupported-node-shape`; write the CommonMark spelling | | an `emoji`, `mention` or `status` whose `text` holds a line ending or a null character, on emit | `unspellable-whitespace`, `unspellable-character` | rides the inline carry | -| an `emoji`, `mention` or `status` directive whose content slot spells a line ending (` `, ` `, `!adf:text{text="\n"}`), on parse | `unspellable-whitespace` | `unsupported-node-shape`; write the node as `!adf:carry` | +| an `emoji`, `mention` or `status` directive whose content slot spells a line ending (` `, ` `, `!adf:text{text="\n"}`), on parse | `unspellable-whitespace` | `unsupported-node-shape`; replace it with a space, or write the node as `!adf:carry` | | a text node holding no text, or holding `content`, on emit | `unsupported-node-shape` | rides the carry | | a `codeBlock` holding other than plain text nodes, on emit | `unsupported-node-shape` | rides the block carry | | a leaf node given a body (`media`) | `unsupported-node-shape` | `malformed-directive` | diff --git a/README.md b/README.md index 96e30f3..0708dc1 100644 --- a/README.md +++ b/README.md @@ -448,7 +448,7 @@ Every call can raise these: | --- | --- | --- | | `engine-limit` | in every call: a conversion that passes the longest array, `Map` or `Set` the JavaScript engine builds, or that the engine reports as out of memory; on JavaScriptCore (Bun, Safari), also one that passes its longest string, 2^31 − 1 characters | retry once; if it repeats, writing: set the document aside, keeping the ADF and passing it over or showing it read-only; reading: shorten the markdown, or nest its lists and quotes less deeply | | `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it | -| `unsupported-node-shape` | parsing, in `losslessMarkdownToAdf`: markdown spells a node with an attribute, value, argument or body its type does not take, or without one it needs, puts a line ending or a null character in an `emoji`, `mention` or `status` content slot, wraps a directive mark around nothing or only hard breaks, writes as a directive a node or mark the lossless flavour spells as CommonMark, or one that, written out as plain CommonMark, would be longer than 536870888 characters, or puts a reserved directive out of place: `!adf:textBreak{}` parting nothing, `!adf:doc` anywhere but as the whole document. Emitting, in `adfToLosslessMarkdown`: a node not riding the carry that carries one mark type twice, a block or a non-text inline node holding `text`, or a leaf block or a non-text inline node holding `content`. Emitting, in `adfToPortableMarkdown`: a line or task list the portable writer cannot spell, a bug in this library. Emitting, in both writers: a document whose markdown would be longer than 536870888 characters, V8's longest string, on every engine; in `adfToPortableMarkdown`, also a document whose text alone is that long, edge whitespace included. In every call: a conversion that passes V8's or SpiderMonkey's longest string | fix what the message names; [`spec/flavour.md`](https://github.com/larvit/adf-codec/blob/main/spec/flavour.md) lists every type's attributes and body. For a line ending in a content slot, replace it with a space; for it or a null character, write the node as `!adf:carry`. For a portable line or task list, report it as a bug. Past 536870888 characters or the engine's longest string, writing: set the document aside, keeping the ADF and passing it over or showing it read-only; reading: shorten the markdown, or nest its lists and quotes less deeply | +| `unsupported-node-shape` | parsing, in `losslessMarkdownToAdf`: markdown spells a node with an attribute, value, argument or body its type does not take, or without one it needs, puts a line ending or a null character in an `emoji`, `mention` or `status` content slot, wraps a directive mark around nothing or only hard breaks, writes as a directive a node or mark the lossless flavour spells as CommonMark, or one that, written out as plain CommonMark, would be longer than 536870888 characters, or puts a reserved directive out of place: `!adf:textBreak{}` parting nothing, `!adf:doc` anywhere but as the whole document. Emitting, in `adfToLosslessMarkdown`: a node not riding the carry that carries one mark type twice, a block or a non-text inline node holding `text`, or a leaf block or a non-text inline node holding `content`. Emitting, in `adfToPortableMarkdown`: a line or task list the portable writer cannot spell, a bug in this library. Emitting, in both writers: a document whose markdown would be longer than 536870888 characters, V8's longest string, on every engine; in `adfToPortableMarkdown`, also a document whose text alone is that long, edge whitespace included. In every call: a conversion that passes V8's or SpiderMonkey's longest string | fix what the message names; [`spec/flavour.md`](https://github.com/larvit/adf-codec/blob/main/spec/flavour.md) lists every type's attributes and body. For a line ending in a content slot, replace it with a space or write the node as `!adf:carry`; for a null character there, write the node as `!adf:carry`. For a portable line or task list, report it as a bug. Past 536870888 characters or the engine's longest string, writing: set the document aside, keeping the ADF and passing it over or showing it read-only; reading: shorten the markdown, or nest its lists and quotes less deeply | ## The guarantees diff --git a/docs/decisions.md b/docs/decisions.md index 9689e44..28a4309 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -437,9 +437,6 @@ handles one cause alike whichever node, attribute or direction raised it. - A code names the cause; where one cause recurs across node types, across one mark's attributes or across directions, one code covers them all and `path` and `message` say which — `unsupported-nesting-depth` is the 500-level guard whichever direction hits it. - A line ending or a null character in an inline directive's content slot is a value the slot does - not take, so `unsupported-node-shape` as below, `message` naming the character; the emitter - carries such a node (2026-10-14). - A claim code names the spelling claimed, never the node that spelling would have built: a malformed `!adf:table` is a `malformed-directive`, and an alignment colon a `malformed-pipe-table` — the flavour's own delimiter row is `-` runs, so the grammar refuses the @@ -458,7 +455,9 @@ handles one cause alike whichever node, attribute or direction raised it. 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 mismatch read the other way: one code across both directions for good, since the call site - knows which direction it called and parting them after `0.1.0` is MAJOR (2026-09-23). + knows which direction it called and parting them after `0.1.0` is MAJOR (2026-09-23). A line + ending or a null character in an `emoji`, `mention` or `status` content slot is such a value; the + emitter carries such a node (2026-10-14). - A non-finite number takes two codes: `unsupported-node-shape` parsing, `not-an-adf-document` emitting — no document holds one, so no round-trip crosses them (2026-09-23). - A document whose markdown would be longer than V8's longest string is `unsupported-node-shape` diff --git a/src/markdown/directive-syntax.ts b/src/markdown/directive-syntax.ts index 8393090..6d17ac6 100644 --- a/src/markdown/directive-syntax.ts +++ b/src/markdown/directive-syntax.ts @@ -105,9 +105,9 @@ export function readSoleStringAttribute(span: DirectiveSpan, key: string): Read< return { value: spelled.decoded } } -// An inline directive never spans lines, and no content slot holds a null character; the emitter carries such a node. +// The emitter carries such a node. export function slotFault(type: string, text: string): ConvertFault | undefined { - if (/[\n\r]/.test(text)) return unsupportedNodeShape(`the ${type} content slot holds a line ending, which no inline directive spans: replace it with a space, or write the node as !adf:carry`) + if (/[\n\r]/.test(text)) return unsupportedNodeShape(`the ${type} content slot holds a line ending, and an inline directive never spans lines: replace it with a space, or write the node as !adf:carry`) return holdsNullCharacter(text) ? unsupportedNodeShape(`the ${type} content slot holds a null character: write the node as !adf:carry`) : undefined } diff --git a/src/markdown/parse/markdown-to-adf.test.ts b/src/markdown/parse/markdown-to-adf.test.ts index ff885c6..90be001 100644 --- a/src/markdown/parse/markdown-to-adf.test.ts +++ b/src/markdown/parse/markdown-to-adf.test.ts @@ -1103,7 +1103,7 @@ test('names the content slot no lone plain text node reads back from', () => { assert.equal(code(losslessMarkdownToAdf('!adf:status[
]{color=yellow}\n')), 'unmappable-html') assert.equal(code(losslessMarkdownToAdf('!adf:date[
]{timestamp=1}\n')), 'unmappable-html') assert.equal(code(losslessMarkdownToAdf('!adf:widget[
]\n')), 'unmappable-html') - const spans = 'unsupported-node-shape: the status content slot holds a line ending, which no inline directive spans: replace it with a space, or write the node as !adf:carry' + const spans = 'unsupported-node-shape: the status content slot holds a line ending, and an inline directive never spans lines: replace it with a space, or write the node as !adf:carry' assert.equal(content(losslessMarkdownToAdf('!adf:status[!adf:text{text="\\n"}]{color=yellow}\n')), spans) assert.equal(content(losslessMarkdownToAdf('!adf:status[a b]{color=yellow}\n')), spans) assert.equal(content(losslessMarkdownToAdf('!adf:status[a b]{color=yellow}\n')), spans)