diff --git a/CHANGELOG.md b/CHANGELOG.md index 41ac55d..1f18b1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,7 @@ ## Unreleased - **Breaking:** `markdownToAdf` and `plainMarkdownToAdf` refuse a value that is not a string with - the new code `not-a-string`, at position `{ line: 1, offset: 0 }`. Before, `null`, `undefined` - and a non-empty array threw a `TypeError`, a `String` object converted its text, and a number - read as an empty document; pass `String(value)` where a boxed string arrives. A `switch` over - `ConvertErrorCode` naming every code needs the new case. + the new code `not-a-string`; `MIGRATION.md` lists what each value did before. - **Breaking:** an attribute value that is not a plain object, array or JSON primitive, such as a `Date`, `Map`, typed array or class instance, makes `isAdfDocument` return `false` and the converters refuse it as `not-an-adf-document`. Before, the converters wrote it as `{}` or its own diff --git a/MIGRATION.md b/MIGRATION.md index 7bdbfeb..3329cd6 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -62,8 +62,9 @@ Markdown the spelling table leaves alone, which `0.2.0` reads as a different doc ### Error codes -`unspellable-character`, `unspellable-line-start` and `unspellable-link` leave `ConvertErrorCode`: -a `switch` naming one stops compiling. A document `adfToMarkdown` refused with one of them now +`unspellable-character`, `unspellable-line-start` and `unspellable-link` leave `ConvertErrorCode` +and `not-a-string` joins it: a `switch` naming one of the three stops compiling, and one naming +every code needs the new case. A document `adfToMarkdown` refused with one of them now converts; markdown the parser refused with one of them now refuses with the code the table gives. | Input | `0.1.0` | `0.2.0` | @@ -83,6 +84,4 @@ converts; markdown the parser refused with one of them now refuses with the code | an empty node the `::taskItem` spelling row names, written with a closer | `unsupported-node-shape` | parses | | `null`, `undefined` or a non-empty array, on parse | throws a `TypeError` | `not-a-string` | | a `String` object, on parse | the document its text spells | `not-a-string`; pass `String(value)` | -| a number, a boolean, a plain object or an empty array, on parse | an empty document | `not-a-string` | - -`ConvertErrorCode` gains `not-a-string`: a `switch` naming every code needs its case. +| any other value that is not a string, such as a number or `{}`, on parse | an empty document | `not-a-string` | diff --git a/README.md b/README.md index 1a81d9d..c300b30 100644 --- a/README.md +++ b/README.md @@ -164,9 +164,9 @@ carries it opaquely and restores it unchanged ([`docs/decisions.md`](https://git `ConvertError` is `{ code, message, path, position? }`. `code` is the exported `ConvertErrorCode`, stable across minors and safe to `switch` on exhaustively with no `default`; `message` is free text -and may change in any release. A parse always names a position, so `markdownToAdf` returns -`ParseError` and its `position` reads without a guard; an emit reads no source and carries `path` -alone; one handler typed on `ConvertError` takes both, which is what the composed `markdownToHtml` +and may change in any release. A parse always names a position, so `markdownToAdf` and +`plainMarkdownToAdf` return `ParseError`, whose `position` reads without a guard; an emit reads no +source and carries `path` alone; one handler typed on `ConvertError` takes both, which is what the composed `markdownToHtml` and `htmlToMarkdown` hand back. `path` is the node's place from the document root, alternating `'content'` and an index, so `path.map((step) => '/' + step).join('')` is a JSON Pointer at the node — the empty path being the document itself. @@ -178,8 +178,7 @@ the identical error: fix the input, or set the document aside. `position` is `{ line, offset }` into the string passed in: `line` counted from 1, `offset` a UTF-16 code unit, a JavaScript string index rather than a codepoint or a byte offset. It points at or before the refusal — currently the start of the line the enclosing block begins on; a later -minor may narrow that, never widen it. `not-a-string`, having no string to point into, names -`{ line: 1, offset: 0 }`. +minor may narrow that, never widen it. For `not-a-string` it is `{ line: 1, offset: 0 }`. Parsing — `markdownToAdf` and `plainMarkdownToAdf`, and `htmlToAdf` at `0.2.0`: @@ -187,7 +186,7 @@ Parsing — `markdownToAdf` and `plainMarkdownToAdf`, and `htmlToAdf` at `0.2.0` | --- | --- | --- | | `malformed-directive` | an `!adf:` the grammar cannot read — a prefix completing no directive, an unclosed container, `[content]` or `{attrs}`, a closer with no container of its name open, a leaf given a body, `{attrs}` out of order or duplicated, invalid JSON in an opaque carry | write the spelling the message names, or keep it literal: escape the prefix — `\!adf:`, block and inline alike — or, for a code fence whose info string opens `adf:`, drop the info string and wrap the fence in `!adf:codeBlock {language="adf:…"}` | | `malformed-pipe-table` | a pipe row that is no pipe table — a missing or ragged `---` delimiter row, an alignment colon in it, or a row not opening with a pipe | open every row with a pipe and give the delimiter row the header's cell count; to keep the lines literal text instead, escape the leading pipe of every one — escaping a single row leaves the next to open a fresh table and fail the same way | -| `not-a-string` | the value handed in is not a string: `null`, `undefined`, a number, an object, a `String` object among them | pass the input as a string, `String(value)` for a `String` object; the message names the type passed | +| `not-a-string` | the value handed in is not a string, such as `null`, a number or a `String` object | pass the input as a string, `String(value)` for a `String` object; the message names the value's `typeof` | | `unknown-directive-name` | a directive whose name is no node or mark this version spells | check the name in `spec/flavour.md`, or escape the prefix as `\!adf:`; the spelling itself is well formed, so a later minor may give the name meaning | | `unmappable-html` | the input holds an HTML construct the documented element set does not map, a comment and a processing instruction among them — at this version that is every raw HTML construct in markdown, the element set landing at `0.2.0` | remove the construct, or write what it holds in the lossless flavour | | `unmappable-image` | an image sits inside other content that is not another image's description, or carries a title | give the image a paragraph of its own and drop the title | diff --git a/docs/decisions.md b/docs/decisions.md index 1ed222b..4fdaa7f 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -383,9 +383,9 @@ handles one cause alike whichever node, attribute or direction raised it. knows which direction it called and parting them after `0.1.0` is MAJOR (2026-09-23). - A non-finite number takes two codes: `unsupported-node-shape` parsing, `not-an-adf-document` emitting — no document holds one, so no round-trip crosses them (2026-09-23). -- A value handed to a reader of markdown or HTML that is not a string is `not-a-string`, its - position `{ line: 1, offset: 0 }` and its path empty: no code read true of it, and - `not-an-adf-document` names the other direction's input (2026-10-05). +- `markdownToAdf`, `plainMarkdownToAdf` and `htmlToAdf` refuse a value that is not a string as + `not-a-string`, at position `{ line: 1, offset: 0 }` with an empty path: no existing code reads + true of it, and `not-an-adf-document` names the emitters' input (2026-10-05). ## `message` and `path` diff --git a/src/markdown/emit/line-escaping.ts b/src/markdown/emit/line-escaping.ts index ba6014a..1f855c9 100644 --- a/src/markdown/emit/line-escaping.ts +++ b/src/markdown/emit/line-escaping.ts @@ -11,7 +11,7 @@ import { readEntityReference } from '../commonmark/entity-references.ts' export type DelimiterRole = 'close' | 'open' -// A segment's kind: `backslash` text, `bracketed` text inside `[…]` (link text, alt, a directive's content), `bracketed-link-target` a link target inside `[…]`, `none` syntax written as is. +// A segment's kind: `backslash` text, `bracketed` text inside `[…]` (link text, alt, a directive's content), `bracketed-link-target` syntax: a link target inside `[…]`, `none` syntax written as is. export type InlineEscaping = 'backslash' | 'bracketed' | 'bracketed-link-target' | 'none' export type NodeRange = { first: number; last: number } diff --git a/src/markdown/parse/inline-content.ts b/src/markdown/parse/inline-content.ts index 4eba6d9..73c7e1f 100644 --- a/src/markdown/parse/inline-content.ts +++ b/src/markdown/parse/inline-content.ts @@ -344,7 +344,7 @@ function holdsImage(pieces: readonly Piece[]): boolean { return pieces.some((piece) => piece.kind === 'image') } -// An `ownMarks` piece restores its link with its node, so only a link a `nodes` piece spells counts. +// Only a link a `nodes` piece spells makes the outer brackets literal; an `ownMarks` node's link is its own, so the own-marks guard in `closeLink` refuses it. function holdsLink(pieces: readonly Piece[]): boolean { return pieces.some((piece) => piece.kind === 'nodes' && marksLink(piece.nodes)) } @@ -647,7 +647,7 @@ function markType(character: string, used: number): string { return used === 2 ? 'strong' : 'em' } -// Nested spellings of one mark (`*(*a*)*`) name it once: a node carries one mark of a type. +// `*(*a*)*` builds one `em`: a node holds at most one mark of each type. function applyMark(nodes: readonly Inline[], mark: AdfMark): Inline[] { return nodes.map((node) => { if (!isNode(node)) return node diff --git a/src/markdown/plain/inline-reduction.ts b/src/markdown/plain/inline-reduction.ts index 2647966..cd3220a 100644 --- a/src/markdown/plain/inline-reduction.ts +++ b/src/markdown/plain/inline-reduction.ts @@ -237,7 +237,7 @@ function leafEdges(leaf: AdfNode, previous: AdfNode | undefined, next: AdfNode | return edges } -// How many of the leaf's marks the edge whitespace keeps, `undefined` where it goes at a line edge. +// How many of the leaf's marks the edge whitespace keeps, `undefined` where it drops at a line edge. function edgeDepth(marks: readonly AdfMark[], neighbour: AdfNode | undefined, whitespace: string): number | undefined { if (whitespace === '') return marks.length const lineEdge = neighbour === undefined || neighbour.type === 'hardBreak' diff --git a/todo.md b/todo.md index b4eb752..326dee5 100644 --- a/todo.md +++ b/todo.md @@ -35,7 +35,7 @@ | 68 | 0.2.0 | | **Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.** | 3 | 3 | 4 | 7 | 1 | 18.2 | | 75 | 0.2.0 | | **Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.** | 2 | 2 | 3 | 7 | 1 | 17.5 | | 65 | 0.2.0 | defect | **Read an image not alone in its paragraph as its alt text linked to its URL, and a titled image alone in its paragraph as the image captioned with its title.** | 5 | 5 | 7 | 7 | 4, 5, 7 | 17.1 | -| 81 | 0.2.0 | | **Fail the gate when the README's error tables list other codes than `ConvertErrorCode`.** | 1 | 2 | 5 | 5 | 3 | 16.3 | +| 81 | 0.2.0 | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 2 | 5 | 5 | 3 | 16.3 | | 49 | 0.2.0 | | **Read a list whose bullet or ordered delimiter changes as two lists, in both readers, and retire `!adf:listBreak`.** | 4 | 5 | 5 | 8 | 4, 5 | 15.5 | | 46 | 0.2.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 5 | 5 | 3, 8 | 14.8 | | 61 | 0.2.0 | decision | **Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it.** | 6 | 8 | 3 | 8 | 1 | 14.0 | @@ -166,7 +166,7 @@ its paragraph, it reads as the image with a `link` mark to `/uri`, which ADF's ` alone in its paragraph, the image reads as its alt text linked to `/uri`, and its URL drops. A writer panel chose this reading 3 of 3 (2026-10-04), by Goal 6, which outranks Goal 7. -### 81. Fail the gate when the README's error tables list other codes than `ConvertErrorCode`. +### 81. Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds. `src/result.test.ts` checks the union against the call sites only, so a code added or removed leaves the README's tables a false contract under a green gate. Assert that the tables list exactly @@ -336,11 +336,8 @@ agree. Found by the technical-principles audit, 2026-10-04. ### 82. Give the directive tests and fixtures random UUID v7 ids in place of `a-1`, `a`, `x` and `h`. -Technical principle: tests use random UUID v7 ids. The placeholders sit in -`markdown-to-adf.test.ts`, `adf-to-markdown.test.ts`, `blocks.test.ts` and -`corpus/errors/directive-value-uncanonical.md`, so no lossless directive test reads the digit-led hex -id an editor writes; `plain-markdown-to-adf.test.ts` already uses v7s. Found by the -technical-principles audit of item 78, 2026-10-05. +Technical principle: tests use random UUID v7 ids; `plain-markdown-to-adf.test.ts` already does. +Found by the technical-principles audit of item 78, 2026-10-05. ### 57. Make each `ci.sh` leg build what it reads, so one leg run alone tests the current tree.