Final wording: the reserved-directive refusal row, the adf:-alone fence, the changelog's carry and scope, the developer panel, the migration's empty shape

This commit is contained in:
2026-10-03 15:17:53 +02:00
parent d68f311a14
commit 4349df9017
7 changed files with 18 additions and 19 deletions
+2 -2
View File
@@ -175,8 +175,8 @@ settle it; otherwise four more read, five of seven settle it, and less is a miss
The verdict lands in `docs/decisions.md`. The verdict lands in `docs/decisions.md`.
A writer panel settles every new or changed markdown or HTML spelling: a reader panel whose readers A writer panel settles every new or changed markdown or HTML spelling: a reader panel whose readers
are the people who read and write that format (README `## Audience`). A question about what an app are the people who read and write that format (README `## Audience`). A panel of the developer
relies on goes to the developer personas instead. personas settles a question about what an app relies on.
### Stated numbers ### Stated numbers
+7 -9
View File
@@ -5,20 +5,18 @@
- **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: text holding an unescaped `!adf:name arg {attrs}`) in place of the `:::`/`::`/`:name` forms: text holding an unescaped
`!adf:` is claimed, and `adf` is an ordinary code block language. Convert stored markdown per `!adf:` is claimed. Convert stored markdown per `MIGRATION.md`.
`MIGRATION.md`.
- **Breaking:** the block carry is a code fence whose info string `adf:<type>` names the node's - **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 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`. claimed, and `adf` is an ordinary code block language. 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` as JSON, for a document of plain objects as - `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc` as `JSON.parse` builds it: two adjacent text
`JSON.parse` builds them: two adjacent text nodes CommonMark would read back as one are parted by nodes CommonMark would read back as one are parted by `!adf:textBreak{}`, an empty `attrs`,
`!adf:textBreak{}`, an empty `attrs`, `content` or `marks` is spelled `{attrs=empty}`, `content` or `marks` is spelled `{attrs=empty}`, `{content=empty}` or `{marks=empty}`, `-0` is
`{content=empty}` or `{marks=empty}`, `-0` is spelled `-0`, and a `codeBlock` of several text spelled `-0`, and a `codeBlock` of several text nodes is a fence per node. A `codeBlock` holding
nodes is a fence per node. A `codeBlock` holding other than plain text nodes rides the block other than plain text nodes rides the block carry, where it was refused.
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
+1 -1
View File
@@ -7,7 +7,7 @@ turning each directive into text and each carried node into an `adf` code block.
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 the recipe below, and rewrite markdown your code writes or matches (templates, prompts, patterns) by the
tables below. Stored ADF needs one change: give a document holding no `content` key `content: []`. tables below. Stored ADF needs one change: give a document holding no `content` key `content: []`.
`0.1.0` read empty markdown to that shape, meaning the empty document. `0.1.0` built that shape from empty markdown, meaning the empty document.
### Convert markdown ### Convert markdown
+1 -1
View File
@@ -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, 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 | | `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 stands out of place: `!adf:textBreak{}` or `!adf:listBreak` parting nothing, `!adf:doc` beside another block | fix what the message names, or give the node the shape it names; `spec/flavour.md` lists every type's attributes and body |
## The guarantees ## The guarantees
+3 -3
View File
@@ -110,9 +110,9 @@ node's JSON without `type`: ```` ```adf:blockCard ````. A type no info string ca
rule a code language follows — leaves the info string `adf:` and keeps `type` in the body. Every rule a code language follows — leaves the info string `adf:` and keeps `type` in the body. Every
info string opening `adf:` is reserved, so a `codeBlock` whose language opens so takes the info string opening `adf:` is reserved, so a `codeBlock` whose language opens so takes the
`language` attribute, and `carry` is an ordinary language. A body holding `type` under a named type, `language` attribute, and `carry` is an ordinary language. A body holding `type` under a named type,
or a bare `adf:` fence whose body's `type` an info string could carry, is `unsupported-node-shape`. or a fence whose info string is `adf:` alone while its body's `type` could be spelled in the info
The reservation claims a fence CommonMark reads as code until `todo.md` item 43 gives CommonMark its string, is `unsupported-node-shape`. The reservation claims a fence CommonMark reads as code until
own reader. `todo.md` item 43 gives CommonMark its own reader.
## A code block is a fence per text node ## A code block is a fence per text node
+3 -2
View File
@@ -181,8 +181,9 @@ product). Block and inline positions canonicalize differently, each fitting wher
- **Block position**: a fenced code block with info string `adf:` and the node's type, body = the - **Block position**: a fenced code block with info string `adf:` and the node's type, body = the
node's JSON without its `type` — two-space indent, object keys sorted: ```` ```adf:blockCard ````. node's JSON without its `type` — two-space indent, object keys sorted: ```` ```adf:blockCard ````.
A type no info string carries back, by the rule a `codeBlock`'s language follows, leaves the info A type no info string carries back, by the rule a `codeBlock`'s language follows, leaves the info
string `adf:` and keeps `type` in the body. A body holding `type` under a named type, or a bare string `adf:` and keeps `type` in the body. A body holding `type` under a named type, or a fence
`adf:` fence whose body's `type` an info string could carry, is a named error. whose info string is `adf:` alone while its body's `type` could be spelled in the info string, is
a named error.
- **Inline position**: `!adf:carry{json="…"}` — compact serialization (keys sorted, no whitespace), - **Inline position**: `!adf:carry{json="…"}` — compact serialization (keys sorted, no whitespace),
JSON-string-escaped into the attribute. JSON-string-escaped into the attribute.
+1 -1
View File
@@ -78,7 +78,7 @@ function heldAttributes(held: Readonly<Record<string, JsonValue | undefined>>):
return attrs return attrs
} }
// An empty attrs, content or marks key, and adjacent text a reader would merge, stay occasional: each takes a spelling outside CommonMark. // An empty attrs, content or marks key, and adjacent text CommonMark would read back as one, stay occasional: each takes a spelling outside CommonMark.
// The copy gives fast-check's null-prototype records the prototype a parsed node has. // The copy gives fast-check's null-prototype records the prototype a parsed node has.
function occasionallyEmpty<T extends AdfMark | AdfNode>(arbitrary: Arbitrary<T>): Arbitrary<T> { function occasionallyEmpty<T extends AdfMark | AdfNode>(arbitrary: Arbitrary<T>): Arbitrary<T> {
return fc.tuple(arbitrary, fc.nat({ max: 9 })).map(([held, roll]) => (roll === 0 ? { ...held } : withoutEmptyKeys(held))) return fc.tuple(arbitrary, fc.nat({ max: 9 })).map(([held, roll]) => (roll === 0 ? { ...held } : withoutEmptyKeys(held)))