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.