From 43292555cd9939ed8a4d3a55a3111ce25e62f6b0 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 16 Sep 2026 23:28:31 +0200 Subject: [PATCH] =?UTF-8?q?13b=20-=20review=20nits:=20README=20code=20span?= =?UTF-8?q?,=20AGENTS.md=20=C2=A72=20example,=20comments,=20block-directiv?= =?UTF-8?q?e=20link=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 6 +++--- README.md | 4 ++-- src/markdown/emit/inline-line.ts | 3 +-- src/markdown/mark-spellings.ts | 2 +- src/markdown/parse/directive-marks.ts | 4 +++- src/markdown/parse/inline-content.ts | 2 +- src/markdown/parse/markdown-to-adf.test.ts | 2 +- 7 files changed, 12 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e53fa57..4ea6928 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,9 +17,9 @@ When losslessness and readability conflict, losslessness wins. The other direction is a canonical fixpoint, not byte-identity: human markdown normalizes, the way back yields the library's canonical spelling, and that spelling round-trips byte-identically — -where there is a way back. CommonMark spells some things the flavour has no escape for — a link -destination or title holding a backslash or newline, a paragraph opening with a code span whose -backticks read back as a fence — so a parse succeeding does not imply a spellable document; +where there is a way back. CommonMark spells some things the flavour has no escape for — a +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. "Equals" is structural equality over editor-normal ADF — adjacent text nodes with identical marks diff --git a/README.md b/README.md index 3f9928f..299928f 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ emit refuses: | `unspellable-line-start` | a paragraph line begins with a code span whose backticks would read back as a code fence | put any text before the code span | | `unspellable-whitespace` | an `emoji`, `mention` or `status` holds a newline in the text its inline directive spells in the content slot | replace it with a space — an inline directive never spans lines | | `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` | a node carries an attribute, value, argument or body its type does not take — or markdown writes as a directive a node the flavour spells as CommonMark | write the shape the message names; `spec/flavour.md` lists every type's attributes and body | +| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take — or markdown writes as a directive a node or mark the flavour spells as CommonMark | write the shape the message names; `spec/flavour.md` lists every type's attributes and body | ## The guarantees @@ -114,7 +114,7 @@ emit refuses: title-less paragraph; mid-text and titled images are error results. Converting back yields the library's canonical spelling, which round-trips byte-identically — where it converts back at all: a parse succeeding is no promise of that, so keep the source until the way back succeeds. - `` ` `` ` `` reads cleanly and then refuses. + ``` ` `` ` ``` reads cleanly and then refuses. - Three CommonMark spellings parse without an error and build a document the reference renders differently: `[](/url)` and `[]()` stay literal text against CommonMark's empty link, a list continuing past a marker change stays one list against CommonMark's two, and a shortcut diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index 3360335..06bfe66 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -41,7 +41,6 @@ export function emitInlineLine(nodes: readonly AdfNode[], container: LineContain return success(emitted.value.line) } -// The parser asks which form a paragraph's opening link takes rather than restating the line rule (AGENTS.md §11). export function openingLinkTakesDirective(nodes: readonly AdfNode[], path: ConvertErrorPath): Result { const emitted = emitLine(nodes, 'paragraph', path) if (!emitted.ok) return emitted @@ -64,7 +63,7 @@ export function tryImageLine(alt: string | undefined, href: string, path: Conver return attempt.ok ? attempt.value.line : undefined } -// A carried node joins no run and the opening link turns directive once, so every pass carries at least one more node. +// Every pass carries at least one more node, or flips openingLinkAsDirective, which happens once — so the loop ends. function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result { const carried = new Set() let openingLinkAsDirective = false diff --git a/src/markdown/mark-spellings.ts b/src/markdown/mark-spellings.ts index d90660b..1970568 100644 --- a/src/markdown/mark-spellings.ts +++ b/src/markdown/mark-spellings.ts @@ -35,7 +35,7 @@ export function markSpelling(type: string): MarkSpelling | undefined { return { attributes, kind: spelling.kind } } -// spec/flavour.md, Marks. `marksInside` counts the marks the link's nodes carry within it: the emitter's depth, the parser's none. +// spec/flavour.md, Marks. `marksInside` counts the marks the link's nodes carry within it: the emitter's depth + 1, the parser's 0. export function commonMarkLink(attrs: AdfAttributes, href: string, nodes: readonly AdfNode[], marksInside: number, bracketed: boolean): CommonMarkLink | undefined { if (Object.keys(attrs).some((key) => key !== 'href' && key !== 'title')) return undefined const title = attrs['title'] diff --git a/src/markdown/parse/directive-marks.ts b/src/markdown/parse/directive-marks.ts index c9f5fc7..f1b7df6 100644 --- a/src/markdown/parse/directive-marks.ts +++ b/src/markdown/parse/directive-marks.ts @@ -20,7 +20,9 @@ export function readDirectiveMark(name: string, attributes: DirectiveAttributes, export function inlineMarkSpellingFault(name: string): ConvertFault | undefined { const spelling = markSpelling(name) if (spelling === undefined) return undefined - return { code: 'unsupported-node-shape', message: `${name} is spelled ${markdownForm(spelling) ?? `${directivePrefix}${name}[…]`}, never as a block directive` } + const directive = `${directivePrefix}${name}[…]` + const forms = spelling.kind === 'link' ? `[x](url) or ${directive}` : (markdownForm(spelling) ?? directive) + return { code: 'unsupported-node-shape', message: `${name} is spelled ${forms}, never as a block directive` } } function markdownForm(spelling: MarkSpelling): string | undefined { diff --git a/src/markdown/parse/inline-content.ts b/src/markdown/parse/inline-content.ts index f7b58ea..25ccfdf 100644 --- a/src/markdown/parse/inline-content.ts +++ b/src/markdown/parse/inline-content.ts @@ -36,7 +36,7 @@ type Piece = type Run = { canClose: boolean; canOpen: boolean; character: string; index: number; length: number } -// `container` is the emitter's line container, `undefined` inside a directive's content slot; `openingSpellableLink` defers a directive link at offset 0 to `assemble`. +// `container` is `undefined` inside a directive's content slot, the emitter's `bracketed`. type Scan = { container: LineContainer | undefined definitions: LinkDefinitions diff --git a/src/markdown/parse/markdown-to-adf.test.ts b/src/markdown/parse/markdown-to-adf.test.ts index 78adb0a..fcceb47 100644 --- a/src/markdown/parse/markdown-to-adf.test.ts +++ b/src/markdown/parse/markdown-to-adf.test.ts @@ -333,6 +333,7 @@ test('names the leaf given a body at its opener, ahead of any refusal the leaf h test('names the position a directive name the other one spells belongs to', () => { assert.equal(content(markdownToAdf('!adf:em\na\n!adf:/em\n')), 'unsupported-node-shape: em is spelled _x_, never as a block directive') assert.equal(content(markdownToAdf('!adf:underline\n')), 'unsupported-node-shape: underline is spelled !adf:underline[…], never as a block directive') + assert.equal(content(markdownToAdf('!adf:link\n')), 'unsupported-node-shape: link is spelled [x](url) or !adf:link[…], never as a block directive') assert.equal(content(markdownToAdf('!adf:text {text=" "}\n')), 'unsupported-node-shape: text takes the inline form, !adf:text{…}, never the block form') assert.equal(content(markdownToAdf('!adf:date {timestamp=1}\n')), 'unsupported-node-shape: date takes the inline form, !adf:date{…}, never the block form') assert.equal(content(markdownToAdf('!adf:paragraph[a]\n')), 'unsupported-node-shape: paragraph takes the block form, !adf:paragraph, never the inline form') @@ -968,7 +969,6 @@ test('names the mark markdown spells, never a directive', () => { assert.equal(content(markdownToAdf('!adf:code[a]\n')), 'unsupported-node-shape: code is spelled `x`, never as a directive') }) -// spec/flavour.md, Marks: the directive link is refused wherever the emitter would pick the markdown spelling. test('refuses the directive link CommonMark could spell, and reads the one it could not', () => { const refused = 'unsupported-node-shape: link takes the directive form only where CommonMark cannot spell it: this one it can, as [text](url "title") or ' assert.equal(content(markdownToAdf('!adf:link[a]{href="/u"}\n')), refused)