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
- 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
+7 -4
View File
@@ -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<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
`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`
+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 |
| --- | --- | --- |
| `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
+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
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
+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 `|`
(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:
-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('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', () => {
+1 -1
View File
@@ -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
@@ -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 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 {
@@ -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', () => {