Flavour spec 1c: inline node syntaxes and marks #5

Merged
lilleman merged 5 commits from flavour-inlines into main 2026-08-24 11:08:38 +02:00
4 changed files with 105 additions and 26 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ Round-trip equality is a property tested over a corpus, not a claim made in pros
- Directives, one grammar for everything markdown lacks: `:::panel info``:::` blocks,
`:mention[@Mikael]{id=5b10a2}` inline. Prior art: CommonMark's generic-directives proposal.
- Plain CommonMark is a subset, with carve-outs (`spec/flavour.md`): literal text shaped like a
directive or a pipe table 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.
- Identity-bearing nodes carry their ids in attributes; a document is only portable within its
site — accepted.
+5 -5
View File
@@ -38,11 +38,11 @@ isAdfDocument(v: unknown): v is AdfDocument
(AGENTS.md §3).
- `htmlToAdf(adfToHtml(doc))` equals `doc` — fidelity HTML cannot express rides `data-*`
attributes.
- Plain CommonMark is valid input to `markdownToAdf`, with two carve-outs — literal text
matching directive or pipe-table syntax is claimed (escapable — `spec/flavour.md`) — and one
gap: a CommonMark image fits only as its own title-less paragraph; mid-text and titled images
are error results. Converting back yields the library's canonical spelling, which round-trips
byte-identically.
- Plain CommonMark is valid input to `markdownToAdf`, with three carve-outs — literal text
matching directive, pipe-table or strikethrough syntax is claimed (escapable —
`spec/flavour.md`) — and one gap: a CommonMark image fits only as its own title-less paragraph;
mid-text and titled images are error results. Converting back yields the library's canonical
spelling, which round-trips byte-identically.
- Foreign HTML maps a documented element set; an unmappable element is an error, never a silent
drop. Well-formed HTML only — no tag-soup recovery.
- The emitted formats are semver surface (AGENTS.md §8).
+97 -18
View File
@@ -1,19 +1,21 @@
# The markdown flavour
The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain
CommonMark is a subset with two carve-outs: literal text that matches directive syntax below or
opens a pipe table is claimed by the flavour (escape the `:` or `|` to keep it literal) — 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 contract (AGENTS.md §8). Per-node syntaxes build on
this grammar in the sections below (inline nodes and marks: todo.md 1c).
CommonMark is a subset with three carve-outs: literal text that matches directive syntax below or
opens 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 CommonMark image fits only as its own
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.
## Canonical form
`adfToMarkdown` emits exactly one spelling; every CommonMark variant of the same document
normalizes to it through the round-trip.
- Emphasis `_em_`, strong `**strong**`; `*` replaces `_` only where `_` cannot parse
(intra-word).
- 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 taken from the
node's `order` attribute. Continuation lines align with the first character after the marker
(two spaces for `- `, three for `1. `); blank lines inside an item are empty lines. Lists are
@@ -22,6 +24,10 @@ normalizes to it through the round-trip.
- 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; 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 a heading — it is `:hardBreak{}`.
@@ -37,15 +43,17 @@ normalizes to it through the round-trip.
## Directives
One grammar for everything CommonMark lacks. A directive name is `[a-z][A-Za-z0-9]*` — the ADF
node names. 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 — so output an old emitter escaped stays escaped, and erroring input gaining
meaning later is MINOR, never a reparse (§8). The name `adf` is reserved for the opaque carry, as
both directive name and fence info string.
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 — so output an old
emitter escaped stays escaped, and erroring input gaining meaning later is MINOR, never a reparse
(§8). The name `adf` is reserved for the opaque carry, as both directive name and fence info
string.
**Inline**: `: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. Each node's section says whether content and attrs are required. `:` opens 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. `:` opens a
directive only when the name is followed immediately by `[` or `{`, and `{attrs}` must follow
`]` (or the name) with no gap — anything else (`10:30`, `:smile:`, a stray `{…}` in text) is
literal text.
@@ -83,8 +91,8 @@ form, and omits empty `{attrs}` except where the `{` itself claims the directive
(`:hardBreak{}`).
**Escaping**: the emitter backslash-escapes whatever literal text would otherwise parse as
directive syntax — the leading `:` of a would-be directive, `]` inside content; a backslash
before `:` in input always yields a literal colon.
directive syntax — the leading `:` of a would-be directive, `]` inside content; outside code
spans and code blocks, a backslash before `:` in input yields a literal colon.
**Malformed directives are error results**, named: an unclosed container at end of input, a body
fence line of the container's length or longer, a bare colon-run line outside any container or
@@ -184,9 +192,10 @@ The moon, at night.
**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. `adfToMarkdown` emits the image
form for exactly that shape — those attrs and no others, no marks, no caption. An image amid
other text, or one carrying a title, is a named error (ADF has a slot for neither).
from the destination, `alt` the description's plain-text content when non-empty. `adfToMarkdown`
emits the image form for exactly that shape — those attrs and no others, no marks, no caption.
An image amid other text, or one carrying a title, is a named error: `mediaInline` carries a
media `collection` + `id`, never a URL, and no media node carries a title.
### Tables
@@ -300,3 +309,73 @@ attributes. The other three share: `extensionKey` (string), `extensionType` (str
```
::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, those with a `text` attribute spell it 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 unmarked text node — adjacent identical-mark text nodes 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 node that takes none is a named
error.
- `date``localId` (string), `timestamp` (string, epoch milliseconds).
- `emoji``id` (string), `localId` (string), `shortName` (string, `:name:`), `text` (string).
- `inlineCard``data` (json), `localId` (string), `url` (string); real payloads carry one or
the other.
- `mediaInline``alt` (string), `collection` (string), `data` (json), `height` (number), `id`
(string), `localId` (string), `occurrenceKey` (string), `type` (`file` `image` `link`), `width`
(number).
- `mention``accessLevel` (`APPLICATION` `CONTAINER` `NONE` `SITE`), `id` (string), `localId`
(string), `text` (string), `userType` (`APP` `DEFAULT` `SPECIAL`).
- `status``color` (`blue` `green` `neutral` `purple` `red` `yellow`), `localId` (string),
`style` (string), `text` (string).
```
:status[In review]{color=yellow} — :mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4}
Shipped :emoji[🎉]{shortName=":tada:"} on :date{timestamp=1756080000000}.
```
**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,
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
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
(AGENTS.md §2).
```
:text{text=" "}Two leading spaces held, and one text node split:text{text="\n"}over two lines.
```
## Marks
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, which hold
no attributes beyond `link`'s `href` and `title`, and are not directive names: `:em[x]` is a named
error. The other four are inline directives, content required non-empty:
- `border``color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 13).
- `subsup``type` (`sub` `sup`).
- `textColor``color` (string, `#rrggbb`).
- `underline` — no attributes.
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.
`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
inline nodes carrying an identical mark, attributes included, at that depth. A run breaks at every
node the emitter carries, so no emitted carry sits inside a mark spelling.
An inline node whose marks no nesting spells — a mark type not listed here, an attribute no
spelling holds, an order putting a code span outside another mark, or `code` on text holding a
newline — rides the inline carry whole. An opaque carry inside a mark spelling is a named error
in input: the carry restores its node exactly, marks included (AGENTS.md §3).
```
:textColor[**Overdue**]{color="#ae2e24"}, H:subsup[2]{type=sub}O, :underline[signed].
:border[:mediaInline{collection=contentId-98237 id=01a032c3-7a90-70c9-88f6-c60f710eda07}]{color="#091e42" size=2}
```
+2 -2
View File
@@ -58,5 +58,5 @@ syntax, the rest rides the opaque carry (§3) until it does too.
| Marks | `border` `code` `em` `link` `strike` `strong` `subsup` `textColor` `underline` |
Plain markdown covers `blockquote`, `bulletList`, `codeBlock`, `heading`, `orderedList`,
`paragraph`, `rule`, `listItem`, `hardBreak`, `text`, and the `code`, `em`, `link`, `strike` and
`strong` marks. Everything else is what the flavour is for.
`paragraph`, `rule`, `listItem`, `hardBreak`, `text`, and the `code`, `em`, `link` and `strong`
marks; `strike` is the flavour's `~~` carve-out. Everything else is what the flavour is for.