3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 11:48:29 +01:00

Answer stability round 1: 1 🔴, 3 🟡 (0 misread); drop the probe files, keep a one-cell delimiter unescaped, and fix the docs

This commit is contained in:
2026-11-09 11:38:14 +01:00
committed by Lillem4n
parent db2c395d05
commit 3b61501fe2
25 changed files with 51 additions and 45 deletions
+1
View File
@@ -38,6 +38,7 @@ In `docs/decisions.md`:
- A callout title keeps its link targets - A callout title keeps its link targets
- The portable flavour's spellings - The portable flavour's spellings
- The portable reader refuses what GFM's reading would drop - The portable reader refuses what GFM's reading would drop
- The portable reader reads what GFM's reading keeps
- The HTML dialect - The HTML dialect
- `htmlToAdf` also reads the dialect as a browser serializes it - `htmlToAdf` also reads the dialect as a browser serializes it
- No runtime dependencies - No runtime dependencies
+7 -4
View File
@@ -2,10 +2,13 @@
## Unreleased ## Unreleased
- `portableMarkdownToAdf` reads GFM's pipe table further: a body row holding fewer cells than the - **Breaking:** `portableMarkdownToAdf` reads more of GFM's pipe table. A body row holding fewer
header is padded with empty cells, a line holding no `|` after the table is a row of one cell, cells than the header is padded with empty cells, where `0.3.1` returned `malformed-pipe-table`.
and `a |` over `--- |` is a one-column table. A row holding more cells stays an error result. A line after the table that holds no `|` and opens no other block is a row of one cell, where it
`MIGRATION.md` shows the change. 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 - `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)`. as a stored image: `[_(image not included)_](https://example.com/a)`.
- **Breaking:** `readAdfDocument` replaces `isAdfDocument`: it returns a `Result<AdfDocument>` whose - **Breaking:** `readAdfDocument` replaces `isAdfDocument`: it returns a `Result<AdfDocument>` whose
+13 -7
View File
@@ -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 `JSON.parse(JSON.stringify(document))`. A document holding more than 10,000,000 JSON values is
`unsupported-node-shape`. `unsupported-node-shape`.
`portableMarkdownToAdf` reads more of GFM's pipe table. A body row holding fewer cells than the `portableMarkdownToAdf` reads more of GFM's pipe table:
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 - A body row holding fewer cells than the header is padded with empty cells, where `0.3.1`
`--- |` is a one-column table, where it read as a paragraph. A markdown string that relied on the returned `malformed-pipe-table`.
old reading takes a blank line between the table and the paragraph. `losslessMarkdownToAdf` reads - A line after a table that holds no `|` and opens no other block is a row of one cell, where it
the first two as before, and now returns `malformed-pipe-table` for `a |` over `--- |` too, as it ended the table and read as a paragraph. Markdown that relied on that takes a blank line between
does for any table without leading pipes. 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` ## From `0.2.0` to `0.3.0`
+6 -5
View File
@@ -602,7 +602,7 @@ Which reader raises which code: [The calls](#the-calls). Codes every direction r
| Code | Fires when | What you can do | | 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-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 | | `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 | | `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 | | `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 - 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. result, where GFM cuts the extra cells.
- `losslessMarkdownToAdf` narrows it further. A body row holding fewer cells is an error result, - `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 where GFM pads it, and a later line without a leading `|` ends the table, where GFM reads it as
needs a leading `|`: a header or delimiter row without one is an error result. It refuses an a row. A header or delimiter row without a leading `|` is an error result, and so is an
alignment colon in the delimiter row too, since ADF holds no column alignment. alignment colon in the delimiter row, since ADF holds no column alignment.
`portableMarkdownToAdf` pads, reads the line as a row, accepts both and drops the alignment. The `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. trailing pipe is canonical output, optional in input.
- Past that and `~~`, `losslessMarkdownToAdf` reads no GFM: an autolink literal and a `- [ ]` - 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 stay text, and a checklist is the `taskList` directive — `portableMarkdownToAdf` turns the
+10 -3
View File
@@ -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 GitHub renders. Goal 7 is a Maintain goal, holding whatever it costs, so it wins over Goal 4, an
Achieve goal. 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 ## The portable reader reads what GFM's reading keeps
a one-column table.
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 ## The HTML dialect
+5 -4
View File
@@ -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 `|` 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, (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 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 holding fewer cells and an alignment colon in the delimiter row — ADF holds no column alignment.
`|` inside a quoted attribute value is already `\u007c`, so the split never reaches it. 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 `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 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 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 padded with empty cells, a line holding no `|` that opens no other block is a body row of one
read and its alignment dropped. cell, and an alignment colon is read and its alignment dropped.
The directive form nests cells as containers of block content inside `tableRow` containers: The directive form nests cells as containers of block content inside `tableRow` containers:
-1
View File
@@ -1 +0,0 @@
import '../html/emit/adf-to-html.ts';
-1
View File
@@ -1 +0,0 @@
import '../markdown/flavour.ts';
-1
View File
@@ -1 +0,0 @@
import '../markdown/flavour.ts';
@@ -1 +0,0 @@
import '../../markdown/flavour.ts';
-1
View File
@@ -1 +0,0 @@
import '../emit/adf-to-html.ts';
-1
View File
@@ -1 +0,0 @@
import '../html/emit/adf-to-html.ts';
@@ -1 +0,0 @@
import '../emit/image.ts';
@@ -1 +0,0 @@
import '../../html/emit/adf-to-html.ts';
-1
View File
@@ -1 +0,0 @@
import '../parse/blocks.ts';
-1
View File
@@ -1 +0,0 @@
import '../../html/emit/adf-to-html.ts';
-1
View File
@@ -1 +0,0 @@
import '../portable/conventions.ts';
+1 -2
View File
@@ -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(':--- | ---:'), 'a | b\\\n\\:--- | ---:\n') assert.equal(broken(':--- | ---:'), 'a | b\\\n\\:--- | ---:\n')
assert.equal(broken('c | d'), 'a | b\\\nc | d\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(readsBack(document(paragraph({ text: 'a |', type: 'text' }, { type: 'hardBreak' }, { text: '--- |', type: 'text' }))), 'a |\\\n--- |\n')
assert.equal(oneColumn, 'a |\\\n\\--- |\n')
}) })
test('escapes only text that would otherwise open a construct', () => { test('escapes only text that would otherwise open a construct', () => {
+1 -1
View File
@@ -533,7 +533,7 @@ function openGfmTable(walk: Walk, opener: string): boolean {
return true 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 { function pipeTableRow(claims: Claims, line: string): string[] | undefined {
const cells = pipeCells(line) const cells = pipeCells(line)
if (cells !== undefined || claims.pipeTables !== 'gfm' || isBlankLine(line) || opensBlock(claims, line)) return cells if (cells !== undefined || claims.pipeTables !== 'gfm' || isBlankLine(line) || opensBlock(claims, line)) return cells
@@ -1 +0,0 @@
import '../emit/spelling-questions.ts';
@@ -1 +0,0 @@
import '../../html/emit/adf-to-html.ts';
@@ -1 +0,0 @@
import '../portable/conventions.ts';
-1
View File
@@ -1 +0,0 @@
import '../emit/adf-to-markdown.ts';
+3 -2
View File
@@ -4,15 +4,16 @@ import { backslashEscape, claimsPipeLine, trimSpace } from './commonmark/grammar
const alignmentCell = /^:-+:?$|^-+:$/ const alignmentCell = /^:-+:?$|^-+:$/
const delimiterCell = /^-+$/ 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 { export function barePipeCells(line: string): string[] | undefined {
const cells = splitPipeCells(line, 0) const cells = splitPipeCells(line, 0)
return cells.length > 1 ? withoutClosingCell(cells) : undefined 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 { export function isBareDelimiterRow(line: string): boolean {
const cells = barePipeCells(line) const cells = barePipeCells(line)
return cells !== undefined && isDelimiterRow(cells) return cells !== undefined && cells.length > 1 && isDelimiterRow(cells)
} }
export function isDelimiterRow(cells: readonly string[]): boolean { export function isDelimiterRow(cells: readonly string[]): boolean {
@@ -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| --- |\nb\n'), [column(['a', 'b'])]) assert.deepEqual(read('| a |\n| --- |\nb\n'), [column(['a', 'b'])])
assert.deepEqual(read('a\n--- |\n'), [said('a --- |')]) assert.deepEqual(read('a\n--- |\n'), [said('a --- |')])
const long = portableMarkdownToAdf('a | b\n--- | ---\nc | d | e\n') for (const refused of ['a | b\n--- | ---\nc | d | e\n', '| a | b |\n| --- |\n']) {
assert.deepEqual(long.ok ? undefined : long.error.code, 'malformed-pipe-table') 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', () => { 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', () => {