3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-13 20:28:28 +01:00

Answer architecture round 1: 0 🔴, 5 🟡; comments and docs name the narrowing, item 112 files GFM's ragged rows, a paragraph's lines carry their positions

This commit is contained in:
2026-10-14 00:38:51 +02:00
committed by Lillem4n
parent c1055046bb
commit 2cc6ea3f57
6 changed files with 42 additions and 25 deletions
+3 -1
View File
@@ -495,7 +495,9 @@ Serves [Goals](#goals) 1, 4 and 5.
- The pipe table that `losslessMarkdownToAdf` reads narrows GFM's twice: every row opens with a
pipe, so GFM's bare form is an error result, and an alignment colon in the delimiter row is an
error too — ADF holds no column alignment. `portableMarkdownToAdf` reads both and drops the
alignment. The trailing pipe is canonical output, optional in input.
alignment, but narrows GFM's table too: a row whose cell count differs from the header's is an
error result where GFM pads or cuts it, and a line holding no `|` ends the table where GFM reads
it as a row. 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 -6
View File
@@ -347,16 +347,17 @@ claimed and must parse as part of a pipe table, else it is a named error — esc
(`\|`) to keep it literal text. The shape a missing leading pipe leaves is claimed too: a line
holding an unescaped `|`, followed inside one paragraph by a line whose cells are all `-` runs,
an alignment colon among them, and match it in count — GFM's table without the outer pipes — is
a named error rather than the prose it reads as; `portableMarkdownToAdf` reads it, and an
alignment colon, as GFM does, dropping the alignment. A pipe table is a header row, a delimiter row whose cells are
runs of one or more `-` (canonical `---`), and body rows; rows follow code-fence indentation.
a named error rather than the prose it reads as. `portableMarkdownToAdf` reads that shape as a
table, its body rows with or without the leading `|` until a line holding none, and drops an
alignment colon's alignment. A pipe table is a header row, a delimiter row whose cells are runs of
one or more `-` (canonical `---`), and body rows; rows follow code-fence 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 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.
header's, and, outside `portableMarkdownToAdf`, 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.
The directive form nests cells as containers of block content inside `tableRow` containers:
+17 -16
View File
@@ -54,12 +54,14 @@ type OpenLeaf = { position: SourcePosition } & (
| { closer: RegExp | undefined; construct: string; kind: 'html' }
| { held: string[]; kind: 'indented-code'; lines: string[] }
| { indentation: number; info: string; kind: 'fenced-code'; lines: string[]; marker: string }
| { kind: 'paragraph'; lines: string[]; positions: SourcePosition[] }
| { kind: 'paragraph'; lines: [ParagraphLine, ...ParagraphLine[]] }
| { kind: 'pipe-table'; rows: [string[], ...string[][]] }
)
type ContainerStart = { kind: 'blockquote'; rest: Line } | { fresh: boolean; indentation: number; kind: 'item'; list: ListBlock; rest: Line }
type ParagraphLine = { position: SourcePosition; text: string }
// The line from an absolute column on: a tab a cut splits keeps the stop it is measured against.
type Line = { column: number; text: string }
@@ -469,11 +471,10 @@ function appendParagraph(walk: Walk, line: string): void {
const leaf = walk.leaf
const text = line.replace(/^[ \t]+/, '')
if (leaf?.kind !== 'paragraph') {
walk.leaf = { kind: 'paragraph', lines: [text], position: walk.position, positions: [walk.position] }
walk.leaf = { kind: 'paragraph', lines: [{ position: walk.position, text }], position: walk.position }
return
}
leaf.lines.push(text)
leaf.positions.push(walk.position)
leaf.lines.push({ position: walk.position, text })
}
function closeLeaf(walk: Walk): void {
@@ -497,17 +498,17 @@ function openGfmTable(walk: Walk, opener: string): boolean {
const paragraph = walk.leaf
if (walk.claims.pipeTables !== 'gfm' || paragraph?.kind !== 'paragraph') return false
const delimiter = pipeCells(opener) ?? barePipeCells(opener)
const header = barePipeCells(paragraph.lines.at(-1) ?? '')
if (delimiter === undefined || header?.length !== delimiter.length || !isDelimiterRow(delimiter)) return false
paragraph.lines.pop()
const position = paragraph.positions.pop() ?? walk.position
if (paragraph.lines.length === 0) walk.leaf = undefined
else closeLeaf(walk)
walk.leaf = { kind: 'pipe-table', position, rows: [header, delimiter] }
const last = paragraph.lines.at(-1)
const header = last === undefined ? undefined : barePipeCells(last.text)
if (last === undefined || delimiter === undefined || header?.length !== delimiter.length || !isDelimiterRow(delimiter)) return false
const [first, ...earlier] = paragraph.lines.slice(0, -1)
walk.leaf = first === undefined ? undefined : { ...paragraph, lines: [first, ...earlier] }
closeLeaf(walk)
walk.leaf = { kind: 'pipe-table', position: last.position, rows: [header, delimiter] }
return true
}
// GFM, Tables: a row without its leading pipe goes on until a line opens another block.
// GFM, Tables: a row without its leading `|` continues the table while it holds a `|` and opens no other block.
function pipeTableRow(claims: Claims, line: string): string[] | undefined {
const cells = pipeCells(line)
if (cells !== undefined || claims.pipeTables !== 'gfm' || opensBlock(claims, line)) return cells
@@ -516,7 +517,7 @@ function pipeTableRow(claims: Claims, line: string): string[] | undefined {
function pipeTableBlock(rows: readonly [string[], ...string[][]], position: SourcePosition, claims: Claims): Block {
const [header, delimiter, ...body] = rows
if (claims.pipeTables !== 'gfm' && delimiter !== undefined && delimiter.some(isPipeAlignment)) {
if (claims.pipeTables === 'flavour' && delimiter !== undefined && delimiter.some(isPipeAlignment)) {
return faultedBlock('a pipe table carries no column alignment ADF could hold: this delimiter row holds an alignment colon', position)
}
if (delimiter === undefined || !isDelimiterRow(delimiter)) {
@@ -554,10 +555,10 @@ function takeParagraph(walk: Walk): Extract<Block, { kind: 'paragraph' }> | unde
const leaf = walk.leaf
if (leaf?.kind !== 'paragraph') return undefined
walk.leaf = undefined
const text = readLinkDefinitions(walk.definitions, leaf.lines.join('\n'))
const text = readLinkDefinitions(walk.definitions, leaf.lines.map((line) => line.text).join('\n'))
if (text === '') return undefined
const kept = leaf.positions[leaf.lines.length - text.split('\n').length]
return { kind: 'paragraph', position: kept ?? leaf.position, text }
const kept = leaf.lines[leaf.lines.length - text.split('\n').length]
return { kind: 'paragraph', position: kept?.position ?? leaf.position, text }
}
function currentBlocks(walk: Walk): Block[] {
@@ -293,6 +293,8 @@ test('reads GFM\'s pipe table without its leading pipes or with alignment colons
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')])
const ragged = portableMarkdownToAdf('Intro.\na | b\n--- | ---\nc | d | e\n')
assert.deepEqual(ragged.ok ? undefined : [ragged.error.code, ragged.error.position], ['malformed-pipe-table', { line: 2, offset: 7 }])
})
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', () => {
+1 -1
View File
@@ -10,7 +10,7 @@ export type Claims = {
carryFence: boolean
directives: boolean
highlights: boolean
// `gfm` reads GFM's whole pipe table, its alignment dropped; `flavour` reads rows opening with `|` over a `-` delimiter row.
// `gfm` reads rows with or without the leading `|`, alignment colons dropped; `flavour` only rows opening with `|` over `-` runs.
pipeTables: 'flavour' | 'gfm' | false
strikethrough: boolean
taskMarkers: boolean
+12 -1
View File
@@ -6,7 +6,7 @@
`Bar = 9`
`Next ID = 112`
`Next ID = 113`
| Goal | W |
|---|---|
@@ -50,6 +50,7 @@
| 68 | 0.3.0 | | **Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.** | 3 | 3 | 4 | 4 | 7 | 1 | 18.2 |
| 75 | 0.3.0 | | **Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.** | 2 | 2 | 4 | 3 | 7 | 1 | 17.5 |
| 81 | 0.3.0 | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 2 | 2 | 5 | 5 | 3 | 16.3 |
| 112 | 0.3.0 | question | **Read a GFM pipe table's ragged and pipe-less body rows, and a one-column table without leading pipes, in `portableMarkdownToAdf` as GFM does, keeping every cell.** | 3 | 3 | 5 | 6 | 6 | 4, 7 | 16.3 |
| 46 | 0.3.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 3 | 5 | 5 | 3, 8 | 14.8 |
| 72 | 0.3.0 | decision | **Re-create `gitea.larvit.se/larvit/adf-codec` as a read-only pull mirror of GitHub, its issues and Actions off and its description pointing at GitHub.** | 2 | 1 | 1 | 3 | 6 | 3 | 13.1 |
| 107 | 0.3.0 | decision | **Spell the block carry's JSON so its length stays linear in the node's JSON.** | 4 | 5 | 7 | 3 | 5 | 1 | 10.8 |
@@ -272,6 +273,16 @@ and hostile. Found by the technical-principles audit, 2026-10-04.
leaves the README's tables a false contract under a green gate. Assert that the tables list exactly
the union. Found by the technical-principles audit of item 78, 2026-10-05.
### 112. Read a GFM pipe table's ragged and pipe-less body rows, and a one-column table without leading pipes, in `portableMarkdownToAdf` as GFM does, keeping every cell.
GFM pads a short row with empty cells, cuts a long one to the header's count, reads a line holding
no `|` after a table as a row, and reads `a |` over `--- |` as a one-column table.
`portableMarkdownToAdf` refuses a ragged row as `malformed-pipe-table`, ends the table at a line
holding no `|`, and reads the one-column shape as a paragraph; the README's pipe-table guarantee
names the first two. Cutting a long row drops content (Goal 7) where Goal 4 asks for GFM's reading:
which wins is the maintainer's. The one-column shape needs `isBareDelimiterRow` in the writer's
escaping to cover it too.
### 46. Publish the bundle size in the README, failing the release pipeline when it drifts.
Lands after item 7, which changes it. The quantity is what a consumer downloads and loads: the