diff --git a/AGENTS.md b/AGENTS.md index 9a3a8de..f80c018 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -172,7 +172,8 @@ both answer to the round-trip corpus and to nothing else where a node has no fix directions must answer alike — whether a list marker interrupts a paragraph — is one function there too, never a copy per direction, however conservative the copy would be. Where the rule is the emitter's own choice, input consults it rather than restating it: the parser asks - `spellsCommonMark` which form the emitter picks, so no fixture the emitter writes can be refused. + `commonMarkSpelling` which form the emitter picks, so no fixture the emitter writes can be + refused, and a spelling the emitter refuses gives its own error rather than a second name for it. - The attribute vocabulary is ADF's: `adf/` walks it and narrows each value to its kind, and a format spells the narrowed value. A spelling that re-checks the type is the check's second copy. Reading a spelling back is the format's own: the reader sits beside the spelling it inverts, so diff --git a/spec/flavour.md b/spec/flavour.md index 6ff1570..73f4085 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -157,8 +157,8 @@ included, is an error result naming it. The flavour never emits raw HTML. The directive name is always the ADF node type. A container's body is the node's `content`; a leaf has none. Every directive parses in any position — `markdownToAdf` builds exactly what is written; validity against ADF's content models stays the author's business (AGENTS.md §14). It -parses only in the form the emitter picks, though: a directive spelling a node CommonMark holds -is a named error, the per-node plain-versus-directive choice below read backwards. +parses only in the form the emitter picks, though: a directive spelling a node the emitter would +have written as CommonMark is a named error. Each section lists attributes as `name (type)`. A parenthesized value set documents what real payloads hold; the type stays string and any value round-trips verbatim. Values map to attrs by diff --git a/src/markdown/directive-syntax.ts b/src/markdown/directive-syntax.ts index 92b1d6a..c5aa55c 100644 --- a/src/markdown/directive-syntax.ts +++ b/src/markdown/directive-syntax.ts @@ -107,7 +107,8 @@ export function unknownDirectiveFault(name: string): ConvertFault { } function keyOrder(left: string, right: string): number { - return left < right ? -1 : 1 + if (left < right) return -1 + return left > right ? 1 : 0 } function quote(text: string): string { diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts index eef32d1..b1a5076 100644 --- a/src/markdown/emit/adf-to-markdown.ts +++ b/src/markdown/emit/adf-to-markdown.ts @@ -86,9 +86,10 @@ function emitBlock(node: AdfNode, path: ConvertErrorPath, depth: number): Result return emitDirectiveBlock(node, directive, path, depth) } -// The plain-versus-directive choice is the emitter's; input reads it back rather than restating it (AGENTS.md §11). -export function spellsCommonMark(node: AdfNode, path: ConvertErrorPath, depth: number): boolean { - return readableBlock(node, path, depth) !== undefined +export function commonMarkSpelling(node: AdfNode, path: ConvertErrorPath, depth: number): Result | undefined { + const readable = readableBlock(node, path, depth) + if (readable === undefined) return undefined + return readable.ok ? success(null) : readable } function readableBlock(node: AdfNode, path: ConvertErrorPath, depth: number): Result | undefined { diff --git a/src/markdown/parse/markdown-to-adf.test.ts b/src/markdown/parse/markdown-to-adf.test.ts index 71d34f0..91c3ad1 100644 --- a/src/markdown/parse/markdown-to-adf.test.ts +++ b/src/markdown/parse/markdown-to-adf.test.ts @@ -148,7 +148,6 @@ test('reads the three directive forms into the nodes the tables name', () => { assert.deepEqual(content(markdownToAdf('Part:hardBreak{}.\n')), [{ content: [text('Part'), hardBreak(), text('.')], type: 'paragraph' }]) }) -// The emitter's plain-versus-directive choice, read backwards: only the form it picks parses. test('names the directive form a node CommonMark spells refuses', () => { const named = (type: string): string => `unsupported-node-shape: ${type} takes the CommonMark spelling, not the directive form` assert.equal(content(markdownToAdf('::rule\n')), named('rule')) @@ -160,6 +159,12 @@ test('names the directive form a node CommonMark spells refuses', () => { assert.deepEqual(content(markdownToAdf('::::bulletList\n:::listItem\n---\n:::\n::::\n')), [bulletList(item({ type: 'rule' }))]) }) +// The spelling the emitter refuses gives the emitter's own error, never a second name for it. +test('gives back the refusal the CommonMark spelling itself raises', () => { + const nested = '::::::::bulletList\n:::::::listItem\n---\n\n::::::bulletList\n:::::listItem\n---\n\n::::bulletList\n:::listItem\n---\n:::\n::::\n:::::\n::::::\n:::::::\n::::::::\n' + assert.equal(code(markdownToAdf(nested)), 'unspelled-block-separation') +}) + test('names the directive name no node reads back to', () => { assert.equal(code(markdownToAdf(':::widget info\nx\n:::\n')), 'unknown-directive-name') assert.equal(content(markdownToAdf('::widget\n')), 'unknown-directive-name: the directive name widget reads back to no node') diff --git a/src/markdown/parse/markdown-to-adf.ts b/src/markdown/parse/markdown-to-adf.ts index a03f404..2aa7782 100644 --- a/src/markdown/parse/markdown-to-adf.ts +++ b/src/markdown/parse/markdown-to-adf.ts @@ -3,12 +3,12 @@ import type { Block, DirectiveBlock } from './blocks.ts' import type { BlockDirectiveNode } from './directive-nodes.ts' import type { LinkDefinitions } from './inline-content.ts' import { carryName } from '../opaque-carry.ts' +import { commonMarkSpelling } from '../emit/adf-to-markdown.ts' import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts' import { largestNesting } from '../../nesting.ts' import { parseBlocks } from './blocks.ts' import { parseInlineContent } from './inline-content.ts' import { readBlockDirectiveNode } from './directive-nodes.ts' -import { spellsCommonMark } from '../emit/adf-to-markdown.ts' export function markdownToAdf(markdown: string): Result { const parsed = parseBlocks(markdown) @@ -58,10 +58,10 @@ function directiveNode(block: DirectiveBlock, definitions: LinkDefinitions, path if (!read.ok) return read const built = directiveBody(read.value, block.blocks, definitions, path, depth) if (!built.ok) return built - if (spellsCommonMark(built.value, path, depth)) { - return failure('unsupported-node-shape', `${built.value.type} takes the CommonMark spelling, not the directive form`, path) - } - return built + const readable = commonMarkSpelling(built.value, path, depth) + if (readable === undefined) return built + if (!readable.ok) return readable + return failure('unsupported-node-shape', `${built.value.type} takes the CommonMark spelling, not the directive form`, path) } function directiveBody(read: BlockDirectiveNode, blocks: Block[] | undefined, definitions: LinkDefinitions, path: ConvertErrorPath, depth: number): Result { diff --git a/todo.md b/todo.md index 207aead..c95c5ca 100644 --- a/todo.md +++ b/todo.md @@ -335,6 +335,9 @@ detail is settled at its own milestone. `corpus.test.ts` too, and `unspelled-block-separation`, which loses its only cause here. The emitter's other refusals survive on causes no fixture in that directory covers, so 3k's one-list pass is where they get fixtures or the directory goes. + Losing that cause closes the last shape input accepts and emit refuses — a CommonMark block + beside a directive one inside a list item — so the parse-then-emit fixpoint the README + promises holds from here rather than only for what the emitter wrote. - [ ] **3i — The inline nodes and the marks.** `inline-nodes/` reads back: the content slot's `text` attribute and the error a slot holding anything but one unmarked text node is; the `:text{text="…"}` whitespace spelling; the four directive marks and their nesting order,