From 2cc6ea3f579a6a34ad80288d0b12b80484bc1293 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 14 Oct 2026 00:38:51 +0200 Subject: [PATCH] =?UTF-8?q?Answer=20architecture=20round=201:=200=20?= =?UTF-8?q?=F0=9F=94=B4,=205=20=F0=9F=9F=A1;=20comments=20and=20docs=20nam?= =?UTF-8?q?e=20the=20narrowing,=20item=20112=20files=20GFM's=20ragged=20ro?= =?UTF-8?q?ws,=20a=20paragraph's=20lines=20carry=20their=20positions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++- spec/flavour.md | 13 ++++---- src/markdown/parse/blocks.ts | 33 ++++++++++--------- .../parse/portable-markdown-to-adf.test.ts | 2 ++ src/markdown/portable/conventions.ts | 2 +- todo.md | 13 +++++++- 6 files changed, 42 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index cb2bb90..e910367 100644 --- a/README.md +++ b/README.md @@ -495,7 +495,9 @@ Serves [Goals](#goals) 1, 4 and 5. - The pipe table that `losslessMarkdownToAdf` reads narrows GFM's twice: every row opens with a pipe, so GFM's bare form is an error result, and an alignment colon in the delimiter row is an error too — ADF holds no column alignment. `portableMarkdownToAdf` reads both and drops the - alignment. The trailing pipe is canonical output, optional in input. + alignment, but narrows GFM's table too: a row whose cell count differs from the header's is an + error result where GFM pads or cuts it, and a line holding no `|` ends the table where GFM reads + it as a row. The trailing pipe is canonical output, optional in input. - Past that and `~~`, `losslessMarkdownToAdf` reads no GFM: an autolink literal and a `- [ ]` marker stay text, and a checklist is the `taskList` directive — `portableMarkdownToAdf` turns the marker into a `taskList`. diff --git a/spec/flavour.md b/spec/flavour.md index c0cfe21..42612f6 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -347,16 +347,17 @@ claimed and must parse as part of a pipe table, else it is a named error — esc (`\|`) to keep it literal text. The shape a missing leading pipe leaves is claimed too: a line holding an unescaped `|`, followed inside one paragraph by a line whose cells are all `-` runs, an alignment colon among them, and match it in count — GFM's table without the outer pipes — is -a named error rather than the prose it reads as; `portableMarkdownToAdf` reads it, and an -alignment colon, as GFM does, dropping the alignment. A pipe table is a header row, a delimiter row whose cells are -runs of one or more `-` (canonical `---`), and body rows; rows follow code-fence indentation. +a named error rather than the prose it reads as. `portableMarkdownToAdf` reads that shape as a +table, its body rows with or without the leading `|` until a line holding none, and drops an +alignment colon's alignment. A pipe table is a header row, a delimiter row whose cells are runs of +one or more `-` (canonical `---`), and body rows; rows follow code-fence indentation. Cells split on unescaped `|` before inline parsing — `\|` stays in the cell text, and the inline layer's ordinary CommonMark escaping yields the pipe; each cell is the inline content of one paragraph, trimmed; canonical form pads cells with single spaces and ends rows with `|` (optional in input). Named errors: a delimiter or body row whose cell count differs from the -header's, and an alignment colon in the delimiter row — ADF holds no column alignment. In a -pipe cell a hard break is `!adf:hardBreak{}` and a literal `|` is `\|`; a `|` inside a quoted -attribute value is already `\u007c`, so the split never reaches it. +header's, and, outside `portableMarkdownToAdf`, an alignment colon in the delimiter row — ADF holds +no column alignment. In a pipe cell a hard break is `!adf:hardBreak{}` and a literal `|` is `\|`; +a `|` inside a quoted attribute value is already `\u007c`, so the split never reaches it. The directive form nests cells as containers of block content inside `tableRow` containers: diff --git a/src/markdown/parse/blocks.ts b/src/markdown/parse/blocks.ts index 3889f9e..488a483 100644 --- a/src/markdown/parse/blocks.ts +++ b/src/markdown/parse/blocks.ts @@ -54,12 +54,14 @@ type OpenLeaf = { position: SourcePosition } & ( | { closer: RegExp | undefined; construct: string; kind: 'html' } | { held: string[]; kind: 'indented-code'; lines: string[] } | { indentation: number; info: string; kind: 'fenced-code'; lines: string[]; marker: string } - | { kind: 'paragraph'; lines: string[]; positions: SourcePosition[] } + | { kind: 'paragraph'; lines: [ParagraphLine, ...ParagraphLine[]] } | { kind: 'pipe-table'; rows: [string[], ...string[][]] } ) type ContainerStart = { kind: 'blockquote'; rest: Line } | { fresh: boolean; indentation: number; kind: 'item'; list: ListBlock; rest: Line } +type ParagraphLine = { position: SourcePosition; text: string } + // The line from an absolute column on: a tab a cut splits keeps the stop it is measured against. type Line = { column: number; text: string } @@ -469,11 +471,10 @@ function appendParagraph(walk: Walk, line: string): void { const leaf = walk.leaf const text = line.replace(/^[ \t]+/, '') if (leaf?.kind !== 'paragraph') { - walk.leaf = { kind: 'paragraph', lines: [text], position: walk.position, positions: [walk.position] } + walk.leaf = { kind: 'paragraph', lines: [{ position: walk.position, text }], position: walk.position } return } - leaf.lines.push(text) - leaf.positions.push(walk.position) + leaf.lines.push({ position: walk.position, text }) } function closeLeaf(walk: Walk): void { @@ -497,17 +498,17 @@ function openGfmTable(walk: Walk, opener: string): boolean { const paragraph = walk.leaf if (walk.claims.pipeTables !== 'gfm' || paragraph?.kind !== 'paragraph') return false const delimiter = pipeCells(opener) ?? barePipeCells(opener) - const header = barePipeCells(paragraph.lines.at(-1) ?? '') - if (delimiter === undefined || header?.length !== delimiter.length || !isDelimiterRow(delimiter)) return false - paragraph.lines.pop() - const position = paragraph.positions.pop() ?? walk.position - if (paragraph.lines.length === 0) walk.leaf = undefined - else closeLeaf(walk) - walk.leaf = { kind: 'pipe-table', position, rows: [header, delimiter] } + const last = paragraph.lines.at(-1) + const header = last === undefined ? undefined : barePipeCells(last.text) + if (last === undefined || delimiter === undefined || header?.length !== delimiter.length || !isDelimiterRow(delimiter)) return false + const [first, ...earlier] = paragraph.lines.slice(0, -1) + walk.leaf = first === undefined ? undefined : { ...paragraph, lines: [first, ...earlier] } + closeLeaf(walk) + walk.leaf = { kind: 'pipe-table', position: last.position, rows: [header, delimiter] } return true } -// GFM, Tables: a row without its leading pipe goes on until a line opens another block. +// GFM, Tables: a row without its leading `|` continues the table while it holds a `|` and opens no other block. function pipeTableRow(claims: Claims, line: string): string[] | undefined { const cells = pipeCells(line) if (cells !== undefined || claims.pipeTables !== 'gfm' || opensBlock(claims, line)) return cells @@ -516,7 +517,7 @@ function pipeTableRow(claims: Claims, line: string): string[] | undefined { function pipeTableBlock(rows: readonly [string[], ...string[][]], position: SourcePosition, claims: Claims): Block { const [header, delimiter, ...body] = rows - if (claims.pipeTables !== 'gfm' && delimiter !== undefined && delimiter.some(isPipeAlignment)) { + if (claims.pipeTables === 'flavour' && delimiter !== undefined && delimiter.some(isPipeAlignment)) { return faultedBlock('a pipe table carries no column alignment ADF could hold: this delimiter row holds an alignment colon', position) } if (delimiter === undefined || !isDelimiterRow(delimiter)) { @@ -554,10 +555,10 @@ function takeParagraph(walk: Walk): Extract | unde const leaf = walk.leaf if (leaf?.kind !== 'paragraph') return undefined walk.leaf = undefined - const text = readLinkDefinitions(walk.definitions, leaf.lines.join('\n')) + const text = readLinkDefinitions(walk.definitions, leaf.lines.map((line) => line.text).join('\n')) if (text === '') return undefined - const kept = leaf.positions[leaf.lines.length - text.split('\n').length] - return { kind: 'paragraph', position: kept ?? leaf.position, text } + const kept = leaf.lines[leaf.lines.length - text.split('\n').length] + return { kind: 'paragraph', position: kept?.position ?? leaf.position, text } } function currentBlocks(walk: Walk): Block[] { diff --git a/src/markdown/parse/portable-markdown-to-adf.test.ts b/src/markdown/parse/portable-markdown-to-adf.test.ts index de5917a..c1777a1 100644 --- a/src/markdown/parse/portable-markdown-to-adf.test.ts +++ b/src/markdown/parse/portable-markdown-to-adf.test.ts @@ -293,6 +293,8 @@ test('reads GFM\'s pipe table without its leading pipes or with alignment colons assert.deepEqual(read('a | b\n--- | --- | ---\n'), [said('a | b --- | --- | ---')]) assert.deepEqual(read('- a | b\n--- | ---\n'), [bare('bulletList', bare('listItem', said('a | b --- | ---')))]) assert.deepEqual(read('a | b\n--- | ---\nc\n'), [bare('table', table.content?.[0] ?? said('')), said('c')]) + const ragged = portableMarkdownToAdf('Intro.\na | b\n--- | ---\nc | d | e\n') + assert.deepEqual(ragged.ok ? undefined : [ragged.error.code, ragged.error.position], ['malformed-pipe-table', { line: 2, offset: 7 }]) }) test('reads what CommonMark reads where no row reads, a directive and a carry fence among it, an image sharing its paragraph with a marker among it', () => { diff --git a/src/markdown/portable/conventions.ts b/src/markdown/portable/conventions.ts index 11f7e56..a02c38e 100644 --- a/src/markdown/portable/conventions.ts +++ b/src/markdown/portable/conventions.ts @@ -10,7 +10,7 @@ export type Claims = { carryFence: boolean directives: boolean highlights: boolean - // `gfm` reads GFM's whole pipe table, its alignment dropped; `flavour` reads rows opening with `|` over a `-` delimiter row. + // `gfm` reads rows with or without the leading `|`, alignment colons dropped; `flavour` only rows opening with `|` over `-` runs. pipeTables: 'flavour' | 'gfm' | false strikethrough: boolean taskMarkers: boolean diff --git a/todo.md b/todo.md index 3917467..cf7d724 100644 --- a/todo.md +++ b/todo.md @@ -6,7 +6,7 @@ `Bar = 9` -`Next ID = 112` +`Next ID = 113` | Goal | W | |---|---| @@ -50,6 +50,7 @@ | 68 | 0.3.0 | | **Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.** | 3 | 3 | 4 | 4 | 7 | 1 | 18.2 | | 75 | 0.3.0 | | **Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.** | 2 | 2 | 4 | 3 | 7 | 1 | 17.5 | | 81 | 0.3.0 | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 2 | 2 | 5 | 5 | 3 | 16.3 | +| 112 | 0.3.0 | question | **Read a GFM pipe table's ragged and pipe-less body rows, and a one-column table without leading pipes, in `portableMarkdownToAdf` as GFM does, keeping every cell.** | 3 | 3 | 5 | 6 | 6 | 4, 7 | 16.3 | | 46 | 0.3.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 3 | 5 | 5 | 3, 8 | 14.8 | | 72 | 0.3.0 | decision | **Re-create `gitea.larvit.se/larvit/adf-codec` as a read-only pull mirror of GitHub, its issues and Actions off and its description pointing at GitHub.** | 2 | 1 | 1 | 3 | 6 | 3 | 13.1 | | 107 | 0.3.0 | decision | **Spell the block carry's JSON so its length stays linear in the node's JSON.** | 4 | 5 | 7 | 3 | 5 | 1 | 10.8 | @@ -272,6 +273,16 @@ and hostile. Found by the technical-principles audit, 2026-10-04. leaves the README's tables a false contract under a green gate. Assert that the tables list exactly the union. Found by the technical-principles audit of item 78, 2026-10-05. +### 112. Read a GFM pipe table's ragged and pipe-less body rows, and a one-column table without leading pipes, in `portableMarkdownToAdf` as GFM does, keeping every cell. + +GFM pads a short row with empty cells, cuts a long one to the header's count, reads a line holding +no `|` after a table as a row, and reads `a |` over `--- |` as a one-column table. +`portableMarkdownToAdf` refuses a ragged row as `malformed-pipe-table`, ends the table at a line +holding no `|`, and reads the one-column shape as a paragraph; the README's pipe-table guarantee +names the first two. Cutting a long row drops content (Goal 7) where Goal 4 asks for GFM's reading: +which wins is the maintainer's. The one-column shape needs `isBareDelimiterRow` in the writer's +escaping to cover it too. + ### 46. Publish the bundle size in the README, failing the release pipeline when it drifts. Lands after item 7, which changes it. The quantity is what a consumer downloads and loads: the