diff --git a/AGENTS.md b/AGENTS.md index 307ddba..464f49b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,8 +28,9 @@ Round-trip equality is a property tested over a corpus, not a claim made in pros - Unknown ADF node: 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, `hardBreak`, `listItem` and `text` where a - block belongs excepted. Where a container's own spelling cannot hold the child it has — a + 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` outside `listItem`, a `codeBlock` outside text — the error result names that instead. - Unmappable foreign HTML element: error result naming the element — never a silent drop. diff --git a/spec/flavour.md b/spec/flavour.md index ed6757a..934d5fc 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -114,10 +114,8 @@ literal-text fallback — a typo that reparses as prose is the silent loss §2 r ## The opaque carry (AGENTS.md §3) A node no section spells where it stands — an unknown type, or a known one whose spelling belongs -to the other position — rides as its raw JSON and restores to a deep-equal node. `hardBreak`, -`listItem` and `text` where a block belongs are the exception: their spelling lives inside another -node's body, so the misplacement is a named error. Block and inline positions canonicalize -differently, each fitting where it sits: +to the other position — rides as its raw JSON and restores to a deep-equal node. Block and inline +positions canonicalize differently, each fitting where it sits: - **Block position**: a fenced code block with info string `adf`, body = the node's JSON — two-space indent, object keys sorted. diff --git a/src/adf-to-markdown.test.ts b/src/adf-to-markdown.test.ts index ffd318b..346f379 100644 --- a/src/adf-to-markdown.test.ts +++ b/src/adf-to-markdown.test.ts @@ -117,6 +117,9 @@ test('carries a node type no section spells', () => { assert.equal(markdown(adfToMarkdown(document({ type: 'blockCard' }))), '```adf\n{\n "type": "blockCard"\n}\n```\n') assert.equal(markdown(adfToMarkdown(document({ type: 'toString' }))), '```adf\n{\n "type": "toString"\n}\n```\n') assert.equal(markdown(adfToMarkdown(document(paragraph({ type: 'blockCard' })))), ':adf{json="{\\"type\\":\\"blockCard\\"}"}\n') + assert.equal(markdown(adfToMarkdown(document({ text: 'x', type: 'text' }))), '```adf\n{\n "text": "x",\n "type": "text"\n}\n```\n') + assert.equal(markdown(adfToMarkdown(document({ type: 'listItem' }))), '```adf\n{\n "type": "listItem"\n}\n```\n') + assert.equal(markdown(adfToMarkdown(document({ type: 'hardBreak' }))), '```adf\n{\n "type": "hardBreak"\n}\n```\n') }) test('carries the code block whose language is the reserved info string', () => { @@ -143,7 +146,6 @@ test('refuses a carried node nested deeper than the emitter carries', () => { }) test('refuses a node whose content model the canonical form cannot emit', () => { - assert.equal(code(adfToMarkdown(document({ type: 'listItem' }))), 'unsupported-node-shape') assert.equal(code(adfToMarkdown(document({ content: [paragraph()], type: 'codeBlock' }))), 'unsupported-node-shape') assert.equal(code(adfToMarkdown(document({ content: [paragraph()], type: 'bulletList' }))), 'unsupported-node-shape') assert.equal(code(adfToMarkdown(document({ type: 'bulletList' }))), 'unsupported-node-shape') diff --git a/src/adf-to-markdown.ts b/src/adf-to-markdown.ts index 7c98bb4..b3ed67f 100644 --- a/src/adf-to-markdown.ts +++ b/src/adf-to-markdown.ts @@ -91,9 +91,6 @@ function emitBlock(node: AdfNode, path: ConvertErrorPath, depth: number): Result if (node.type === 'table') return emitTable(node, directive, path, depth) return emitDirectiveBlock(node, directive, path, depth) } - if (node.type === 'hardBreak' || node.type === 'listItem' || node.type === 'text') { - return failure('unsupported-node-shape', `a ${node.type} node cannot stand where a block belongs`, path) - } return commonMarkLine(carriedBlock(node, path)) } diff --git a/todo.md b/todo.md index 3a65451..f3a0a48 100644 --- a/todo.md +++ b/todo.md @@ -46,11 +46,6 @@ detail is settled at its own milestone. `::paragraph` beside a CommonMark block included — and, since a `mediaSingle`'s spelling now follows whether CommonMark can spell its URL, two sibling images differing only by an `&` land in the same refusal. - **Also blocked**: whether `hardBreak`, `listItem` and `text` where a block belongs carry too, - rather than the shape error §3 records. The prior art splits — a markdown serializer spells a - node by type without checking the position it stands in (`mdast-util-to-markdown`, - `prosemirror-markdown`), while Atlassian's own validator calls it `INVALID_CONTENT` or wraps - it in `unsupportedBlock`/`unsupportedInline` — so §8 leaves the pick here. - [x] **1d1 — The CommonMark subset**: blockquote, bulletList, codeBlock, heading, orderedList, paragraph, rule, listItem, hardBreak, text, code spans, and the `code`, `em`, `link`, `strike` and `strong` marks — one mark per text node; nesting is 1d3's.