Define the writer panel, split the carry fence into its own changelog entry, and say how an adf: fence stays code

This commit is contained in:
2026-10-03 15:13:50 +02:00
parent e4eecebeab
commit a2c620da27
5 changed files with 29 additions and 25 deletions
+3 -3
View File
@@ -174,9 +174,9 @@ allow, rendered, shuffled, with no rationale and nothing saying what is implemen
settle it; otherwise four more read, five of seven settle it, and less is a missing goal, asked. settle it; otherwise four more read, five of seven settle it, and less is a missing goal, asked.
The verdict lands in `docs/decisions.md`. The verdict lands in `docs/decisions.md`.
Every new or changed markdown or HTML spelling goes to such a panel, seated by the people who read A writer panel settles every new or changed markdown or HTML spelling: a reader panel whose readers
and write that format (README `## Audience`); a question about what an app relies on seats the are the people who read and write that format (README `## Audience`). A question about what an app
developer personas. relies on goes to the developer personas instead.
### Stated numbers ### Stated numbers
+12 -9
View File
@@ -4,18 +4,21 @@
- **Breaking:** directives, the inline opaque carry among them (now `!adf:carry{json="…"}`), are - **Breaking:** directives, the inline opaque carry among them (now `!adf:carry{json="…"}`), are
spelled under an `!adf:` prefix (`!adf:name … !adf:/name`, `!adf:name[content]{attrs}`, spelled under an `!adf:` prefix (`!adf:name … !adf:/name`, `!adf:name[content]{attrs}`,
`!adf:name arg {attrs}`) in place of the `:::`/`::`/`:name` forms, and the block carry is a code `!adf:name arg {attrs}`) in place of the `:::`/`::`/`:name` forms: text holding an unescaped
fence whose info string `adf:<type>` names the node's type, its body the node's JSON without `!adf:` is claimed, and `adf` is an ordinary code block language. Convert stored markdown per
`type`: text holding an unescaped `!adf:` and a code fence whose info string opens `adf:` are `MIGRATION.md`.
claimed, and `adf` is an ordinary code block language. Convert stored markdown per `MIGRATION.md`. - **Breaking:** the block carry is a code fence whose info string `adf:<type>` names the node's
type, its body the node's JSON without `type`, and a code fence whose info string opens `adf:` is
claimed. Convert stored markdown per `MIGRATION.md`.
- **Breaking:** `markdownToAdf` and `plainMarkdownToAdf` read markdown holding no block as a - **Breaking:** `markdownToAdf` and `plainMarkdownToAdf` read markdown holding no block as a
document whose `content` is empty, as Atlassian's schema requires; `!adf:doc {content=none}` document whose `content` is empty, as Atlassian's schema requires; `!adf:doc {content=none}`
spells a document holding no `content` key. spells a document holding no `content` key.
- `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc`: two adjacent text nodes a reader would - `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc` as JSON, for a document of plain objects as
join are parted by `!adf:textBreak{}`, an empty `attrs`, `content` or `marks` is spelled `JSON.parse` builds them: two adjacent text nodes CommonMark would read back as one are parted by
`{attrs=empty}`, `{content=empty}` or `{marks=empty}`, `-0` is spelled `-0`, and a `codeBlock` `!adf:textBreak{}`, an empty `attrs`, `content` or `marks` is spelled `{attrs=empty}`,
of several text nodes is a fence per node. A `codeBlock` holding other than plain text nodes `{content=empty}` or `{marks=empty}`, `-0` is spelled `-0`, and a `codeBlock` of several text
rides the block carry, where it was refused. nodes is a fence per node. A `codeBlock` holding other than plain text nodes rides the block
carry, where it was refused.
- **Breaking:** `unspellable-link` leaves `ConvertErrorCode`; a link whose `href` or `title` no - **Breaking:** `unspellable-link` leaves `ConvertErrorCode`; a link whose `href` or `title` no
CommonMark escape spells is written as `!adf:link[text]{attrs}`. CommonMark escape spells is written as `!adf:link[text]{attrs}`.
- **Breaking:** some directive refusals carry `malformed-directive` where they carried - **Breaking:** some directive refusals carry `malformed-directive` where they carried
+4 -4
View File
@@ -5,9 +5,9 @@
Directives moved under the `!adf:` prefix. `0.2.0` reads `0.1.0`'s spelling without an error, Directives moved under the `!adf:` prefix. `0.2.0` reads `0.1.0`'s spelling without an error,
turning each directive into text and each carried node into an `adf` code block. Before `0.2.0` turning each directive into text and each carried node into an `adf` code block. Before `0.2.0`
reads any `0.1.0` markdown, convert what is stored or in flight (an open editor, a queue) with the reads any `0.1.0` markdown, convert what is stored or in flight (an open editor, a queue) with the
recipe below, and rewrite markdown your code writes or matches (templates, prompts, patterns) by recipe below, and rewrite markdown your code writes or matches (templates, prompts, patterns) by the
the tables below. Stored ADF needs one change: a document `0.1.0` read from empty markdown holds tables below. Stored ADF needs one change: give a document holding no `content` key `content: []`.
no `content` key, and gets `content: []`, the empty document it meant. `0.1.0` read empty markdown to that shape, meaning the empty document.
### Convert markdown ### Convert markdown
@@ -23,7 +23,7 @@ import { markdownToAdf as markdownToAdf010 } from 'adf-codec-0.1'
function migrateMarkdown(stored: string) { function migrateMarkdown(stored: string) {
const parsed = markdownToAdf010(stored) const parsed = markdownToAdf010(stored)
// 0.1.0's reader was editor-normal, so a document with no content key always meant an empty one. // 0.1.0 dropped an empty content array, so a document with no content key meant an empty one.
return parsed.ok ? adfToMarkdown({ ...parsed.value, content: parsed.value.content ?? [] }) : parsed return parsed.ok ? adfToMarkdown({ ...parsed.value, content: parsed.value.content ?? [] }) : parsed
} }
``` ```
+9 -8
View File
@@ -175,7 +175,7 @@ Parsing — `markdownToAdf` and `plainMarkdownToAdf`, and `htmlToAdf` at `0.2.0`
| Code | Fires when | What you can do | | Code | Fires when | What you can do |
| --- | --- | --- | | --- | --- | --- |
| `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 wrap a code fence whose info string opens `adf:` in `!adf:codeBlock {language="adf:…"}` with a bare fence | | `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 | | `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 |
| `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 | | `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-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 |
@@ -198,7 +198,7 @@ emit refuses:
| `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-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-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 | | `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, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it | | `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it |
| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take, or lacks one it needs — or markdown writes as a directive a node or mark the lossless flavour spells as CommonMark | write the shape the message names, or remove the reserved directive it names; `spec/flavour.md` lists every type's attributes and body | | `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take, or lacks one it needs — or markdown writes as a directive a node or mark the lossless flavour spells as CommonMark, or a reserved directive (`!adf:textBreak{}`, `!adf:listBreak`, `!adf:doc`) stands where it parts nothing | make the edit the message opens with; `spec/flavour.md` lists every type's attributes and body |
## The guarantees ## The guarantees
@@ -213,12 +213,13 @@ Serves Goals 1, 3 and 4.
name every exception. name every exception.
- Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML `unmappable-html` - Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML `unmappable-html`
names, with four carve-outs — literal text matching directive, pipe-table or strikethrough syntax, names, with four carve-outs — literal text matching directive, pipe-table or strikethrough syntax,
and a code fence whose info string opens `adf:`, are claimed (escapable — `spec/flavour.md`) — and and a code fence whose info string opens `adf:`, are claimed (each can be kept literal —
one gap: a CommonMark image fits only as its own title-less paragraph; mid-text and titled images `spec/flavour.md`) — and one gap: a CommonMark image fits only as its own title-less paragraph;
are error results, save an image inside another's description, which flattens into the alt text. mid-text and titled images are error results, save an image inside another's description, which
Converting back yields the library's canonical spelling, which round-trips byte-identically — flattens into the alt text. Converting back yields the library's canonical spelling, which
where it converts back at all: a parse succeeding is no promise of that, so keep the source until round-trips byte-identically — where it converts back at all: a parse succeeding is no promise of
the way back succeeds. ``` ` `` ` ``` reads cleanly and then refuses. that, so keep the source until the way back succeeds. ``` ` `` ` ``` reads cleanly and then
refuses.
- Four CommonMark spellings parse without an error and build a document the reference - Four CommonMark spellings parse without an error and build a document the reference
implementation renders differently: `[](/url)` and `[]()` stay literal text against CommonMark's implementation renders differently: `[](/url)` and `[]()` stay literal text against CommonMark's
empty link, a list continuing past a marker change stays one list against CommonMark's two, a empty link, a list continuing past a marker change stays one list against CommonMark's two, a
+1 -1
View File
@@ -4,7 +4,7 @@ The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` p
CommonMark is a subset apart from raw HTML (below), with four carve-outs: literal text that matches CommonMark is a subset apart from raw HTML (below), with four carve-outs: literal text that matches
directive syntax below or reads as a pipe table is claimed by the flavour, a matched `~~` pair directive syntax below or reads as a pipe table is claimed by the flavour, a matched `~~` pair
spells `strike` (escape the `!adf:`, `|` or `~` to keep it literal), and a code fence whose info spells `strike` (escape the `!adf:`, `|` or `~` to keep it literal), and a code fence whose info
string opens `adf:` is the opaque carry (wrap it as a bare fence in string opens `adf:` is the opaque carry (drop the info string and wrap the fence in
`!adf:codeBlock {language="adf:…"}` to keep it code) — and one gap: a CommonMark image fits only as `!adf:codeBlock {language="adf:…"}` to keep it code) — 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 its own title-less paragraph — mid-text and titled images are named errors. The emitted form is
contract (`docs/decisions.md` §The formats are API). Per-node syntaxes build on this grammar in the contract (`docs/decisions.md` §The formats are API). Per-node syntaxes build on this grammar in the