3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 11:48:29 +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
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()`.
+2 -2
View File
@@ -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<EmittedBlock> {
const inner = walkBlocks(nodeContent(node), path, depth + 1, writing)
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'>
// 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<WrittenFlavour, ReadClaims & WrittenClaims>> = {
commonmark: { carryFence: false, directives: false, highlights: false, pipeTables: false, strikethrough: false },
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 { 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<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.
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
+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.
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.