Flavour spec 1a: the directive grammar, canonical form, opaque carry
CI / gate (push) Successful in 7s
CI / gate (push) Successful in 7s
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
`<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.
|
||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user