diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a4ec7..4aa3a73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 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. - `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 77574f8..a03be8b 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -22,6 +22,14 @@ 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. + ## From `0.2.0` to `0.3.0` Every reader skips code text when it spells a mark other than a link, and `losslessMarkdownToAdf` diff --git a/README.md b/README.md index 5288624..7f7c1c7 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, or a delimiter or body row whose cell count differs from the header's; in `losslessMarkdownToAdf` also an alignment colon, and a header or delimiter row not opening with a pipe | add a `---` row under the header and give every row the header's cell count; 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 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 `\\|` | | `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 | @@ -727,13 +727,14 @@ Serves [Goals](#goals) 1, 4 and 5. levels, a document meeting an `unsupported-node-shape` cause listed for it, for a writer or for every call, and a conversion `engine-limit` stops. Show the refusal and keep the document read-only; saving markdown you could not produce is the loss the round-trip exists to stop. -- Both readers narrow GFM's pipe table. A body row whose cell count differs from the header's is - an error result, where GFM pads or cuts it. A line holding no `|` ends the table, where GFM reads - it as a row. -- `losslessMarkdownToAdf` also needs a leading `|`: a header or delimiter row without one is an - error result, and a later line without one ends the table. It refuses an alignment colon in the - delimiter row too, since ADF holds no column alignment. `portableMarkdownToAdf` accepts both and - drops the alignment. The trailing pipe is canonical output, optional in input. +- 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 + 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/docs/decisions.md b/docs/decisions.md index 856952b..f9d9d22 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -388,10 +388,14 @@ and 7. Valid while a refusal is a result and the caller can fix the markdown it Where GFM's reading of a shape drops content, `portableMarkdownToAdf` refuses the shape. A pipe table body row holding more cells than its header is `malformed-pipe-table`, where GFM cuts the -extra cells. Cutting the cells breaks Goal 7. Building a table GFM does not, such as a wider one, breaks -Goal 4. A refusal builds no table, but it still breaks Goal 4: a bot gets an error for a row GitHub -renders. Goal 7 is a Maintain goal, holding whatever it costs, so it wins over Goal 4, an Achieve -goal. +extra cells. Cutting the cells breaks Goal 7. Building a table GFM does not, such as a wider one, +breaks Goal 4. A refusal builds no table, but it still breaks Goal 4: a bot gets an error for a row +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 HTML dialect diff --git a/docs/reading-cost.md b/docs/reading-cost.md index 33636ef..eb64a42 100644 --- a/docs/reading-cost.md +++ b/docs/reading-cost.md @@ -980,3 +980,20 @@ Read most (opens and searches): - `ci.sh` 1 ======= >>>>>>> origin/main + +## Reading cost, chunk a624aacf239f, finished 2026-11-09T10:24:02Z + +167585 new input tokens before the first commit. + +Read most (opens and searches): + +- `todo.md` 7 +- `README.md` 6 +- `docs/decisions.md` 6 +- `spec/flavour.md` 4 +- `docs/kovetskiy-mark.md` 3 +- `CHANGELOG.md` 2 +- `ci.sh` 2 +- `corpus/README.md` 1 +- `corpus/round-trip/block-nodes/expand.md` 1 +- `corpus/round-trip/block-nodes/panel.md` 1 diff --git a/spec/flavour.md b/spec/flavour.md index 5579d39..502332d 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -365,13 +365,15 @@ 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, in `losslessMarkdownToAdf`, 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 +(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. -`portableMarkdownToAdf` reads GFM's table: any row may drop its leading `|`, a body row continues -the table while it holds a `|` and opens no other block, and an alignment colon is read and its -alignment dropped. +`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. 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 new file mode 100644 index 0000000..6aa7f87 --- /dev/null +++ b/src/adf/boundary-probe-html.ts @@ -0,0 +1 @@ +import '../html/emit/adf-to-html.ts'; diff --git a/src/adf/boundary-probe.ts b/src/adf/boundary-probe.ts new file mode 100644 index 0000000..962816a --- /dev/null +++ b/src/adf/boundary-probe.ts @@ -0,0 +1 @@ +import '../markdown/flavour.ts'; diff --git a/src/html/boundary-probe.ts b/src/html/boundary-probe.ts new file mode 100644 index 0000000..962816a --- /dev/null +++ b/src/html/boundary-probe.ts @@ -0,0 +1 @@ +import '../markdown/flavour.ts'; diff --git a/src/html/parse/boundary-probe-markdown.ts b/src/html/parse/boundary-probe-markdown.ts new file mode 100644 index 0000000..14a0e26 --- /dev/null +++ b/src/html/parse/boundary-probe-markdown.ts @@ -0,0 +1 @@ +import '../../markdown/flavour.ts'; diff --git a/src/html/parse/boundary-probe.ts b/src/html/parse/boundary-probe.ts new file mode 100644 index 0000000..5b3ebcd --- /dev/null +++ b/src/html/parse/boundary-probe.ts @@ -0,0 +1 @@ +import '../emit/adf-to-html.ts'; diff --git a/src/markdown/boundary-probe-html.ts b/src/markdown/boundary-probe-html.ts new file mode 100644 index 0000000..6aa7f87 --- /dev/null +++ b/src/markdown/boundary-probe-html.ts @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..274d62a --- /dev/null +++ b/src/markdown/directive/boundary-probe-emit.ts @@ -0,0 +1 @@ +import '../emit/image.ts'; diff --git a/src/markdown/directive/boundary-probe-html.ts b/src/markdown/directive/boundary-probe-html.ts new file mode 100644 index 0000000..0eefb8a --- /dev/null +++ b/src/markdown/directive/boundary-probe-html.ts @@ -0,0 +1 @@ +import '../../html/emit/adf-to-html.ts'; diff --git a/src/markdown/directive/boundary-probe.ts b/src/markdown/directive/boundary-probe.ts new file mode 100644 index 0000000..8bf220e --- /dev/null +++ b/src/markdown/directive/boundary-probe.ts @@ -0,0 +1 @@ +import '../parse/blocks.ts'; diff --git a/src/markdown/emit/boundary-probe-html.ts b/src/markdown/emit/boundary-probe-html.ts new file mode 100644 index 0000000..0eefb8a --- /dev/null +++ b/src/markdown/emit/boundary-probe-html.ts @@ -0,0 +1 @@ +import '../../html/emit/adf-to-html.ts'; diff --git a/src/markdown/emit/boundary-probe.ts b/src/markdown/emit/boundary-probe.ts new file mode 100644 index 0000000..110f4e5 --- /dev/null +++ b/src/markdown/emit/boundary-probe.ts @@ -0,0 +1 @@ +import '../portable/conventions.ts'; diff --git a/src/markdown/emit/escaping.test.ts b/src/markdown/emit/escaping.test.ts index 3f2dd30..0f79cac 100644 --- a/src/markdown/emit/escaping.test.ts +++ b/src/markdown/emit/escaping.test.ts @@ -34,6 +34,8 @@ 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') }) 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 2db0c36..055d1e0 100644 --- a/src/markdown/parse/blocks.ts +++ b/src/markdown/parse/blocks.ts @@ -18,6 +18,7 @@ import { setextHeadingLevel, thematicBreakTail, type ThematicBreakTail, + trimSpace, } from '../commonmark/grammar.ts' import { barePipeCells, isDelimiterRow, isPipeAlignment, malformedPipeTable, pipeCells } from '../pipe-table-syntax.ts' import { blockDirectiveForm } from '../directive/block.ts' @@ -532,11 +533,11 @@ function openGfmTable(walk: Walk, opener: string): boolean { return true } -// GFM, Tables: a row without its leading `|` continues the table while it holds a `|` and opens no other block. +// 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. function pipeTableRow(claims: Claims, line: string): string[] | undefined { const cells = pipeCells(line) - if (cells !== undefined || claims.pipeTables !== 'gfm' || opensBlock(claims, line)) return cells - return barePipeCells(line) + if (cells !== undefined || claims.pipeTables !== 'gfm' || isBlankLine(line) || opensBlock(claims, line)) return cells + return barePipeCells(line) ?? [trimSpace(line)] } function pipeTableBlock(rows: readonly [string[], ...string[][]], position: SourcePosition, claims: Claims): Block { @@ -547,9 +548,11 @@ function pipeTableBlock(rows: readonly [string[], ...string[][]], position: Sour if (delimiter === undefined || !isDelimiterRow(delimiter)) { return faultedBlock('a pipe table underlines its header with a row of `-` runs: this one has none; \\| at the start of every row keeps them literal text', position) } - const ragged = [delimiter, ...body].find((row) => row.length !== header.length) + const padsShortRows = claims.pipeTables === 'gfm' + const ragged = [delimiter, ...body].find((row) => (padsShortRows && row !== delimiter ? row.length > header.length : row.length !== header.length)) if (ragged !== undefined) return faultedBlock(`a pipe table row holds ${cellCount(ragged.length)} where its header holds ${cellCount(header.length)}`, position) - return { kind: 'table', position, rows: [header, ...body] } + const padded = body.map((row) => [...row, ...Array.from({ length: header.length - row.length }, () => '')]) + return { kind: 'table', position, rows: [header, ...padded] } } // spec/flavour.md, Tables: GFM's table without the leading pipes, which no line of it claims. diff --git a/src/markdown/parse/boundary-probe-allowed.ts b/src/markdown/parse/boundary-probe-allowed.ts new file mode 100644 index 0000000..c3def07 --- /dev/null +++ b/src/markdown/parse/boundary-probe-allowed.ts @@ -0,0 +1 @@ +import '../emit/spelling-questions.ts'; diff --git a/src/markdown/parse/boundary-probe-html.ts b/src/markdown/parse/boundary-probe-html.ts new file mode 100644 index 0000000..0eefb8a --- /dev/null +++ b/src/markdown/parse/boundary-probe-html.ts @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..110f4e5 --- /dev/null +++ b/src/markdown/parse/boundary-probe-portable.ts @@ -0,0 +1 @@ +import '../portable/conventions.ts'; diff --git a/src/markdown/parse/boundary-probe.ts b/src/markdown/parse/boundary-probe.ts new file mode 100644 index 0000000..a2a9ef0 --- /dev/null +++ b/src/markdown/parse/boundary-probe.ts @@ -0,0 +1 @@ +import '../emit/adf-to-markdown.ts'; diff --git a/src/markdown/parse/markdown-to-adf.test.ts b/src/markdown/parse/markdown-to-adf.test.ts index 15762ea..fbaa05f 100644 --- a/src/markdown/parse/markdown-to-adf.test.ts +++ b/src/markdown/parse/markdown-to-adf.test.ts @@ -259,6 +259,9 @@ test('names the pipe table whose rows open with no pipe', () => { assert.equal(content(losslessMarkdownToAdf('Intro.\na | b\n--- | ---\n')), bare) assert.equal(content(losslessMarkdownToAdf('a | b\n--- | ---\n===\n')), bare) assert.equal(content(losslessMarkdownToAdf('a | b\n:--- | ---:\n')), bare) + assert.equal(content(losslessMarkdownToAdf('a |\n--- |\n')), bare) + assert.deepEqual(content(losslessMarkdownToAdf('| a | b |\n| --- | --- |\nc\n')), [table(row(cell('tableHeader', text('a')), cell('tableHeader', text('b')))), paragraph('c')]) + assert.equal(content(losslessMarkdownToAdf('| a | b |\n| --- | --- |\n| c |\n')), 'malformed-pipe-table: a pipe table row holds 1 cell where its header holds 2 cells') assert.deepEqual(content(losslessMarkdownToAdf('a | b\nc | d\n')), [paragraph('a | b c | d')]) assert.deepEqual(content(losslessMarkdownToAdf('a | b\n--- | --- | ---\n')), [paragraph('a | b --- | --- | ---')]) assert.deepEqual(content(losslessMarkdownToAdf('a \\| b\n--- | ---\n')), [paragraph('a | b --- | ---')]) diff --git a/src/markdown/pipe-table-syntax.ts b/src/markdown/pipe-table-syntax.ts index 23f6492..ed98b97 100644 --- a/src/markdown/pipe-table-syntax.ts +++ b/src/markdown/pipe-table-syntax.ts @@ -4,10 +4,10 @@ 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. +// 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. export function barePipeCells(line: string): string[] | undefined { const cells = splitPipeCells(line, 0) - return cells.length > 1 ? cells : undefined + return cells.length > 1 ? withoutClosingCell(cells) : undefined } export function isBareDelimiterRow(line: string): boolean { @@ -33,7 +33,7 @@ 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 { - return claimsPipeLine(line) ? splitPipeCells(line, 1) : undefined + return claimsPipeLine(line) ? withoutClosingCell(splitPipeCells(line, 1)) : undefined } export function spellPipeDelimiter(columns: number): string { @@ -60,6 +60,10 @@ function splitPipeCells(line: string, start: number): string[] { index += 1 } cells.push(trimSpace(line.slice(cellStart))) + return cells +} + +function withoutClosingCell(cells: string[]): string[] { if (cells.length > 1 && cells.at(-1) === '') cells.pop() return cells } diff --git a/src/markdown/portable/portable-markdown-to-adf.test.ts b/src/markdown/portable/portable-markdown-to-adf.test.ts index fa77469..767e626 100644 --- a/src/markdown/portable/portable-markdown-to-adf.test.ts +++ b/src/markdown/portable/portable-markdown-to-adf.test.ts @@ -378,11 +378,9 @@ test('reads GFM\'s pipe table without its leading pipes or with alignment colons assert.deepEqual(read('a | b\n| --- | --- |\nc | d\n'), [table]) assert.deepEqual(read('Intro.\na | b\n--- | ---\nc | d\n\nAfter.\n'), [said('Intro.'), table, said('After.')]) assert.deepEqual(read('> a | b\n> --- | ---\n> c | d\n'), [bare('blockquote', table)]) - assert.deepEqual(read('a | b\n--- | ---\nc | d\nAfter.\n'), [table, said('After.')]) assert.deepEqual(read('a | b\n--- | ---\nc | d\n# h | i\n'), [table, { attrs: { level: 1 }, content: [text('h | i')], type: 'heading' }]) 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')]) assert.deepEqual(read('a | b\nc | d\n'), [said('a | b c | d')]) assert.deepEqual(read('| a | b |\n| --- | --- |\n | c | d |\n'), [bare('table', table.content?.[0] ?? said('')), bare('codeBlock', text('| c | d |'))]) assert.deepEqual(read('a | b\n--- | ---\n c | d\n'), [bare('table', table.content?.[0] ?? said('')), bare('codeBlock', text('c | d'))]) @@ -391,6 +389,27 @@ test('reads GFM\'s pipe table without its leading pipes or with alignment colons assert.deepEqual(ragged.ok ? undefined : [ragged.error.code, ragged.error.position], ['malformed-pipe-table', { line: 2, offset: 7 }]) }) +test('reads GFM\'s short row padded with empty cells, a line holding no pipe as a row, and a one-column table', () => { + const cell = (type: string, value: string): AdfNode => bare(type, value === '' ? { type: 'paragraph' } : said(value)) + const header = bare('tableRow', cell('tableHeader', 'a'), cell('tableHeader', 'b')) + const short = bare('table', header, bare('tableRow', cell('tableCell', 'c'), cell('tableCell', ''))) + assert.deepEqual(read('a | b\n--- | ---\nc\n'), [short]) + assert.deepEqual(read('| a | b |\n| --- | --- |\n| c |\n'), [short]) + assert.deepEqual(read('a | b\n--- | ---\nc | d\nAfter.\n'), [ + bare('table', header, bare('tableRow', cell('tableCell', 'c'), cell('tableCell', 'd')), bare('tableRow', cell('tableCell', 'After.'), cell('tableCell', ''))), + ]) + assert.deepEqual(read('a | b\n--- | ---\nc\n\nAfter.\n'), [short, said('After.')]) + assert.deepEqual(read('a | b\n--- | ---\nc\n# h\n'), [short, { attrs: { level: 1 }, content: [text('h')], type: 'heading' }]) + const column = (rows: readonly string[]): AdfNode => + bare('table', ...rows.map((value, index) => bare('tableRow', cell(index === 0 ? 'tableHeader' : 'tableCell', value)))) + 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 --- |')]) + const long = portableMarkdownToAdf('a | b\n--- | ---\nc | d | e\n') + assert.deepEqual(long.ok ? undefined : long.error.code, 'malformed-pipe-table') +}) + 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', () => { assert.deepEqual(read('!adf:panel\n'), [said('!adf:panel')]) assert.deepEqual(read('a !adf:carry{json="{}"} \\!adf:x\n'), [said('a !adf:carry{json="{}"} !adf:x')]) diff --git a/todo.md b/todo.md index 0a2877e..b777088 100644 --- a/todo.md +++ b/todo.md @@ -13,7 +13,6 @@ | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | |---|---|---|---|---|---|---|---|---|---|---|---| | 153 | 0.4.0 | | question | **Decide whether `src/markdown/` root may import `emit/`, `parse/` or `portable/`, and hold the answer in `.oxlintrc.json` and §The source parts by ADF and format.** | 1 | 2 | 2 | 3 | 2 | code health by 3 | 26.2 | -| 112 | 0.4.0 | | | **Pad a GFM pipe table's short rows with empty cells, read its body rows holding no pipe, and read a one-column table without leading pipes in `portableMarkdownToAdf`.** | 2 | 1 | 2 | 3 | 3 | 4 by 4 | 6.2 | | 80 | 0.4.0 | | | **Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.** | 2 | 1 | 2 | 3 | 3 | 6 by 4 | 6.0 | | 171 | 0.4.0 | | principle | **Assert the facts an error message must carry, never its whole sentence, in `markdown-to-adf.test.ts`, `document.test.ts` and `html-to-adf.test.ts`.** | 3 | 1 | 1 | 3 | 6 | code health by 3 | 4.4 | | 173 | 0.4.0 | | principle | **Move each CommonMark spelling's description out of `commonMarkSpellings`, beside the emitter's spelling it describes.** | 2 | 1 | 1 | 2 | 4 | code health by 1 | 4.4 | @@ -62,15 +61,6 @@ a paragraph opening with a spellable `!adf:link` (§Nothing recurses unbounded). `src/markdown/**` bans only `html/`; banning `emit/`, `parse/` and `portable/` for the root's own files, with its `ci.sh` probe, extends the decision. -### 112. Pad a GFM pipe table's short rows with empty cells, read its body rows holding no pipe, and read a one-column table without leading pipes in `portableMarkdownToAdf`. - -GFM pads a short row with empty cells, reads a line holding no `|` after a table as a row, and -reads `a |` over `--- |` as a one-column table. `portableMarkdownToAdf` refuses a short row as -`malformed-pipe-table`, ends the table at a line holding no `|`, and reads the one-column shape as a -paragraph. A long row stays refused (`docs/decisions.md` §The portable reader refuses what GFM's -reading would drop), so the README's pipe-table guarantee keeps the long row for both readers, and -the short row and the line holding no `|` for `losslessMarkdownToAdf` alone. The one-column shape needs `isBareDelimiterRow` in the writer's escaping to cover it too. - ### 80. Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document. Today `readAdfDocument` and the converters refuse every node, such as one with a `Date` in an