From 3b61501fe2c7981c85a1968850667e9276a3b08a Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 9 Nov 2026 11:38:14 +0100 Subject: [PATCH] =?UTF-8?q?Answer=20stability=20round=201:=201=20?= =?UTF-8?q?=F0=9F=94=B4,=203=20=F0=9F=9F=A1=20(0=20misread);=20drop=20the?= =?UTF-8?q?=20probe=20files,=20keep=20a=20one-cell=20delimiter=20unescaped?= =?UTF-8?q?,=20and=20fix=20the=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 1 + CHANGELOG.md | 11 ++++++---- MIGRATION.md | 20 ++++++++++++------- README.md | 11 +++++----- docs/decisions.md | 13 +++++++++--- spec/flavour.md | 9 +++++---- src/adf/boundary-probe-html.ts | 1 - src/adf/boundary-probe.ts | 1 - src/html/boundary-probe.ts | 1 - src/html/parse/boundary-probe-markdown.ts | 1 - src/html/parse/boundary-probe.ts | 1 - src/markdown/boundary-probe-html.ts | 1 - src/markdown/directive/boundary-probe-emit.ts | 1 - src/markdown/directive/boundary-probe-html.ts | 1 - src/markdown/directive/boundary-probe.ts | 1 - src/markdown/emit/boundary-probe-html.ts | 1 - src/markdown/emit/boundary-probe.ts | 1 - src/markdown/emit/escaping.test.ts | 3 +-- src/markdown/parse/blocks.ts | 2 +- src/markdown/parse/boundary-probe-allowed.ts | 1 - src/markdown/parse/boundary-probe-html.ts | 1 - src/markdown/parse/boundary-probe-portable.ts | 1 - src/markdown/parse/boundary-probe.ts | 1 - src/markdown/pipe-table-syntax.ts | 5 +++-- .../portable/portable-markdown-to-adf.test.ts | 6 ++++-- 25 files changed, 51 insertions(+), 45 deletions(-) delete mode 100644 src/adf/boundary-probe-html.ts delete mode 100644 src/adf/boundary-probe.ts delete mode 100644 src/html/boundary-probe.ts delete mode 100644 src/html/parse/boundary-probe-markdown.ts delete mode 100644 src/html/parse/boundary-probe.ts delete mode 100644 src/markdown/boundary-probe-html.ts delete mode 100644 src/markdown/directive/boundary-probe-emit.ts delete mode 100644 src/markdown/directive/boundary-probe-html.ts delete mode 100644 src/markdown/directive/boundary-probe.ts delete mode 100644 src/markdown/emit/boundary-probe-html.ts delete mode 100644 src/markdown/emit/boundary-probe.ts delete mode 100644 src/markdown/parse/boundary-probe-allowed.ts delete mode 100644 src/markdown/parse/boundary-probe-html.ts delete mode 100644 src/markdown/parse/boundary-probe-portable.ts delete mode 100644 src/markdown/parse/boundary-probe.ts diff --git a/AGENTS.md b/AGENTS.md index ba52199..5829458 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,7 @@ In `docs/decisions.md`: - A callout title keeps its link targets - The portable flavour's spellings - The portable reader refuses what GFM's reading would drop +- The portable reader reads what GFM's reading keeps - The HTML dialect - `htmlToAdf` also reads the dialect as a browser serializes it - No runtime dependencies diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aa3a73..ccb9416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,13 @@ ## Unreleased -- `portableMarkdownToAdf` reads GFM's pipe table further: a body row holding fewer cells than the - header is padded with empty cells, a line holding no `|` after the table is a row of one cell, - and `a |` over `--- |` is a one-column table. A row holding more cells stays an error result. - `MIGRATION.md` shows the change. +- **Breaking:** `portableMarkdownToAdf` reads more of GFM's pipe table. A body row holding fewer + cells than the header is padded with empty cells, where `0.3.1` returned `malformed-pipe-table`. + A line after the table that holds no `|` and opens no other block is a row of one cell, where it + was a paragraph. A row holding more cells stays an error result. +- **Breaking:** `a |` over `--- |` is a one-column table. `portableMarkdownToAdf` reads it as one, + and `losslessMarkdownToAdf` returns `malformed-pipe-table` for it, as for any table without + leading pipes, where `0.3.1` read both as a paragraph. `MIGRATION.md` shows the change. - `adfToPortableMarkdown` keeps a link around the note standing in place of a node it marks, such as a stored image: `[_(image not included)_](https://example.com/a)`. - **Breaking:** `readAdfDocument` replaces `isAdfDocument`: it returns a `Result` whose diff --git a/MIGRATION.md b/MIGRATION.md index a03be8b..e738b91 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -22,13 +22,19 @@ class that keeps its fields behind getters (Vue 2, MobX) is refused too: hand in `JSON.parse(JSON.stringify(document))`. A document holding more than 10,000,000 JSON values is `unsupported-node-shape`. -`portableMarkdownToAdf` reads more of GFM's pipe table. A body row holding fewer cells than the -header is padded with empty cells, where `0.3.1` returned `malformed-pipe-table`. A line holding no -`|` after a table is a row of one cell, where it ended the table and read as a paragraph. `a |` over -`--- |` is a one-column table, where it read as a paragraph. A markdown string that relied on the -old reading takes a blank line between the table and the paragraph. `losslessMarkdownToAdf` reads -the first two as before, and now returns `malformed-pipe-table` for `a |` over `--- |` too, as it -does for any table without leading pipes. +`portableMarkdownToAdf` reads more of GFM's pipe table: + +- A body row holding fewer cells than the header is padded with empty cells, where `0.3.1` + returned `malformed-pipe-table`. +- A line after a table that holds no `|` and opens no other block is a row of one cell, where it + ended the table and read as a paragraph. Markdown that relied on that takes a blank line between + the table and the paragraph. +- Two lines that read as a paragraph in `0.3.1`, a line ending in `|` over a line of `-` runs + ending in `|` (`a |` over `--- |`), are a one-column table. `losslessMarkdownToAdf` returns + `malformed-pipe-table` for them too, as for any table without leading pipes. In either reader, + write the first line as `a \|` to keep both as text. + +`losslessMarkdownToAdf` reads the first two as before. ## From `0.2.0` to `0.3.0` diff --git a/README.md b/README.md index 7f7c1c7..b21cda3 100644 --- a/README.md +++ b/README.md @@ -602,7 +602,7 @@ Which reader raises which code: [The calls](#the-calls). Codes every direction r | Code | Fires when | What you can do | | --- | --- | --- | | `malformed-directive` | an `!adf:` the grammar cannot read — a prefix completing no directive, an unclosed container, `[content]` or `{attrs}`, a closer with no container of its name open, a leaf given a body, `{attrs}` out of order or duplicated, invalid JSON in an opaque carry | write the spelling the message names, or keep it literal: escape the prefix — `\!adf:`, block and inline alike — or, for a code fence whose info string opens `adf:`, drop the info string and wrap the fence in `!adf:codeBlock {language="adf:…"}` | -| `malformed-pipe-table` | a pipe row that is no pipe table — a missing `---` delimiter row, a delimiter row whose cell count differs from the header's, or a body row holding more cells than the header; in `losslessMarkdownToAdf` also a body row holding fewer cells, an alignment colon, and a header or delimiter row not opening with a pipe | add a `---` row under the header and give no row more cells than the header, and for `losslessMarkdownToAdf` the header's count exactly; for `losslessMarkdownToAdf`, also open every row with a pipe and drop the colons. To keep the lines literal text, escape the leading pipe of every one — escaping a single row leaves the next to open a fresh table and fail the same way — or, for a table without leading pipes, write one `\|` of its delimiter row as `\\|` | +| `malformed-pipe-table` | a pipe row that is no pipe table — a missing `---` delimiter row, a delimiter row whose cell count differs from the header's, or a body row holding more cells than the header; in `losslessMarkdownToAdf` also a body row holding fewer cells, an alignment colon, and a header or delimiter row not opening with a pipe | add a `---` row of the header's cell count under the header and give no body row more cells; for `losslessMarkdownToAdf`, also give every row the header's count, open every row with a pipe and drop the colons. To keep the lines literal text, escape the leading pipe of every one — escaping a single row leaves the next to open a fresh table and fail the same way — or, for a table without leading pipes, write one `\|` of its delimiter row as `\\|` | | `not-a-string` | the value handed in is not a string, such as `null`, a number or a `String` object | pass the input as a string, `String(value)` for a `String` object; the message names the value's type | | `unknown-directive-name` | a directive whose name is no node or mark this version spells | check the name in [`spec/flavour.md`](https://github.com/larvit/adf-codec/blob/main/spec/flavour.md), or escape the prefix as `\!adf:`; the spelling itself is well formed, so a later minor may give the name meaning | | `unmappable-html` | the markdown holds raw HTML, a comment and a processing instruction among them; the HTML handed to `htmlToAdf` holds HTML `adfToHtml` does not write, in a browser's spelling or its own, such as an edit to what a carry element shows, unless a code under [Either direction](#either-direction) names the cause | remove the construct, or write what it holds as markdown; for `htmlToAdf`, write the HTML as [`spec/html.md`](https://github.com/larvit/adf-codec/blob/main/spec/html.md) spells it | @@ -730,10 +730,11 @@ Serves [Goals](#goals) 1, 4 and 5. - Both readers narrow GFM's pipe table. A body row holding more cells than the header is an error result, where GFM cuts the extra cells. - `losslessMarkdownToAdf` narrows it further. A body row holding fewer cells is an error result, - where GFM pads it, and a line holding no `|` ends the table, where GFM reads it as a row. It - needs a leading `|`: a header or delimiter row without one is an error result. It refuses an - alignment colon in the delimiter row too, since ADF holds no column alignment. - `portableMarkdownToAdf` pads, reads the line as a row, accepts both and drops the alignment. The + where GFM pads it, and a later line without a leading `|` ends the table, where GFM reads it as + a row. A header or delimiter row without a leading `|` is an error result, and so is an + alignment colon in the delimiter row, since ADF holds no column alignment. + `portableMarkdownToAdf` pads a short row, reads a line holding no `|` as a row of one cell unless + it opens another block, reads a table without leading pipes, and drops the alignment. 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 diff --git a/docs/decisions.md b/docs/decisions.md index f9d9d22..07db9bb 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -393,9 +393,16 @@ breaks Goal 4. A refusal builds no table, but it still breaks Goal 4: a bot gets GitHub renders. Goal 7 is a Maintain goal, holding whatever it costs, so it wins over Goal 4, an Achieve goal. -A shape whose GFM reading drops nothing is read as GFM reads it: a short body row is padded with -empty cells, a line holding no `|` after the table is a row of one cell, and `a |` over `--- |` is -a one-column table. + +## The portable reader reads what GFM's reading keeps + +2026-11-09, plan item 112, approved by the maintainer. Goals 4 and 7. Valid while GFM's reading +of a shape drops no content. + +Where GFM's reading of a shape drops nothing, `portableMarkdownToAdf` reads it as GFM does. A pipe +table's short body row is padded with empty cells, a line holding no `|` that opens no other +block is a body row of one cell, and `a |` over `--- |` is a one-column table. `losslessMarkdownToAdf` +keeps its stricter reading (§The portable reader refuses what GFM's reading would drop). ## The HTML dialect diff --git a/spec/flavour.md b/spec/flavour.md index 502332d..b3301f8 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -367,13 +367,14 @@ inline layer's ordinary CommonMark escaping yields the pipe; each cell is the in one paragraph, trimmed; canonical form pads cells with single spaces and ends rows with `|` (optional in input). Named errors: a delimiter row whose cell count differs from the header's, a body row holding more cells than the header, and, in `losslessMarkdownToAdf`, a body row -holding fewer cells 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. +holding fewer cells 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. `portableMarkdownToAdf` reads GFM's table: any row may drop its leading `|`, a header over a delimiter row of one cell reads as a one-column table (`a |` over `--- |`), a body row continues the table until a blank line or a line opening another block, a body row holding fewer cells is -padded with empty cells, a line holding no `|` is a body row of one cell, and an alignment colon is -read and its alignment dropped. +padded with empty cells, a line holding no `|` that opens no other block is a body row of one +cell, and an alignment colon is read and its alignment dropped. The directive form nests cells as containers of block content inside `tableRow` containers: diff --git a/src/adf/boundary-probe-html.ts b/src/adf/boundary-probe-html.ts deleted file mode 100644 index 6aa7f87..0000000 --- a/src/adf/boundary-probe-html.ts +++ /dev/null @@ -1 +0,0 @@ -import '../html/emit/adf-to-html.ts'; diff --git a/src/adf/boundary-probe.ts b/src/adf/boundary-probe.ts deleted file mode 100644 index 962816a..0000000 --- a/src/adf/boundary-probe.ts +++ /dev/null @@ -1 +0,0 @@ -import '../markdown/flavour.ts'; diff --git a/src/html/boundary-probe.ts b/src/html/boundary-probe.ts deleted file mode 100644 index 962816a..0000000 --- a/src/html/boundary-probe.ts +++ /dev/null @@ -1 +0,0 @@ -import '../markdown/flavour.ts'; diff --git a/src/html/parse/boundary-probe-markdown.ts b/src/html/parse/boundary-probe-markdown.ts deleted file mode 100644 index 14a0e26..0000000 --- a/src/html/parse/boundary-probe-markdown.ts +++ /dev/null @@ -1 +0,0 @@ -import '../../markdown/flavour.ts'; diff --git a/src/html/parse/boundary-probe.ts b/src/html/parse/boundary-probe.ts deleted file mode 100644 index 5b3ebcd..0000000 --- a/src/html/parse/boundary-probe.ts +++ /dev/null @@ -1 +0,0 @@ -import '../emit/adf-to-html.ts'; diff --git a/src/markdown/boundary-probe-html.ts b/src/markdown/boundary-probe-html.ts deleted file mode 100644 index 6aa7f87..0000000 --- a/src/markdown/boundary-probe-html.ts +++ /dev/null @@ -1 +0,0 @@ -import '../html/emit/adf-to-html.ts'; diff --git a/src/markdown/directive/boundary-probe-emit.ts b/src/markdown/directive/boundary-probe-emit.ts deleted file mode 100644 index 274d62a..0000000 --- a/src/markdown/directive/boundary-probe-emit.ts +++ /dev/null @@ -1 +0,0 @@ -import '../emit/image.ts'; diff --git a/src/markdown/directive/boundary-probe-html.ts b/src/markdown/directive/boundary-probe-html.ts deleted file mode 100644 index 0eefb8a..0000000 --- a/src/markdown/directive/boundary-probe-html.ts +++ /dev/null @@ -1 +0,0 @@ -import '../../html/emit/adf-to-html.ts'; diff --git a/src/markdown/directive/boundary-probe.ts b/src/markdown/directive/boundary-probe.ts deleted file mode 100644 index 8bf220e..0000000 --- a/src/markdown/directive/boundary-probe.ts +++ /dev/null @@ -1 +0,0 @@ -import '../parse/blocks.ts'; diff --git a/src/markdown/emit/boundary-probe-html.ts b/src/markdown/emit/boundary-probe-html.ts deleted file mode 100644 index 0eefb8a..0000000 --- a/src/markdown/emit/boundary-probe-html.ts +++ /dev/null @@ -1 +0,0 @@ -import '../../html/emit/adf-to-html.ts'; diff --git a/src/markdown/emit/boundary-probe.ts b/src/markdown/emit/boundary-probe.ts deleted file mode 100644 index 110f4e5..0000000 --- a/src/markdown/emit/boundary-probe.ts +++ /dev/null @@ -1 +0,0 @@ -import '../portable/conventions.ts'; diff --git a/src/markdown/emit/escaping.test.ts b/src/markdown/emit/escaping.test.ts index 0f79cac..9f7b354 100644 --- a/src/markdown/emit/escaping.test.ts +++ b/src/markdown/emit/escaping.test.ts @@ -34,8 +34,7 @@ test('escapes the delimiter row a hard break leaves opening a pipe table with no assert.equal(broken('--- | ---'), 'a | b\\\n\\--- | ---\n') assert.equal(broken(':--- | ---:'), 'a | b\\\n\\:--- | ---:\n') assert.equal(broken('c | d'), 'a | b\\\nc | d\n') - const oneColumn = markdown(adfToLosslessMarkdown(document(paragraph({ text: 'a |', type: 'text' }, { type: 'hardBreak' }, { text: '--- |', type: 'text' })))) - assert.equal(oneColumn, 'a |\\\n\\--- |\n') + assert.equal(readsBack(document(paragraph({ text: 'a |', type: 'text' }, { type: 'hardBreak' }, { text: '--- |', type: 'text' }))), 'a |\\\n--- |\n') }) test('escapes only text that would otherwise open a construct', () => { diff --git a/src/markdown/parse/blocks.ts b/src/markdown/parse/blocks.ts index 055d1e0..cb66f73 100644 --- a/src/markdown/parse/blocks.ts +++ b/src/markdown/parse/blocks.ts @@ -533,7 +533,7 @@ function openGfmTable(walk: Walk, opener: string): boolean { return true } -// GFM, Tables: a row without its leading `|` continues the table until a blank line or a line opening another block; a line holding no `|` is one cell. +// GFM, Tables: a row without its leading `|` continues the table until a blank line or a line opening another block, and a line holding no `|` is one cell. function pipeTableRow(claims: Claims, line: string): string[] | undefined { const cells = pipeCells(line) if (cells !== undefined || claims.pipeTables !== 'gfm' || isBlankLine(line) || opensBlock(claims, line)) return cells diff --git a/src/markdown/parse/boundary-probe-allowed.ts b/src/markdown/parse/boundary-probe-allowed.ts deleted file mode 100644 index c3def07..0000000 --- a/src/markdown/parse/boundary-probe-allowed.ts +++ /dev/null @@ -1 +0,0 @@ -import '../emit/spelling-questions.ts'; diff --git a/src/markdown/parse/boundary-probe-html.ts b/src/markdown/parse/boundary-probe-html.ts deleted file mode 100644 index 0eefb8a..0000000 --- a/src/markdown/parse/boundary-probe-html.ts +++ /dev/null @@ -1 +0,0 @@ -import '../../html/emit/adf-to-html.ts'; diff --git a/src/markdown/parse/boundary-probe-portable.ts b/src/markdown/parse/boundary-probe-portable.ts deleted file mode 100644 index 110f4e5..0000000 --- a/src/markdown/parse/boundary-probe-portable.ts +++ /dev/null @@ -1 +0,0 @@ -import '../portable/conventions.ts'; diff --git a/src/markdown/parse/boundary-probe.ts b/src/markdown/parse/boundary-probe.ts deleted file mode 100644 index a2a9ef0..0000000 --- a/src/markdown/parse/boundary-probe.ts +++ /dev/null @@ -1 +0,0 @@ -import '../emit/adf-to-markdown.ts'; diff --git a/src/markdown/pipe-table-syntax.ts b/src/markdown/pipe-table-syntax.ts index ed98b97..c312a9b 100644 --- a/src/markdown/pipe-table-syntax.ts +++ b/src/markdown/pipe-table-syntax.ts @@ -4,15 +4,16 @@ import { backslashEscape, claimsPipeLine, trimSpace } from './commonmark/grammar const alignmentCell = /^:-+:?$|^-+:$/ const delimiterCell = /^-+$/ -// spec/flavour.md, Tables: the cells of a row no leading `|` claimed — GFM's form without the outer pipes, one `|` making a one-column row. +// spec/flavour.md, Tables: the cells of a row no leading `|` claimed — GFM's form without the outer pipes; a closing `|` after one cell makes a one-column row. export function barePipeCells(line: string): string[] | undefined { const cells = splitPipeCells(line, 0) return cells.length > 1 ? withoutClosingCell(cells) : undefined } +// A one-cell row is no delimiter here: the hard break before it leaves its header line ending in `\`, which splits into two cells. export function isBareDelimiterRow(line: string): boolean { const cells = barePipeCells(line) - return cells !== undefined && isDelimiterRow(cells) + return cells !== undefined && cells.length > 1 && isDelimiterRow(cells) } export function isDelimiterRow(cells: readonly string[]): boolean { diff --git a/src/markdown/portable/portable-markdown-to-adf.test.ts b/src/markdown/portable/portable-markdown-to-adf.test.ts index 767e626..ceacf59 100644 --- a/src/markdown/portable/portable-markdown-to-adf.test.ts +++ b/src/markdown/portable/portable-markdown-to-adf.test.ts @@ -406,8 +406,10 @@ test('reads GFM\'s short row padded with empty cells, a line holding no pipe as assert.deepEqual(read('a |\n:--- |\nb\n'), [column(['a', 'b'])]) assert.deepEqual(read('| a |\n| --- |\nb\n'), [column(['a', 'b'])]) assert.deepEqual(read('a\n--- |\n'), [said('a --- |')]) - const long = portableMarkdownToAdf('a | b\n--- | ---\nc | d | e\n') - assert.deepEqual(long.ok ? undefined : long.error.code, 'malformed-pipe-table') + for (const refused of ['a | b\n--- | ---\nc | d | e\n', '| a | b |\n| --- |\n']) { + const result = portableMarkdownToAdf(refused) + assert.deepEqual(result.ok ? undefined : result.error.code, 'malformed-pipe-table', refused) + } }) 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', () => {