5b4: the README's consumer surface #47
@@ -25,38 +25,91 @@ Pure functions, no I/O, no configuration. ADF is the hub: markdown↔HTML compos
|
|||||||
```ts
|
```ts
|
||||||
adfToMarkdown(doc: AdfDocument): Result<string>
|
adfToMarkdown(doc: AdfDocument): Result<string>
|
||||||
markdownToAdf(markdown: string): Result<AdfDocument, ParseError>
|
markdownToAdf(markdown: string): Result<AdfDocument, ParseError>
|
||||||
adfToHtml(doc: AdfDocument): Result<string>
|
|
||||||
htmlToAdf(html: string): Result<AdfDocument, ParseError>
|
|
||||||
markdownToHtml(markdown: string): Result<string> // via ADF
|
|
||||||
htmlToMarkdown(html: string): Result<string> // via ADF
|
|
||||||
isAdfDocument(v: unknown): v is AdfDocument
|
isAdfDocument(v: unknown): v is AdfDocument
|
||||||
|
|
||||||
|
adfToHtml(doc: AdfDocument): Result<string> // 0.3.0
|
||||||
|
htmlToAdf(html: string): Result<AdfDocument, ParseError> // 0.3.0
|
||||||
|
markdownToHtml(markdown: string): Result<string> // 0.3.0, via ADF
|
||||||
|
htmlToMarkdown(html: string): Result<string> // 0.3.0, via ADF
|
||||||
```
|
```
|
||||||
|
|
||||||
`Result<T>` is `{ ok: true; value: T } | { ok: false; error: ConvertError }` — nothing throws.
|
`Result<T>` is `{ ok: true; value: T } | { ok: false; error: ConvertError }` — nothing throws.
|
||||||
`ConvertError` is `{ code, message, path, position? }`: a code from a closed set, the path of the
|
|
||||||
node it names from the document root, and — parsing — a `{ line, offset }` into the string passed
|
|
||||||
in, at the start of the line the refused block begins on, `line` counted from 1.
|
|
||||||
|
|
||||||
Parsing a source always names where in it the refusal sits, so `markdownToAdf` and `htmlToAdf`
|
## The errors
|
||||||
return the narrowed `ParseError`, whose `position` is there to read without a guard. Every other
|
|
||||||
direction emits, from a document with no source behind it, and carries `path` alone — including
|
An ADF node type this version does not know is not an error: it rides both formats opaquely and
|
||||||
`markdownToHtml` and `htmlToMarkdown`, where half the refusals come from the emit half.
|
restores unchanged (AGENTS.md §3).
|
||||||
|
|
||||||
|
`ConvertError` is `{ code, message, path, position? }`. `code` is 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 `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.
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
Parsing — `markdownToAdf`, and `htmlToAdf` at `0.3.0`:
|
||||||
|
|
||||||
|
| Code | Fires when | What you can do |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `malformed-directive` | a `:::` block or `:name[…]` inline directive the grammar cannot read — an unclosed fence or `[content]`, `{attrs}` out of order or duplicated, invalid JSON in an `adf` carry | write the spelling the message names, or escape the line — `\:::` for a block, `\:` for an inline one — to keep it literal text |
|
||||||
|
| `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; a backslash before a pipe keeps it literal text |
|
||||||
|
| `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 colon; the spelling itself is well formed, so a later minor may give the name meaning |
|
||||||
|
| `unmappable-html` | the markdown holds a raw HTML tag, comment or processing instruction | remove it or write it in the flavour — ADF holds no raw-HTML node, and the element mapping lands at `0.3.0` |
|
||||||
|
| `unmappable-image` | an image sits inside other content, or carries a title | give the image a paragraph of its own and drop the title, or write the `mediaSingle` directive form |
|
||||||
|
|
||||||
|
Emitting — `adfToMarkdown`, and `adfToHtml` at `0.3.0`:
|
||||||
|
|
||||||
|
| Code | Fires when | What you can do |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `not-an-adf-document` | the value handed in is no ADF document — a missing or wrong `type`, a stray key, a node that is not a node | guard the boundary you receive JSON at with `isAdfDocument`; the message names the branch that refused |
|
||||||
|
| `unsupported-document-version` | the document's `version` is not 1 | convert a version-1 document — no markdown spelling carries another |
|
||||||
|
|
||||||
|
Either direction — a parse reaches the emitter's own refusals too, asking it which CommonMark
|
||||||
|
spelling a node takes:
|
||||||
|
|
||||||
|
| Code | Fires when | What you can do |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `unspellable-character` | text or a code block holds a carriage return or a null character, which CommonMark rewrites wherever it sits | strip or replace the character; no escape carries it through the round-trip |
|
||||||
|
| `unspellable-line-start` | a paragraph line begins with a code span whose backticks would read back as a code fence | put any text before the code span |
|
||||||
|
| `unspellable-link` | a link `href` or `title` holds what no canonical escape spells — a backslash, a newline, a control character, an entity reference, an angle bracket beside a space | percent-encode the destination (`%5C` for the backslash, `%26` for the `&` that opens the entity), or drop the title |
|
||||||
|
| `unspellable-whitespace` | an `emoji`, `mention` or `status` holds a newline in the text its inline directive spells in the content slot | replace it with a space — an inline directive never spans lines |
|
||||||
|
| `unsupported-nesting-depth` | blocks, marks or a carried node's JSON nest past 500 levels | flatten the document; the limit is fixed, and it is what stands between a deep document and a stack overflow |
|
||||||
|
| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take — or markdown writes as a directive a node the flavour spells as CommonMark | write the shape the message names; `spec/flavour.md` lists every type's attributes and body |
|
||||||
|
|
||||||
## The guarantees
|
## The guarantees
|
||||||
|
|
||||||
- `markdownToAdf(adfToMarkdown(doc))` equals `doc` — unknown node types included, carried opaquely
|
- `markdownToAdf(adfToMarkdown(doc))` equals `doc` — unknown node types included, carried opaquely
|
||||||
(AGENTS.md §3).
|
(AGENTS.md §3).
|
||||||
- `htmlToAdf(adfToHtml(doc))` equals `doc` — fidelity HTML cannot express rides `data-*`
|
- Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML below, with three
|
||||||
attributes.
|
carve-outs — literal text matching directive, pipe-table or strikethrough syntax is claimed
|
||||||
- Plain CommonMark is valid input to `markdownToAdf`, with three carve-outs — literal text
|
(escapable — `spec/flavour.md`) — and one gap: a CommonMark image fits only as its own
|
||||||
matching directive, pipe-table or strikethrough syntax is claimed (escapable —
|
title-less paragraph; mid-text and titled images are error results. Converting back yields the
|
||||||
`spec/flavour.md`) — and one gap: a CommonMark image fits only as its own title-less paragraph;
|
library's canonical spelling, which round-trips byte-identically.
|
||||||
mid-text and titled images are error results. Converting back yields the library's canonical
|
- Raw HTML in markdown input is an error result, never a silent drop — a tag, a comment and a
|
||||||
spelling, which round-trips byte-identically.
|
processing instruction alike. ADF holds no raw-HTML node; the element mapping ships at `0.3.0`.
|
||||||
- Foreign HTML maps a documented element set; an unmappable element is an error, never a silent
|
- Not every document converts back: `adfToMarkdown` is partial on valid ADF — a text node holding
|
||||||
drop. Well-formed HTML only — no tag-soup recovery.
|
a carriage return, a link destination or title no canonical escape spells, a paragraph line
|
||||||
|
beginning with a code span whose backticks read back as a fence. Show the refusal and keep the
|
||||||
|
document read-only; saving markdown you could not produce is the loss the round-trip exists to
|
||||||
|
stop.
|
||||||
|
- The pipe table narrows GFM's twice: every row opens with a pipe, so GFM's bare form is an error
|
||||||
|
result rather than the prose it reads as, and an alignment colon in the delimiter row is an
|
||||||
|
error too — ADF holds no column alignment. The trailing pipe is canonical output, optional in
|
||||||
|
input.
|
||||||
|
- Past that and `~~`, no GFM: an autolink literal and a `- [ ]` marker stay text, and a checklist
|
||||||
|
is the `taskList` directive.
|
||||||
- A document nested deeper than 500 levels is an error result, not a stack overflow.
|
- A document nested deeper than 500 levels is an error result, not a stack overflow.
|
||||||
- The emitted formats are semver surface (AGENTS.md §8).
|
- The emitted formats are semver surface (AGENTS.md §8).
|
||||||
|
- **`0.3.0`** — `htmlToAdf(adfToHtml(doc))` equals `doc`; fidelity HTML cannot express rides
|
||||||
|
`data-*` attributes. Foreign HTML maps a documented element set, an unmappable element is an
|
||||||
|
error, and well-formed HTML only — no tag-soup recovery.
|
||||||
|
|
||||||
## Who it is for
|
## Who it is for
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -1,9 +1,10 @@
|
|||||||
# The markdown flavour
|
# The markdown flavour
|
||||||
|
|
||||||
The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain
|
The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain
|
||||||
CommonMark is a subset with three carve-outs: literal text that matches directive syntax below or
|
CommonMark is a subset apart from raw HTML (below), with three carve-outs: literal text that
|
||||||
reads as a pipe table is claimed by the flavour, and a matched `~~` pair spells `strike` (escape the
|
matches directive syntax below or reads as a pipe table is claimed by the flavour, and a matched
|
||||||
`:`, `|` or `~` to keep it literal) — and one gap: a CommonMark image fits only as its own
|
`~~` pair spells `strike` (escape the `:`, `|` or `~` to keep it literal) — and one gap: a
|
||||||
|
CommonMark image fits only as its own
|
||||||
title-less paragraph — mid-text and titled images are named errors. The emitted form is contract
|
title-less paragraph — mid-text and titled images are named errors. The emitted form is contract
|
||||||
(AGENTS.md §8). Per-node syntaxes build on this grammar in the sections below.
|
(AGENTS.md §8). Per-node syntaxes build on this grammar in the sections below.
|
||||||
|
|
||||||
|
|||||||
@@ -392,6 +392,11 @@ Under **3 — `markdownToAdf` (`0.1.0`)**:
|
|||||||
**Settled** (the maintainer, 2026-09-01): ADF's own `A` is "Atlassian", and "converter" is
|
**Settled** (the maintainer, 2026-09-01): ADF's own `A` is "Atlassian", and "converter" is
|
||||||
the one-way lossy tool §2 exists to replace, where a codec is both directions. It names the
|
the one-way lossy tool §2 exists to replace, where a codec is both directions. It names the
|
||||||
hub, not the formats around it.
|
hub, not the formats around it.
|
||||||
|
- [x] **5b — The consumer's error surface (`0.1.0`).** A product-owner read of the public surface
|
||||||
|
found the error result legible to the library and opaque to the consumer holding it, and the
|
||||||
|
README documenting no part of it. The sub-items are that read's answers, and they land before
|
||||||
|
5 because §8 freezes the code list at `0.1.0` and 5b4's table is what reads the list before
|
||||||
|
the freeze closes it.
|
||||||
- [x] **5b1 — The error's source position.** A parse error names an ADF path into a document the
|
- [x] **5b1 — The error's source position.** A parse error names an ADF path into a document the
|
||||||
caller does not hold yet — `unmappable-html` at `["content", 5]` for a `<span>` on line
|
caller does not hold yet — `unmappable-html` at `["content", 5]` for a `<span>` on line
|
||||||
12 — and no coordinate into the markdown string it passed in. `ConvertError` gains an
|
12 — and no coordinate into the markdown string it passed in. `ConvertError` gains an
|
||||||
@@ -455,3 +460,25 @@ Under **3 — `markdownToAdf` (`0.1.0`)**:
|
|||||||
so the emitter escapes that line's first character rather than refusing the document.
|
so the emitter escapes that line's first character rather than refusing the document.
|
||||||
`spec/flavour.md` had two directive blocks inside a container taking no blank line; the
|
`spec/flavour.md` had two directive blocks inside a container taking no blank line; the
|
||||||
rule both directions keep is that a pair holding one takes none.
|
rule both directions keep is that a pair holding one takes none.
|
||||||
|
- [x] **5b4 — The README's consumer surface.** §8 invites an exhaustive switch on `code` and no
|
||||||
|
code name appears in the README, so it gains a table — code, when it fires, what the
|
||||||
|
consumer does — grouped by direction, over the thirteen names 5b3 settled. Four things a
|
||||||
|
reader who has not opened the code cannot know: raw
|
||||||
|
HTML is core CommonMark and every construct in input is an error until `0.3.0`, which the
|
||||||
|
guarantees' "three carve-outs and one gap" denies and which is the bot and LLM personas'
|
||||||
|
most common failure; `adfToHtml`, `htmlToAdf`, `markdownToHtml` and `htmlToMarkdown` sit
|
||||||
|
unmarked in the code block people copy from, as do the two HTML guarantee bullets, and take
|
||||||
|
a `0.3.0` mark or leave the block; `adfToMarkdown` is partial on valid ADF — a text node
|
||||||
|
holding a carriage return, a link destination no canonical escape spells — which the viewer
|
||||||
|
persona needs told along with
|
||||||
|
what to do about it; and GFM past tables and strikethrough is literal text, task lists
|
||||||
|
taking `:::taskList`. One sentence for the LLM persona: `code` is stable across minors,
|
||||||
|
`message` is free text. The type-level surface freezes at the same moment and gets the same
|
||||||
|
read: what `index.ts` exports and what it withholds, `ParseError` against `ConvertError`
|
||||||
|
where a direction reads a source, and `ConvertFault` staying internal — the README table
|
||||||
|
names the shapes a consumer switches on, so the two audits are one.
|
||||||
|
The direction grouping is read off the call sites rather than the code prefixes, which do
|
||||||
|
not partition by direction: the parser asks the emitter which CommonMark spelling a node
|
||||||
|
takes (§11), so six codes reach a `markdownToAdf` caller as well as an `adfToMarkdown` one.
|
||||||
|
The trailing pipe of a pipe-table row is optional in input, not required; the leading one
|
||||||
|
is what every row must carry.
|
||||||
|
|||||||
@@ -140,31 +140,11 @@ The numbering is the order the work was planned in, not the order it ships.
|
|||||||
`0.1.0` keeps — 3e names three shapes that parse and then refuse — so the release narrows
|
`0.1.0` keeps — 3e names three shapes that parse and then refuse — so the release narrows
|
||||||
that sentence or lists them.
|
that sentence or lists them.
|
||||||
- [x] **5a — Rename to `@larvit/adf-codec`.**
|
- [x] **5a — Rename to `@larvit/adf-codec`.**
|
||||||
- [ ] **5b — The consumer's error surface (`0.1.0`).** A product-owner read of the public surface
|
- [x] **5b — The consumer's error surface.**
|
||||||
found the error result legible to the library and opaque to the consumer holding it, and the
|
|
||||||
README documenting no part of it. The sub-items are that read's answers, and they land before
|
|
||||||
5 because §8 freezes the code list at `0.1.0` and 5b4's table is what reads the list before
|
|
||||||
the freeze closes it.
|
|
||||||
- [x] **5b1 — The error's source position.**
|
- [x] **5b1 — The error's source position.**
|
||||||
- [x] **5b2 — The error messages.**
|
- [x] **5b2 — The error messages.**
|
||||||
- [x] **5b3 — The code list and the flavour's gaps.**
|
- [x] **5b3 — The code list and the flavour's gaps.**
|
||||||
- [ ] **5b4 — The README's consumer surface.** §8 invites an exhaustive switch on `code` and no
|
- [x] **5b4 — The README's consumer surface.**
|
||||||
code name appears in the README, so it gains a table — code, when it fires, what the
|
|
||||||
consumer does — grouped by direction, over the thirteen names 5b3 settled. Four things a
|
|
||||||
reader who has not opened the code cannot know: raw
|
|
||||||
HTML is core CommonMark and every construct in input is an error until `0.3.0`, which the
|
|
||||||
guarantees' "three carve-outs and one gap" denies and which is the bot and LLM personas'
|
|
||||||
most common failure; `adfToHtml`, `htmlToAdf`, `markdownToHtml` and `htmlToMarkdown` sit
|
|
||||||
unmarked in the code block people copy from, as do the two HTML guarantee bullets, and take
|
|
||||||
a `0.3.0` mark or leave the block; `adfToMarkdown` is partial on valid ADF — a text node
|
|
||||||
holding a carriage return, a link destination no canonical escape spells — which the viewer
|
|
||||||
persona needs told along with
|
|
||||||
what to do about it; and GFM past tables and strikethrough is literal text, task lists
|
|
||||||
taking `:::taskList`. One sentence for the LLM persona: `code` is stable across minors,
|
|
||||||
`message` is free text. The type-level surface freezes at the same moment and gets the same
|
|
||||||
read: what `index.ts` exports and what it withholds, `ParseError` against `ConvertError`
|
|
||||||
where a direction reads a source, and `ConvertFault` staying internal — the README table
|
|
||||||
names the shapes a consumer switches on, so the two audits are one.
|
|
||||||
- [ ] **6 — The HTML dialect spec (`0.3.0`).** Element-by-element mapping, the `data-*` fidelity
|
- [ ] **6 — The HTML dialect spec (`0.3.0`).** Element-by-element mapping, the `data-*` fidelity
|
||||||
scheme, the opaque-carry form, and the documented foreign-element set `htmlToAdf` accepts.
|
scheme, the opaque-carry form, and the documented foreign-element set `htmlToAdf` accepts.
|
||||||
- [ ] **7 — HTML, ship `0.3.0`.** `adfToHtml`, `htmlToAdf`, the composed `markdownToHtml` /
|
- [ ] **7 — HTML, ship `0.3.0`.** `adfToHtml`, `htmlToAdf`, the composed `markdownToHtml` /
|
||||||
|
|||||||
Reference in New Issue
Block a user