diff --git a/docs/decisions.md b/docs/decisions.md index f6f4a64..79ecf08 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -911,9 +911,8 @@ is the markdown flavour's choice, not ADF's. ## 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 -boundaries' lint 2026-10-22, the maintainer; the portable flavour's own directory, item 62, -2026-10-22. Goal 2. Valid while each format has a reader and a -writer through ADF. +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 writer through ADF. `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 @@ -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 what both read where there are two. -A flavour's own code, both directions included, sits in its own directory: `markdown/portable/`. -It hands `parse/` and `emit/` the blocks it reads and writes its own way, so neither imports it. -Syntax that `parse/` and `emit/` read for a claim in `flavourClaims`, such as `==`, sits in -`markdown/` itself. The directive grammar, both -directions included, sits in `markdown/directive/`, which imports neither `emit/` nor `parse/`. -Otherwise each format directory parts into `emit/` (ADF→format) and -`parse/` (format→ADF), the rest of it holding what both directions read. A construct's reader lives -there beside the regex the emitter escapes against, so the two cannot drift; a reader with no emit +A flavour's own code, both directions included, sits in its own directory: `markdown/portable/`. The +portable reader and writer hand `parse/` and `emit/` their own block readers and writers +(`FlavourBlockReaders`, `FlavourBlockWriters`), so neither imports `portable/`. Syntax that `parse/` +and `emit/` read under a `flavourClaims` claim, such as `==`, sits outside `portable/`. The +directive grammar, both directions included, sits in `markdown/directive/`, which imports neither +`emit/` nor `parse/`. Otherwise each format directory parts into `emit/` (ADF→format) and `parse/` +(format→ADF), the rest of it holding what both directions read. A construct's reader lives there +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 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 @@ -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 importing `markdown/portable/`, and keeps `markdown/directive/` from importing `emit/` or `parse/`, -as `no-restricted-imports` overrides. They are lint rules because the -technical principles refuse a test that scans the source, and a lint rule reads every import -spelling: `from`, a bare `import`, double quotes and a dynamic `import()`. +as `no-restricted-imports` overrides. They are lint rules because the technical principles refuse a +test that scans the source, and a lint rule reads every import spelling: `from`, a bare `import`, +double quotes and a dynamic `import()`. diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts index c9f51f3..75bb83a 100644 --- a/src/markdown/emit/adf-to-markdown.ts +++ b/src/markdown/emit/adf-to-markdown.ts @@ -22,7 +22,7 @@ type BlockContainer = 'directive' | 'document' | 'list-item' type Delimiters = readonly [string, string] // headroom: the nesting levels the deepest node below may still spend before `largestNesting` refuses it. 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 = { claimsItem: (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 } -// 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 { const inner = walkBlocks(nodeContent(node), path, depth + 1, writing) if (!inner.ok) return inner diff --git a/src/markdown/flavour.ts b/src/markdown/flavour.ts index 8ca367d..b0aecd7 100644 --- a/src/markdown/flavour.ts +++ b/src/markdown/flavour.ts @@ -2,7 +2,7 @@ export type Flavour = 'commonmark' | 'lossless' | 'portable' export type WrittenFlavour = Exclude -// 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 = { carryFence: boolean directives: boolean @@ -17,7 +17,7 @@ type WrittenClaims = Claims & { pipeTables: 'flavour' | 'gfm'; strikethrough: tr // Directives exclude highlights: a directive's slot reads `==`. 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> = { commonmark: { carryFence: false, directives: false, highlights: false, pipeTables: false, strikethrough: false }, lossless: { carryFence: true, directives: true, highlights: false, pipeTables: 'flavour', strikethrough: true }, diff --git a/src/markdown/parse/markdown-to-adf.ts b/src/markdown/parse/markdown-to-adf.ts index be9fd89..6ccb158 100644 --- a/src/markdown/parse/markdown-to-adf.ts +++ b/src/markdown/parse/markdown-to-adf.ts @@ -19,7 +19,7 @@ import { spellStringAttribute, unsupportedNodeShape } from '../directive/syntax. import { spellsEmpty } from '../directive/empty-keys.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 = { bulletList: (items: readonly Block[][], reading: FlavourReading, path: ConvertErrorPath, depth: number, index: number) => Result quote: (blocks: readonly Block[], reading: FlavourReading, path: ConvertErrorPath, depth: number) => Result diff --git a/src/markdown/portable/portable-markdown-to-adf.ts b/src/markdown/portable/portable-markdown-to-adf.ts index e8c2b1e..1955ad5 100644 --- a/src/markdown/portable/portable-markdown-to-adf.ts +++ b/src/markdown/portable/portable-markdown-to-adf.ts @@ -16,7 +16,6 @@ type Paragraph = Extract // 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 } -// Inside an expand, a folded callout reads as a nestedExpand. const expandBlockReaders: FlavourBlockReaders = { bulletList: bulletBlocks, quote: quoteReader('nestedExpand') } 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 })) } -// 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'] { return (blocks, reading, path, depth) => { const [first, ...body] = blocks diff --git a/todo.md b/todo.md index ac3f460..a8f78ad 100644 --- a/todo.md +++ b/todo.md @@ -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. -Mutation testing of item 62's move left about 40 mutants alive in code it moved verbatim, so the -gaps predate it: `tryTaskList` and `taskBlocks` in `src/markdown/portable/block-spelling.ts`, and +Mutation testing left about 40 mutants alive in code moved verbatim into `src/markdown/portable/` +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 `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.