From e68dbc41a38c4dc7592b6e91c518ca126dfffb5c Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 1 Sep 2026 19:13:13 +0200 Subject: [PATCH] Answer the stability pass: the strip that changed nothing, and the words the body earns --- spec/flavour.md | 8 +++++--- src/markdown/code-language.ts | 2 +- src/markdown/commonmark-grammar.ts | 2 +- src/markdown/emit/adf-to-markdown.ts | 2 +- src/markdown/parse/blocks.ts | 1 - src/markdown/parse/markdown-to-adf.test.ts | 6 +++++- src/markdown/parse/markdown-to-adf.ts | 3 +-- src/markdown/pipe-table-syntax.ts | 11 +++++------ 8 files changed, 19 insertions(+), 16 deletions(-) diff --git a/spec/flavour.md b/spec/flavour.md index 68bc70a..878bafe 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -22,8 +22,9 @@ normalizes to it through the round-trip. between a nested list and a CommonMark block above it — one wherever the nested list's own marker cannot interrupt a paragraph (an ordered list whose first number is not 1, or a list whose first item is empty), whatever block sits above it. Blank lines between items normalize - away, and so does the marker change CommonMark starts a second list on: ADF records no - tightness, and one `- ` spelling leaves two adjacent lists of a type no way back. + away, and no list opens beside one of its own kind — the marker change CommonMark starts a + second list on merges instead: ADF records no tightness, and one `- ` spelling leaves two + adjacent lists of a kind no way back. - Blockquotes prefix lines with `> `; a blank line inside a blockquote is a bare `>`. - ATX headings (`#` … `######`); setext input normalizes to ATX. - Code fences ``` with the node's language as info string, the fence lengthened past any backtick @@ -192,7 +193,8 @@ form. 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 fence carries no info string; writing it in the slot that rule leaves empty, or in both, is a named error. The body is - an ordinary fence, so its info string decodes escapes and entity references as any other does. + one ordinary code block, and a fence's info string decodes escapes and entity references as any + other does. - `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. diff --git a/src/markdown/code-language.ts b/src/markdown/code-language.ts index 6d0b4b7..27f3a07 100644 --- a/src/markdown/code-language.ts +++ b/src/markdown/code-language.ts @@ -5,7 +5,7 @@ import { holdsEntityReference } from './entity-references.ts' export type LanguageSlot = { info: string; kind: 'fence' } | { kind: 'attribute' } | { kind: 'none' } -// spec/flavour.md, The CommonMark blocks: the one slot a codeBlock's language rides, both directions. +// spec/flavour.md, The CommonMark blocks: the one slot a codeBlock's language rides. export function languageSlot(language: JsonValue | undefined): LanguageSlot { if (language === undefined) return { kind: 'none' } if (typeof language !== 'string' || language === '' || language === carryName) return { kind: 'attribute' } diff --git a/src/markdown/commonmark-grammar.ts b/src/markdown/commonmark-grammar.ts index 7a9a828..fd93d8d 100644 --- a/src/markdown/commonmark-grammar.ts +++ b/src/markdown/commonmark-grammar.ts @@ -60,7 +60,7 @@ const firstCharacterOpeners = [atxHeadingOpener, /^>/, bulletListOpener, codeFen const emailNameSource = "[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+" const emailLabelSource = '[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?' const emailAutolink = new RegExp(`<${emailNameSource}@${emailLabelSource}(?:\\.${emailLabelSource})*>`, 'y') -const orderedListOpener = /^(\d{1,9})([.)])(?:[ \t]|$)/ +const orderedListOpener = /^(\d{1,9})(?:[.)])(?:[ \t]|$)/ const setextUnderline = /^(=+|-+)[ \t]*$/ const thematicBreak = /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/ const unicodeWhitespace = /[\t\n\f\r \p{Zs}]/u diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts index 5b5bfef..7125c34 100644 --- a/src/markdown/emit/adf-to-markdown.ts +++ b/src/markdown/emit/adf-to-markdown.ts @@ -6,8 +6,8 @@ import { carriesOnly, isAdfDocument } from '../../adf/document.ts' import { emitInlineLine } from './inline-line.ts' import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts' import { fencedCodeBlock } from '../backtick-runs.ts' -import { languageSlot } from '../code-language.ts' import { holdsNullCharacter, isThematicBreak, markerInterruptsParagraph } from '../commonmark-grammar.ts' +import { languageSlot } from '../code-language.ts' import { largestNesting } from '../../nesting.ts' import { spellDirectiveHeader } from './block-directive-spelling.ts' import { tryImage } from './image.ts' diff --git a/src/markdown/parse/blocks.ts b/src/markdown/parse/blocks.ts index 125c435..0564db0 100644 --- a/src/markdown/parse/blocks.ts +++ b/src/markdown/parse/blocks.ts @@ -383,7 +383,6 @@ function closeLeaf(walk: Walk): void { else currentBlocks(walk).push({ kind: 'code', language: leaf.kind === 'fenced-code' ? decodeTextEscapes(leaf.info) : '', text: leaf.lines.join('\n') }) } -// spec/flavour.md, Tables: the delimiter row underlines the header and leaves the body its cell count. function pipeTableBlock(rows: readonly [string[], ...string[][]]): Block { const [header, delimiter, ...body] = rows if (delimiter !== undefined && delimiter.some(isPipeAlignment)) { diff --git a/src/markdown/parse/markdown-to-adf.test.ts b/src/markdown/parse/markdown-to-adf.test.ts index d32f96f..c5f612c 100644 --- a/src/markdown/parse/markdown-to-adf.test.ts +++ b/src/markdown/parse/markdown-to-adf.test.ts @@ -135,6 +135,9 @@ test('reads the codeBlock directive body as the node content, the info string it assert.deepEqual(content(markdownToAdf(':::codeBlock {language=""}\n```\nx\n```\n:::\n')), [ { attrs: { language: '' }, content: [text('x')], type: 'codeBlock' }, ]) + assert.deepEqual(content(markdownToAdf(':::codeBlock {wrap=true}\n fn()\n:::\n')), [ + { attrs: { wrap: true }, content: [text('fn()')], type: 'codeBlock' }, + ]) // The body is a CommonMark fence, so its info string decodes escapes the way any other fence's does. assert.deepEqual(content(markdownToAdf(':::codeBlock {wrap=true}\n```\\#c\nx\n```\n:::\n')), [ { attrs: { language: '#c', wrap: true }, content: [text('x')], type: 'codeBlock' }, @@ -305,7 +308,7 @@ test('names the argument and the body a node takes no reading for', () => { assert.equal(content(markdownToAdf(':::paragraph\n:::\n')), 'unsupported-node-shape: an empty paragraph takes the leaf form, ::') assert.equal(content(markdownToAdf(':::paragraph\nOne.\n\nTwo.\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body') assert.equal(content(markdownToAdf(':::paragraph\n---\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body') - assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\nx\n:::\n')), 'unsupported-node-shape: codeBlock takes one fenced code block as its body') + assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\nx\n:::\n')), 'unsupported-node-shape: codeBlock takes one code block as its body') assert.equal(content(markdownToAdf(':::paragraph\n![a](/u)\n:::\n')), 'unmappable-image: no ADF node carries an image inside a paragraph') assert.equal(content(markdownToAdf('Part :date[now]{timestamp=1}.\n')), 'unsupported-node-shape: date takes no content') assert.equal( @@ -392,6 +395,7 @@ test('reads a bullet list, the marker width setting the continuation', () => { assert.deepEqual(content(markdownToAdf('- a\n\n+ b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))]) assert.deepEqual(content(markdownToAdf('- a\n-\n\n- c\n')), [bulletList(item(paragraph('a')), item(), item(paragraph('c')))]) assert.deepEqual(content(markdownToAdf('- a\n1. b\n')), [bulletList(item(paragraph('a'))), orderedList(1, item(paragraph('b')))]) + assert.deepEqual(content(markdownToAdf('- a\n\n[r]: /u\n\n- b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))]) assert.deepEqual(content(markdownToAdf('-\n\n Part.\n')), [bulletList(item()), paragraph('Part.')]) }) diff --git a/src/markdown/parse/markdown-to-adf.ts b/src/markdown/parse/markdown-to-adf.ts index 9009f5f..52d6928 100644 --- a/src/markdown/parse/markdown-to-adf.ts +++ b/src/markdown/parse/markdown-to-adf.ts @@ -79,10 +79,9 @@ function directiveBody(read: BlockDirectiveNode, blocks: Block[] | undefined, de return inlineBodyNode(node, blocks, definitions, path) } -// spec/flavour.md, The CommonMark blocks: the language rides the one slot fenceInfo picks for it. function codeDirectiveNode(node: AdfNode, blocks: readonly Block[], path: ConvertErrorPath): Result { const only = blocks.length === 1 ? blocks[0] : undefined - if (only?.kind !== 'code') return failure('unsupported-node-shape', `${node.type} takes one fenced code block as its body`, path) + if (only?.kind !== 'code') return failure('unsupported-node-shape', `${node.type} takes one code block as its body`, path) const attribute = node.attrs?.['language'] const fromFence = only.language !== '' const slot = languageSlot(fromFence ? only.language : attribute) diff --git a/src/markdown/pipe-table-syntax.ts b/src/markdown/pipe-table-syntax.ts index 5935d40..f665a75 100644 --- a/src/markdown/pipe-table-syntax.ts +++ b/src/markdown/pipe-table-syntax.ts @@ -19,22 +19,21 @@ export function malformedPipeTable(message: string): ConvertFault { // spec/flavour.md, Tables: the cells of a claimed row, the closing `|` the spelling writes optional here. export function pipeCells(line: string): string[] | undefined { if (!claimsPipeLine(line)) return undefined - const row = line.replace(/[ \t]+$/, '') const cells: string[] = [] let start = 1 let index = 1 - while (index < row.length) { - if (backslashEscape(row, index) !== undefined) { + while (index < line.length) { + if (backslashEscape(line, index) !== undefined) { index += 2 continue } - if (row.charAt(index) === '|') { - cells.push(trimSpace(row.slice(start, index))) + if (line.charAt(index) === '|') { + cells.push(trimSpace(line.slice(start, index))) start = index + 1 } index += 1 } - cells.push(trimSpace(row.slice(start))) + cells.push(trimSpace(line.slice(start))) if (cells.length > 1 && cells.at(-1) === '') cells.pop() return cells }