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

Refuse a pipe table whose padding would add more cells than the table holds

This commit is contained in:
2026-11-10 17:03:53 +01:00
committed by Lillem4n
parent 3b61501fe2
commit 785388d426
6 changed files with 32 additions and 7 deletions
+2 -1
View File
@@ -3,7 +3,8 @@
## Unreleased
- **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`.
cells than the header is padded with empty cells, where `0.3.1` returned `malformed-pipe-table`;
a table whose padding would add more cells than the table holds is `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,
+5 -4
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 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 `\\|` |
| `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, a body row holding more cells than the header, or short rows whose padding would add more cells than the table holds; 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 |
@@ -733,9 +733,10 @@ Serves [Goals](#goals) 1, 4 and 5.
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.
`portableMarkdownToAdf` pads a short row with empty cells, as long as the padding adds no more
cells than the table holds, 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
marker into a `taskList`.
+7
View File
@@ -404,6 +404,13 @@ table's short body row is padded with empty cells, a line holding no `|` that op
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).
Padding builds cells the markdown never holds, so a header of N columns over M one-cell rows would
build N×M cells from N+M cells of source, against Goal 9. `portableMarkdownToAdf` pads a table only
while the cells the padding adds stay within the cells the table's source holds (header, delimiter
and body rows); past that, the table is `malformed-pipe-table`, and the markdown writes the empty
cells out. The output then stays within twice the input's cells, and the bound needs no number of
its own.
## The HTML dialect
2026-08-23, the maintainer; Atlassian's HTML+ 2026-10-28, the maintainer. Goals 6 and 8. Valid
+3 -2
View File
@@ -373,8 +373,9 @@ 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 `|` that opens no other block 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 padding of a table may add no more
cells than its header, delimiter row and body rows hold; past that it is a named error.
The directive form nests cells as containers of block content inside `tableRow` containers:
+9
View File
@@ -551,6 +551,15 @@ function pipeTableBlock(rows: readonly [string[], ...string[][]], position: Sour
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)
let held = header.length + delimiter.length
let padding = 0
for (const row of body) {
held += row.length
padding += header.length - row.length
}
if (padding > held) {
return faultedBlock(`a pipe table's short rows would pad ${cellCount(padding)}, more than the ${cellCount(held)} it holds: write the empty cells out`, position)
}
const padded = body.map((row) => [...row, ...Array.from({ length: header.length - row.length }, () => '')])
return { kind: 'table', position, rows: [header, ...padded] }
}
@@ -406,6 +406,12 @@ 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 wide = (rows: number): string => `a | b | c | d\n--- | --- | --- | ---\n${'x\n'.repeat(rows)}`
assert.ok(portableMarkdownToAdf(wide(4)).ok)
const padded = portableMarkdownToAdf(wide(5))
assert.deepEqual(padded.ok ? undefined : [padded.error.code, padded.error.position], ['malformed-pipe-table', { line: 1, offset: 0 }])
const amplified = portableMarkdownToAdf(`${'a|'.repeat(2000)}\n${'-|'.repeat(2000)}\n${'x\n'.repeat(2000)}`)
assert.deepEqual(amplified.ok ? undefined : amplified.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)