3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 11:48:29 +01:00
Files
adf-codec/spec/flavour.md
T

602 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# The markdown flavour
The grammar of the lossless flavour, the extended markdown `adfToLosslessMarkdown` emits and
`losslessMarkdownToAdf` parses. 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 spells `strike` (escape the `!adf:`, `|` or `~` to keep it
literal), and a code fence whose info string opens `adf:` is the opaque carry (drop the info string
and wrap the fence in `!adf:codeBlock {language="adf:…"}` to keep it code). `commonMarkToAdf`
claims none of the four. The emitted form is contract (`docs/decisions.md`
§The formats are API). Per-node syntaxes build on this grammar in the sections below.
## Canonical form
`adfToLosslessMarkdown` emits exactly one spelling; every CommonMark variant of the same document
normalizes to it through the round-trip.
- Emphasis `_em_`, strong `**strong**`, strike `~~strike~~`; `*` replaces `_` only where `_`
cannot parse (intra-word). Strike is GFM strikethrough narrowed to exactly two tildes — a
single tilde or a run of three or more is literal text — and block structure resolves before
inline, so a `~~~` line opens a CommonMark tilde code fence.
- Bullet lists `- `; ordered lists incrementing `1.` `2.` `3.`, the first number the node's `order`
attribute. CommonMark starts a new list where the bullet character or the delimiter changes, so
adjacent lists of a kind alternate markers, `- ` `* ` `- ` and `1.` `1)` `1.`. An item whose first
line would read as a rule after `* ` opens with a backslash, `* \**`. Continuation lines align
with the first character after the marker (two spaces for `- `, three for `1. `); blank lines
inside an item are empty lines, none between a nested list and a CommonMark block above it — one
wherever the nested list's own marker cannot interrupt a paragraph (an ordered list whose first
number is not 1, or a list whose first item is empty), whatever block sits above it. Blank lines
between items normalize away. A list whose item holds a line of spaces or tabs alone, which a list
item reads back empty, takes the directive form.
- Blockquotes prefix lines with `> `; a blank line inside a blockquote is a bare `>`.
- ATX headings (`#` … `######`); setext input normalizes to ATX.
- Code fences ``` with the node's language as info string, the fence lengthened past any backtick
run in the content — the longest run anywhere plus one, at least three, counting mid-line runs
no closing fence could match; indented-code input normalizes to fences.
- Code spans: a backtick string one longer than the longest backtick run in the text, the text
padded with one space on each side where it begins or ends with a backtick, or begins and ends
with a space without being all spaces. The content is literal — inline parsing does not see
inside it, directive syntax and `~~` included.
- Thematic break `---`.
- 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
carries an attribute, it is the inline directive.
- An empty paragraph — real payloads carry them — is an `!adf:paragraph` … `!adf:/paragraph` pair
holding nothing, and one whose `content` is an empty array the pair
`!adf:paragraph {content=empty}` … `!adf:/paragraph` (Attributes).
- 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
unbalanced, and a quote inside the title; a balanced pair stays bare. `<url>` autolink form only
when the text equals the destination and the destination is a valid CommonMark autolink
(absolute URI) — inside an inline directive's `[content]`, one holding no backtick, no
unbalanced bracket and no inline directive opener.
- Paragraphs on one line — no soft wrapping; a soft line break in input becomes a single space.
- Entity references in input decode to their characters; output backslash-escapes only where text
would otherwise parse as syntax, scanning the assembled line rather than each text node: escape
the leading delimiter of a construct that would otherwise open, re-scan from there, and repeat.
A backtick run escapes whole. The escaped line is then read back. Where it opens a code span at a
text backtick, every text backtick in the line escapes: CommonMark reads no escape inside a code
span, so a backtick string beside an escape can still close one. Where it opens a link or an
image at a text `[`, every text `[` in the line escapes. Where the reader refuses the line and
records no code span, link or image opening at a text backtick or `[`, every text backtick and
`[` in the line escapes.
Where a paragraph opens with what reads as a link reference definition, which resolves before
any inline construct binds (a `]` inside a code span or `{attrs}` counts), an opening text `[`
escapes and an opening link takes the directive link (Marks).
An emphasis delimiter run in text escapes where CommonMark can open **or** close with it, so
`*not emphasis*` is `\*not emphasis\*` — no delimiter the emitter did not write reaches the
matching below, which is what lets the emitter decide its own pairings.
- Blocks separated by one blank line at document level, inside a blockquote and between CommonMark
blocks; inside a directive container a pair holding a directive block takes none. No trailing
whitespace outside a code block's content, single trailing newline. A document whose `content` is
an empty array is the empty string, and markdown holding no block reads back to it; a document
holding no `content` key is the leaf `!adf:doc {content=none}` as its only block, which is a
named error anywhere else or spelled any other way.
## Directives
One grammar for everything CommonMark lacks, namespaced: every directive opens with the literal
`!adf:`. A directive name is `[a-z][A-Za-z0-9]*` — the ADF node and mark names the sections below
spell as directives. Recognition is syntactic and name-set-independent: anything matching the forms
below parses as a directive regardless of whether the name is known, and an unknown name is an error
result naming it at the opener, whatever follows it — so output an old emitter escaped stays
escaped, and erroring input gaining meaning later is MINOR, never a reparse (`docs/decisions.md`
§The formats are API). Each name belongs to one position, and a name the other one spells — a mark
or an inline node written as a block directive, a block node written inline — is a different error,
naming the spelling it takes. Three reserved names read back to no node: `carry` for the inline
opaque carry, `doc` for a document holding no `content` key (Canonical form), and `textBreak` for
the leaf that parts two text nodes (Inline nodes). Every fence info string opening `adf:` is
reserved for the block carry (The opaque carry).
**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 construct CommonMark binds first — a code span,
an autolink URI, a link destination or title — holds its `!adf:` as content; a directive's
`[content]`, which binds ahead of bracket matching, does not. 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
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, and `{attrs}` must
follow `]` (or the name) with no gap. An inline directive binds as a unit before bracket matching,
the way a code span does: a `]` or `(` inside its `{attrs}` is the directive's, never the enclosing
content's, and a `(` after its closing `]` opens no link.
**Block container**:
```
!adf:name arg {attrs}
block content
!adf:/name
```
`arg` is one optional bare token whose meaning each node defines (e.g. the panel type). The body is
block markdown. The closer names the innermost open container and carries nothing after the name;
one naming another node, or standing where no container is open, is a named error. Opening and
closing is what nests, so the opener reads the same at every depth, and a closer crosses no list
item or blockquote edge — a container opened inside one closes inside it. Directive block lines
follow code-fence indentation (up to three spaces relative to their container); an `!adf:` inside a
code fence or opaque carry is content.
**Block leaf**: `!adf:name arg {attrs}` — the same opener with no closer, `arg` reading as 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 (`docs/decisions.md` §The formats are API).
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
directive block line is tolerated in input, never emitted.
**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
with JSON string escaping (`\"` `\\` `\n` `\t` `\uXXXX`, …) — total over
Unicode, and raw newlines never appear inside quotes. A quoted value also escapes `` ` ``, `&`,
`<` and `|` as `\u0060`, `\u0026`, `\u003c` and `\u007c`; `*`, `_`, `~`, `[`, `]` and `(` resolve
after a directive binds and stay raw. The closing `}` is the first one outside quotes, since a
quoted value holds `}` unescaped. All values are strings at the grammar level; each node's section
assigns types.
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 `{` is what
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}` on a block line
or after a `[content]`, and a number or `json` value outside its canonical JSON spelling are each a
named error naming the spelling to write instead.
`attrs`, `content` and `marks` are reserved keys on every directive — block, inline node and mark —
whose bare value `empty` spells the node's or mark's key holding an empty object or array:
`!adf:underline[a]{attrs=empty}`, `!adf:date{content=empty}`, `!adf:hardBreak{marks=empty}`. A
container spelling `content=empty` closes with no body; `attrs=empty` stands beside no other
attribute, argument or content slot; and an inline node spelling `marks=empty` stands inside no
mark spelling. Any other value of a reserved key is a named error, except on a block's `marks`
(Block nodes).
**Escaping**: the emitter backslash-escapes whatever literal text would otherwise parse as
directive syntax — every literal `!adf:`, `]` inside content, a bracket a link's destination and
title inside content leave unbalanced, a backtick there that would open a code span, a `{` right
after a directive's closing `]`, which would otherwise be read as the attributes it has none of;
outside code spans and code blocks, `\!adf:` in input yields the literal text.
**Malformed directives are error results**, named: an unclosed container at end of input, a closer
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 the round-trip refuses.
## The opaque carry
A node no section spells where it stands (`docs/decisions.md` §Unknown nodes ride the carry) — 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 `adf:` and the node's type, body = the
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
string `adf:` and keeps `type` in the body. A body holding `type` under a named type, or a fence
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),
JSON-string-escaped into the attribute.
Every info string opening `adf:` is reserved: a genuine `codeBlock` whose `language` opens so takes
the attribute the section below keeps for a language no info string holds, so the reservation
stays absolute. In block-directive position `!adf:carry` is a named error — the carry's block form
is the fence.
## Raw HTML in input
CommonMark input may contain raw HTML. Every markdown reader routes each construct through the
foreign HTML element mapping (`docs/decisions.md` §Foreign HTML sorts three ways) — 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
The directive name is always the ADF node type. A container's body is the node's `content`; a leaf
has none. Every directive parses in any position — `losslessMarkdownToAdf` builds exactly what is
written; validity against ADF's content models stays the author's business (`docs/decisions.md` §No
schema validation). It parses only in the form the emitter picks, though: a directive spelling a
node the emitter would 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"`); `numbers`, a list of one number or more, as those spellings joined by commas
(`colwidth="340,420"`); and `json` values as the inline carry's serialization (compact, keys
sorted), quoted. `-0` is spelled `-0`. `losslessMarkdownToAdf` builds an `attrs`, `content` or
`marks` key only where the markdown spells one, an empty one through its reserved key (Attributes), so a document
reads back deep-equal (`docs/decisions.md` §Equality is deep). A node CommonMark spells takes the
directive form to hold an empty key.
Marks on a block node ride the reserved attribute key `marks`. It holds the marks in the array's
order, one space apart, each as its type followed by its `{attrs}` as a directive spells them:
`!adf:layoutSection {marks="breakout{mode=wide}"}`,
`!adf:paragraph {marks="indentation{level=2} fragment{localId=f1 name=\"Price list\"}"}`. Where
§Marks does not list a mark's type or one of its attributes, or a value holds another type than
§Marks gives it, the whole array is a `json` value instead:
`marks="[{\"attrs\":{\"color\":\"#fff\"},\"type\":\"backgroundColor\"}]"`. An empty array is
`marks=empty`.
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 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
list, a value that is not the section's type, or an arg-slot value that is no bare token. In
markdown input the same mismatch is a named error.
### The CommonMark blocks
CommonMark spells `blockquote`, `bulletList`, `codeBlock`, `heading`, `listItem`, `orderedList`,
`paragraph` and `rule`, and keeps that spelling wherever it holds what the node carries. What it
cannot — `localId` (string) on any of them, marks, and the values below — takes the directive
form.
- `blockquote`, `bulletList`, `listItem` — containers, block body. Attributes: `localId` (string).
- `codeBlock` — container, body one fenced code block per text node, each fence's info string the
language and its content the node's text; a node holding no `content` key is one empty fence.
Attributes: `hideLineNumbers` (boolean), `language` (string), `localId` (string), `uniqueId`
(string), `wrap` (boolean). A language no info string carries back — empty, opening the reserved
`adf:`, or holding a backtick, a backslash, a control character, edge whitespace or an entity
reference — rides the `language` attribute instead and the fences carry no info string, and so
does a language beside `content=empty`, which has no fence; writing it in the slot that rule
leaves empty, or in both, is a named error, and so are fences whose info strings differ and an
empty fence beside another. Each fence is an ordinary code block, and its info string decodes
escapes and entity references as any other does. A node holding a child no fence holds — any but
a text node holding text free of carriage returns and null characters, and carrying no marks,
`attrs` or `content` — rides the block carry.
- `heading` — container, inline body. Attributes: `level` (number), `localId` (string). `level` is
the `#` count, so a heading carrying none, or one that is no whole number from 1 to 6, has no
CommonMark spelling.
- `orderedList` — container of `listItem`, block body. Attributes: `localId` (string), `order`
(number). `order` is the first marker, so a list carrying none, one that is `-0` or no whole
number from 0, or one whose markers would run past 999999999, has no CommonMark spelling.
- `paragraph` — container, inline body. Attributes: `localId` (string).
- `rule` — leaf. Attributes: `color` (string, `#rrggbb`), `localId` (string), `style` (`dashed`
`dotted` `fade` `sketch` `solid`), `weight` (number, 1–3).
````
!adf:codeBlock {localId=01a03d5c-9b21-73f4-8e6a-0c47b1d9e2f8 wrap=true}
```rust
fn main() {}
```
!adf:/codeBlock
````
### Panel
- `panel` — container; the arg is `panelType` (`custom` `error` `info` `note` `success` `tip`
`warning`). Attributes: `localId` (string), `panelColor` (string), `panelIcon` (string),
`panelIconId` (string), `panelIconText` (string) — the editor writes the last four for `custom`
panels.
```
!adf:panel warning
Check the collation before importing.
!adf:/panel
```
### Expand
- `expand`, `nestedExpand` — containers, no arg; same syntax, two node types, the name picks
which. Attributes: `localId` (string), `title` (string).
```
!adf:expand {title="Full build log"}
…
!adf:/expand
```
### The media family
- `media` — leaf. Attributes: `alt` (string), `collection` (string), `height` (number), `id`
(string), `localId` (string), `occurrenceKey` (string), `type` (`external` `file` `link`),
`url` (string), `width` (number). `file` and `link` media carry `collection` + `id`;
`external` media carry `url`.
- `mediaSingle` — container: one `!adf:media`, then optionally one `!adf:caption`. Attributes:
`layout` (`align-end` `align-start` `center` `full-width` `wide` `wrap-left` `wrap-right`),
`localId` (string), `width` (number), `widthType` (`percentage` `pixel`).
- `caption` — container, inline body. Attributes: `localId` (string).
- `mediaGroup` — container of `!adf:media` leaves. Attributes: none.
```
!adf:mediaSingle {layout=center width=50}
!adf:media {collection=MediaServicesSample id=4478e39c-cf9b-41d1-ba92-68589487cd75 type=file}
!adf:caption
The moon, at night.
!adf:/caption
!adf:/mediaSingle
```
**The CommonMark image.** A paragraph whose entire inline content is one image `![alt](url)` is a
`mediaSingle` with attrs exactly `{"layout":"center"}` holding an `external` `media` — `url` from
the destination, `alt` the description's plain-text content when non-empty — a link or image inside
it contributing its own text, a node spelling its text in the content slot contributing that text,
and a break of either kind a space. A non-empty title, `![alt](url "title")`, adds a `caption`
holding the title as one text node, and a link whose entire text is the image,
`[![alt](url)](href)`, marks the `media`. `adfToLosslessMarkdown` writes `![alt](url)` only for this
shape: a `mediaSingle` with those attrs and no others, no marks and no caption, holding a `media`
with no attrs beyond `alt`, `type` and `url`. The `media` may carry one mark, a link with no attrs
beyond `href` and `title`, written `[![alt](url)](href "title")`. Where CommonMark cannot spell the
image's alt text or URL, or the link's `href` or title, the directive form is written instead; an
empty `alt` counts as one CommonMark cannot spell.
`mediaInline` carries a media `collection` + `id`, never a URL, so anywhere else an image reads as
its alt text linked to its destination, the link titled by the image's title: beside other
content, in a heading, a table cell or an `!adf:paragraph`, and in the paragraph a portable task
marker opens. A `[!WORD]` marker with no `+` or `-` stands apart from the lines after it, and so
does a `+` or `-` callout's title line, so an image alone on the marker line or after the title
reads as the image. An empty alt text reads as the destination, as an empty link text does.
Inside a link's text, the alt text is plain text the link marks. This holds for an `!adf:link`'s
text too. After the link comes ` (image)` for each image, in the order the images stand. Each
`image` links to its image's destination and carries the image's title. An image whose destination
is the link's own takes no ` (image)`, unless the image has a non-empty title the link lacks
(`docs/decisions.md` §An image reads as an image only alone in its paragraph).
### Tables
One header row plus plain inline cells is a pipe table; anything richer is the directive form
(`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.
```
| Part | Qty |
| --- | --- |
| Bolt M8 | 40 |
```
Claiming at block level, symmetric with directives: a line opening with an unescaped `|` is
claimed and must parse as part of a pipe table, else it is a named error — escape the pipe
(`\|`) to keep it literal text. The shape a missing leading pipe leaves is claimed too: a line
holding an unescaped `|`, followed inside one paragraph by a line whose cells are all `-` runs,
alignment colons allowed, and match it in count — GFM's table without the outer pipes — is a
named error rather than the prose it reads as. A pipe table is a header row, a delimiter row whose
cells are runs of one or more `-` (canonical `---`), and body rows; rows follow code-fence
indentation.
Cells split on unescaped `|` before inline parsing — `\|` stays in the cell text, and the
inline layer's ordinary CommonMark escaping yields the pipe; each cell is the inline content of
one paragraph, trimmed; canonical form pads cells with single spaces and ends rows with `|`
(optional in input). Named errors: a delimiter row whose cell count differs from the header's,
a body row holding more cells than the header, and, in `losslessMarkdownToAdf`, a body row
holding fewer cells and an alignment colon in the delimiter row — ADF holds no column alignment.
In a 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.
`portableMarkdownToAdf` reads GFM's table: any row may drop its leading `|`, a header over a
delimiter row of one cell reads as a one-column table (`a |` over `--- |`), a body row continues
the table until a blank line or a line opening another block, a body row holding fewer cells is
padded with empty cells, a line holding no `|` that opens no other block is a body row of one cell,
and an alignment colon is read and its alignment dropped. The padding of a table may add no more
cells than its header, delimiter row and body rows hold; past that it is a named error.
The directive form nests cells as containers of block content inside `tableRow` containers:
```
!adf:table {isNumberColumnEnabled=true width=760}
!adf:tableRow
!adf:tableHeader {colspan=2 colwidth="340,420"}
Assembly
!adf:/tableHeader
!adf:/tableRow
!adf:tableRow
!adf:tableCell {background="#deebff"}
Bolt M8
!adf:/tableCell
!adf:tableCell {valign=top}
40
!adf:/tableCell
!adf:/tableRow
!adf:/table
```
- `table` — container of `tableRow` containers. Attributes: `displayMode` (`default` `fixed`),
`isNumberColumnEnabled` (boolean), `layout` (`align-end` `align-start` `center` `default`
`full-width` `wide`), `localId` (string), `width` (number, pixels).
- `tableRow` — container of cells. Attributes: `localId` (string).
- `tableCell`, `tableHeader` — containers, block body. Attributes: `background` (string),
`colspan` (number), `colwidth` (numbers, one pixel width per spanned column), `localId` (string),
`rowspan` (number), `valign` (`bottom` `middle` `top`).
### Task and decision lists
- `taskList` — container of `taskItem`, `blockTaskItem` and nested `taskList` directives.
Attributes: `localId` (string).
- `taskItem` — container, inline body; the arg is the state (`DONE` `TODO`). Attributes:
`localId` (string).
- `blockTaskItem` — container, block body; the arg is the state as `taskItem`. Attributes:
`localId` (string).
- `decisionList` — container of `decisionItem` directives. Attributes: `localId` (string).
- `decisionItem` — container, inline body. Attributes: `localId` (string), `state` (string —
free-form; the editor writes `DECIDED`).
```
!adf:taskList {localId=0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90}
!adf:taskItem DONE {localId=0198f3a2-8d52-70b1-8c4f-5e9f3d2b7ca1}
Write the spec
!adf:/taskItem
!adf:taskItem TODO {localId=0198f3a2-9e63-7d80-a15b-6fa04e3c8db2}
Ship it
!adf:/taskItem
!adf:/taskList
```
### Layout
- `layoutSection` — container of `layoutColumn` containers. Attributes: `columnRuleStyle` (`solid`),
`localId` (string).
- `layoutColumn` — container, block body. Attributes: `localId` (string), `valign` (`bottom`
`middle` `top`), `width` (number — percent).
```
!adf:layoutSection
!adf:layoutColumn {width=50}
Left.
!adf:/layoutColumn
!adf:layoutColumn {width=50}
Right.
!adf:/layoutColumn
!adf:/layoutSection
```
### Extensions
- `extension`, `bodiedExtension`, `multiBodiedExtension` — a leaf, a container with a block body,
and a container of `extensionFrame` containers. Attributes: `extensionKey` (string),
`extensionType` (string), `layout` (`default` `full-width` `wide`), `localId` (string),
`parameters` (json), `text` (string).
- `extensionFrame` — container, block body. Attributes: none.
```
!adf:extension {extensionKey=toc extensionType="com.atlassian.confluence.macro.core" parameters="{\"maxLevel\":2}"}
```
### Sync blocks
- `syncBlock`, `bodiedSyncBlock` — a leaf and a container with a block body. Attributes:
`localId` (string), `resourceId` (string).
```
!adf:syncBlock {localId=0198f3a2-af74-7e91-b26c-70b15f4d9ec3 resourceId="ari:cloud:confluence:site/page/123"}
```
## Inline nodes
Attributes and the carry fallback read as in the block sections, the carry in its inline form. Of
the nodes below, `emoji`, `mention` and `status` spell their `text` attribute in the content slot as
plain text: `[]` is the empty string, absent content is the absent attribute, non-empty content
parsing to anything but one text node carrying neither marks, attributes nor content is a named
error, and so is a `text` key in `{attrs}`. An enclosing mark spelling does not reach into the slot.
An `emoji`, `mention` or `status` whose `text` attribute holds a line ending or a null character
rides the inline carry; a content slot spelling one is a named error.
The rest take no content, `!adf:text` included; content on a node that takes none is a named error.
- `date` — Attributes: `localId` (string), `timestamp` (string, epoch milliseconds).
- `emoji` — Attributes: `id` (string), `localId` (string), `shortName` (string, `:name:`), `text`
(string).
- `hardBreak` — Attributes: `localId` (string), `text` (string).
- `inlineCard` — Attributes: `data` (json), `localId` (string), `url` (string); real payloads
carry one or the other.
- `mediaInline` — Attributes: `alt` (string), `collection` (string), `data` (json), `height`
(number), `id` (string), `localId` (string), `occurrenceKey` (string), `type` (`file` `image`
`link`), `width` (number).
- `mention` — Attributes: `accessLevel` (`APPLICATION` `CONTAINER` `NONE` `SITE`), `id` (string),
`localId` (string), `text` (string), `userType` (`APP` `DEFAULT` `SPECIAL`).
- `status` — Attributes: `color` (`blue` `green` `neutral` `purple` `red` `yellow`), `localId`
(string), `style` (string), `text` (string).
```
!adf:status[In review]{color=yellow} — !adf:mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4}
Shipped !adf:emoji[🎉]{shortName=":tada:"} on !adf:date{timestamp=1756080000000}.
```
**Text CommonMark cannot hold plainly.** A carriage return in text is `&#13;`, which CommonMark
reads back as the character. Three things are spelled `!adf:text{text="…"}`: a newline inside a text
node; a null character, which CommonMark reads as U+FFFD; 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. The reserved key carries the node's text,
escaped by the attribute grammar and never literal: pipe cells trim and pad. The emitter wraps the
run alone and leaves the rest plain text, which the spelled run joins on reading. Input reads that
spelling alone: the value is one run of spaces and tabs, one run of newlines, or one run of null
characters, 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.
```
**Adjacent text nodes.** CommonMark reads two adjacent text nodes back as one where neither is
carried, neither holds `attrs` or an empty key, and their marks are identical, attributes included.
The reserved leaf `!adf:textBreak{}` parts such a pair, inside every mark spelling the two share; a
code span holds no directive, so it closes and reopens. It builds no node and reads only between
two such nodes: elsewhere, or with `[content]` or `{attrs}`, it is a named error (`docs/decisions.md`
§`!adf:textBreak{}` parts text CommonMark would join). A text node holding `attrs`, `content` or an
empty key, or holding no text, rides the inline carry.
```
Hello, !adf:textBreak{}world — **Hello, !adf:textBreak{}world** — `a`!adf:textBreak{}`b`
```
## Marks
An inline node's marks ride the spelling wrapped around them, never the block sections' reserved
`marks` key. `code`, `em`, `strike` and `strong` keep their markdown spellings, and are not
directive names: `!adf:em[x]` is a named error. `border`, `subsup`, `textColor` and `underline` are
inline directives, content required, holding a node that takes the mark. `link` keeps its
markdown spelling wherever CommonMark holds it — `<url>` for a bare autolink-shaped text, else
`[text](url "title")` — and is the inline directive `!adf:link[text]{attrs}` only where CommonMark
does not: an attribute besides `href` and `title`, an `href` or `title` no canonical escape spells
(a control character, a backslash, an entity reference, an angle bracket beside a space or opening a
bare destination, a newline in the title), or a link opening a paragraph whose markdown spelling
would read as a link reference definition. Every such spelling carries an `href`: a directive link
CommonMark could spell is a named error, and so is one spelling none. No link wraps a link at any
nesting, which is CommonMark's own rule: a `[text]` already holding one leaves the outer brackets
literal text, and the directive form, open to no literal reading, is a named error. ADF holds no
empty text node, so a link whose text is empty reads as its destination linked, `[](/url)` as
`/url`, and one whose destination is empty too, `[]()`, as nothing. A link text holding only hard
breaks reads the same, followed by its hard breaks. A link text holding something else, but no node
that takes a link, is a named error.
- `border` — Attributes: `color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 1–3).
- `code`, `em`, `strike`, `strong` — Attributes: none.
- `link` — Attributes: `collection` (string), `href` (string), `id` (string), `occurrenceKey`
(string), `title` (string).
- `subsup` — Attributes: `type` (`sub` `sup`).
- `textColor` — Attributes: `color` (string, `#rrggbb`).
- `underline` — Attributes: none.
A block's `marks` key also holds these, which no inline spelling wraps:
- `alignment` — Attributes: `align` (`center` `end`).
- `breakout` — Attributes: `mode` (`full-width` `wide`), `width` (number).
- `fragment` — Attributes: `localId` (string), `name` (string).
- `indentation` — Attributes: `level` (number, 1–6).
A spelling adds its mark to every inline node it wraps that Atlassian's schema gives the mark. Text
takes every mark but `border`. Text marked `code` takes no other spelled mark but `link`:
``**`npm ci`**`` reads as plain code. Of the marks a spelling makes, `date`, `emoji`, `hardBreak`, `inlineCard`, `mention`
and `status` take none, and `mediaInline` takes only `border` and `link`. Nesting is the
marks array in order, outermost first: `_!adf:underline[x]_` gives marks `[em, underline]`,
`!adf:underline[_x_]` the reverse. `adfToLosslessMarkdown` nests in the order the array holds rather than sorting it — `docs/decisions.md` §Equality is deep 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: `attrs: {}` differs from no `attrs`,
and a directive spells it `{attrs=empty}`. A run breaks at every node the emitter carries, so no
emitted carry sits inside a mark spelling. An `em`, `strike`, `strong` or `link` run also crosses a
node between two of its nodes when that node cannot hold the run's mark, unless the node holds an
empty `marks`, which no spelling inside a mark holds. So `_One\` then `Two_` is one `em` run over two
texts, the hard break between them unmarked, and ``**Run `npm ci` first**`` is one `strong` run
around unmarked code. A line that needs any fallback, a carry or a dropped mark, breaks every run on
it at those nodes instead. `adfToPortableMarkdown` crosses hard breaks and code spans the same way,
and drops whitespace beside a hard break a run crosses, where CommonMark strips it.
An inline node whose marks no nesting spells — a node holding a mark Atlassian's schema withholds
from it, a mark type the first list above does not hold, an attrs key its spelling does not list, a value that is not the
spelling's type, an attribute the spelling needs and the mark lacks, an empty `attrs` on a mark CommonMark spells, an order
putting a code span outside another mark, `code` over anything but a text node holding text, or over
text holding a line ending or a null character, or a spelling CommonMark's flanking rules cannot
open or close where the run sits (`un**-real**istic`), or one CommonMark's matching pairs elsewhere
— the 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. So does every node
holding `em`, `strike` or `strong` in a line still read back wrong after 32 tries. An opaque carry inside
a mark spelling is a named error in input: the carry restores its node exactly, marks included
(`docs/decisions.md` §Unknown nodes ride the carry). An inline card a link marks, a known block holding
`text`, a known non-text inline node holding `text` or non-empty `content`, and a known block that
takes no content, such as `rule`, holding non-empty `content` don't ride the carry:
`adfToLosslessMarkdown` refuses each as `unsupported-node-shape`, and so does `losslessMarkdownToAdf`
reading a carry that holds one, since Atlassian's schema allows none of these.
```
!adf:textColor[**Overdue**]{color="#ae2e24"}, H!adf:subsup[2]{type=sub}O, !adf:underline[signed].
!adf:link[the release]{collection=contentId-98237 href="https://example.com/release notes"}
!adf:border[!adf:mediaInline{collection=contentId-98237 id=01a032c3-7a90-70c9-88f6-c60f710eda07}]{color="#091e42" size=2}
```