Compare commits
3 Commits
e964abaa4b
...
cf9b12b973
| Author | SHA1 | Date | |
|---|---|---|---|
| cf9b12b973 | |||
| 97bf2440fd | |||
| 6391928fb4 |
@@ -306,15 +306,12 @@ someone spells it or pins it.
|
||||
`matchEmphasis` transcribes the reference `process_emphasis` line for line, and its closer walk and
|
||||
opener search stay whole: broken into named steps they drift from the algorithm being faithful is
|
||||
the whole point of.
|
||||
- A readable spelling tried ahead of a general one — a CommonMark block, the image, the pipe
|
||||
table, a pipe cell — gives way with `undefined` for every shape it cannot spell, and fails only
|
||||
where the general form fails on the same node. Refusing there refuses a document the general
|
||||
form spells, so a refusal the general form does not share belongs in the general form or
|
||||
nowhere — save the nested list a tight spelling would swallow, whose refusal the
|
||||
tight-versus-blank answer owns (`todo.md` 2b). A readable spelling that must spell its subtree
|
||||
before it can give way — the list, whose thematic-break first line and blank lines exist only
|
||||
spelled — hands that one walk to the general form instead: giving way after the walk walks
|
||||
again at every level, doubling per level (4b).
|
||||
- A readable spelling tried ahead of a general one takes the `try` prefix and fails only where the
|
||||
general form fails on the same node (20): refusing there refuses a document the general form
|
||||
spells, so a refusal the general form does not share belongs in the general form or nowhere. A
|
||||
readable spelling that must spell its subtree before it can give way — the list, whose
|
||||
thematic-break first line and blank lines exist only spelled — hands that one walk to the general
|
||||
form instead: giving way after the walk walks again at every level, doubling per level (4b).
|
||||
- Nothing recurses unbounded: the guards walk iteratively, and blocks, marks and JSON values — an
|
||||
attribute's and a carried node's alike — are all held to 500 levels, so a deep document is a
|
||||
`Result` rather than the stack overflow that waits near 2000. A level is one block-list
|
||||
|
||||
@@ -26,7 +26,7 @@ type Walk = { blocks: readonly PlacedBlock[]; headroom: number }
|
||||
type WalkedItem = { node: AdfNode; walk: Walk }
|
||||
|
||||
const largestListMarker = 999999999
|
||||
// Bare because emitList admits no item carrying attributes, marks or text.
|
||||
// Bare because tryList admits no item carrying attributes, marks or text.
|
||||
const listItemOpener = spellDirectiveOpener('listItem', undefined, '')
|
||||
|
||||
export function adfToMarkdown(document: AdfDocument): Result<string> {
|
||||
@@ -115,13 +115,13 @@ function readableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, mem
|
||||
}
|
||||
|
||||
function spellReadableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result<EmittedBlock> | undefined {
|
||||
if (node.type === 'blockquote') return emitBlockquote(node, path, depth, memo)
|
||||
if (node.type === 'bulletList' || node.type === 'orderedList') return emitList(node, path, depth, memo)
|
||||
if (node.type === 'codeBlock') return emitCodeBlock(node, path)
|
||||
if (node.type === 'heading') return emitHeading(node, path)
|
||||
if (node.type === 'blockquote') return tryBlockquote(node, path, depth, memo)
|
||||
if (node.type === 'bulletList' || node.type === 'orderedList') return tryList(node, path, depth, memo)
|
||||
if (node.type === 'codeBlock') return tryCodeBlock(node, path)
|
||||
if (node.type === 'heading') return tryHeading(node, path)
|
||||
if (node.type === 'mediaSingle') return readableText(tryImage(node, path))
|
||||
if (node.type === 'paragraph') return emitParagraph(node, path)
|
||||
if (node.type === 'rule') return emitRule(node)
|
||||
if (node.type === 'paragraph') return tryParagraph(node, path)
|
||||
if (node.type === 'rule') return readableText(tryRule(node))
|
||||
if (node.type === 'table') return readableText(tryPipeTable(node, path))
|
||||
return undefined
|
||||
}
|
||||
@@ -164,7 +164,7 @@ function emitDirectiveBody(node: AdfNode, directive: BlockDirective, opener: str
|
||||
return success(directivePair(node, opener, joinBlocks(walk.value.blocks, 'directive'), walk.value.headroom))
|
||||
}
|
||||
|
||||
function emitBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result<EmittedBlock> | undefined {
|
||||
function tryBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result<EmittedBlock> | undefined {
|
||||
if (!carriesOnly(node, [])) return undefined
|
||||
const inner = walkBlocks(nodeContent(node), path, depth + 1, memo)
|
||||
if (!inner.ok) return inner
|
||||
@@ -175,7 +175,7 @@ function emitBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number, me
|
||||
return success(commonMarkText(text, inner.value.headroom))
|
||||
}
|
||||
|
||||
function emitCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
||||
function tryCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
||||
if (!carriesOnly(node, ['language'])) return undefined
|
||||
const slot = languageSlot(nodeAttrs(node)['language'])
|
||||
if (slot.kind === 'attribute') return undefined
|
||||
@@ -214,7 +214,7 @@ function codeBlockText(node: AdfNode, path: ConvertErrorPath): Result<string> {
|
||||
return success(text)
|
||||
}
|
||||
|
||||
function emitHeading(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
||||
function tryHeading(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
||||
if (!carriesOnly(node, ['level'])) return undefined
|
||||
const level = nodeAttrs(node)['level']
|
||||
if (typeof level !== 'number' || !Number.isInteger(level) || level < 1 || level > 6) return undefined
|
||||
@@ -226,7 +226,7 @@ function emitHeading(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock
|
||||
return success(commonMarkText(`${hashes} ${line.value}`))
|
||||
}
|
||||
|
||||
function emitList(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result<EmittedBlock> | undefined {
|
||||
function tryList(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result<EmittedBlock> | undefined {
|
||||
const ordered = node.type === 'orderedList'
|
||||
if (!carriesOnly(node, ordered ? ['order'] : [])) return undefined
|
||||
const items = nodeContent(node)
|
||||
@@ -243,7 +243,7 @@ function emitList(node: AdfNode, path: ConvertErrorPath, depth: number, memo: Sp
|
||||
}
|
||||
const lines: string[] = []
|
||||
for (const [offset, item] of walked.entries()) {
|
||||
const line = listItemLines(item.walk.blocks, ordered ? `${start + offset}. ` : '- ')
|
||||
const line = tryListItemLines(item.walk.blocks, ordered ? `${start + offset}. ` : '- ')
|
||||
if (line === undefined) {
|
||||
if (headroom < 1) return tooDeep(path)
|
||||
return emitDirectiveBlock(node, ordered ? blockDirectives.orderedList : blockDirectives.bulletList, path, depth, () => success({ blocks: directiveItems(walked), headroom: headroom - 1 }))
|
||||
@@ -265,7 +265,7 @@ function listStart(node: AdfNode, items: number): number | undefined {
|
||||
return start + items - 1 > largestListMarker ? undefined : start
|
||||
}
|
||||
|
||||
function listItemLines(blocks: readonly PlacedBlock[], marker: string): string | undefined {
|
||||
function tryListItemLines(blocks: readonly PlacedBlock[], marker: string): string | undefined {
|
||||
const inner = joinBlocks(blocks, 'list-item')
|
||||
if (inner === '') return marker.trimEnd()
|
||||
const body = inner.split('\n')
|
||||
@@ -276,7 +276,7 @@ function listItemLines(blocks: readonly PlacedBlock[], marker: string): string |
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function emitParagraph(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
||||
function tryParagraph(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
||||
const content = nodeContent(node)
|
||||
if (content.length === 0 || !carriesOnly(node, [])) return undefined
|
||||
const line = emitInlineLine(content, 'paragraph', path)
|
||||
@@ -284,7 +284,7 @@ function emitParagraph(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlo
|
||||
return success(commonMarkText(line.value))
|
||||
}
|
||||
|
||||
function emitRule(node: AdfNode): Result<EmittedBlock> | undefined {
|
||||
function tryRule(node: AdfNode): string | undefined {
|
||||
if (!carriesOnly(node, []) || nodeContent(node).length > 0) return undefined
|
||||
return success(commonMarkText('---'))
|
||||
return '---'
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ function emitMarkedRun(nodes: readonly AdfNode[], mark: AdfMark, depth: number,
|
||||
if (attributes === undefined) return success({ carry: range })
|
||||
if (spelling.kind === 'code') return emitCodeSpan(nodes, depth, range, path)
|
||||
if (spelling.kind === 'emphasis') return emitEmphasis(nodes, spelling.spelling, depth, range, context)
|
||||
const link = spelling.kind === 'link' ? emitLink(nodes, mark, depth, range, context) : undefined
|
||||
const link = spelling.kind === 'link' ? tryLink(nodes, mark, depth, range, context) : undefined
|
||||
if (link !== undefined) return link
|
||||
const inner = emitRun(nodes, depth + 1, index, { ...context, bracketed: true, spansLines: false })
|
||||
if (!inner.ok) return inner
|
||||
@@ -300,8 +300,7 @@ function needsPadding(text: string): boolean {
|
||||
return text.startsWith(' ') && text.endsWith(' ') && /[^ ]/.test(text)
|
||||
}
|
||||
|
||||
// `undefined` where the link takes the directive form the caller spells.
|
||||
function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range: NodeRange, context: InlineContext): Result<Emission> | undefined {
|
||||
function tryLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range: NodeRange, context: InlineContext): Result<Emission> | undefined {
|
||||
const href = linkHref(nodeAttrs(mark))
|
||||
if (href === undefined) return success({ carry: range })
|
||||
const opening = depth === 0 && range.first === 0 && context.openingLinkAsDirective
|
||||
|
||||
@@ -922,6 +922,25 @@ The done `todo.md` items in full, as they were written. `todo.md` keeps a one-li
|
||||
moved: a construct rises on its second consumer, so `linkHref` (`markdown/mark-spellings.ts`)
|
||||
and the external-image read (`markdown/emit/image.ts`), both pure ADF attribute reads, move
|
||||
when `html/` reads them (7).
|
||||
- [x] **20 — The give-way channel is unmistakable (`0.2.0`).** `emitBlockquote`, `emitCodeBlock`,
|
||||
`emitHeading`, `emitList`, `emitParagraph` and `emitRule` return
|
||||
`Result<EmittedBlock> | undefined`, where `undefined` gives way to the directive form and an
|
||||
error refuses the document. Give the six the `try` prefix the repo already uses for
|
||||
`tryImage`, `tryPipeTable` and `tryPipeCell`, or a return type that cannot hold both, so a
|
||||
newcomer meeting an unspellable shape cannot reach for `failure` and silently narrow what
|
||||
converts. Named as the first thing a new senior would break, and it lands on §1.
|
||||
**Done** (2026-09-21): both answers, each where it fits. Five of the six reach a refusal the
|
||||
directive form shares — a walk, `codeBlockText`, `emitInlineLine` — so moving it out is the
|
||||
second walk 4b removed, and they take the prefix alone. `tryRule` refuses nothing, so it
|
||||
takes the narrower type as well, `string | undefined` through `readableText`, which is what
|
||||
`tryImage` and `tryPipeTable` already return. `emitLink` and `listItemLines` are the same
|
||||
channel outside the six — the CommonMark link and the tight list item, each tried ahead of a
|
||||
directive form — so they take the prefix too, and the comment naming `emitLink`'s `undefined`
|
||||
arm goes with the rename. §11's readable-spelling rule carries the prefix and gave up the set
|
||||
it enumerated: the prefix is the set now, which is what stops the next spelling being named
|
||||
by the eight beside it. The same rule's nested-list carve-out went with it, naming a refusal
|
||||
2b's tight-versus-blank answer had already removed — `unspellable-adjacent-lists` is gone from
|
||||
`result.ts` and `separationBetween` returns a `string`.
|
||||
- [x] **28 — `emitLine`'s retry loop cannot spin (`0.2.0`).** `emit/inline-line.ts:67` is a
|
||||
`for (;;)` that re-emits the line until every unspellable node has been carried, and its
|
||||
termination rests on a comment: each pass carries at least one more node, or flips
|
||||
|
||||
@@ -8,7 +8,9 @@ milestone. A done item shrinks to its title here; its full text moves to `todo-h
|
||||
Start a session with: `Read AGENTS.md and todo.md, then do what todo.md's "Next session" says.`
|
||||
|
||||
1. `git fetch origin` first and read this file at `origin/main`, then branch off it, not the
|
||||
worktree left behind: a checkout behind the remote reads a merged item as unchecked.
|
||||
worktree left behind: a checkout behind the remote reads a merged item as unchecked. `tea pr
|
||||
list` in the same breath — an open PR is a chunk already in flight whatever line 3 says, and
|
||||
finishing it is the session.
|
||||
2. The first unchecked item in shipping order, per AGENTS.md §15 — the order the Milestones line
|
||||
states, which wins over where an item's bullet sits: a newly filed item is written beside the
|
||||
one it came in with, not at its own place in the order. Where that item has no release, the
|
||||
@@ -20,7 +22,7 @@ Start a session with: `Read AGENTS.md and todo.md, then do what todo.md's "Next
|
||||
## Milestones
|
||||
|
||||
Shipping order: 3h, 3i, 3j, 5a, 5b, 5c, 5d, 5 → `0.1.0` (shipped 2026-09-05); 3k, 11, 4, 12, 13, 4b,
|
||||
4c, 14, 15, 16, 18, 4d, 28, 17, 29, 19, 20, 21, 22, 23, 24, 25, 30, 26, 27, 10, 6, 7, 5f, 5g →
|
||||
4c, 14, 15, 16, 18, 4d, 28, 17, 29, 19, 20, 21, 22, 23, 24, 25, 30, 26, 27, 10, 6, 7, 31, 5f, 5g →
|
||||
`0.2.0`;
|
||||
8, 9 → TBD; 5e last.
|
||||
The numbering is the order the work was planned in, not the order it ships. Everything known and
|
||||
@@ -41,14 +43,18 @@ HTML doubles the importers and the file count they touch, and 25 to 27 because t
|
||||
panel says the next reader pays for.
|
||||
29 and 30 come from 17's prose pass (2026-09-20). 29 reads first because every goal is what a later
|
||||
ask is settled against, 19's included; 30 sits beside 25, the other chunk rereading AGENTS.md.
|
||||
31 comes from 20's gate runs (2026-09-21) and reads beside 5f, the other chunk putting a measured
|
||||
number under the pipeline.
|
||||
|
||||
- [ ] **20 — The give-way channel is unmistakable (`0.2.0`).** `emitBlockquote`, `emitCodeBlock`,
|
||||
`emitHeading`, `emitList`, `emitParagraph` and `emitRule` return
|
||||
`Result<EmittedBlock> | undefined`, where `undefined` gives way to the directive form and an
|
||||
error refuses the document. Give the six the `try` prefix the repo already uses for
|
||||
`tryImage`, `tryPipeTable` and `tryPipeCell`, or a return type that cannot hold both, so a
|
||||
newcomer meeting an unspellable shape cannot reach for `failure` and silently narrow what
|
||||
converts. Named as the first thing a new senior would break, and it lands on §1.
|
||||
- [ ] **31 — The branch figure the floor is read against is stable (`0.2.0`).** Three Node test
|
||||
legs over one unchanged tree reported `emit/inline-line.ts` at 95.83%, 96.23% and 96.23%,
|
||||
and the total at 98.80%, 98.84% and 98.84% (2026-09-21). `--experimental-test-coverage`
|
||||
counts branches off V8's own coverage, which the runner's parallel files and V8's
|
||||
optimization make run-dependent, so the number the floor is read against is not the code's
|
||||
alone. The floor of 98 holds today on 0.8 points of slack and §10 says it only ever moves
|
||||
upward, so the first raise to the measured figure reddens a run that changed nothing. Make
|
||||
the measurement repeatable, or state the number the floor may be raised to and why it is not
|
||||
the measured one.
|
||||
- [ ] **21 — The ADF tables carry ADF's nouns (`0.2.0`).** `adf/block-directives.ts` and
|
||||
`adf/inline-directives.ts` hold the ADF node tables — `paragraph`, `heading`, `blockquote`
|
||||
and `rule` among them — under the markdown flavour's word, inside the directory §11 forbids
|
||||
@@ -280,6 +286,7 @@ ask is settled against, 19's included; 30 sits beside 25, the other chunk reread
|
||||
- [x] **17 — A machine-enforced size ratchet (`0.2.0`).**
|
||||
- [x] **18 — The subtree the directive spelling asks about (`0.2.0`).**
|
||||
- [x] **19 — A home for what both formats read (`0.2.0`).**
|
||||
- [x] **20 — The give-way channel is unmistakable (`0.2.0`).**
|
||||
- [x] **28 — `emitLine`'s retry loop cannot spin (`0.2.0`).**
|
||||
- [x] **29 — The README reads raw HTML as refused for good (`0.2.0`).**
|
||||
- [x] **30 — AGENTS.md says each thing once (`0.2.0`).**
|
||||
|
||||
Reference in New Issue
Block a user