From 365a2d2ce0fe604410be322be6757c1ca4049cf1 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 10:02:51 +0200 Subject: [PATCH 1/5] Flavour spec 1c: inline node syntaxes and marks --- README.md | 10 ++--- spec/flavour.md | 98 +++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 87 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 819053c..a70ce34 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/spec/flavour.md b/spec/flavour.md index c91e5f0..747fee4 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -1,19 +1,19 @@ # 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, +opens a pipe table or runs `~~` 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. ## 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). - 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 @@ -37,15 +37,15 @@ 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. 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. Each section below says whether content and attrs are 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. @@ -184,9 +184,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 +301,68 @@ 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 six nodes below, those with a `text` attribute spell it in the content slot as plain text: +content parsing to anything but one unmarked text node is a named error, absent content is the +absent attribute, and a `text` key in `{attrs}` is a named error. The rest take no content. + +- `date` — `localId` (string), `timestamp` (string, epoch milliseconds). +- `emoji` — `id` (string), `localId` (string), `shortName` (string, `:name:`), `text` (string). +- `inlineCard` — `data` (json), `localId` (string), `url` (string); a card carries one or the + other, never both. +- `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); content required. + +``` +: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, +a mark 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 and a text node's 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`. The other four are inline +directives, content required: + +- `border` — `color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 1–3). +- `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 that mark at that depth. + +A text node whose marks no nesting spells — a mark type not listed here, an attribute no +spelling holds, or an order putting a code span outside another mark — rides the inline carry +whole. An opaque carry inside a mark spelling is a named error: 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.52.0 From 13686b01c6aec5fa7a928069af7918c8ccc460f7 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 10:24:17 +0200 Subject: [PATCH 2/5] Pin the code-span and strikethrough grammars, break mark runs at a carry --- spec/flavour.md | 65 ++++++++++++++++++++++++++++--------------------- todo.md | 4 +-- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/spec/flavour.md b/spec/flavour.md index 747fee4..53a0273 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -1,11 +1,11 @@ # The markdown flavour The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain -CommonMark is a subset with three carve-outs: literal text that matches directive syntax below, -opens a pipe table or runs `~~` 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. +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 @@ -13,7 +13,9 @@ Per-node syntaxes build on this grammar in the sections below. normalizes to it through the round-trip. - Emphasis `_em_`, strong `**strong**`, strike `~~strike~~`; `*` replaces `_` only where `_` - cannot parse (intra-word). + 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,9 @@ 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, padded with + one space on each side where the text begins or ends with a backtick, or begins and ends with a + space without being all spaces. - 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,18 +42,19 @@ 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 and mark 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 section below says whether content and attrs are 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. +literal bracket. A space at either edge of `[content]` is part of it. Each section below says +whether content and attrs are 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. **Container block**: @@ -306,8 +312,9 @@ attributes. The other three share: `extensionKey` (string), `extensionType` (str Attributes and the carry fallback read as in the block sections, the carry in its inline form. Of the six nodes below, those with a `text` attribute spell it in the content slot as plain text: -content parsing to anything but one unmarked text node is a named error, absent content is the -absent attribute, and a `text` key in `{attrs}` is a named error. The rest take no content. +`[]` is the empty string, absent content is the absent attribute, non-empty content parsing to +anything but one unmarked text node is a named error, and so is a `text` key in `{attrs}`. The +rest take no content. - `date` — `localId` (string), `timestamp` (string, epoch milliseconds). - `emoji` — `id` (string), `localId` (string), `shortName` (string, `:name:`), `text` (string). @@ -329,10 +336,11 @@ 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, -a mark 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). +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. @@ -342,8 +350,8 @@ and pad. The emitter wraps the whitespace run alone and leaves the rest plain te An inline node's marks and a text node's 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`. The other four are inline -directives, content required: +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: - `border` — `color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 1–3). - `subsup` — `type` (`sub` `sup`). @@ -354,12 +362,13 @@ A spelling adds its mark to every inline node it wraps, and nesting is the marks 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 that mark at that depth. +inline nodes carrying that mark at that depth. A run breaks at every node the emitter carries, so +no emitted carry sits inside a mark spelling. -A text node whose marks no nesting spells — a mark type not listed here, an attribute no -spelling holds, or an order putting a code span outside another mark — rides the inline carry -whole. An opaque carry inside a mark spelling is a named error: the carry restores its node -exactly, marks included (AGENTS.md §3). +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]. diff --git a/todo.md b/todo.md index 1de24d6..6f38155 100644 --- a/todo.md +++ b/todo.md @@ -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. -- 2.52.0 From 94c0274f0efe8437a2d22db562682d1f3a302319 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 10:34:14 +0200 Subject: [PATCH 3/5] Attach the code-span padding rule to the text it pads --- spec/flavour.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/flavour.md b/spec/flavour.md index 53a0273..02d4e8c 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -24,9 +24,9 @@ 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, padded with - one space on each side where the text begins or ends with a backtick, or begins and ends with a - space without being all spaces. +- 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. - 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{}`. @@ -191,9 +191,9 @@ 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: `mediaInline` carries a media -`collection` + `id`, never a URL, and no media node carries a title. +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 -- 2.52.0 From e8a23110e88c8e8dbe5eb87715363ac50b4b9028 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 10:56:36 +0200 Subject: [PATCH 4/5] Make code-span content literal, pin the content slot's edges, order and errors --- AGENTS.md | 2 +- spec/flavour.md | 31 +++++++++++++++++-------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2fd056b..ab0d639 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/spec/flavour.md b/spec/flavour.md index 02d4e8c..9ce7441 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -26,7 +26,8 @@ normalizes to it through the round-trip. 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. + 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{}`. @@ -51,10 +52,11 @@ 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. A space at either edge of `[content]` is part of it. Each section below says -whether content and attrs are 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. +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 and attrs are 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. **Container block**: @@ -89,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 @@ -313,20 +315,21 @@ attributes. The other three share: `extensionKey` (string), `extensionType` (str Attributes and the carry fallback read as in the block sections, the carry in its inline form. Of the six 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 is a named error, and so is a `text` key in `{attrs}`. The -rest take no content. +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}`. 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); a card carries one or the - other, never both. +- `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); content required. + `style` (string), `text` (string). ``` :status[In review]{color=yellow} — :mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4} @@ -362,8 +365,8 @@ A spelling adds its mark to every inline node it wraps, and nesting is the marks 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 that mark at that depth. A run breaks at every node the emitter carries, so -no emitted carry sits inside a mark spelling. +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 -- 2.52.0 From 034f4457a1c37a1064d281bcd20fcbdbee28ce6d Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 11:07:38 +0200 Subject: [PATCH 5/5] Stop a mark spelling at the content slot, require mark content non-empty --- spec/flavour.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/spec/flavour.md b/spec/flavour.md index 9ce7441..0a92971 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -53,8 +53,8 @@ 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. Whitespace at either edge of `[content]`, space or tab, is part of it and -survives inline parsing. Each section below says whether content and attrs are required. `:` opens -a directive only when the name is followed immediately by `[` or `{`, and `{attrs}` must follow +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. @@ -313,11 +313,12 @@ attributes. The other three share: `extensionKey` (string), `extensionType` (str ## Inline nodes Attributes and the carry fallback read as in the block sections, the carry in its inline form. Of -the six 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}`. The rest take no content, `:text` included; -content on a node that takes none is a named error. +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). @@ -351,10 +352,10 @@ the rest plain text; `markdownToAdf` merges adjacent text nodes carrying identic ## Marks -An inline node's marks and a text node's 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: +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, 1–3). - `subsup` — `type` (`sub` `sup`). -- 2.52.0