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 ## Unreleased
- **Breaking:** `portableMarkdownToAdf` reads more of GFM's pipe table. A body row holding fewer - **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 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. 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, - **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 | | 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 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 | | `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 |
@@ -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 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 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. 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 `portableMarkdownToAdf` pads a short row with empty cells, as long as the padding adds no more
it opens another block, reads a table without leading pipes, and drops the alignment. The cells than the table holds, reads a line holding no `|` as a row of one cell unless it opens
trailing pipe is canonical output, optional in input. 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 `- [ ]` - 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
marker into a `taskList`. 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` 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). 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 ## The HTML dialect
2026-08-23, the maintainer; Atlassian's HTML+ 2026-10-28, the maintainer. Goals 6 and 8. Valid 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 `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 `|` that opens no other block is a body row of one padded with empty cells, a line holding no `|` that opens no other block is a body row of one cell,
cell, and an alignment colon is read and its alignment dropped. 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: 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 padsShortRows = claims.pipeTables === 'gfm'
const ragged = [delimiter, ...body].find((row) => (padsShortRows && row !== delimiter ? row.length > header.length : row.length !== header.length)) 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) 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 }, () => '')]) const padded = body.map((row) => [...row, ...Array.from({ length: header.length - row.length }, () => '')])
return { kind: 'table', position, rows: [header, ...padded] } 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| --- |\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 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']) { for (const refused of ['a | b\n--- | ---\nc | d | e\n', '| a | b |\n| --- |\n']) {
const result = portableMarkdownToAdf(refused) const result = portableMarkdownToAdf(refused)
assert.deepEqual(result.ok ? undefined : result.error.code, 'malformed-pipe-table', refused) assert.deepEqual(result.ok ? undefined : result.error.code, 'malformed-pipe-table', refused)