From 186050f906e3db31da5cbf392d7aee6609a950e5 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sun, 8 Nov 2026 22:00:59 +0100 Subject: [PATCH] =?UTF-8?q?Answer=20prose=20round=201:=200=20=F0=9F=94=B4,?= =?UTF-8?q?=2014=20=F0=9F=9F=A1=20(0=20misread);=20correct=20comments=20th?= =?UTF-8?q?at=20misstated=20the=20code,=20fix=20item=20174's=20figures=20a?= =?UTF-8?q?nd=20tagline,=20retitle=20item=20173?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/html/carry-content.ts | 2 +- src/html/data-attributes.ts | 4 ++-- src/html/parse/html-to-adf.ts | 2 +- src/markdown/emit/adf-to-markdown.ts | 2 +- src/markdown/emit/inline-line.ts | 4 ++-- src/markdown/emit/unspellable-runs.ts | 3 +-- src/markdown/portable/editor-normal.ts | 2 +- src/markdown/portable/inline-reduction.ts | 8 ++++---- todo.md | 23 ++++++++++++----------- 9 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/html/carry-content.ts b/src/html/carry-content.ts index e58cd3e..716fb0e 100644 --- a/src/html/carry-content.ts +++ b/src/html/carry-content.ts @@ -6,7 +6,7 @@ import { nodeAttrs, nodeContent } from '../adf/document.ts' import { partsShownText, referenceName, shownImage, shownLink, shownText, shownUrl } from '../adf/shown-text.ts' import { shownValue } from './escaping.ts' -// One object per block a node stands in, so a block's content after a nested block parts from its content before; `code` the code block it is, if any. +// Compared by reference: each block makes one, so its content after a nested block starts a new shown block. `code`: this block, where it is a code block. type BlockPart = { code: AdfNode | undefined } type ShownBlock = { html: string; part: BlockPart } diff --git a/src/html/data-attributes.ts b/src/html/data-attributes.ts index 1d40240..7ed630c 100644 --- a/src/html/data-attributes.ts +++ b/src/html/data-attributes.ts @@ -22,7 +22,7 @@ export function dataAttributes(node: AdfNode, vocabulary: AttributeVocabulary, w return attributes } -// What `dataAttributes` wrote, its pairs' values unescaped. A pair it does not write in that order is no HTML of the dialect; a name or value no attribute of the node spells is the node's shape, as a directive's is (docs/decisions.md §Which code a cause takes). +// What `dataAttributes` wrote, its pairs' values unescaped. A pair it does not write in that order is no HTML of the dialect; a `data-*` name or a value no attribute of the node spells is the node's shape, as a directive's is (docs/decisions.md §Which code a cause takes). export function readDataAttributes(pairs: readonly (readonly [string, string])[], element: string, vocabulary: AttributeVocabulary, written: readonly string[]): Read { const attrs: AdfAttributes = {} let previous = '' @@ -38,7 +38,7 @@ export function readDataAttributes(pairs: readonly (readonly [string, string])[] return { value: attrs } } -// As `readDataAttributes`, past `leading`, an attribute the element writes ahead of them, where it stands first; the caller reads `leading`. +// As `readDataAttributes`, skipping `leading` where it is the first pair and refusing it later; the caller reads `leading`'s value. export function readDataAttributesAfter(leading: string, pairs: readonly (readonly [string, string])[], element: string, vocabulary: AttributeVocabulary, written: readonly string[]): Read { const repeated = pairs.findIndex(([name], index) => index > 0 && name === leading) const data = readDataAttributes(pairs.slice(pairs[0]?.[0] === leading ? 1 : 0, repeated < 0 ? pairs.length : repeated), element, vocabulary, written) diff --git a/src/html/parse/html-to-adf.ts b/src/html/parse/html-to-adf.ts index df00ad5..fcecb5a 100644 --- a/src/html/parse/html-to-adf.ts +++ b/src/html/parse/html-to-adf.ts @@ -28,7 +28,7 @@ type BlockReader = (html: string, tag: StartTag, place: Place, open: Open) => Re type BlockStep = { end: number; kind: 'node'; node: AdfNode } | { end: number; kind: 'opened'; open: Open } -// A container whose blocks, items, rows or cells are being read: `node` holds what its opening tag gave, `depth` is its children's as `adfToHtml` counts it, and `closer` ends it. An open table has kind `rows` and no `closer`: `section` names the one its rows stand in, and `closerOf` derives its closer from it. +// A container whose blocks, items, rows or cells are being read: `node` holds what its opening tag gave, `depth` is its children's as `adfToHtml` counts it, and `closer` ends it. An open table has kind `rows` and no `closer`: `section` names the table section, head or body, its rows stand in. type Open = { content: AdfNode[]; depth: number; node: AdfNode; place: Place | undefined } & ({ closer: string; kind: 'blocks' | 'item' | 'list' | 'row' } | { kind: 'rows'; section: TableSection }) // A row's step names the section the table's rows stand in from that row on. diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts index 466b3d4..c958ea2 100644 --- a/src/markdown/emit/adf-to-markdown.ts +++ b/src/markdown/emit/adf-to-markdown.ts @@ -35,7 +35,7 @@ export type PlacedBlocks = { blocks: readonly PlacedBlock[]; headroom: number } // CommonMark starts a new list where the marker changes, so every second list in a run of one kind takes `alternateText`. type SpelledBlock = { spelling: 'commonmark' | 'directive'; text: BlockText } | { alternateText: BlockText; interruptsParagraph: boolean; ordered: boolean; spelling: 'list'; text: BlockText } type Spell = () => Result | undefined -// ask: spell the node, keeping the spelling unless refused; take: spell the node, reading and dropping what is kept. +// take: read and drop the node's kept spelling, else spell it; ask: as take, keeping a fresh spelling unless refused. export type SpellingMemo = Readonly Result | undefined>> type WalkedItem = { node: AdfNode; walk: PlacedBlocks } export type WrittenAs = { flavour: 'lossless' } | { blocks: FlavourBlockWriters; flavour: 'portable' } diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index e9349af..b161920 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -30,7 +30,7 @@ type Emission = { carries: [NodeRange, ...NodeRange[]]; segments?: undefined } | type Fallback = [NodeRange, ...NodeRange[]] | 'opening-link' -// How a line is judged and what its fallbacks do: the lossless writer carries nodes, the portable reduction drops marks. +// How a line is judged and what its fallbacks do: the writer carries nodes, the portable reduction drops marks. export type FallbackAction = { giveUpEmphasis: (line: Line) => Line judge: (line: Line, grouping: RunGrouping) => Result> @@ -118,7 +118,7 @@ function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: Con // spec/flavour.md, Marks: the spanned line where it reads back, else the parted line, taking one fallback a pass. export function settledLine(line: Line, action: FallbackAction): Result { - // Terminates because `take` refuses a pass that changes nothing. + // Terminates while each `take` refuses or moves one way: the writer carries more nodes, the reduction keeps fewer marks. for (let current = line, pass = 1; ; pass += 1) { let parting = current if (pass === 1 || action.spansEachPass) { diff --git a/src/markdown/emit/unspellable-runs.ts b/src/markdown/emit/unspellable-runs.ts index 8326016..925d3e4 100644 --- a/src/markdown/emit/unspellable-runs.ts +++ b/src/markdown/emit/unspellable-runs.ts @@ -26,7 +26,7 @@ type PairSweep = { // How another pair stands when the judged one falls: `moves` where a carry or a merge may move its delimiters, `unsettled` where it may stand or have fallen. type Standing = 'fallen' | 'moves' | 'stands' | 'unsettled' -// `merges` where a drop may merge runs at the pair's ends, so its delimiters may move; `falls` where it goes back with the first misread pair, itself included unless it merges; `kept` otherwise. +// `merges` where a drop may merge runs at the pair's ends, so its delimiters may move; `falls` where it goes back with the first misread pair, as the first pair does unless it merges; `kept` otherwise. type Verdict = 'falls' | 'kept' | 'merges' // The runs this pass takes back: the first emphasis run the line misreads and every run that falls with it, else every misflanked highlight run. @@ -109,7 +109,6 @@ function endsAlone(edge: AdfNode | undefined, beside: AdfNode | undefined, mark: return marks.slice(marks.findIndex((held) => identicalMark(held, mark)) + 1).every((held) => !besideTypes.has(held.type)) } -// A pair falls with the first where it falls cleanly, and cannot read whichever character it is written with. function fallsWithFirst(sweep: PairSweep, judged: number): boolean { if (!fallsCleanly(sweep, judged)) return false const spellings = pairSpellings(sweep, judged, judged) diff --git a/src/markdown/portable/editor-normal.ts b/src/markdown/portable/editor-normal.ts index ab65c86..efdf3df 100644 --- a/src/markdown/portable/editor-normal.ts +++ b/src/markdown/portable/editor-normal.ts @@ -40,7 +40,7 @@ export function editorNormalForm(document: AdfDocument): { normal: AdfDocument; return { normal, sources } } -// A node's index in a list of siblings, or in a list of one, counted in the document the siblings were normalized from. +// The node's offset from the list's first node, counted in the document they were normalized from; `index` where either is not in `sources`. export function sourceIndex(nodes: readonly AdfNode[], index: number, sources: SourceIndexes): number { const first = nodes[0] const node = nodes[index] diff --git a/src/markdown/portable/inline-reduction.ts b/src/markdown/portable/inline-reduction.ts index 5b8ada5..434807c 100644 --- a/src/markdown/portable/inline-reduction.ts +++ b/src/markdown/portable/inline-reduction.ts @@ -17,7 +17,7 @@ import { spellDestination } from '../commonmark/link-syntax.ts' // What stands beside a leaf's edge: whether a line edge does, and how many runs hold the leaf and its neighbour both. type Beside = { lineEdge: boolean; sharedRuns: number } -// How many of the leaf's marks edge whitespace keeps, or that it drops at a line edge. +// `keep`: how many of the leaf's marks its edge whitespace keeps; `drop`: the whitespace goes, at a line edge. type EdgeOutcome = { kind: 'drop' } | { depth: number; kind: 'keep' } type PlacedLeaf = { after: Beside; before: Beside; leaf: AdfNode } @@ -34,7 +34,7 @@ export function reduceInline(nodes: readonly AdfNode[], container: LineContainer const leaves = portableLeaves(nodes, container, place) if (!leaves.ok) return leaves const { path } = place - // Ends: withoutFallback refuses a pass that drops no mark, and trimmedEdges adds none. + // Terminates: withoutFallback refuses a pass that drops no mark, and trimmedEdges adds none. return settledLine(leaves.value, { giveUpEmphasis: withoutEmphasis, judge: (line, grouping) => portableJudgement(line, grouping, container, path), @@ -43,7 +43,7 @@ export function reduceInline(nodes: readonly AdfNode[], container: LineContainer }) } -// The leaves before the spanned trim `portableJudgement` gives them. +// Trimmed once for `highlightsInsideSharedMarks`; `portableJudgement` trims each grouping's line again. export function portableLeaves(nodes: readonly AdfNode[], container: LineContainer, place: ReductionPlace): Result { const leaves = inlineLeaves(nodes, container, place) if (!leaves.ok) return leaves @@ -248,7 +248,7 @@ function edgeOutcome(marks: readonly AdfMark[], { lineEdge, sharedRuns }: Beside return depth === 0 && lineEdge ? { kind: 'drop' } : { depth, kind: 'keep' } } -// Each grouping's line trimmed by its own runs, which a fallback's indexes name. +// Trims the line by `grouping`'s runs and judges it; a fallback's indexes name the trimmed line. export function portableJudgement(leaves: readonly AdfNode[], grouping: RunGrouping, container: LineContainer, path: ConvertErrorPath): Result> { const line = trimmedEdges(leaves, grouping) const fallback = portableLineFallback(line, container, path, grouping) diff --git a/todo.md b/todo.md index e954f03..f9eba48 100644 --- a/todo.md +++ b/todo.md @@ -28,7 +28,7 @@ | 112 | 0.4.0 | | | **Pad a GFM pipe table's short rows with empty cells, read its body rows holding no pipe, and read a one-column table without leading pipes in `portableMarkdownToAdf`.** | 2 | 1 | 2 | 3 | 3 | 4 by 4 | 6.2 | | 80 | 0.4.0 | | | **Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.** | 2 | 1 | 2 | 3 | 3 | 6 by 4 | 6.0 | | 171 | 0.4.0 | | principle | **Assert the facts an error message must carry, never its whole sentence, in `markdown-to-adf.test.ts`, `document.test.ts` and `html-to-adf.test.ts`.** | 3 | 1 | 1 | 3 | 6 | code health by 3 | 4.4 | -| 173 | 0.4.0 | | principle | **Keep each description `commonMarkSpellings` gives a refused block beside the emitter's spelling it describes.** | 2 | 1 | 1 | 2 | 4 | code health by 1 | 4.4 | +| 173 | 0.4.0 | | principle | **Move each CommonMark spelling's description out of `commonMarkSpellings`, beside the emitter's spelling it describes.** | 2 | 1 | 1 | 2 | 4 | code health by 1 | 4.4 | | 81 | 0.4.0 | | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 1 | 1 | 1 | 2 | 3 by 2 | 4.2 | | 147 | 0.4.0 | | | **Ship `adfToPlainText`, writing a document's content with no markup, under a README goal for plain text.** | 2 | 2 | 1 | 3 | 4 | 3 by 4 | 4.2 | | 141 | 0.4.0 | | decision | **Write `strike` as HTML in `adfToHtml` and read it back in `htmlToAdf`, in place of the carry element.** | 1 | 1 | 1 | 1 | 2 | 6 by 2 | 4.0 | @@ -36,7 +36,7 @@ | 136 | 0.4.0 | | | **Write underline, text and background colours, subscript, superscript, alignment and indentation as HTML in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.** | 2 | 2 | 2 | 4 | 4 | 6 by 2 | 2.0 | | 134 | 0.4.0 | | decision | **Write panels, expands, task lists and decision lists as semantic HTML in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.** | 2 | 2 | 2 | 4 | 5 | 6 by 3 | 1.8 | | 46 | 0.4.0 | 134, 135, 136 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 2 | 1 | 3 | 4 | 3 by 3 | 3.2 | -| 174 | 0.4.0 | | defect | **Bound the passes `trimmedEdges` takes by a constant, so a line of alternating code and emphasised blanks trims in linear time.** | 3 | 1 | 2 | 4 | 7 | 9 by 3 | 2.3 | +| 174 | 0.4.0 | | defect | **Bound the passes `trimmedEdges` takes by a constant, so a line of alternating code and emphasized blanks trims in linear time.** | 3 | 1 | 2 | 4 | 7 | 9 by 3 | 2.3 | | 51 | 0.4.0 | | | **Match a reference label to its definition under Unicode case folding.** | 2 | 1 | 2 | 3 | 4 | 4 by 2 | 2.1 | | 148 | 0.4.0 | | | **Ship conversion between ADF and Jira wiki markup both ways, under a README goal for wiki markup.** | 3 | 2 | 2 | 5 | 7 | 3 by 5 | 0.9 | | 149 | 0.4.0 | | | **Read any HTML into ADF, in a call or option a developer panel picks, under a README goal for foreign HTML.** | 3 | 2 | 2 | 5 | 7 | 3 by 5 | 0.9 | @@ -216,22 +216,23 @@ document's whole tree. Give the node's path, and name its type and the attribute ### 171. Assert the facts an error message must carry, never its whole sentence, in `markdown-to-adf.test.ts`, `document.test.ts` and `html-to-adf.test.ts`. -README → Audience says a message is free text that may change in any release, yet `content()` in +README → Audience says no persona may rely on a message's wording, yet `content()` in `src/markdown/parse/markdown-to-adf.test.ts` folds a failure into `${code}: ${message}`, about 103 assertions compare that whole sentence, and `document.test.ts` (7) and `html-to-adf.test.ts` (3) pin `message:` the same way. A rewording fails tests with no behaviour changed, while the facts §`message` and `path` requires (the violation, the escape named) are never asserted on their own. -Assert `code`, `path`, `position` and `assert.match(message, …)` on the facts the text carries. +Assert `code`, `path` and `position` exactly, and match `message` against the facts it must carry. Technical principle: "Pin wording only where the wording is the contract". Found by the principles audit of the release review of 0.3.0, 2026-11-08. -### 173. Keep each description `commonMarkSpellings` gives a refused block beside the emitter's spelling it describes. +### 173. Move each CommonMark spelling's description out of `commonMarkSpellings`, beside the emitter's spelling it describes. `commonMarkSpellings` (`src/markdown/parse/markdown-to-adf.ts`) restates in prose the spellings `spellReadableBlock` (`src/markdown/emit/adf-to-markdown.ts`) owns, such as "each item behind `- `, or behind `* ` where …". A changed spelling leaves the refusal naming the old one, and a node missing from the map is refused naming no spelling. Put each description beside the spelling and -export it through `emit/spelling-questions.ts`, the one import `parse/` may take from `emit/`. +export it through `emit/spelling-questions.ts`, the one import `parse/` may take from `emit/`, and +drop the comment at `markdown-to-adf.ts:145` that keeps them apart. Technical principle: DRY. Found by the principles audit of the release review of 0.3.0, 2026-11-08. ### 81. Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds. @@ -284,11 +285,11 @@ or report the unminified gzip). The figure lands in README §The package beside dependencies" claim, and in a size column of the comparison table. Measured today, unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped 45.6 kB. -### 174. Bound the passes `trimmedEdges` takes by a constant, so a line of alternating code and emphasised blanks trims in linear time. +### 174. Bound the passes `trimmedEdges` takes by a constant, so a line of alternating code and emphasized blanks trims in linear time. `trimmedEdges` (`src/markdown/portable/inline-reduction.ts`) repeats a pass until nothing changes, and each pass costs O(leaves). Leaves alternating `' '` marked code with `' '` marked em take -floor(n/4) + 2 passes: 26, 251 and 2 501 at n = 100, 1 000 and 10 000, so the trim is quadratic, +ceil(n/4) + 1 passes: 26, 251 and 2 501 at n = 100, 1 000 and 10 000, so the trim is quadratic, against goal 9. The same holds for `'x'` code beside `' '` em, and for `[link, code] 'x'` beside `[link, em] ' '`. The smallest line is six leaves `' '` marked code, em, code, em, code, em, which takes 3 passes. The passes in a random search of 20 000 short lines never went above 4. @@ -299,9 +300,9 @@ pass strips em from those two, and the next pair is exposed. A single pass canno directly: every pass judges all leaves against one snapshot, and `edgeOutcome` (drop at a line edge while a run is shared, keep when not) isn't monotone. A hard break between `[link, em] ' '` and `[link, em] ' b'` trims to `b` today, but to a linked space and `b` if the first leaf's drop is seen -first. Three ways forward: make the edge rule monotone, which changes output in edge cases and +first. Two ways forward: make the edge rule monotone, which changes output in edge cases and needs a decision and a writer panel; keep run extents under truncation in `runsSharedWithNext` -(`emit/inline-runs.ts`), exact but large; or state the cost as an exception under goal 9. Count passes +(`emit/inline-runs.ts`), exact but large. Count passes to measure, never time (§Cost fixes are measured, never timed). Related: item 42 trims blanks inside one leaf. Found by the principles audit of the release review of 0.3.0, 2026-11-08. @@ -338,7 +339,7 @@ choice. Split into items once the API is picked. 279, 771, 917, 926, 962 and 1118 of `src/markdown/parse/markdown-to-adf.test.ts`; the most are in `adf-schema.test.ts` (18), `limits.test.ts` (12) and `portable-markdown-to-adf.test.ts` (10). The first failing reader stops the test and the failure never names the reader. Write -`for (const read of readers) test(`… in ${read.name}`, …)` at the top level. Technical principle: +``for (const read of readers) test(`… in ${read.name}`, …)`` at the top level. Technical principle: "No loops or branches in a test body; parametrize the cases". Found by the principles audit of the release review of 0.3.0, 2026-11-08.