12a: spec/flavour.md and AGENTS.md spell the !adf: grammar
CI / gate (push) Successful in 24s
CI / publish (push) Successful in 4s

This commit was merged in pull request #82.
This commit is contained in:
2026-09-16 09:16:13 +02:00
parent 4ac4a119e4
commit 16af0b9592
2 changed files with 126 additions and 118 deletions
+13 -8
View File
@@ -43,8 +43,11 @@ Round-trip equality is a property tested over a corpus, not a claim made in pros
## 4. The flavour ## 4. The flavour
- Directives, one grammar for everything markdown lacks: `:::panel info` … `:::` blocks, - Directives, one grammar for everything markdown lacks, namespaced under `!adf:`: `!adf:panel info`
`:mention[@Mikael]{id=5b10a2}` inline. Prior art: CommonMark's generic-directives proposal. … `!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 - 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. 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. - Tables: one header row plus plain inline cells → pipe table; anything richer → directive form.
@@ -98,15 +101,17 @@ against the README's personas.
The emitted markdown and HTML are contracts. After 1.0: previously-emitted output parsing The emitted markdown and HTML are contracts. After 1.0: previously-emitted output parsing
differently, or not at all, is MAJOR; new syntax while old output still round-trips is MINOR. differently, or not at all, is MAJOR; new syntax while old output still round-trips is MINOR.
Pre-1.0, normal 0.x rules. Pre-1.0, normal 0.x rules. A spelled node's content model is part of that contract — leaf or
container is the model, not the syntax — so giving a spelled node's model content it had not, or
taking it away, is MAJOR whatever ADF's own schema does.
The error surface is a contract too. `ConvertError` is `{ code, message, path, position? }` — the The error surface is a contract too. `ConvertError` is `{ code, message, path, position? }` — the
code from a closed list a consumer may switch exhaustively, the message free text, the path the code from a closed list a consumer may switch exhaustively, the message free text, the path the
node's place from the document root, the position where a parse read the refusal in its input. node's place from the document root, the position where a parse read the refusal in its input.
A message names the violation, not the rule alone — a rule by itself states a truth the reader A message names the violation, not the rule alone — a rule by itself states a truth the reader
must invert before it reads as a failure — and where the flavour's claim refuses ordinary prose it must invert before it reads as a failure — and where the flavour's claim refuses ordinary prose it
names the escape that unclaims the form claimed: `\:::` for a directive line, `\|` for every pipe names the escape that unclaims the form claimed: `\!adf:` for a directive, block line and inline
row, `\:` for an inline directive. alike, `\|` for every pipe row.
Adding, removing or renaming a code is breaking, so a milestone meeting a new failure cause Adding, removing or renaming a code is breaking, so a milestone meeting a new failure cause
reuses a code where one fits; the list is complete at `0.1.0`. A code names the reuses a code where one fits; the list is complete at `0.1.0`. A code names the
cause; where one cause recurs across node types, across one mark's attributes or across cause; where one cause recurs across node types, across one mark's attributes or across
@@ -116,7 +121,7 @@ direction hits it, `unspellable-link` the destination and the title alike. Where
apart, the line between them is what they name: `unspellable-character` is a character CommonMark apart, the line between them is what they name: `unspellable-character` is a character CommonMark
rewrites wherever text holds it, `unspellable-whitespace` the newline no inline directive's rewrites wherever text holds it, `unspellable-whitespace` the newline no inline directive's
content slot spans, in either direction. A claim code names the spelling claimed, never the node that spelling would have built: content slot spans, in either direction. A claim code names the spelling claimed, never the node that spelling would have built:
a malformed `:::table` is a `malformed-directive`, and an alignment colon a `malformed-pipe-table` — a malformed `!adf:table` is a `malformed-directive`, and an alignment colon a `malformed-pipe-table` —
the flavour's own delimiter row is `-` runs, so the grammar refuses the colon rather than ADF's the flavour's own delimiter row is `-` runs, so the grammar refuses the colon rather than ADF's
missing column model doing it. A refusal no spelling recovers from is a gap in the flavour rather missing column model doing it. A refusal no spelling recovers from is a gap in the flavour rather
than a code: give the flavour the spelling and the code goes, which the freeze is the last moment than a code: give the flavour the spelling and the code goes, which the freeze is the last moment
@@ -125,8 +130,8 @@ spelling writes rides the carry with its node. A directive whose name reads back
`unknown-directive-name` rather than a claim code — the spelling is well formed, and telling that `unknown-directive-name` rather than a claim code — the spelling is well formed, and telling that
apart from a typo is what a consumer switches on when a later MINOR gives the name meaning. A apart from a typo is what a consumer switches on when a later MINOR gives the name meaning. A
reserved name is a known name, so never that code, and the two the flavour reserves part on form: reserved name is a known name, so never that code, and the two the flavour reserves part on form:
a form the grammar does not have is a claim code — `::adf`, whose carry is the fence — and a a form the grammar does not have is a claim code — `!adf:carry`, whose carry is the fence — and a
well-formed form in the wrong place is `unsupported-node-shape`, `::listBreak` parting anything well-formed form in the wrong place is `unsupported-node-shape`, `!adf:listBreak` parting anything
but two adjacent lists of one type. What but two adjacent lists of one type. What
the grammar itself refuses stays a claim code, key order among it; a well-formed directive the the grammar itself refuses stays a claim code, key order among it; a well-formed directive the
node tables refuse — an attribute a node does not hold or spells elsewhere, a value outside its node tables refuse — an attribute a node does not hold or spells elsewhere, a value outside its
+113 -110
View File
@@ -3,7 +3,7 @@
The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain
CommonMark is a subset apart from raw HTML (below), with three carve-outs: literal text that CommonMark is a subset apart from raw HTML (below), with three carve-outs: literal text that
matches directive syntax below or reads as a pipe table is claimed by the flavour, and a matched matches directive syntax below or reads as a pipe table is claimed by the flavour, and a matched
`~~` pair spells `strike` (escape the `:`, `|` or `~` to keep it literal) — and one gap: a `~~` pair spells `strike` (escape the `!adf:`, `|` or `~` to keep it literal) — and one gap: a
CommonMark image fits only as its own CommonMark image fits only as its own
title-less paragraph — mid-text and titled images are named errors. The emitted form is contract title-less paragraph — mid-text and titled images are named errors. The emitted form is contract
(AGENTS.md §8). Per-node syntaxes build on this grammar in the sections below. (AGENTS.md §8). Per-node syntaxes build on this grammar in the sections below.
@@ -25,7 +25,7 @@ normalizes to it through the round-trip.
whose first item is empty), whatever block sits above it. Blank lines between items normalize whose first item is empty), whatever block sits above it. Blank lines between items normalize
away, and no list opens beside one of its own kind — the marker change CommonMark starts a away, and no list opens beside one of its own kind — the marker change CommonMark starts a
second list on merges instead: ADF records no tightness, so one `- ` spelling reads two second list on merges instead: ADF records no tightness, so one `- ` spelling reads two
adjacent lists of a kind back as one. The leaf `::listBreak` parts them, taking the separation adjacent lists of a kind back as one. The leaf `!adf:listBreak` parts them, taking the separation
any directive block takes where it sits. It builds no node, and it reads only between two any directive block takes where it sits. It builds no node, and it reads only between two
adjacent lists of one type: elsewhere, or carrying an argument, `{attrs}` or a body, it is a adjacent lists of one type: elsewhere, or carrying an argument, `{attrs}` or a body, it is a
named error. A list whose item holds a line of spaces or tabs alone, which a list item reads named error. A list whose item holds a line of spaces or tabs alone, which a list item reads
@@ -43,7 +43,8 @@ normalizes to it through the round-trip.
- Hard break: backslash at end of line (survives editors that trim trailing spaces). Where - Hard break: backslash at end of line (survives editors that trim trailing spaces). Where
CommonMark admits no spelling — the end of a block, inside an ATX heading — or where the node CommonMark admits no spelling — the end of a block, inside an ATX heading — or where the node
carries an attribute, it is the inline directive. carries an attribute, it is the inline directive.
- An empty paragraph — real payloads carry them — is `::paragraph`. - An empty paragraph — real payloads carry them — is an `!adf:paragraph` … `!adf:/paragraph` pair
holding nothing.
- Links `[text](url)`; `<…>` around a destination containing spaces, `<>` an empty one beside a - Links `[text](url)`; `<…>` around a destination containing spaces, `<>` an empty one beside a
title; title in double quotes. A backslash escapes a parenthesis the destination leaves title; title in double quotes. A backslash escapes a parenthesis the destination leaves
unbalanced, and a quote inside the title; a balanced pair stays bare. `<url>` autolink form only unbalanced, and a quote inside the title; a balanced pair stays bare. `<url>` autolink form only
@@ -72,56 +73,59 @@ normalizes to it through the round-trip.
## Directives ## Directives
One grammar for everything CommonMark lacks. A directive name is `[a-z][A-Za-z0-9]*` — the ADF One grammar for everything CommonMark lacks, namespaced: every directive opens with the literal
node and mark names the sections below spell as directives. Recognition is syntactic and `!adf:`. A directive name is `[a-z][A-Za-z0-9]*` — the ADF node and mark names the sections below
name-set-independent: anything matching the forms below parses as a directive regardless of spell as directives. Recognition is syntactic and name-set-independent: anything matching the forms
whether the name is known, and an unknown name is an error result naming it — so output an old below parses as a directive regardless of whether the name is known, and an unknown name is an
emitter escaped stays escaped, and erroring input gaining meaning later is MINOR, never a reparse error result naming it at the opener, whatever follows it — so output an old emitter escaped stays
(§8). Each name belongs to one position, and a name the other one spells — a mark or an inline escaped, and erroring input gaining meaning later is MINOR, never a reparse (§8). Each name belongs
node written as a block directive, a block node written inline — is a different error, naming the to one position, and a name the other one spells — a mark or an inline node written as a block
spelling it takes. Two reserved names read back to no node: `adf` for the opaque carry, as both directive, a block node written inline — is a different error, naming the spelling it takes. Two
directive name and fence info string, and `listBreak` for the leaf that parts two adjacent lists reserved names read back to no node: `carry` for the opaque carry, as both directive name and fence
(Canonical form). info string, and `listBreak` for the leaf that parts two adjacent lists (Canonical form).
**Inline**: `:name[content]{attrs}`, on one line — an inline directive never spans lines. **Claiming**: an unescaped `!adf:` claims wherever it stands. What follows picks the form: `/name`
closes a container, and a name picks by what follows it in turn — a space or the line's end a block
line, `[` or `{` an inline directive. A `!adf:` completing none of the three is a named error, and
`\!adf:` is the literal, block and inline alike. A claimed block line also ends a lazy continuation:
the blockquote or list item whose paragraph CommonMark would fold it into closes instead.
**Inline**: `!adf:name[content]{attrs}`, on one line — an inline directive never spans lines.
`[content]` is inline markdown; brackets inside balance as in CommonMark link text, `\]` for a `[content]` is inline markdown; brackets inside balance as in CommonMark link text, `\]` for a
literal bracket. Whitespace at either edge of `[content]`, space or tab, is part of it and literal bracket. Whitespace at either edge of `[content]`, space or tab, is part of it and
survives inline parsing. Each section below says whether content is required. `:` opens a survives inline parsing. Each section below says whether content is required, and `{attrs}` must
directive only when the name is followed immediately by `[` or `{`, and `{attrs}` must follow follow `]` (or the name) with no gap. An inline directive binds as a unit before bracket matching,
`]` (or the name) with no gap — anything else (`10:30`, `:smile:`, a stray `{…}` in text) is the way a code span does: a `]` or `(` inside its `{attrs}` is the directive's, never the enclosing
literal text. An inline directive binds as a unit before bracket matching, the way a code span content's, and a `(` after its closing `]` opens no link.
does: a `]` or `(` inside its `{attrs}` is the directive's, never the enclosing content's, and a
`(` after its closing `]` opens no link.
**Container block**: **Block container**:
``` ```
:::name arg {attrs} !adf:name arg {attrs}
block content block content
::: !adf:/name
``` ```
The fence is three or more colons. `arg` is one optional bare token whose meaning each node `arg` is one optional bare token whose meaning each node defines (e.g. the panel type). The body is
defines (e.g. the panel type). The body is block markdown. The closing fence is a line of at block markdown. The closer names the innermost open container and carries nothing after the name;
least the opening's length and closes the innermost open container however long its run, and a one naming another node, or standing where no container is open, is a named error. Opening and
container's fence is longer than every directive fence line anywhere in its body, however deeply a list item or blockquote nests it; a colon run inside a code closing is what nests, so the opener reads the same at every depth, and a closer crosses no list
fence or opaque carry is content. Canonical form uses minimal lengths. item or blockquote edge — a container opened inside one closes inside it. Directive block lines
Directive fence lines follow code-fence indentation (up to three spaces relative to their follow code-fence indentation (up to three spaces relative to their container); an `!adf:` inside a
container). code fence or opaque carry is content.
**Leaf block**: `::name arg {attrs}` — a block-position node with no body, `arg` reading as **Block leaf**: `!adf:name arg {attrs}` — the same opener with no closer, `arg` reading as above.
above.
Which of the two a node takes is its content model, never the spelling: a model taking content is
written as an opener–closer pair and one taking none as a leaf, so a leaf given a body and a
container missing its closer are each a named error. A node holding no content whose model takes
some is an empty pair. A spelled node's content model is contract in consequence — changing one is
MAJOR (AGENTS.md §8).
Canonical spacing is the only spacing input reads: one space parts the name, `arg` and `{attrs}`, Canonical spacing is the only spacing input reads: one space parts the name, `arg` and `{attrs}`,
and one parts each attribute pair, with no padding inside the braces. Trailing whitespace on a and one parts each attribute pair, with no padding inside the braces. Trailing whitespace on a
directive block line is tolerated in input, never emitted. directive block line is tolerated in input, never emitted.
**Claiming at block level**, symmetric with inline: a line whose leading run of two or more
colons is followed immediately by a name character is claimed and must parse fully as a container
opening or a leaf, else it is a named error. A bare colon-run line is a closing fence while a
container is open, a named error otherwise. A claimed line also ends a lazy continuation: the
blockquote or list item whose paragraph CommonMark would fold it into closes instead.
**Attributes**: `{key=value key2="two words"}`. `{attrs}` is optional in every form, and `{}` is **Attributes**: `{key=value key2="two words"}`. `{attrs}` is optional in every form, and `{}` is
valid — no attributes. A bare value matches `[A-Za-z0-9_-]+`; any other value is double-quoted valid — no attributes. A bare value matches `[A-Za-z0-9_-]+`; any other value is double-quoted
with JSON string escaping (`\"` `\\` `\n` `\t` `\uXXXX`, …) — total over with JSON string escaping (`\"` `\\` `\n` `\t` `\uXXXX`, …) — total over
@@ -131,24 +135,23 @@ after a directive binds and stay raw. The closing `}` is the first one outside q
quoted value holds `}` unescaped. All values are strings at the grammar level; each node's section quoted value holds `}` unescaped. All values are strings at the grammar level; each node's section
assigns types. assigns types.
Canonical form orders keys alphabetically, spells values bare wherever allowed, escapes inside Canonical form orders keys alphabetically, spells values bare wherever allowed, escapes inside
quotes in the shortest form each escape has, and omits empty `{attrs}` except where the `{` itself quotes in the shortest form each escape has, and omits empty `{attrs}` except where the `{` is what
claims the directive (`:hardBreak{}`). Input reads that spelling alone: keys out of order, a value ends the name (`!adf:hardBreak{}`). Input reads that spelling alone: keys out of order, a value
quoted where bare carries it, an escape longer than it need be, an empty `{attrs}` the name or the quoted where bare carries it, an escape longer than it need be, an empty `{attrs}` on a block line
`[content]` already claims, and a number or `json` value outside its canonical JSON spelling are or after a `[content]`, and a number or `json` value outside its canonical JSON spelling are each a
each a named error naming the spelling to write instead. named error naming the spelling to write instead.
**Escaping**: the emitter backslash-escapes whatever literal text would otherwise parse as **Escaping**: the emitter backslash-escapes whatever literal text would otherwise parse as
directive syntax — the leading `:` of a would-be directive, `]` inside content, a bracket a link's directive syntax — every literal `!adf:`, `]` inside content, a bracket a link's destination and
destination and title inside content leave unbalanced, a backtick there that would open a code span title inside content leave unbalanced, a backtick there that would open a code span, a `{` right
and a `:` there that would open an inline directive, a `{` right after a directive's closing `]`, after a directive's closing `]`, which would otherwise be read as the attributes it has none of;
which would otherwise be read as the attributes it has none of; outside code spans and code blocks, outside code spans and code blocks, `\!adf:` in input yields the literal text.
a backslash before `:` in input yields a literal colon.
**Malformed directives are error results**, named: an unclosed container at end of input, a body **Malformed directives are error results**, named: an unclosed container at end of input, a closer
fence line of the container's length or longer, a bare colon-run line outside any container or naming no open container or a node other than the innermost open one, a leaf given a body, an
shorter than the fence it would close, an inline `[content]` or `{attrs}` left unclosed at end of `!adf:` completing no directive, an inline `[content]` or `{attrs}` left unclosed at end of line,
line, unparseable or duplicate-keyed attrs, invalid JSON in an opaque carry. Never a silent unparseable or duplicate-keyed attrs, invalid JSON in an opaque carry. Never a silent literal-text
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 §2 refuses.
## The opaque carry (AGENTS.md §3) ## The opaque carry (AGENTS.md §3)
@@ -158,16 +161,15 @@ a node the emitter spells natively: it restores unreinterpreted, and the next em
canonically (AGENTS.md §2). Block and inline positions canonicalize differently, each fitting canonically (AGENTS.md §2). Block and inline positions canonicalize differently, each fitting
where it sits: where it sits:
- **Block position**: a fenced code block with info string `adf`, body = the node's JSON — - **Block position**: a fenced code block with info string `carry`, body = the node's JSON —
two-space indent, object keys sorted. two-space indent, object keys sorted.
- **Inline position**: `:adf{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.
The info string `adf` is reserved: a genuine `codeBlock` whose `language` is exactly `adf` takes The info string `carry` is reserved: a genuine `codeBlock` whose `language` is exactly `carry` takes
the attribute the section below keeps for a language no info string holds, so the reservation the attribute the section below keeps for a language no info string holds, so the reservation
stays absolute. stays absolute.
In block-directive positions (`::adf`, `:::adf`) the reserved name is a named error — the In block-directive position `!adf:carry` is a named error — the carry's block form is the fence.
carry's block form is the fence.
## Raw HTML in input ## Raw HTML in input
@@ -193,9 +195,9 @@ editor-normal ADF reads an empty attrs object, marks array or content array as t
(AGENTS.md §2) — the grammar's empty-`{attrs}` omission already collapses the two spellings. (AGENTS.md §2) — 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 Marks on a block node ride the reserved attribute key `marks` — the node's marks array as a
`json` value: `::::layoutSection {marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"}`. `json` value: `!adf:layoutSection {marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"}`.
A section saying its body is inline takes at most one paragraph, whose inline content becomes A section saying its body is inline takes at most one paragraph, whose inline content becomes
the node's `content`; any other body is a named error, and a node holding no content is the leaf. the node's `content`; any other body is a named error, and an empty pair is a node holding none.
A node the sections cannot spell rides the opaque carry: an attrs key its section does not A node the sections cannot spell rides the opaque carry: an attrs key its section does not
list, a value that is not the section's type, or an arg-slot value that is no bare token. In list, a value that is not the section's type, or an arg-slot value that is no bare token. In
@@ -212,7 +214,7 @@ form.
- `codeBlock` — container, body one fenced code block whose info string is the language and whose - `codeBlock` — container, body one fenced code block whose info string is the language and whose
content is the node's. Attributes: `hideLineNumbers` (boolean), `language` (string), `localId` content is the node's. Attributes: `hideLineNumbers` (boolean), `language` (string), `localId`
(string), `uniqueId` (string), `wrap` (boolean). A language no info string carries back — empty, (string), `uniqueId` (string), `wrap` (boolean). A language no info string carries back — empty,
the reserved `adf`, or holding a backtick, a backslash, a control character, edge whitespace or the reserved `carry`, or holding a backtick, a backslash, a control character, edge whitespace or
an entity reference — rides the `language` attribute instead and the fence carries no info an entity reference — rides the `language` attribute instead and the fence carries no info
string; writing it in the slot that rule leaves empty, or in both, is a named error. The body is string; writing it in the slot that rule leaves empty, or in both, is a named error. The body is
one ordinary code block, and a fence's info string decodes escapes and entity references as any one ordinary code block, and a fence's info string decodes escapes and entity references as any
@@ -227,11 +229,11 @@ form.
- `rule` — leaf. Attributes: `localId` (string). - `rule` — leaf. Attributes: `localId` (string).
```` ````
:::codeBlock {localId=01a03d5c-9b21-73f4-8e6a-0c47b1d9e2f8 wrap=true} !adf:codeBlock {localId=01a03d5c-9b21-73f4-8e6a-0c47b1d9e2f8 wrap=true}
```rust ```rust
fn main() {} fn main() {}
``` ```
::: !adf:/codeBlock
```` ````
### Panel ### Panel
@@ -242,9 +244,9 @@ fn main() {}
panels. panels.
``` ```
:::panel warning !adf:panel warning
Check the collation before importing. Check the collation before importing.
::: !adf:/panel
``` ```
### Expand ### Expand
@@ -253,9 +255,9 @@ Check the collation before importing.
which. Attributes: `localId` (string), `title` (string). which. Attributes: `localId` (string), `title` (string).
``` ```
:::expand {title="Full build log"} !adf:expand {title="Full build log"}
… …
::: !adf:/expand
``` ```
### The media family ### The media family
@@ -264,19 +266,19 @@ Check the collation before importing.
(string), `localId` (string), `occurrenceKey` (string), `type` (`external` `file` `link`), (string), `localId` (string), `occurrenceKey` (string), `type` (`external` `file` `link`),
`url` (string), `width` (number). `file` and `link` media carry `collection` + `id`; `url` (string), `width` (number). `file` and `link` media carry `collection` + `id`;
`external` media carry `url`. `external` media carry `url`.
- `mediaSingle` — container: one `::media`, then optionally one `:::caption`. Attributes: - `mediaSingle` — container: one `!adf:media`, then optionally one `!adf:caption`. Attributes:
`layout` (`align-end` `align-start` `center` `full-width` `wide` `wrap-left` `wrap-right`), `layout` (`align-end` `align-start` `center` `full-width` `wide` `wrap-left` `wrap-right`),
`localId` (string), `width` (number), `widthType` (`percentage` `pixel`). `localId` (string), `width` (number), `widthType` (`percentage` `pixel`).
- `caption` — container, inline body. Attributes: `localId` (string). - `caption` — container, inline body. Attributes: `localId` (string).
- `mediaGroup` — container of `::media` leaves. Attributes: none. - `mediaGroup` — container of `!adf:media` leaves. Attributes: none.
``` ```
::::mediaSingle {layout=center width=50} !adf:mediaSingle {layout=center width=50}
::media {collection=MediaServicesSample id=4478e39c-cf9b-41d1-ba92-68589487cd75 type=file} !adf:media {collection=MediaServicesSample id=4478e39c-cf9b-41d1-ba92-68589487cd75 type=file}
:::caption !adf:caption
The moon, at night. The moon, at night.
::: !adf:/caption
:::: !adf:/mediaSingle
``` ```
**The CommonMark image.** A paragraph whose entire inline content is one image `![alt](url)` is **The CommonMark image.** A paragraph whose entire inline content is one image `![alt](url)` is
@@ -319,27 +321,27 @@ inline layer's ordinary CommonMark escaping yields the pipe; each cell is the in
one paragraph, trimmed; canonical form pads cells with single spaces and ends rows with `|` one paragraph, trimmed; canonical form pads cells with single spaces and ends rows with `|`
(optional in input). Named errors: a delimiter or body row whose cell count differs from the (optional in input). Named errors: a delimiter or body row whose cell count differs from the
header's, and an alignment colon in the delimiter row — ADF holds no column alignment. In a header's, and an alignment colon in the delimiter row — ADF holds no column alignment. In a
pipe cell a hard break is `:hardBreak{}` and a literal `|` is `\|`; a `|` inside a quoted pipe cell a hard break is `!adf:hardBreak{}` and a literal `|` is `\|`; a `|` inside a quoted
attribute value is already `\u007c`, so the split never reaches it. attribute value is already `\u007c`, so the split never reaches it.
The directive form nests cells as containers of block content inside `tableRow` containers: The directive form nests cells as containers of block content inside `tableRow` containers:
``` ```
:::::table {isNumberColumnEnabled=true width=760} !adf:table {isNumberColumnEnabled=true width=760}
::::tableRow !adf:tableRow
:::tableHeader {colspan=2 colwidth="[340,420]"} !adf:tableHeader {colspan=2 colwidth="[340,420]"}
Assembly Assembly
::: !adf:/tableHeader
:::: !adf:/tableRow
::::tableRow !adf:tableRow
:::tableCell {background="#deebff"} !adf:tableCell {background="#deebff"}
Bolt M8 Bolt M8
::: !adf:/tableCell
:::tableCell {valign=top} !adf:tableCell {valign=top}
40 40
::: !adf:/tableCell
:::: !adf:/tableRow
::::: !adf:/table
``` ```
- `table` — container of `tableRow` containers. Attributes: `displayMode` (`default` `fixed`), - `table` — container of `tableRow` containers. Attributes: `displayMode` (`default` `fixed`),
@@ -363,14 +365,14 @@ Bolt M8
free-form; the editor writes `DECIDED`). free-form; the editor writes `DECIDED`).
``` ```
::::taskList {localId=0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90} !adf:taskList {localId=0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90}
:::taskItem DONE {localId=0198f3a2-8d52-70b1-8c4f-5e9f3d2b7ca1} !adf:taskItem DONE {localId=0198f3a2-8d52-70b1-8c4f-5e9f3d2b7ca1}
Write the spec Write the spec
::: !adf:/taskItem
:::taskItem TODO {localId=0198f3a2-9e63-7d80-a15b-6fa04e3c8db2} !adf:taskItem TODO {localId=0198f3a2-9e63-7d80-a15b-6fa04e3c8db2}
Ship it Ship it
::: !adf:/taskItem
:::: !adf:/taskList
``` ```
### Layout ### Layout
@@ -380,14 +382,14 @@ Ship it
`middle` `top`), `width` (number — percent). `middle` `top`), `width` (number — percent).
``` ```
::::layoutSection !adf:layoutSection
:::layoutColumn {width=50} !adf:layoutColumn {width=50}
Left. Left.
::: !adf:/layoutColumn
:::layoutColumn {width=50} !adf:layoutColumn {width=50}
Right. Right.
::: !adf:/layoutColumn
:::: !adf:/layoutSection
``` ```
### Extensions ### Extensions
@@ -399,7 +401,7 @@ Right.
- `extensionFrame` — container, block body. Attributes: none. - `extensionFrame` — container, block body. Attributes: none.
``` ```
::extension {extensionKey=toc extensionType="com.atlassian.confluence.macro.core" parameters="{\"maxLevel\":2}"} !adf:extension {extensionKey=toc extensionType="com.atlassian.confluence.macro.core" parameters="{\"maxLevel\":2}"}
``` ```
### Sync blocks ### Sync blocks
@@ -408,7 +410,7 @@ Right.
`localId` (string), `resourceId` (string). `localId` (string), `resourceId` (string).
``` ```
::syncBlock {localId=0198f3a2-af74-7e91-b26c-70b15f4d9ec3 resourceId="ari:cloud:confluence:site/page/123"} !adf:syncBlock {localId=0198f3a2-af74-7e91-b26c-70b15f4d9ec3 resourceId="ari:cloud:confluence:site/page/123"}
``` ```
## Inline nodes ## Inline nodes
@@ -418,7 +420,7 @@ the nodes below, `emoji`, `mention` and `status` spell their `text` attribute in
as plain text: `[]` is the empty string, absent content is the absent attribute, non-empty content as plain text: `[]` is the empty string, absent content is the absent attribute, non-empty content
parsing to anything but one unmarked text node — adjacent text nodes with identical marks and no parsing to anything but one unmarked text node — adjacent text nodes with identical marks and no
attributes merged first — is a named error, and so is a `text` key in `{attrs}`. An enclosing mark attributes merged first — is a named error, and so is a `text` key in `{attrs}`. An enclosing mark
spelling does not reach into the slot. The rest take no content, `:text` included; content on a spelling does not reach into the slot. The rest take no content, `!adf:text` included; content on a
node that takes none is a named error. node that takes none is a named error.
- `date` — Attributes: `localId` (string), `timestamp` (string, epoch milliseconds). - `date` — Attributes: `localId` (string), `timestamp` (string, epoch milliseconds).
@@ -436,15 +438,15 @@ node that takes none is a named error.
(string), `style` (string), `text` (string). (string), `style` (string), `text` (string).
``` ```
:status[In review]{color=yellow} — :mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4} !adf:status[In review]{color=yellow} — !adf:mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4}
Shipped :emoji[🎉]{shortName=":tada:"} on :date{timestamp=1756080000000}. Shipped !adf:emoji[🎉]{shortName=":tada:"} on !adf:date{timestamp=1756080000000}.
``` ```
**Whitespace CommonMark cannot hold.** A newline inside a text node, and a space or tab where **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, 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 an em, strong or strike spelling's inner edges, a pipe cell's edges — is spelled
`:text{text="…"}`, the reserved key carrying the node's text, escaped by the attribute grammar `!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 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 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 attributes (AGENTS.md §2). Input reads that spelling alone: the value is one run of spaces and
@@ -452,14 +454,14 @@ tabs, or one run of newlines, and anything else — a mixed run, or text CommonM
is a named error. is a named error.
``` ```
:text{text=" "}Two leading spaces held, and one text node split:text{text="\n"}over two lines. !adf:text{text=" "}Two leading spaces held, and one text node split!adf:text{text="\n"}over two lines.
``` ```
## Marks ## Marks
An inline node's marks ride the spelling wrapped around them, never the block sections' reserved An inline node's marks ride the spelling wrapped around them, never the block sections' reserved
`marks` key. `code`, `em`, `link`, `strike` and `strong` keep their markdown spellings, and are `marks` key. `code`, `em`, `link`, `strike` and `strong` keep their markdown spellings, and are
not directive names: `:em[x]` is a named error. `border`, `subsup`, `textColor` and `underline` not directive names: `!adf:em[x]` is a named error. `border`, `subsup`, `textColor` and `underline`
are inline directives, content required non-empty. are inline directives, content required non-empty.
- `border` — Attributes: `color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 1–3). - `border` — Attributes: `color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 1–3).
@@ -470,7 +472,8 @@ are inline directives, content required non-empty.
- `underline` — Attributes: none. - `underline` — Attributes: none.
A spelling adds its mark to every inline node it wraps, and nesting is the marks array in order, A spelling adds its mark to every inline node it wraps, and nesting is the marks array in order,
outermost first: `_:underline[x]_` gives marks `[em, underline]`, `:underline[_x_]` the reverse. outermost first: `_!adf:underline[x]_` gives marks `[em, underline]`, `!adf:underline[_x_]` the
reverse.
`adfToMarkdown` nests in the order the array holds rather than sorting it — §2's equality `adfToMarkdown` nests in the order the array holds rather than sorting it — §2's equality
restores the array, not a set — and opens each spelling once over the longest run of adjacent restores the array, not a set — and opens each spelling once over the longest run of adjacent
inline nodes carrying an identical mark, attributes included, at that depth. A run breaks at every inline nodes carrying an identical mark, attributes included, at that depth. A run breaks at every
@@ -488,7 +491,7 @@ opaque carry inside a mark spelling is a named error in input: the carry restore
exactly, marks included (AGENTS.md §3). exactly, marks included (AGENTS.md §3).
``` ```
:textColor[**Overdue**]{color="#ae2e24"}, H:subsup[2]{type=sub}O, :underline[signed]. !adf:textColor[**Overdue**]{color="#ae2e24"}, H!adf:subsup[2]{type=sub}O, !adf:underline[signed].
:border[:mediaInline{collection=contentId-98237 id=01a032c3-7a90-70c9-88f6-c60f710eda07}]{color="#091e42" size=2} !adf:border[!adf:mediaInline{collection=contentId-98237 id=01a032c3-7a90-70c9-88f6-c60f710eda07}]{color="#091e42" size=2}
``` ```