From ae519e233e590f6cbb4c607c510c8636ded5972c Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 00:15:26 +0200 Subject: [PATCH] Flavour spec 1a: the directive grammar, canonical form, opaque carry --- README.md | 3 +- spec/flavour.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ todo.md | 15 ++++++---- 3 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 spec/flavour.md diff --git a/README.md b/README.md index 569d55c..974e074 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ Lossless conversion between **Atlassian Document Format** (ADF), an extended markdown flavour, and an HTML dialect. -**Status: scaffold only, no conversion code yet.** Plan: `todo.md`. Decisions: `AGENTS.md`. +**Status: scaffold only, no conversion code yet.** Plan: `todo.md`. Decisions: `AGENTS.md`. The +flavour's grammar: [`spec/flavour.md`](spec/flavour.md). ## What it is for diff --git a/spec/flavour.md b/spec/flavour.md new file mode 100644 index 0000000..964fdda --- /dev/null +++ b/spec/flavour.md @@ -0,0 +1,76 @@ +# The markdown flavour + +The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain +CommonMark is a subset: the flavour adds directives, never changes CommonMark meaning. The emitted +form is contract (AGENTS.md §8). Per-node syntaxes build on this grammar in sections that follow +(todo.md 1b–1c). + +## 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). +- Bullet lists `- `, two-space continuation indent. Ordered lists incrementing `1.` `2.` `3.`, + the first number taken from the node's `order` attribute. +- 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. +- Thematic break `---`. +- Hard break: backslash at end of line (survives editors that trim trailing spaces). +- Links `[text](url)`; `<…>` around a destination containing spaces; title in double quotes; + `` autolink when the link text equals its destination. +- Paragraphs on one line — no soft wrapping; soft line breaks in input collapse per CommonMark. +- Entity references in input decode to their characters; output backslash-escapes only where text + would otherwise parse as syntax. +- Blocks separated by one blank line, no trailing whitespace, single trailing newline. + +## Directives + +One grammar for everything CommonMark lacks. Names are the ADF node names (camelCase). + +**Inline**: `:name[content]{attrs}`. `[content]` is inline markdown; each node's section says +whether content and attrs are required. `:` opens a directive only when the name is followed +immediately by `[` or `{` — anything else (`10:30`, `:smile:`) is literal text. + +**Container block**: + +``` +:::name arg {attrs} +block content +::: +``` + +The fence is three or more colons. `arg` is one optional bare token whose meaning each node +defines (e.g. the panel type). The body is block markdown. The closing fence is a line of at +least the opening's length, and a container's fence is longer than every directive fence line in +its body, so closers are unambiguous — the code-fence rule. Canonical form uses minimal lengths. + +**Leaf block**: `::name {attrs}` — a block-position node with no body. + +**Attributes**: `{key=value key2="two words"}`. A bare value matches `[A-Za-z0-9_-]+`; anything +else is double-quoted with `\"` and `\\` as the only escapes. All values are strings at the +grammar level; each node's section assigns types. Canonical form orders keys alphabetically and +spells values bare wherever allowed. + +**Escaping**: the emitter backslash-escapes a `:` whose text would otherwise parse as a +directive; a backslash before `:` in input always yields a literal colon. + +## The opaque carry (AGENTS.md §3) + +A node type the library does not know rides as its raw JSON and restores to a deep-equal node: + +- **Block position**: a fenced code block with info string `adf`, body = the node's JSON, + serialized canonically — two-space indent, object keys sorted. +- **Inline position**: `:adf{json="…"}`, same serialization, quote-escaped. + +The info string `adf` is reserved: a genuine `codeBlock` whose `language` is exactly `adf` is +itself emitted through the opaque carry, so the reservation stays absolute and stays lossless. + +## Raw HTML in input + +CommonMark input may contain raw HTML. `markdownToAdf` routes each construct through the foreign +HTML element mapping (AGENTS.md §3; specified with the HTML dialect, todo.md milestone 6) — 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. diff --git a/todo.md b/todo.md index dc365f4..e0ffafe 100644 --- a/todo.md +++ b/todo.md @@ -8,11 +8,16 @@ detail is settled at its own milestone. - [x] **0 — Scaffold.** `package.json` per §6, `tsconfig.json`, `.npmrc` (`save-exact=true`), the Docker tooling, `renovate.json` (§9), and `.gitea/workflows/ci.yml` gating branches: `runs-on: docker-host`, actions pinned to semver tags. -- [ ] **1 — The flavour spec.** The markdown flavour written as this repo's specification before - any implementation: the directive grammar (attributes, escaping, nesting), each node's - syntax from the inventory below, the opaque-carry spelling, the pipe-vs-directive table - rule, and what CommonMark's raw-HTML constructs become in ADF, which has no raw-HTML node — - likely the §3 element mapping, error otherwise. Start the corpus (§10) from this spec. +- [x] **1a — The directive grammar** (`spec/flavour.md`): inline/block/leaf directive forms, + attributes, escaping, nesting, canonical form, the opaque-carry spelling, the raw-HTML + input policy. +- [ ] **1b — Block node syntaxes** in `spec/flavour.md`: panel, expand/nestedExpand, the media + family, the pipe-vs-directive table rule and the directive table form, task and decision + lists, layout, extensions, syncBlock. +- [ ] **1c — Inline node syntaxes and marks** in `spec/flavour.md`: mention, emoji, status, date, + inlineCard, mediaInline; underline, subsup, textColor, border. +- [ ] **1d — Corpus start** (§10): checked-in ADF ↔ canonical-markdown fixture pairs per spec'd + node. - [ ] **2 — `adfToMarkdown`.** First real code — decide here where §10's coverage check lives. - [ ] **3 — `markdownToAdf`.** The CommonMark parser is the largest single component. - [ ] **4 — Round-trip property tests** over the corpus, both ways — the thing that proves 2 and 3.