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
2 changed files with 39 additions and 30 deletions
Showing only changes of commit 13686b01c6 - Show all commits
+37 -28
View File
@@ -1,11 +1,11 @@
# The markdown flavour # The markdown flavour
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 with three carve-outs: literal text that matches directive syntax below, CommonMark is a subset with three carve-outs: literal text that matches directive syntax below or
opens a pipe table or runs `~~` is claimed by the flavour (escape the `:`, `|` or `~` to keep opens a pipe table is claimed by the flavour, and a matched `~~` pair spells `strike` (escape the
it literal) — and one gap: a CommonMark image fits only as its own title-less paragraph — `:`, `|` or `~` to keep it literal) — and one gap: a CommonMark image fits only as its own
mid-text and titled images are named errors. The emitted form is contract (AGENTS.md §8). title-less paragraph — mid-text and titled images are named errors. The emitted form is contract
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.
## Canonical form ## Canonical form
@@ -13,7 +13,9 @@ Per-node syntaxes build on this grammar in the sections below.
normalizes to it through the round-trip. normalizes to it through the round-trip.
- Emphasis `_em_`, strong `**strong**`, strike `~~strike~~`; `*` replaces `_` only where `_` - 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 - 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 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 (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. - ATX headings (`#``######`); setext input normalizes to ATX.
- Code fences ``` with the node's language as info string, the fence lengthened past any backtick - 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. 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 `---`. - Thematic break `---`.
- 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 a heading — it is `:hardBreak{}`. 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 ## 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. 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 node and mark names the sections below spell as directives. Recognition is syntactic and
forms below parses as a directive regardless of whether the name is known, and an unknown name is name-set-independent: anything matching the forms below parses as a directive regardless of
an error result naming it — so output an old emitter escaped stays escaped, and erroring input whether the name is known, and an unknown name is an error result naming it — so output an old
gaining meaning later is MINOR, never a reparse (§8). The name `adf` is reserved for the opaque emitter escaped stays escaped, and erroring input gaining meaning later is MINOR, never a reparse
carry, as both directive name and fence info string. (§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. **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 `[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 literal bracket. A space at either edge of `[content]` is part of it. Each section below says
directive only when the name is followed immediately by `[` or `{`, and `{attrs}` must follow whether content and attrs are required. `:` opens a directive only when the name is followed
`]` (or the name) with no gap — anything else (`10:30`, `:smile:`, a stray `{…}` in text) is immediately by `[` or `{`, and `{attrs}` must follow `]` (or the name) with no gap — anything
literal text. else (`10:30`, `:smile:`, a stray `{…}` in text) is literal text.
**Container block**: **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 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: 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 `[]` is the empty string, absent content is the absent attribute, non-empty content parsing to
absent attribute, and a `text` key in `{attrs}` is a named error. The rest take no content. 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). - `date``localId` (string), `timestamp` (string, epoch milliseconds).
- `emoji``id` (string), `localId` (string), `shortName` (string, `:name:`), `text` (string). - `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 **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,
a mark spelling's inner edges, a pipe cell's edges — is spelled `:text{text="…"}`, the reserved an em, strong or strike spelling's inner edges, a pipe cell's edges — is spelled
key carrying the node's text, escaped by the attribute grammar and never literal: pipe cells trim `:text{text="…"}`, the reserved key carrying the node's text, escaped by the attribute grammar
and pad. The emitter wraps the whitespace run alone and leaves the rest plain text; and never literal: pipe cells trim and pad. The emitter wraps the whitespace run alone and leaves
`markdownToAdf` merges adjacent text nodes carrying identical marks (AGENTS.md §2). 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. :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 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 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 spellings, which hold no attributes beyond `link`'s `href` and `title`, and are not directive
directives, content required: names: `:em[x]` is a named error. The other four are inline directives, content required:
- `border``color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 13). - `border``color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 13).
- `subsup``type` (`sub` `sup`). - `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. 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 `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 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 An inline 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 spelling holds, an order putting a code span outside another mark, or `code` on text holding a
whole. An opaque carry inside a mark spelling is a named error: the carry restores its node newline — rides the inline carry whole. An opaque carry inside a mark spelling is a named error
exactly, marks included (AGENTS.md §3). 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]. :textColor[**Overdue**]{color="#ae2e24"}, H:subsup[2]{type=sub}O, :underline[signed].
+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` | | Marks | `border` `code` `em` `link` `strike` `strong` `subsup` `textColor` `underline` |
Plain markdown covers `blockquote`, `bulletList`, `codeBlock`, `heading`, `orderedList`, Plain markdown covers `blockquote`, `bulletList`, `codeBlock`, `heading`, `orderedList`,
`paragraph`, `rule`, `listItem`, `hardBreak`, `text`, and the `code`, `em`, `link`, `strike` and `paragraph`, `rule`, `listItem`, `hardBreak`, `text`, and the `code`, `em`, `link` and `strong`
`strong` marks. Everything else is what the flavour is for. marks; `strike` is the flavour's `~~` carve-out. Everything else is what the flavour is for.