From cbacf5d598484c1485eb47dddc5fcd3c8b5ca16e Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 26 Aug 2026 16:40:15 +0200 Subject: [PATCH] Widen the carry's spec trigger to the position, and name the raw attribute value 2e must escape --- AGENTS.md | 14 +++++++------- README.md | 2 +- spec/flavour.md | 7 +++++-- todo.md | 15 ++++++++++----- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c13b05f..60544bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,11 +27,11 @@ Round-trip equality is a property tested over a corpus, not a claim made in pros ## 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. A - known node the position leaves unspelled carries the same way — validity against ADF's content - models is not the library's business (§14) — except `hardBreak`, `listItem` and `text` where a - block belongs: their spelling lives inside another node's body, so the misplacement is an error - result naming it. + 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, `hardBreak`, `listItem` and `text` where a + block belongs excepted. 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. @@ -121,8 +121,8 @@ live Atlassian APIs; property-generated ADF trees; the CommonMark spec suite aga returns `string | undefined`, never a `Result`: any failure is the fallback signal, and the general form owns the refusal. Refusing there refuses a document the general form spells. - Nothing recurses unbounded: the guards walk iteratively, and blocks, marks and JSON values — an - attribute's and a carried node's alike — are all held to 500 levels, so a deep document is a `Result` rather than the stack overflow that - waits near 2000. + attribute's and a carried node's alike — are all held to 500 levels, so a deep document is a + `Result` rather than the stack overflow that waits near 2000. - No casts: `as`, `as unknown as`, non-null `!`. A boundary owes a type guard validating the fields it claims (`isAdfDocument`); past it everything is typed. Make invalid states unrepresentable. diff --git a/README.md b/README.md index 6668127..aa3c5d2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Lossless conversion between **Atlassian Document Format** (ADF), an extended markdown flavour, and an HTML dialect. -**Status: pre-release — `adfToMarkdown` is built, nothing else is.** +**Status: pre-release — `adfToMarkdown` only, and not yet every document.** Plan: `todo.md`. Decisions: `AGENTS.md`. The flavour's grammar: [`spec/flavour.md`](spec/flavour.md). diff --git a/spec/flavour.md b/spec/flavour.md index f97ce42..ed6757a 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -113,8 +113,11 @@ literal-text fallback — a typo that reparses as prose is the silent loss §2 r ## The opaque carry (AGENTS.md §3) -A node type the library does not know rides as its raw JSON and restores to a deep-equal node. -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. `hardBreak`, +`listItem` and `text` where a block belongs are the exception: their spelling lives inside another +node's body, so the misplacement is a named error. Block and inline positions canonicalize +differently, each fitting where it sits: - **Block position**: a fenced code block with info string `adf`, body = the node's JSON — two-space indent, object keys sorted. diff --git a/todo.md b/todo.md index 677d80c..607222d 100644 --- a/todo.md +++ b/todo.md @@ -104,9 +104,13 @@ detail is settled at its own milestone. The carry's fallback triggers land here too: `spec/flavour.md` carries a node its section cannot spell — an attrs key no section lists, a value that is not the section's type, an arg slot holding no bare token, marks no nesting spells — where the emitter still refuses, - which leaves 1d's blocked CommonMark nodes as the only refusal. The `\u007c` rule covers - the inline carry's `json` value with them: the carry spells `escaping: 'none'` today so - that `tryPipeCell` refuses the pipe form for a carry whose JSON holds a `|`. + which leaves the refusals a container's own spelling owns. The `\u007c` rule is wider than + the pipe: a quoted attribute value spells `` ` ``, `&` and `<` raw today, so a backtick + pair in an `inlineCard`'s `data`, a `status`'s `style` or the inline carry's `json` reads + back as a code span, an entity or raw HTML inside the value. One escape settles all four, + and the pick decides whether a directive or a code span wins where they overlap (3). The + carry spells `escaping: 'none'` meanwhile, which is what makes `tryPipeCell` refuse the + pipe form for a carry whose JSON holds a pipe. The gate gains the collision property here: no two corpus documents may emit the same bytes — one spelling for two documents is a round-trip break no parser can undo, and it is provable without one. It also settles the emitter's one known approximation: delimiter @@ -123,8 +127,9 @@ detail is settled at its own milestone. every raw-HTML construct in input is an error result. The CommonMark spec suite runs against it from here (§10). The parser owes `~` the same `can_open`/`can_close` the emitter assumes — CommonMark flanking, as for `*` — which `spec/flavour.md` does not yet pin, and - the precedence between a directive and a code span that overlap, which the emitter already - spells leftmost-first (`` `a`:adf{json="…`…"}`b` ``). + the precedence between a directive and the constructs a raw attribute value opens inside it — + a code span, an entity, raw HTML — which one directive alone already reaches until 2e's + escape lands. `src/` gets its hierarchy at the same split — `adf/`, `markdown/`, `html/`, the grammar module shared inside `markdown/` — while the rename is still mechanical. Three files do not move whole: `block-directives.ts` and