Read the block nodes back, and stop a marker change splitting a list #40

Merged
lilleman merged 6 commits from block-nodes-read-back into main 2026-09-01 20:23:29 +02:00
4 changed files with 45 additions and 34 deletions
Showing only changes of commit 5a41547222 - Show all commits
+2 -1
View File
@@ -6,7 +6,8 @@ One directory per contract kind, each landing with its milestone:
document, byte for byte, and that `markdownToAdf` must read back to it (AGENTS.md §2). Grouped
by what the fixture exercises.
- `normalization/``<name>.md` + `<name>.json`: markdown input, and the document
`markdownToAdf` must build from it. One-way; the markdown is not canonical.
`markdownToAdf` must build from it, which must in turn emit and read back to itself. The
markdown is not canonical.
- `errors/``<name>.md`: markdown input that must not convert. A `<name>.error` beside it
pins which error.
- `real-payloads/``<name>.json`: sanitized live ADF, round-tripped ADF→markdown→ADF. No
+14 -2
View File
@@ -223,6 +223,18 @@ export function setextHeadingLevel(line: string): number | undefined {
return underline.startsWith('=') ? 1 : 2
}
export function trimSpace(text: string): string {
return text.replace(/^[ \t]+|[ \t]+$/g, '')
function spaceOrTab(character: string): boolean {
return character === ' ' || character === '\t'
}
export function trimSpace(text: string): string {
let start = 0
while (start < text.length && spaceOrTab(text.charAt(start))) start += 1
return trimTrailingSpace(text.slice(start))
}
export function trimTrailingSpace(text: string): string {
let end = text.length
while (end > 0 && spaceOrTab(text.charAt(end - 1))) end -= 1
return text.slice(0, end)
}
+3 -5
View File
@@ -1,7 +1,7 @@
import type { AdfMark, AdfNode } from '../../adf/document.ts'
import type { EmphasisPairing } from '../emphasis-matching.ts'
import type { LinkDefinition } from '../link-syntax.ts'
import { backslashEscape, decodeTextEscapes, inlineHtmlConstruct, readBracketedAutolink, readEmailAutolink } from '../commonmark-grammar.ts'
import { backslashEscape, decodeTextEscapes, inlineHtmlConstruct, readBracketedAutolink, readEmailAutolink, trimTrailingSpace } from '../commonmark-grammar.ts'
import { backtickRun, closingBacktickRun } from '../backtick-runs.ts'
import { delimiterFlags, matchEmphasis, runLength } from '../emphasis-matching.ts'
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
@@ -28,9 +28,7 @@ type Run = { canClose: boolean; canOpen: boolean; character: string; index: numb
type Scan = { definitions: LinkDefinitions; path: ConvertErrorPath; pending: string; pieces: Piece[]; source: string }
const hardBreakSpaces = / {2,}$/
const imageAlone = 'an image fits only as a paragraph of its own'
const trailingSpace = /[ \t]+$/
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath): Result<InlineContent> {
const scan: Scan = { definitions, path, pending: '', pieces: [], source }
@@ -95,7 +93,7 @@ function readBackslash(scan: Scan, index: number): number {
}
function readLineEnding(scan: Scan, index: number): number {
const hard = hardBreakSpaces.test(scan.pending)
const hard = scan.pending.endsWith(' ')
flush(scan, true)
if (hard) pushNode(scan, { type: 'hardBreak' })
else scan.pending = ' '
@@ -154,7 +152,7 @@ function readDirective(scan: Scan, index: number): Result<number> {
}
function flush(scan: Scan, strip: boolean): void {
const raw = strip ? scan.pending.replace(trailingSpace, '') : scan.pending
const raw = strip ? trimTrailingSpace(scan.pending) : scan.pending
scan.pending = ''
if (raw !== '') scan.pieces.push({ kind: 'nodes', nodes: [{ text: decodeTextEscapes(raw), type: 'text' }] })
}
+25 -25
View File
@@ -281,31 +281,6 @@ Under **3 — `markdownToAdf` (`0.1.0`)**:
`spec/flavour.md`'s closing-fence sentence now says: a run reaching past the innermost
leaves the fence it did not close a named error, which §2 prefers to closing more than the
author wrote.
- [x] **3h — The block nodes.** `block-nodes/` reads back: the `codeBlock` directive's fenced body and the
`language` attribute a bare fence leaves it; the media family's composition; and both
table forms, the pipe table's cell split and its named errors. `fenceInfo` is a rule both
directions answer alike and moves to the `markdown/` root with the language attribute.
**Settled** (the maintainer, 2026-08-27): 1d's last pick, the one
`container-block-separation` holds — a CommonMark block and a directive block sit adjacent
in a container body with no blank line between them. That reduces the three cases to one
rule, separation only where its absence would merge the blocks: the `:::` fence is
separation already, and 3c's claim ends the lazy continuation that would otherwise swallow
it. The fixture becomes a round-trip pair, and with `nested-list-separation` and 3e's pair
that empties `corpus/unspellable/`: this chunk settles the directory's own guard in
`corpus.test.ts` too, and `unspelled-block-separation`, which loses its only cause here.
The emitter's other refusals survive on causes no fixture in that directory covers, so
3k's one-list pass is where they get fixtures or the directory goes.
**Settled** (the maintainer, 2026-09-01): losing that cause closed one of the shapes input
accepted and emit refused, not the last. Two adjacent lists of a kind are what
`adfToMarkdown` refuses and one `- ` spelling cannot hold apart, and the walk reached them
two ways — a marker change, which CommonMark opens a second list on, and an empty last item,
whose blank line pops the container the list's identity hung from. The parser opens no list
beside one of its own kind instead, the way it already drops the blank lines between items;
3k owes the CommonMark suite an exception where the reference HTML holds two `<ul>`. The
`normalization/` arm emits each document and reads it back from here, so the population that
class lives in is checked rather than read. The README's canonical-fixpoint sentence still
claims more than the parser keeps — 3e's three shapes — which stays milestone 5's to
narrow.
- [x] **3g — The node tables read backwards.** `commonmark-subset/` reads back, the first
directory to. A parsed directive becomes its node: the name to the type and an unknown one
to a named error, the arg to the attribute it names, each value to the type its section
@@ -334,3 +309,28 @@ Under **3 — `markdownToAdf` (`0.1.0`)**:
path and returns `Result`, so no second reader took it. The drift guard earned itself on
the way in: the spec's `text` attribute was missing from three inline table entries, which
the content slot spells and the vocabulary walk already passes over.
- [x] **3h — The block nodes.** `block-nodes/` reads back: the `codeBlock` directive's fenced body and the
`language` attribute a bare fence leaves it; the media family's composition; and both
table forms, the pipe table's cell split and its named errors. `fenceInfo` is a rule both
directions answer alike and moves to the `markdown/` root with the language attribute.
**Settled** (the maintainer, 2026-08-27): 1d's last pick, the one
`container-block-separation` holds — a CommonMark block and a directive block sit adjacent
in a container body with no blank line between them. That reduces the three cases to one
rule, separation only where its absence would merge the blocks: the `:::` fence is
separation already, and 3c's claim ends the lazy continuation that would otherwise swallow
it. The fixture becomes a round-trip pair, and with `nested-list-separation` and 3e's pair
that empties `corpus/unspellable/`: this chunk settles the directory's own guard in
`corpus.test.ts` too, and `unspelled-block-separation`, which loses its only cause here.
The emitter's other refusals survive on causes no fixture in that directory covers, so
3k's one-list pass is where they get fixtures or the directory goes.
**Settled** (the maintainer, 2026-09-01): losing that cause closed one of the shapes input
accepted and emit refused, not the last. Two adjacent lists of a kind are what
`adfToMarkdown` refuses and one `- ` spelling cannot hold apart, and the walk reached them
two ways — a marker change, which CommonMark opens a second list on, and an empty last item,
whose blank line pops the container the list's identity hung from. The parser opens no list
beside one of its own kind instead, the way it already drops the blank lines between items;
3k owes the CommonMark suite an exception where the reference HTML holds two `<ul>`. The
`normalization/` arm emits each document and reads it back from here, so the population that
class lives in is checked rather than read. The README's canonical-fixpoint sentence still
claims more than the parser keeps — 3e's three shapes — which stays milestone 5's to
narrow.