Flavour spec 1a: the directive grammar, canonical form, opaque carry #3

Merged
lilleman merged 7 commits from flavour-grammar into main 2026-08-24 00:49:26 +02:00
3 changed files with 88 additions and 6 deletions
Showing only changes of commit ae519e233e - Show all commits
+2 -1
View File
@@ -3,7 +3,8 @@
Lossless conversion between **Atlassian Document Format** (ADF), an extended markdown flavour, and Lossless conversion between **Atlassian Document Format** (ADF), an extended markdown flavour, and
an HTML dialect. 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 ## What it is for
+76
View File
@@ -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 1b1c).
## 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;
`<url>` 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.
+10 -5
View File
@@ -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 - [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: Docker tooling, `renovate.json` (§9), and `.gitea/workflows/ci.yml` gating branches:
`runs-on: docker-host`, actions pinned to semver tags. `runs-on: docker-host`, actions pinned to semver tags.
- [ ] **1 — The flavour spec.** The markdown flavour written as this repo's specification before - [x] **1a — The directive grammar** (`spec/flavour.md`): inline/block/leaf directive forms,
any implementation: the directive grammar (attributes, escaping, nesting), each node's attributes, escaping, nesting, canonical form, the opaque-carry spelling, the raw-HTML
syntax from the inventory below, the opaque-carry spelling, the pipe-vs-directive table input policy.
rule, and what CommonMark's raw-HTML constructs become in ADF, which has no raw-HTML node — - [ ] **1b — Block node syntaxes** in `spec/flavour.md`: panel, expand/nestedExpand, the media
likely the §3 element mapping, error otherwise. Start the corpus (§10) from this spec. 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. - [ ] **2 — `adfToMarkdown`.** First real code — decide here where §10's coverage check lives.
- [ ] **3 — `markdownToAdf`.** The CommonMark parser is the largest single component. - [ ] **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. - [ ] **4 — Round-trip property tests** over the corpus, both ways — the thing that proves 2 and 3.