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

Answer prose round 1: 0 🔴, 7 🟡; state the seam's comments and the decision exactly, and reflow

This commit is contained in:
2026-10-22 21:19:04 +02:00
parent 7837721d56
commit ff220e67ad
6 changed files with 21 additions and 23 deletions
+13 -14
View File
@@ -911,9 +911,8 @@ is the markdown flavour's choice, not ADF's.
## The source parts by ADF and format ## The source parts by ADF and format
2026-08-27, placement 2026-09-18, `adf/`'s bar 2026-09-21, `plain/` 2026-10-03, the import 2026-08-27, placement 2026-09-18, `adf/`'s bar 2026-09-21, `plain/` 2026-10-03, the import
boundaries' lint 2026-10-22, the maintainer; the portable flavour's own directory, item 62, boundaries' lint 2026-10-22, the maintainer; `parse/` and `emit/` free of `portable/`,
2026-10-22. Goal 2. Valid while each format has a reader and a 2026-10-22. Goal 2. Valid while each format has a reader and a writer through ADF.
writer through ADF.
`src/adf/` holds ADF's own knowledge, imports no format, and is where a construct both formats read `src/adf/` holds ADF's own knowledge, imports no format, and is where a construct both formats read
lives: the question is answered in ADF's vocabulary — a node type, an attribute kind, a content lives: the question is answered in ADF's vocabulary — a node type, an attribute kind, a content
@@ -926,14 +925,14 @@ them. A primitive knowing neither ADF nor a format stays at `src/` root. A const
`spec/flavour.md` draws, and a placement nothing here settles goes beside its only reader, or in `spec/flavour.md` draws, and a placement nothing here settles goes beside its only reader, or in
what both read where there are two. what both read where there are two.
A flavour's own code, both directions included, sits in its own directory: `markdown/portable/`. A flavour's own code, both directions included, sits in its own directory: `markdown/portable/`. The
It hands `parse/` and `emit/` the blocks it reads and writes its own way, so neither imports it. portable reader and writer hand `parse/` and `emit/` their own block readers and writers
Syntax that `parse/` and `emit/` read for a claim in `flavourClaims`, such as `==`, sits in (`FlavourBlockReaders`, `FlavourBlockWriters`), so neither imports `portable/`. Syntax that `parse/`
`markdown/` itself. The directive grammar, both and `emit/` read under a `flavourClaims` claim, such as `==`, sits outside `portable/`. The
directions included, sits in `markdown/directive/`, which imports neither `emit/` nor `parse/`. directive grammar, both directions included, sits in `markdown/directive/`, which imports neither
Otherwise each format directory parts into `emit/` (ADF→format) and `emit/` nor `parse/`. Otherwise each format directory parts into `emit/` (ADF→format) and `parse/`
`parse/` (format→ADF), the rest of it holding what both directions read. A construct's reader lives (format→ADF), the rest of it holding what both directions read. A construct's reader lives there
there beside the regex the emitter escapes against, so the two cannot drift; a reader with no emit beside the regex the emitter escapes against, so the two cannot drift; a reader with no emit
counterpart goes in `parse/`, unless it is part of a construct that side already holds — a grammar counterpart goes in `parse/`, unless it is part of a construct that side already holds — a grammar
stays in one file rather than splitting across the seam. A rule both directions must answer alike — stays in one file rather than splitting across the seam. A rule both directions must answer alike —
whether a list marker interrupts a paragraph — is one function there too, never a copy per whether a list marker interrupts a paragraph — is one function there too, never a copy per
@@ -944,6 +943,6 @@ is the only import `parse/` takes from `emit/`, through `emit/spelling-questions
`.oxlintrc.json` enforces that limit for `markdown/parse/`, keeps `parse/` and `emit/` from `.oxlintrc.json` enforces that limit for `markdown/parse/`, keeps `parse/` and `emit/` from
importing `markdown/portable/`, and keeps `markdown/directive/` from importing `emit/` or `parse/`, importing `markdown/portable/`, and keeps `markdown/directive/` from importing `emit/` or `parse/`,
as `no-restricted-imports` overrides. They are lint rules because the as `no-restricted-imports` overrides. They are lint rules because the technical principles refuse a
technical principles refuse a test that scans the source, and a lint rule reads every import test that scans the source, and a lint rule reads every import spelling: `from`, a bare `import`,
spelling: `from`, a bare `import`, double quotes and a dynamic `import()`. double quotes and a dynamic `import()`.
+2 -2
View File
@@ -22,7 +22,7 @@ type BlockContainer = 'directive' | 'document' | 'list-item'
type Delimiters = readonly [string, string] type Delimiters = readonly [string, string]
// headroom: the nesting levels the deepest node below may still spend before `largestNesting` refuses it. // headroom: the nesting levels the deepest node below may still spend before `largestNesting` refuses it.
export type EmittedBlock = SpelledBlock & { headroom: number } export type EmittedBlock = SpelledBlock & { headroom: number }
// A flavour's own blocks, spelled before CommonMark's, and the openings its reader claims, which a list item or quote escapes. // spell: the flavour's own blocks, tried before CommonMark's; claimsItem, claimsQuote: whether a list item's or quote's text opens with a marker the flavour's reader would take, so the writer escapes it.
export type FlavourBlockWriters = { export type FlavourBlockWriters = {
claimsItem: (text: string) => boolean claimsItem: (text: string) => boolean
claimsQuote: (text: string) => boolean claimsQuote: (text: string) => boolean
@@ -202,7 +202,7 @@ function alertEscaped(body: string, writing: Writing): string {
return writing.flavour === 'portable' && writing.blocks.claimsQuote(body) ? `\\${body}` : body return writing.flavour === 'portable' && writing.blocks.claimsQuote(body) ? `\\${body}` : body
} }
// Each quote speller calls it directly, not through a wrapper, so a quote level spends one frame between walks (docs/decisions.md §Nothing recurses unbounded). // Quote spellers call this directly, never through a wrapper: each frame a quote level adds lowers the depth the stack holds (docs/decisions.md §Nothing recurses unbounded).
export function quotedBlocks(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing, bodyText: (body: string) => string): Result<EmittedBlock> { export function quotedBlocks(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing, bodyText: (body: string) => string): Result<EmittedBlock> {
const inner = walkBlocks(nodeContent(node), path, depth + 1, writing) const inner = walkBlocks(nodeContent(node), path, depth + 1, writing)
if (!inner.ok) return inner if (!inner.ok) return inner
+2 -2
View File
@@ -2,7 +2,7 @@ export type Flavour = 'commonmark' | 'lossless' | 'portable'
export type WrittenFlavour = Exclude<Flavour, 'commonmark'> export type WrittenFlavour = Exclude<Flavour, 'commonmark'>
// What a reader reads past CommonMark, and so what its writer escapes, but for the portable callouts and task lists that `FlavourBlockReaders` and `FlavourBlockWriters` carry (docs/decisions.md §Each call names its flavour). // What a reader reads past CommonMark, and so what its writer escapes; the portable flavour's alerts, callouts and task markers are left to `FlavourBlockReaders` and `FlavourBlockWriters` (docs/decisions.md §Each call names its flavour).
export type Claims = { export type Claims = {
carryFence: boolean carryFence: boolean
directives: boolean directives: boolean
@@ -17,7 +17,7 @@ type WrittenClaims = Claims & { pipeTables: 'flavour' | 'gfm'; strikethrough: tr
// Directives exclude highlights: a directive's slot reads `==`. // Directives exclude highlights: a directive's slot reads `==`.
type ReadClaims = Claims & ({ directives: false } | { directives: true; highlights: false }) type ReadClaims = Claims & ({ directives: false } | { directives: true; highlights: false })
// A folded callout inside an expand directive would read as an expand, not a nestedExpand, so the portable flavour reads no directives. // Pinned false: inside an expand directive, a folded callout would read as an expand, not a nestedExpand.
export const flavourClaims: Readonly<{ commonmark: ReadClaims; portable: { directives: false } } & Record<WrittenFlavour, ReadClaims & WrittenClaims>> = { export const flavourClaims: Readonly<{ commonmark: ReadClaims; portable: { directives: false } } & Record<WrittenFlavour, ReadClaims & WrittenClaims>> = {
commonmark: { carryFence: false, directives: false, highlights: false, pipeTables: false, strikethrough: false }, commonmark: { carryFence: false, directives: false, highlights: false, pipeTables: false, strikethrough: false },
lossless: { carryFence: true, directives: true, highlights: false, pipeTables: 'flavour', strikethrough: true }, lossless: { carryFence: true, directives: true, highlights: false, pipeTables: 'flavour', strikethrough: true },
+1 -1
View File
@@ -19,7 +19,7 @@ import { spellStringAttribute, unsupportedNodeShape } from '../directive/syntax.
import { spellsEmpty } from '../directive/empty-keys.ts' import { spellsEmpty } from '../directive/empty-keys.ts'
import { withinEngineLimits } from '../length-bound.ts' import { withinEngineLimits } from '../length-bound.ts'
// A flavour's own reading of a quote and of a bullet list, which may read to several blocks; `path` is the container's, `index` the list's among its blocks. // A flavour's own reading of a quote, and of a bullet list into one or more blocks; `path` is the container's, `index` the list's place among the container's blocks.
export type FlavourBlockReaders = { export type FlavourBlockReaders = {
bulletList: (items: readonly Block[][], reading: FlavourReading, path: ConvertErrorPath, depth: number, index: number) => Result<AdfNode[]> bulletList: (items: readonly Block[][], reading: FlavourReading, path: ConvertErrorPath, depth: number, index: number) => Result<AdfNode[]>
quote: (blocks: readonly Block[], reading: FlavourReading, path: ConvertErrorPath, depth: number) => Result<AdfNode> quote: (blocks: readonly Block[], reading: FlavourReading, path: ConvertErrorPath, depth: number) => Result<AdfNode>
@@ -16,7 +16,6 @@ type Paragraph = Extract<Block, { kind: 'paragraph' }>
// The paragraph a task marker opens: a task item holds no image, so a lone one there reads as its linked alt text. // The paragraph a task marker opens: a task item holds no image, so a lone one there reads as its linked alt text.
type TaskParagraph = { kind: 'taskParagraph'; position: SourcePosition; text: string } type TaskParagraph = { kind: 'taskParagraph'; position: SourcePosition; text: string }
// Inside an expand, a folded callout reads as a nestedExpand.
const expandBlockReaders: FlavourBlockReaders = { bulletList: bulletBlocks, quote: quoteReader('nestedExpand') } const expandBlockReaders: FlavourBlockReaders = { bulletList: bulletBlocks, quote: quoteReader('nestedExpand') }
const portableBlockReaders: FlavourBlockReaders = { bulletList: bulletBlocks, quote: quoteReader('expand') } const portableBlockReaders: FlavourBlockReaders = { bulletList: bulletBlocks, quote: quoteReader('expand') }
@@ -48,7 +47,7 @@ function paragraphsOf(position: SourcePosition, ...texts: string[]): Paragraph[]
return texts.filter((text) => text !== '').map((text) => ({ kind: 'paragraph', position, text })) return texts.filter((text) => text !== '').map((text) => ({ kind: 'paragraph', position, text }))
} }
// The closure is the reader rather than a wrapper around one, so a quote level spends one stack frame (docs/decisions.md §Nothing recurses unbounded). // The closure is the reader itself, never a wrapper calling one: a wrapper adds a stack frame to every quote level (docs/decisions.md §Nothing recurses unbounded).
function quoteReader(foldedType: 'expand' | 'nestedExpand'): FlavourBlockReaders['quote'] { function quoteReader(foldedType: 'expand' | 'nestedExpand'): FlavourBlockReaders['quote'] {
return (blocks, reading, path, depth) => { return (blocks, reading, path, depth) => {
const [first, ...body] = blocks const [first, ...body] = blocks
+2 -2
View File
@@ -142,8 +142,8 @@ wait for its publish). Found while planning discoverability, 2026-10-04.
### 128. Pin the portable flavour's block reading and writing with tests: callout titles past one line, a link beside other marks in a title, the nesting limit of callouts and task lists, refusals inside tasks and their paths, and empty or non-paragraph tasks. ### 128. Pin the portable flavour's block reading and writing with tests: callout titles past one line, a link beside other marks in a title, the nesting limit of callouts and task lists, refusals inside tasks and their paths, and empty or non-paragraph tasks.
Mutation testing of item 62's move left about 40 mutants alive in code it moved verbatim, so the Mutation testing left about 40 mutants alive in code moved verbatim into `src/markdown/portable/`
gaps predate it: `tryTaskList` and `taskBlocks` in `src/markdown/portable/block-spelling.ts`, and on 2026-10-22, so the gaps predate the move: `tryTaskList` and `taskBlocks` in `src/markdown/portable/block-spelling.ts`, and
`markerLine`, `quoteReader`, `titleText`, `taskBlocks` and `readTaskBlock` in `markerLine`, `quoteReader`, `titleText`, `taskBlocks` and `readTaskBlock` in
`src/markdown/portable/portable-markdown-to-adf.ts`. Each test takes its expected value from `src/markdown/portable/portable-markdown-to-adf.ts`. Each test takes its expected value from
`spec/flavour.md` or `docs/decisions.md`, never from the code. `spec/flavour.md` or `docs/decisions.md`, never from the code.