mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Answer prose round 1: 0 🔴, 14 🟡 (0 misread); correct comments that misstated the code, fix item 174's figures and tagline, retitle item 173
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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<AdfAttributes> {
|
||||
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<AdfAttributes> {
|
||||
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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<EmittedBlock> | 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<Record<'ask' | 'take', (node: AdfNode, depth: number, spell: Spell) => Result<EmittedBlock> | undefined>>
|
||||
type WalkedItem = { node: AdfNode; walk: PlacedBlocks }
|
||||
export type WrittenAs = { flavour: 'lossless' } | { blocks: FlavourBlockWriters; flavour: 'portable' }
|
||||
|
||||
@@ -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<Line, LineFallback, Written> = {
|
||||
giveUpEmphasis: (line: Line) => Line
|
||||
judge: (line: Line, grouping: RunGrouping) => Result<LineJudgement<Line, LineFallback, Written>>
|
||||
@@ -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, LineFallback, Written>(line: Line, action: FallbackAction<Line, LineFallback, Written>): Result<Written> {
|
||||
// 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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<readonly AdfNode[], PortableLineFallback, AdfNode[]>(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<AdfNode[]> {
|
||||
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<LineJudgement<readonly AdfNode[], PortableLineFallback, AdfNode[]>> {
|
||||
const line = trimmedEdges(leaves, grouping)
|
||||
const fallback = portableLineFallback(line, container, path, grouping)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user