36a - §1–§6's decisions move to docs/decisions.md, indexed from AGENTS.md
This commit is contained in:
@@ -1,106 +1,33 @@
|
||||
# Working in this repo
|
||||
|
||||
Decisions a reader would otherwise relitigate, and the rules for every collaborator, human or
|
||||
agent. Using the library: `README.md`. What is still to build: `todo.md`; a bare `(28)` cites
|
||||
that item's entry in `todo-history.md`.
|
||||
The rules for every collaborator, human or agent, and an index of the decisions a reader would
|
||||
otherwise relitigate. Using the library: `README.md`. What is still to build: `todo.md`; a bare
|
||||
`(28)` cites that item's entry in `todo-history.md`.
|
||||
|
||||
## 1. ADF is the hub
|
||||
## Decisions
|
||||
|
||||
README Goal 2. The lossy pair is the plain flavour: the markdown grammar's reader and writer with
|
||||
the flavour set, its spellings — alerts, callouts, task markers, `==` — read and written there, so
|
||||
a marker line and a backslash reach them intact; what the flavour cannot spell reduces ADF→ADF
|
||||
ahead of the writer (the maintainer, 2026-09-27; 35).
|
||||
In `docs/decisions.md`:
|
||||
|
||||
## 2. The round-trip is the product
|
||||
|
||||
`markdownToAdf(adfToMarkdown(doc))` and `htmlToAdf(adfToHtml(doc))` must equal `doc` — anything
|
||||
less silently destroys content an editor could not represent, in a document it did not author.
|
||||
When losslessness and readability conflict, losslessness wins.
|
||||
|
||||
The other direction is a canonical fixpoint, not byte-identity: human markdown normalizes, the way
|
||||
back yields the library's canonical spelling, and that spelling round-trips byte-identically —
|
||||
where there is a way back. CommonMark spells some things the flavour has no escape for — a
|
||||
paragraph opening with a code span whose backticks read back as a fence — so a parse succeeding
|
||||
does not imply a spellable document;
|
||||
`corpus/commonmark-spec/exceptions.json` names those.
|
||||
|
||||
"Equals" is structural equality over editor-normal ADF — adjacent text nodes with identical marks
|
||||
and no attributes merged, JSON number semantics, an empty attrs object, marks array or content
|
||||
array the absent key — the only domain markdown can restore.
|
||||
|
||||
Round-trip equality is a property tested over a corpus, not a claim made in prose.
|
||||
|
||||
## 3. Unknown input policy
|
||||
|
||||
- Unknown ADF node: carried opaquely — raw JSON rides a dedicated syntax in both formats and
|
||||
restores to a deep-equal node. The round-trip holds for documents newer than the library. So
|
||||
does a known node no section spells where it stands: a markdown serializer spells a node by type
|
||||
without checking its position, and refusing loses a document ADF itself keeps in an
|
||||
`unsupportedBlock`. Where a container's own spelling cannot hold the child it has — a
|
||||
`bulletList` outside `listItem`, a `codeBlock` outside text — the error result names that
|
||||
instead.
|
||||
- Unmappable foreign HTML element: error result naming the element — never a silent drop.
|
||||
- Bare `@name` / `:smile:` in typed text: stays a text node. Only directives produce
|
||||
mention/emoji/media nodes; resolving names to ids needs I/O, which is the consumer's job.
|
||||
|
||||
## 4. The flavour
|
||||
|
||||
- Directives, one grammar for everything markdown lacks, namespaced under `!adf:`: `!adf:panel info`
|
||||
… `!adf:/panel` blocks, `!adf:mention[@Mikael]{id=5b10a2}` inline, `\!adf:` the one escape. Not
|
||||
CommonMark's generic-directives proposal: its `:::` claims a form prose writes, and its
|
||||
fence-length discipline ties a container's opener to its own body, where closing from the opener
|
||||
nests by itself and leaf versus container falls out of the node's content model.
|
||||
- Plain CommonMark is a subset, with carve-outs (`spec/flavour.md`): literal text shaped like a
|
||||
directive, a pipe table or a `~~` pair is claimed — plus one image gap.
|
||||
- Tables: one header row plus plain inline cells → pipe table; anything richer → directive form.
|
||||
- Links: `[text](url "title")`, or `<url>` for a bare autolink-shaped text, wherever CommonMark
|
||||
spells the mark; `!adf:link[text]{attrs}` where it does not — an attribute CommonMark cannot
|
||||
hold, an `href` or `title` no canonical escape spells, a paragraph opening whose CommonMark
|
||||
spelling would read as a link reference definition — and a directive link CommonMark could spell
|
||||
is refused (the maintainer, 2026-09-13). No link wraps a link — the bracket form goes literal,
|
||||
the directive form refused — which is CommonMark's prose where its reference implementation
|
||||
nests one `<a>` in another (the maintainer, 2026-09-17).
|
||||
- Identity-bearing nodes carry their ids in attributes; a document is only portable within its
|
||||
site — accepted.
|
||||
- The HTML dialect mirrors this: semantic elements, stable `adf-*` classes, `data-*` for what HTML
|
||||
cannot express, text always escaped. No stylesheet ships.
|
||||
|
||||
## 5. Dependencies
|
||||
|
||||
`dependencies` is empty. A runtime dependency enters only through a decision entry here stating
|
||||
why ~20 lines of own code cannot do the job, who maintains it, and what auditing it costs. So the
|
||||
CommonMark and HTML parsers are written in this repo. A table a standard fixes is data rather than
|
||||
a dependency: HTML5's 2125 semicolon-terminated character references ship packed in their own
|
||||
module, so entity decoding is complete without one. The CommonMark spec suite is the same shape of
|
||||
data and ships vendored at `corpus/commonmark-spec/` rather than as the `commonmark-spec` dev
|
||||
dependency — that package is CommonJS-only, and Renovate auto-bumping a spec version would silently
|
||||
point the vendored exception list's example numbers at a renumbered suite. A spec bump is a
|
||||
deliberate re-pin, exceptions re-derived by hand beside it. Atlassian's ADF JSON Schemas ship
|
||||
vendored the same way, at `spec/adf-schema/`, rather than as the `@atlaskit/adf-schema` dev
|
||||
dependency — CommonJS-only, some fifty packages with React among them, and a release most days for
|
||||
Renovate to automerge — re-pinned by hand when a payload or a report shows the need.
|
||||
`devDependencies`: `fast-check` earns its place shrinking a failing generated document to the nodes
|
||||
that break it, `oxlint` measuring §10's size ratchet — TypeScript 7 is a native compiler publishing
|
||||
no in-process parser, only the `unstable/` AST surface an out-of-process handshake reaches.
|
||||
|
||||
## 6. The package contract
|
||||
|
||||
- Runs on any ES2022 engine, not only Node — a browser as readily as a server. The shipped source
|
||||
is ECMAScript and nothing else: no host import, no host global, no DOM. `tsconfig.build.json` is
|
||||
that gate, typechecking and emitting the shipped files alone, so `node:fs`, `process` and an
|
||||
ES2024 method are compile errors here rather than a consumer's crash there. The standard is the line, never an
|
||||
engine list: one implementing it in part — Hermes is the live doubt, on §10's property escapes
|
||||
and on lookbehind — is out of scope rather than a bug. Node's test runner, the corpus reads and
|
||||
the build are the repo's own,
|
||||
never the library's, and `engines.node` states the floor the shipped JavaScript needs — `>=18` —
|
||||
never the higher one those repo-only tools want.
|
||||
- ESM only — no CommonJS build, no dual-package hazard.
|
||||
- One entrypoint: built JavaScript, `.d.ts` beside it. Do not add a TypeScript-source entrypoint —
|
||||
Node refuses to type-strip under `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`),
|
||||
so it cannot serve an npm consumer.
|
||||
- Published to public npmjs as `@larvit/adf-codec`. Public source: the Gitea repo
|
||||
goes public, LICENSE in place, before the first publish.
|
||||
- Exact versions: `save-exact=true` in `.npmrc`.
|
||||
- Plain markdown is a flavour of the grammar
|
||||
- The round-trip is the product
|
||||
- Markdown in is a canonical fixpoint
|
||||
- Equality is editor-normal
|
||||
- Unknown nodes ride the carry
|
||||
- Foreign HTML is refused by name
|
||||
- Names stay text
|
||||
- Directives under `!adf:`
|
||||
- CommonMark is a subset
|
||||
- Tables
|
||||
- Links
|
||||
- Ids stay site-local
|
||||
- The HTML dialect
|
||||
- No runtime dependencies
|
||||
- Standards ship as data
|
||||
- fast-check
|
||||
- Any ES2022 engine
|
||||
- ESM only
|
||||
- One built entrypoint
|
||||
- Public on npm
|
||||
|
||||
## 7. Nothing about any consumer
|
||||
|
||||
@@ -191,6 +118,7 @@ types in `src/result.ts` hold its shape.
|
||||
than taking the gate's `dist`: the lockfile is committed, the image is patch-pinned and `tsc` is
|
||||
deterministic, so the two builds agree, and promoting an artifact would make the release path
|
||||
depend on a store that the gate would then have to keep.
|
||||
- Exact versions: `save-exact=true` in `.npmrc`.
|
||||
- Renovate watches devDependencies, Docker pins and action tags; automerges everything on green CI.
|
||||
- Docker images pin the full patch version (`node:24.19.0-alpine3.24`, never `node:24`), as
|
||||
specific as the publisher tags: `oven/bun:1.4.0-alpine` pins Bun's patch and leaves the base
|
||||
@@ -219,16 +147,16 @@ resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` s
|
||||
`node-floor.js` round-trips the installed package under a Node pinned to `engines.node`'s floor.
|
||||
|
||||
A fourth engine reads the build rather than the source: a headless Firefox loads `dist/index.js`
|
||||
over HTTP and converts the round-trip, normalization and error fixtures and the real payloads —
|
||||
the `commonmark-spec` sort is the Node suite's to check — which is §6's browser half and the only
|
||||
SpiderMonkey there is — the gate's other three engines are two V8s and a JavaScriptCore that is
|
||||
not Safari's.
|
||||
A WebDriver session is what carries a verdict back out, the driver and the page's server sharing
|
||||
one network namespace so each is the other's `127.0.0.1`; `--headless --screenshot` has no such
|
||||
channel, and loading `dist/index.js` in a globals-stripped realm buys one by not running a browser.
|
||||
The leg re-checks the conversions and nothing else — each fixture's emitted markdown, its parsed
|
||||
document, its error code — leaving the corpus's pairing, uniqueness, source positions and
|
||||
byte-level equality to the Node suite that owns them.
|
||||
over HTTP and converts the round-trip, normalization and error fixtures and the real payloads — the
|
||||
`commonmark-spec` sort is the Node suite's to check — which is the browser half of
|
||||
`docs/decisions.md` §Any ES2022 engine and the only SpiderMonkey there is — the gate's other three
|
||||
engines are two V8s and a JavaScriptCore that is not Safari's. A WebDriver session is what carries a
|
||||
verdict back out, the driver and the page's server sharing one network namespace so each is the
|
||||
other's `127.0.0.1`; `--headless --screenshot` has no such channel, and loading `dist/index.js` in a
|
||||
globals-stripped realm buys one by not running a browser. The leg re-checks the conversions and
|
||||
nothing else — each fixture's emitted markdown, its parsed document, its error code — leaving the
|
||||
corpus's pairing, uniqueness, source positions and byte-level equality to the Node suite that owns
|
||||
them.
|
||||
|
||||
Every leg announces its name and, where a container is in play, the image, before it runs and its
|
||||
elapsed time after, `publish.sh` alongside `ci.sh`, so a long run reads as progress rather than as
|
||||
@@ -244,14 +172,16 @@ functions, and a branch floor that only ever moves upward. It sits below 100 bec
|
||||
compared against `undefined` — have a half no valid document reaches.
|
||||
|
||||
The size ratchet is the other such number, `.oxlintrc.json`'s single rule over the files
|
||||
`tsconfig.build.json` builds: a per-function line ceiling, set at that set's worst and moving only
|
||||
downward. It covers the built files alone, since one ceiling over the tests too would have to be
|
||||
their worst, loosening the guard over the shipped code. It guards against drift and never drives a
|
||||
refactor, so no cyclomatic rule and no second lint rule join it: neither measure picked out what
|
||||
nine readers found hard (the comprehension panel, 2026-09-20). Three switches guard a silent green:
|
||||
`IIFEs: true`, since oxlint exempts an IIFE otherwise; an explicit `-c`, so a config gone missing
|
||||
fails the leg instead of falling back to oxlint's own defaults; and `--deny-warnings`, since a rule
|
||||
from a category this config never names arrives as a warning it exits 0 on.
|
||||
`tsconfig.build.json` builds — `oxlint`, since TypeScript 7 is a native compiler publishing no
|
||||
in-process parser, only the `unstable/` AST surface an out-of-process handshake reaches: a
|
||||
per-function line ceiling, set at that set's worst and moving only downward. It covers the built
|
||||
files alone, since one ceiling over the tests too would have to be their worst, loosening the guard
|
||||
over the shipped code. It guards against drift and never drives a refactor, so no cyclomatic rule
|
||||
and no second lint rule join it: neither measure picked out what nine readers found hard (the
|
||||
comprehension panel, 2026-09-20). Three switches guard a silent green: `IIFEs: true`, since oxlint
|
||||
exempts an IIFE otherwise; an explicit `-c`, so a config gone missing fails the leg instead of
|
||||
falling back to oxlint's own defaults; and `--deny-warnings`, since a rule from a category this
|
||||
config never names arrives as a warning it exits 0 on.
|
||||
|
||||
The corpus, all checked in: hand-built fixtures per node and combination; real ADF Atlassian's
|
||||
editor wrote; the CommonMark spec suite against `markdownToAdf` and `markdownToHtml`.
|
||||
@@ -269,11 +199,11 @@ of a bullet; fenced examples are skipped. It guards the attributes alone: nodes
|
||||
content model share a bullet, and the argument attribute is spelled ahead of `Attributes: `, so
|
||||
both answer to the round-trip corpus and to nothing else where a node has no fixture.
|
||||
|
||||
The tables answer to Atlassian's schema too (§5): for every node and mark they spell, the attribute
|
||||
names and kinds equal what `full.json` and `stage-0.json` hold between them. Value sets stay
|
||||
documentation, since any value round-trips. What the schema holds and the tables do not spell is
|
||||
pinned by name — an attribute as a gap, a type as carried — so a re-pin adding either goes red until
|
||||
someone spells it or pins it.
|
||||
The tables answer to Atlassian's schema too (`docs/decisions.md` §Standards ship as data): for every
|
||||
node and mark they spell, the attribute names and kinds equal what `full.json` and `stage-0.json`
|
||||
hold between them. Value sets stay documentation, since any value round-trips. What the schema holds
|
||||
and the tables do not spell is pinned by name — an attribute as a gap, a type as carried — so a
|
||||
re-pin adding either goes red until someone spells it or pins it.
|
||||
|
||||
## 11. Code rules
|
||||
|
||||
@@ -388,12 +318,13 @@ One-line commit messages and PR titles; short PR summaries. No AI-attribution ma
|
||||
|
||||
## 14. Non-goals
|
||||
|
||||
No network or filesystem I/O, no name→id resolution (§3), no ADF schema
|
||||
validation or exported validator — a refusal that keeps the round-trip is not schema validation,
|
||||
so the one a spelled node carrying the same mark type twice earns stays, and input nesting a
|
||||
spelling inside its own kind (`*(*a*)*`) names that mark once, no shipped CSS (§4), no
|
||||
streaming APIs, no performance budget past §11's scanning rule — nothing here is tuned, and no
|
||||
figure is promised. A CLI is a later goal (`todo.md`), not a non-goal.
|
||||
No network or filesystem I/O, no name→id resolution (`docs/decisions.md` §Names stay text), no ADF
|
||||
schema validation or exported validator — a refusal that keeps the round-trip is not schema
|
||||
validation, so the one a spelled node carrying the same mark type twice earns stays, and input
|
||||
nesting a spelling inside its own kind (`*(*a*)*`) names that mark once, no shipped CSS
|
||||
(`docs/decisions.md` §The HTML dialect), no streaming APIs, no performance budget past §11's
|
||||
scanning rule — nothing here is tuned, and no figure is promised. A CLI is a later goal (`todo.md`),
|
||||
not a non-goal.
|
||||
|
||||
## 15. The working loop
|
||||
|
||||
@@ -441,8 +372,8 @@ The verdict lands in the item it settles (the maintainer, 2026-09-25).
|
||||
### Rules the loop has settled (the maintainer, 2026-09-18)
|
||||
|
||||
- A finding inside the chunk's item is fixed in the chunk. Outside it, a new `todo.md` item, always
|
||||
in a release, weighed against every item on that release by the personas and §1–§3 — an item it
|
||||
outweighs moves later. A weighing no rule decides is asked as a gap.
|
||||
in a release, weighed against every item on that release by the personas and Goals 1 and 2 — an
|
||||
item it outweighs moves later. A weighing no rule decides is asked as a gap.
|
||||
- A stated number — 500 levels, the branch floor — is kept; a chunk that cannot keep it asks,
|
||||
naming the number it can reach. A number the code needs and no rule states is a gap.
|
||||
- An earliest release with no items left and nothing shipped toward it is planned as the chunk:
|
||||
|
||||
@@ -143,7 +143,7 @@ saving what this pair read replaces mentions, attachments and macros with text.
|
||||
## The errors
|
||||
|
||||
An ADF node type this version does not know is not an error: it is carried opaquely and restores
|
||||
unchanged (AGENTS.md §3).
|
||||
unchanged (`docs/decisions.md §Unknown nodes ride the carry`).
|
||||
|
||||
`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
|
||||
@@ -195,7 +195,7 @@ emit refuses:
|
||||
## The guarantees
|
||||
|
||||
- `markdownToAdf(adfToMarkdown(doc))` equals `doc` — unknown node types included, carried opaquely
|
||||
(AGENTS.md §3).
|
||||
(`docs/decisions.md §Unknown nodes ride the carry`).
|
||||
- Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML `unmappable-html`
|
||||
names, with three carve-outs — literal text matching directive, pipe-table or strikethrough
|
||||
syntax is claimed (escapable — `spec/flavour.md`) — and one gap: a CommonMark image fits only as
|
||||
@@ -234,4 +234,4 @@ emit refuses:
|
||||
ESM only, no runtime dependencies, public npmjs. 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: `AGENTS.md` §5–6.
|
||||
Contract: `docs/decisions.md`.
|
||||
|
||||
+4
-4
@@ -3,8 +3,8 @@
|
||||
One directory per contract kind, each landing with its milestone:
|
||||
|
||||
- `round-trip/` — `<name>.json` + `<name>.md`: the markdown `adfToMarkdown` must emit for that
|
||||
document, byte for byte, and that `markdownToAdf` must read back to it (AGENTS.md §2). Grouped
|
||||
by what the fixture exercises.
|
||||
document, byte for byte, and that `markdownToAdf` must read back to it (`docs/decisions.md` §The
|
||||
round-trip is the product). Grouped by what the fixture exercises.
|
||||
- `normalization/` — `<name>.md` + `<name>.json`: markdown input, and the document
|
||||
`markdownToAdf` must build from it, which must in turn emit and read back to itself. The
|
||||
markdown is not canonical.
|
||||
@@ -19,8 +19,8 @@ One directory per contract kind, each landing with its milestone:
|
||||
model), `unspellable` (parses but the flavour has no spelling) or `pending` (a parser gap a later
|
||||
milestone may close).
|
||||
|
||||
JSON is editor-normal (AGENTS.md §2), two-space indent, keys sorted. `spec.json` is the vendored,
|
||||
upstream machine-readable suite, byte-exact from
|
||||
JSON is editor-normal (`docs/decisions.md` §Equality is editor-normal), two-space indent, keys
|
||||
sorted. `spec.json` is the vendored, upstream machine-readable suite, byte-exact from
|
||||
[spec.commonmark.org](https://spec.commonmark.org/0.31.2/spec.json) (CommonMark 0.31.2, © John
|
||||
MacFarlane, [CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/)), and is not
|
||||
re-serialized by the corpus gate.
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# Decisions
|
||||
|
||||
## Plain markdown is a flavour of the grammar
|
||||
|
||||
2026-09-27, the maintainer. Goal 2. Valid while the plain flavour's spellings are ones the markdown
|
||||
grammar can read and write.
|
||||
|
||||
The lossy pair is the plain flavour: the markdown grammar's reader and writer with the flavour set,
|
||||
its spellings — alerts, callouts, task markers, `==` — read and written there, so a marker line and
|
||||
a backslash reach them intact; what the flavour cannot spell reduces ADF→ADF ahead of the writer.
|
||||
|
||||
## The round-trip is the product
|
||||
|
||||
2026-08-23, the maintainer. Goal 1. Valid while a consumer saves back through the lossless pair.
|
||||
|
||||
`markdownToAdf(adfToMarkdown(doc))` and `htmlToAdf(adfToHtml(doc))` must equal `doc` — anything
|
||||
less silently destroys content an editor could not represent, in a document it did not author.
|
||||
When losslessness and readability conflict, losslessness wins. Round-trip equality is a property
|
||||
tested over a corpus, not a claim made in prose.
|
||||
|
||||
## Markdown in is a canonical fixpoint
|
||||
|
||||
2026-08-23, the maintainer. Goals 1 and 3. Valid while markdown input may be written by hand.
|
||||
|
||||
The other direction is a canonical fixpoint, not byte-identity: human markdown normalizes, the way
|
||||
back yields the library's canonical spelling, and that spelling round-trips byte-identically —
|
||||
where there is a way back. CommonMark spells some things the flavour has no escape for — a
|
||||
paragraph opening with a code span whose backticks read back as a fence — so a parse succeeding
|
||||
does not imply a spellable document; `corpus/commonmark-spec/exceptions.json` names those.
|
||||
|
||||
## Equality is editor-normal
|
||||
|
||||
2026-08-24, the maintainer. Goal 1. Valid while markdown cannot tell apart the ADF shapes this
|
||||
merges.
|
||||
|
||||
"Equals" is structural equality over editor-normal ADF — adjacent text nodes with identical marks
|
||||
and no attributes merged, JSON number semantics, an empty attrs object, marks array or content
|
||||
array the absent key — the only domain markdown can restore.
|
||||
|
||||
## Unknown nodes ride the carry
|
||||
|
||||
2026-08-23, extended to misplaced known nodes 2026-08-26, the maintainer. Goal 1. Valid while ADF
|
||||
gains node types faster than this library spells them.
|
||||
|
||||
An unknown ADF node is carried opaquely — raw JSON rides a dedicated syntax in both formats and
|
||||
restores to a deep-equal node. The round-trip holds for documents newer than the library. So does
|
||||
a known node no section spells where it stands: a markdown serializer spells a node by type without
|
||||
checking its position, and refusing loses a document ADF itself keeps in an `unsupportedBlock`.
|
||||
Where a container's own spelling cannot hold the child it has — a `bulletList` outside `listItem`,
|
||||
a `codeBlock` outside text — the error result names that instead.
|
||||
|
||||
## Foreign HTML is refused by name
|
||||
|
||||
2026-08-23, the maintainer. Goals 1 and 6. Valid until the HTML dialect's element set lands
|
||||
(`todo.md`, 6).
|
||||
|
||||
An unmappable foreign HTML element is an error result naming the element — never a silent drop.
|
||||
|
||||
## Names stay text
|
||||
|
||||
2026-08-23, the maintainer. Goal 7. Valid while resolving a name to an id needs I/O.
|
||||
|
||||
A bare `@name` or `:smile:` in typed text stays a text node. Only directives produce
|
||||
mention/emoji/media nodes; resolving names to ids is the consumer's job.
|
||||
|
||||
## Directives under `!adf:`
|
||||
|
||||
2026-08-23, prefixed `!adf:` 2026-09-16, the maintainer. Goals 3 and 4. Valid while prose does not
|
||||
write `!adf:`.
|
||||
|
||||
Directives are one grammar for everything markdown lacks, namespaced under `!adf:`:
|
||||
`!adf:panel info` … `!adf:/panel` blocks, `!adf:mention[@Mikael]{id=5b10a2}` inline, `\!adf:` the
|
||||
one escape. Not CommonMark's generic-directives proposal: its `:::` claims a form prose writes, and
|
||||
its fence-length discipline ties a container's opener to its own body, where closing from the
|
||||
opener nests by itself and leaf versus container falls out of the node's content model.
|
||||
|
||||
## CommonMark is a subset
|
||||
|
||||
2026-08-23, the maintainer. Goal 3. Valid while the carve-outs stay the flavour's only claims.
|
||||
|
||||
Plain CommonMark is a subset, with carve-outs (`spec/flavour.md`): literal text shaped like a
|
||||
directive, a pipe table or a `~~` pair is claimed — plus one image gap.
|
||||
|
||||
## Tables
|
||||
|
||||
2026-08-23, the maintainer. Goal 4. Valid while a pipe table holds only one header row and inline
|
||||
cells.
|
||||
|
||||
One header row plus plain inline cells → pipe table; anything richer → directive form.
|
||||
|
||||
## Links
|
||||
|
||||
2026-09-13, nesting 2026-09-17, the maintainer. Goals 1 and 4. Valid while CommonMark's link
|
||||
syntax is what readers edit.
|
||||
|
||||
`[text](url "title")`, or `<url>` for a bare autolink-shaped text, wherever CommonMark spells the
|
||||
mark; `!adf:link[text]{attrs}` where it does not — an attribute CommonMark cannot hold, an `href` or
|
||||
`title` no canonical escape spells, a paragraph opening whose CommonMark spelling would read as a
|
||||
link reference definition — and a directive link CommonMark could spell is refused. No link wraps a
|
||||
link — the bracket form goes literal, the directive form refused — which is CommonMark's prose
|
||||
where its reference implementation nests one `<a>` in another.
|
||||
|
||||
## Ids stay site-local
|
||||
|
||||
2026-08-23, the maintainer. Goal 1. Valid while ADF ids are minted per site.
|
||||
|
||||
Identity-bearing nodes carry their ids in attributes; a document is only portable within its site —
|
||||
accepted.
|
||||
|
||||
## The HTML dialect
|
||||
|
||||
2026-08-23, the maintainer. Goals 4 and 7. Valid while HTML output is read by consumers styling it
|
||||
themselves.
|
||||
|
||||
The HTML dialect mirrors the markdown flavour: semantic elements, stable `adf-*` classes, `data-*`
|
||||
for what HTML cannot express, text always escaped. No stylesheet ships.
|
||||
|
||||
## No runtime dependencies
|
||||
|
||||
2026-08-23, the maintainer. Goal 7. Valid while Goal 7 names no runtime dependencies.
|
||||
|
||||
`dependencies` is empty. A runtime dependency enters only through an entry here stating why ~20
|
||||
lines of own code cannot do the job, who maintains it, and what auditing it costs. So the CommonMark
|
||||
and HTML parsers are written in this repo.
|
||||
|
||||
## Standards ship as data
|
||||
|
||||
2026-08-30, the CommonMark suite 2026-09-05 and ADF's schemas 2026-09-13, the maintainer. Goals 1
|
||||
and 7. Valid while each table's upstream package is CommonJS-only or heavy.
|
||||
|
||||
A table a standard fixes is data rather than a dependency: HTML5's 2125 semicolon-terminated
|
||||
character references ship packed in their own module, so entity decoding is complete without one.
|
||||
The CommonMark spec suite is the same shape of data and ships vendored at `corpus/commonmark-spec/`
|
||||
rather than as the `commonmark-spec` dev dependency — that package is CommonJS-only, and Renovate
|
||||
auto-bumping a spec version would silently point the vendored exception list's example numbers at a
|
||||
renumbered suite. A spec bump is a deliberate re-pin, exceptions re-derived by hand beside it.
|
||||
Atlassian's ADF JSON Schemas ship vendored the same way, at `spec/adf-schema/`, rather than as the
|
||||
`@atlaskit/adf-schema` dev dependency — CommonJS-only, some fifty packages with React among them,
|
||||
and a release most days for Renovate to automerge — re-pinned by hand when a payload or a report
|
||||
shows the need.
|
||||
|
||||
## fast-check
|
||||
|
||||
2026-09-14, the maintainer. Goal 1. Valid while a failing generated document needs shrinking by
|
||||
hand otherwise.
|
||||
|
||||
`fast-check` earns its place as a devDependency shrinking a failing generated document to the
|
||||
nodes that break it.
|
||||
|
||||
## Any ES2022 engine
|
||||
|
||||
2026-09-01, the maintainer. Goal 7. Valid while ES2022 is the floor browsers and servers share.
|
||||
|
||||
The library runs on any ES2022 engine, not only Node — a browser as readily as a server. The
|
||||
shipped source is ECMAScript and nothing else: no host import, no host global, no DOM.
|
||||
`tsconfig.build.json` is that gate, typechecking and emitting the shipped files alone, so
|
||||
`node:fs`, `process` and an ES2024 method are compile errors here rather than a consumer's crash
|
||||
there. The standard is the line, never an engine list: one implementing it in part — Hermes is the
|
||||
live doubt, on the Unicode property escapes emphasis matching leans on and on lookbehind — is out
|
||||
of scope rather than a bug. Node's test runner, the corpus reads and the build are the repo's own,
|
||||
never the library's, and `engines.node` states the floor the shipped JavaScript needs — `>=18` —
|
||||
never the higher one those repo-only tools want.
|
||||
|
||||
## ESM only
|
||||
|
||||
2026-08-23, the maintainer. Goal 7. Valid while every supported engine loads ES modules.
|
||||
|
||||
No CommonJS build, no dual-package hazard.
|
||||
|
||||
## One built entrypoint
|
||||
|
||||
2026-08-23, the maintainer. Goal 7. Valid while Node refuses to type-strip under `node_modules`.
|
||||
|
||||
Built JavaScript, `.d.ts` beside it. Do not add a TypeScript-source entrypoint — Node refuses to
|
||||
type-strip under `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so it cannot serve
|
||||
an npm consumer.
|
||||
|
||||
## Public on npm
|
||||
|
||||
2026-08-23, the maintainer. Goal 7 and the Audience. Valid while the audience installs from public
|
||||
npm.
|
||||
|
||||
Published to public npmjs as `@larvit/adf-codec`. Public source: the Gitea repo goes public,
|
||||
LICENSE in place, before the first publish.
|
||||
+29
-27
@@ -154,15 +154,15 @@ outside code spans and code blocks, `\!adf:` in input yields the literal text.
|
||||
naming no open container or a node other than the innermost open one, a leaf given a body, an
|
||||
`!adf:` completing no directive, an inline `[content]` or `{attrs}` left unclosed at end of line,
|
||||
unparseable or duplicate-keyed attrs, invalid JSON in an opaque carry. Never a silent literal-text
|
||||
fallback — a typo that reparses as prose is the silent loss §2 refuses.
|
||||
fallback — a typo that reparses as prose is the silent loss the round-trip refuses.
|
||||
|
||||
## The opaque carry (AGENTS.md §3)
|
||||
## The opaque carry (`docs/decisions.md` §Unknown nodes ride the carry)
|
||||
|
||||
A node no section spells where it stands — an unknown type, or a known one whose spelling belongs
|
||||
to the other position — rides as its raw JSON and restores to a deep-equal node. A carry may hold
|
||||
a node the emitter spells natively: it restores unreinterpreted, and the next emit spells it
|
||||
canonically (AGENTS.md §2). Block and inline positions canonicalize differently, each fitting
|
||||
where it sits:
|
||||
A node no section spells where it stands — an unknown type, or a known one whose spelling belongs to
|
||||
the other position — rides as its raw JSON and restores to a deep-equal node. A carry may hold a
|
||||
node the emitter spells natively: it restores unreinterpreted, and the next emit spells it
|
||||
canonically (`docs/decisions.md` §The round-trip is the product). Block and inline positions
|
||||
canonicalize differently, each fitting where it sits:
|
||||
|
||||
- **Block position**: a fenced code block with info string `carry`, body = the node's JSON —
|
||||
two-space indent, object keys sorted.
|
||||
@@ -177,9 +177,10 @@ In block-directive position `!adf:carry` is a named error — the carry's block
|
||||
## Raw HTML in input
|
||||
|
||||
CommonMark input may contain raw HTML. `markdownToAdf` routes each construct through the foreign
|
||||
HTML element mapping (AGENTS.md §3; specified with the HTML dialect, todo.md milestone 6) — ADF
|
||||
has no raw-HTML node, so a construct without a mapping, comments and processing instructions
|
||||
included, is an error result naming it. The flavour never emits raw HTML.
|
||||
HTML element mapping (`docs/decisions.md` §Foreign HTML is refused by name; specified with the HTML
|
||||
dialect, todo.md milestone 6) — ADF has no raw-HTML node, so a construct without a mapping, comments
|
||||
and processing instructions included, is an error result naming it. The flavour never emits raw
|
||||
HTML.
|
||||
|
||||
## Block nodes
|
||||
|
||||
@@ -192,10 +193,11 @@ have written as CommonMark is a named error.
|
||||
Each section lists attributes as `name (type)`. A parenthesized value set documents what real
|
||||
payloads hold; the type stays string and any value round-trips verbatim. Values map to attrs by
|
||||
type: strings verbatim, numbers and booleans in canonical JSON spelling — quoted where not bare
|
||||
(`width="33.33"`) — and `json` values as the inline carry's serialization (compact, keys
|
||||
sorted), quoted. `markdownToAdf` emits `attrs`, `content` and `marks` keys only when non-empty;
|
||||
editor-normal ADF reads an empty attrs object, marks array or content array as the absent key
|
||||
(AGENTS.md §2) — the grammar's empty-`{attrs}` omission already collapses the two spellings.
|
||||
(`width="33.33"`) — and `json` values as the inline carry's serialization (compact, keys sorted),
|
||||
quoted. `markdownToAdf` emits `attrs`, `content` and `marks` keys only when non-empty; editor-normal
|
||||
ADF reads an empty attrs object, marks array or content array as the absent key (`docs/decisions.md`
|
||||
§Equality is editor-normal) — the grammar's empty-`{attrs}` omission already collapses the two
|
||||
spellings.
|
||||
|
||||
Marks on a block node ride the reserved attribute key `marks` — the node's marks array as a
|
||||
`json` value: `!adf:layoutSection {marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"}`.
|
||||
@@ -300,10 +302,10 @@ other text, or one carrying a title, is a named error: `mediaInline` carries a m
|
||||
### Tables
|
||||
|
||||
One header row plus plain inline cells is a pipe table; anything richer is the directive form
|
||||
(AGENTS.md §4). Precisely: a table emits as a pipe table exactly when the `table`, every row
|
||||
and every cell carry no attrs and no marks, the first row is all `tableHeader` and the rest all
|
||||
`tableCell`, every row has the header's cell count, and every cell holds exactly one attr-less,
|
||||
mark-less paragraph — an empty cell holds one empty paragraph — with no `|` anywhere the
|
||||
(`docs/decisions.md` §Tables). Precisely: a table emits as a pipe table exactly when the `table`,
|
||||
every row and every cell carry no attrs and no marks, the first row is all `tableHeader` and the
|
||||
rest all `tableCell`, every row has the header's cell count, and every cell holds exactly one
|
||||
attr-less, mark-less paragraph — an empty cell holds one empty paragraph — with no `|` anywhere the
|
||||
inline layer spells as syntax: a code span, an autolink, a link destination or title. A `|` there
|
||||
takes the directive form instead. A pipe table parses back to exactly that shape.
|
||||
|
||||
@@ -449,14 +451,14 @@ Shipped !adf:emoji[🎉]{shortName=":tada:"} on !adf:date{timestamp=175608000000
|
||||
```
|
||||
|
||||
**Whitespace CommonMark cannot hold.** A newline inside a text node, and a space or tab where
|
||||
CommonMark strips or refuses one — a block's inline content edges, either side of a line break,
|
||||
an em, strong or strike spelling's inner edges, a pipe cell's edges — is spelled
|
||||
`!adf:text{text="…"}`, the reserved key carrying the node's text, escaped by the attribute grammar
|
||||
and never literal: pipe cells trim and pad. The emitter wraps the whitespace run alone and leaves
|
||||
the rest plain text; `markdownToAdf` merges adjacent text nodes carrying identical marks and no
|
||||
attributes (AGENTS.md §2). Input reads that spelling alone: the value is one run of spaces and
|
||||
tabs, or one run of newlines, and anything else — a mixed run, or text CommonMark carries plainly —
|
||||
is a named error.
|
||||
CommonMark strips or refuses one — a block's inline content edges, either side of a line break, an
|
||||
em, strong or strike spelling's inner edges, a pipe cell's edges — is spelled `!adf:text{text="…"}`,
|
||||
the reserved key carrying the node's text, escaped by the attribute grammar and never literal: pipe
|
||||
cells trim and pad. The emitter wraps the whitespace run alone and leaves the rest plain text;
|
||||
`markdownToAdf` merges adjacent text nodes carrying identical marks and no attributes
|
||||
(`docs/decisions.md` §Equality is editor-normal). Input reads that spelling alone: the value is one
|
||||
run of spaces and tabs, or one run of newlines, and anything else — a mixed run, or text CommonMark
|
||||
carries plainly — is a named error.
|
||||
|
||||
```
|
||||
!adf:text{text=" "}Two leading spaces held, and one text node split!adf:text{text="\n"}over two lines.
|
||||
@@ -502,7 +504,7 @@ close where the run sits (`un**-real**istic`), or one CommonMark's matching pair
|
||||
intra-word `*` runs together with a neighbouring `**`, and the multiple-of-3 rule can leave the
|
||||
merged run's pairing to another delimiter — rides the inline carry whole. An opaque carry inside a
|
||||
mark spelling is a named error in input: the carry restores its node exactly, marks included
|
||||
(AGENTS.md §3).
|
||||
(`docs/decisions.md` §Unknown nodes ride the carry).
|
||||
|
||||
```
|
||||
!adf:textColor[**Overdue**]{color="#ae2e24"}, H!adf:subsup[2]{type=sub}O, !adf:underline[signed].
|
||||
|
||||
@@ -7,11 +7,19 @@
|
||||
one no goal serves is proposed as a goal and asked. Sources: `AGENTS.md`'s body, the settled text
|
||||
in this file's items, and `todo-history.md`, deleted with the bare `(28)` citations into it once
|
||||
nothing cites it. Split by `AGENTS.md` section where one chunk is too big.
|
||||
- **36b — Move §8, §9 and §14's decisions.** The code list's rules, release automation and the
|
||||
non-goals.
|
||||
- **36c — Move §10 and §11's decisions.** The engine legs, floors, size ratchet, bounds, spellings
|
||||
and layout; the style rules stay working rules.
|
||||
- **36d — Move the settled text in `todo.md`'s items and §15's dated rules, and point §15's
|
||||
"the rule that closes it, landing here" at `docs/decisions.md`.** `AGENTS.md`'s sections are
|
||||
renumbered once only working rules remain, their citations with them.
|
||||
- **36e — Move `todo-history.md`'s decisions, re-point its citations and delete it.**
|
||||
- **35 — Read and write plain markdown as a flavour of the markdown grammar.** Per Goal 2 and
|
||||
AGENTS.md §1, `plainMarkdownToAdf` is `markdownToAdf`'s parser and `adfToPlainMarkdown`
|
||||
`adfToMarkdown`'s writer, each with the plain flavour set; 10's rows are read and written there,
|
||||
and the lift goes (the maintainer, 2026-09-27). The exports, their refusals and 10's rows stay as
|
||||
they are.
|
||||
`docs/decisions.md` §Plain markdown is a flavour of the grammar, `plainMarkdownToAdf` is
|
||||
`markdownToAdf`'s parser and `adfToPlainMarkdown` `adfToMarkdown`'s writer, each with the plain
|
||||
flavour set; 10's rows are read and written there, and the lift goes (the maintainer, 2026-09-27).
|
||||
The exports, their refusals and 10's rows stay as they are.
|
||||
- **35a — Read the plain flavour in the parser and delete the lift.** 10's rows are read while
|
||||
parsing, and `plain-lift.ts` is deleted, its tests reading through `plainMarkdownToAdf`. `>
|
||||
[!faq]- Why?` with the body on the next `>` line reads to an expand titled `Why?` whose body
|
||||
@@ -49,13 +57,14 @@
|
||||
the document ever saw, so nothing is lost; unwrapping them would put `alert(1)` on the page as
|
||||
prose. A `style` attribute is a separate question — `textColor` and `backgroundColor` are the
|
||||
marks it could reach — and is not read at `0.2.0`, the work outweighing what it buys.
|
||||
So the set sorts every element three ways, and that is what AGENTS.md §3 gains in place of "error
|
||||
result naming the element": a container around document content unwraps, content ADF cannot hold
|
||||
is an error result naming it, and what is not document content at all drops whole. A comment sorts
|
||||
into the second rather than the third because a person wrote those words on purpose. 10's
|
||||
"Rejected in the survey" line names raw HTML and comments and does not contradict this: it rejects
|
||||
them as spellings the lossy pair writes and reads back, where `plainMarkdownToAdf` reads through
|
||||
`markdownToAdf`'s parser and so inherits whatever this set accepts.
|
||||
So the set sorts every element three ways, and that is what `docs/decisions.md` §Foreign HTML is
|
||||
refused by name becomes in place of "error result naming the element": a container around document
|
||||
content unwraps, content ADF cannot hold is an error result naming it, and what is not document
|
||||
content at all drops whole. A comment sorts into the second rather than the third because a person
|
||||
wrote those words on purpose. 10's "Rejected in the survey" line names raw HTML and comments and
|
||||
does not contradict this: it rejects them as spellings the lossy pair writes and reads back, where
|
||||
`plainMarkdownToAdf` reads through `markdownToAdf`'s parser and so inherits whatever this set
|
||||
accepts.
|
||||
- **7 — Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed
|
||||
through ADF.** CommonMark spec suite runs against `markdownToHtml` from here (§10). The README's
|
||||
tagline and `package.json`'s `description` regain HTML (5g).
|
||||
|
||||
Reference in New Issue
Block a user