Goals become one-line aims, their detail moves to the sections that meet them
CI / gate (push) Successful in 1m43s
CI / publish (push) Has been skipped

This commit is contained in:
2026-10-02 17:34:17 +02:00
parent e949f2099c
commit 94eaee6bae
5 changed files with 83 additions and 92 deletions
+26 -44
View File
@@ -23,47 +23,21 @@ represent.
## Goals
In priority order.
The most useful ADF conversion library available, by these goals in priority order:
1. **Lossless, and every call returns.** The round-trip holds for every document the lossless
conversions take, node types this version does not know included; one that has no spelling is
refused and says where, never silently reduced. No input makes a call loop forever or overflow
the stack. Every goal below gives way to this one.
2. **ADF is the hub.** Every format and flavour converts to and from ADF, and no two others
convert directly: markdown↔HTML composes through ADF. Adding a format or flavour costs one
reader and one writer. A flavour of a grammar shares that grammar's reader and writer and adds
only its own spellings.
3. **Plain CommonMark is input.** Markdown written for something else converts — the exceptions
below are the whole of them — and every spelling a flavour claims on top of CommonMark is
escapable, so each flavour is opt-in.
4. **Output a person can edit.** A node CommonMark can spell gets that spelling; the lossless
flavour's directive form carries only what CommonMark cannot hold.
5. **Lossy conversion keeps the content.** `adfToPlainMarkdown` and `plainMarkdownToAdf` drop what
plain markdown cannot hold — format, design, structure — never content the document holds: what
a reader of the rendered document sees or follows, its text, images and link targets. The lossy
pair creates and exports; it never saves back over the document it read — a document's identity
(task, mention, media ids) survives a round trip only through the lossless pair.
6. **What happens is what the audience expects.** Where the goals leave a choice, a conversion
takes the one its audience would predict, reading the input as written.
7. **Failures are values.** Nothing throws, and `code` is a closed list — as much a contract as
the emitted formats are.
8. **Nothing in the way.** No runtime dependencies, no I/O, no configuration, no host API: ESM on
any ES2022 engine, in a browser as readily as on a server, installed from public npm. The public
surface is the conversions, their types, `isAdfDocument`, and what a consumer needs to check a
guarantee this README makes; a helper is exported only when a persona cannot do without it.
9. **Correct before fast.** Each format means what its own specification says — markdown as the
CommonMark spec reads it, well-formed HTML as the HTML standard parses it — both in what this
library reads and in what a conforming parser reads from what it writes. A call takes a whole
document and returns a whole result.
10. **Fast once correct.** Conversion time grows linearly with the document wherever the goals
above allow it; a faster path that risks one of them is not taken.
11. **Source a contributor can hold.** Any one function reads in one sitting, and no change makes
the longest one longer.
1. **Lossless, and every call returns a result, never a throw.**
2. **ADF is the hub.**
3. **Each format reads and writes as its standard says.**
4. **Our markdown is CommonMark, extended only where CommonMark has no spelling.**
5. **No surprises: output reads and edits the way its audience expects.**
6. **Lossy conversion drops form, never content.**
7. **Runs in any JavaScript engine, with nothing to install, configure or connect.**
8. **Fast, and linear in the document's size.**
9. **Easy to find, and clear at a glance what it does.**
## Audience
Application developers embedding the library, addressed as personas rather than named consumers
(AGENTS.md §1). All four rely on the guarantees below and on `code` being a closed list; none may
Application developers embedding the library, in four personas. All four rely on the guarantees below and on `code` being a closed list; none may
rely on an error message's wording, which is free text.
- **Viewer/editor app** — shows a document, lets a human edit, posts it back. Relies on the
@@ -94,7 +68,8 @@ if (result.ok) {
}
```
Pure functions, no I/O, no configuration.
Serves Goals 1 and 7. Pure functions, each taking a whole document and returning a whole result;
no I/O, no configuration.
```ts
adfToMarkdown(doc: AdfDocument): Result<string>
@@ -114,9 +89,10 @@ htmlToMarkdown(html: string): Result<string> // 0.2.0, via ADF
## Plain markdown
Plain markdown is a second flavour of the same grammar. `adfToPlainMarkdown` writes markdown other
tools render — GitHub, GitLab, Obsidian and the like — keeping the content and dropping the rest:
attributes, colours, layout, identity. It refuses only
Serves Goal 6. Plain markdown is a second flavour of the same grammar. `adfToPlainMarkdown` writes
markdown other tools render — GitHub, GitLab, Obsidian and the like — keeping the content and
dropping the rest: attributes, colours, layout, identity. Content is what a reader of the rendered
document sees or follows: its text, images and link targets. It refuses only
`not-an-adf-document`, `unsupported-document-version` and `unsupported-nesting-depth`, and writes
no directive.
@@ -167,7 +143,7 @@ read replaces mentions, attachments and macros with text.
## The errors
An ADF node type this version does not know is not an error: the lossless pair carries it opaquely
Serves Goal 1. An ADF node type this version does not know is not an error: the lossless pair carries it opaquely
and restores it unchanged ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)).
`ConvertError` is `{ code, message, path, position? }`. `code` is the exported `ConvertErrorCode`,
@@ -219,6 +195,11 @@ emit refuses:
## The guarantees
Serves Goals 1, 3 and 4.
- Markdown means what the CommonMark spec says, and well-formed HTML what the HTML standard
parses, both in what this library reads and in what a conforming parser reads from what it
writes; the bullets below name every exception.
- `markdownToAdf(adfToMarkdown(doc))` equals `doc` — unknown node types included, carried opaquely
([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)).
- Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML `unmappable-html`
@@ -247,7 +228,8 @@ emit refuses:
input.
- Past that and `~~`, no GFM: an autolink literal and a `- [ ]` marker stay text, and a checklist
is the `taskList` directive — `plainMarkdownToAdf` turns the marker into a `taskList`.
- 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, and no input
makes a call loop forever.
- The emitted formats are semver surface
([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#the-formats-are-api)).
- **`0.2.0`** — `htmlToAdf(adfToHtml(doc))` equals `doc`; fidelity HTML cannot express rides
@@ -257,7 +239,7 @@ emit refuses:
## The package
ESM only, no runtime dependencies, public npm. Built JavaScript with `.d.ts` beside it.
Serves Goal 7. ESM only, no runtime dependencies, public npm. Built JavaScript with `.d.ts` beside it.
Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs under Node,
Deno and Bun, and a headless Firefox converts the corpus through the built entrypoint.
Contract: [`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#any-es2022-engine), §Any