Merge pull request 'Flavour spec 1b: block node syntaxes' (#4) from flavour-blocks into main
CI / gate (push) Successful in 4s
CI / gate (push) Successful in 4s
This commit was merged in pull request #4.
This commit is contained in:
@@ -19,7 +19,8 @@ The other direction is a canonical fixpoint, not byte-identity: human markdown n
|
||||
back yields the library's canonical spelling, and that spelling round-trips byte-identically.
|
||||
|
||||
"Equals" is structural equality over editor-normal ADF — adjacent text nodes with identical marks
|
||||
merged, JSON number semantics — the only domain markdown can restore.
|
||||
merged, JSON number semantics, an empty attrs object, marks array or content array the absent
|
||||
key — the only domain markdown can restore.
|
||||
|
||||
Round-trip equality is a property tested over a corpus, not a claim made in prose.
|
||||
|
||||
@@ -35,8 +36,8 @@ Round-trip equality is a property tested over a corpus, not a claim made in pros
|
||||
|
||||
- Directives, one grammar for everything markdown lacks: `:::panel info` … `:::` blocks,
|
||||
`:mention[@Mikael]{id=5b10a2}` inline. Prior art: CommonMark's generic-directives proposal.
|
||||
- Plain CommonMark is a subset, with one carve-out (`spec/flavour.md`): directive-shaped literal
|
||||
text is claimed.
|
||||
- Plain CommonMark is a subset, with carve-outs (`spec/flavour.md`): literal text shaped like a
|
||||
directive or a pipe table is claimed — plus one image gap.
|
||||
- Tables: one header row plus plain inline cells → pipe table; anything richer → directive form.
|
||||
- Identity-bearing nodes carry their ids in attributes; a document is only portable within its
|
||||
site — accepted.
|
||||
@@ -152,4 +153,7 @@ public, and creating the `NPM_TOKEN` secret.
|
||||
|
||||
A continuous loop session (`/loop`) counts as a chain of sessions: one chunk per iteration, each
|
||||
iteration starting by re-reading `AGENTS.md` and `todo.md` and trusting them over anything
|
||||
remembered from earlier iterations. The loop stops when only maintainer-reserved acts remain.
|
||||
remembered from earlier iterations. The loop session is a thin driver: each chunk's work runs in
|
||||
a fresh-context subagent holding this file as its charter, and the driver only relays maintainer
|
||||
questions, runs the review flow, merges, and cleans up. The loop stops when only
|
||||
maintainer-reserved acts remain.
|
||||
|
||||
@@ -38,9 +38,11 @@ isAdfDocument(v: unknown): v is AdfDocument
|
||||
(AGENTS.md §3).
|
||||
- `htmlToAdf(adfToHtml(doc))` equals `doc` — fidelity HTML cannot express rides `data-*`
|
||||
attributes.
|
||||
- Plain CommonMark is valid input to `markdownToAdf`, with one carve-out: literal text matching
|
||||
directive syntax is claimed (escapable — `spec/flavour.md`). Converting back yields the
|
||||
library's canonical spelling, which round-trips byte-identically.
|
||||
- Plain CommonMark is valid input to `markdownToAdf`, with two carve-outs — literal text
|
||||
matching directive or pipe-table syntax is claimed (escapable — `spec/flavour.md`) — and one
|
||||
gap: a CommonMark image fits only as its own title-less paragraph; mid-text and titled images
|
||||
are error results. Converting back yields the library's canonical spelling, which round-trips
|
||||
byte-identically.
|
||||
- Foreign HTML maps a documented element set; an unmappable element is an error, never a silent
|
||||
drop. Well-formed HTML only — no tag-soup recovery.
|
||||
- The emitted formats are semver surface (AGENTS.md §8).
|
||||
|
||||
+192
-3
@@ -1,9 +1,11 @@
|
||||
# The markdown flavour
|
||||
|
||||
The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain
|
||||
CommonMark is a subset with one carve-out: literal text that matches directive syntax below is
|
||||
claimed by the flavour (escape the `:` to keep it literal). The emitted form is contract
|
||||
(AGENTS.md §8). Per-node syntaxes build on this grammar in sections that follow (todo.md 1b–1c).
|
||||
CommonMark is a subset with two carve-outs: literal text that matches directive syntax below or
|
||||
opens a pipe table 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 (inline nodes and marks: todo.md 1c).
|
||||
|
||||
## Canonical form
|
||||
|
||||
@@ -111,3 +113,190 @@ CommonMark input may contain raw HTML. `markdownToAdf` routes each construct thr
|
||||
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.
|
||||
|
||||
## Block nodes
|
||||
|
||||
The directive name is always the ADF node type. A container's body is the node's `content`; a
|
||||
leaf has none. Every directive parses in any position — `markdownToAdf` builds exactly what is
|
||||
written; validity against ADF's content models stays the author's business (AGENTS.md §14).
|
||||
|
||||
Each section lists attributes as `name (type)`. A parenthesized value set documents what real
|
||||
payloads hold; the type stays string and any value round-trips verbatim. Values map to attrs by
|
||||
type: strings verbatim, numbers and booleans in canonical JSON spelling — quoted where not bare
|
||||
(`width="33.33"`) — and `json` values as the inline carry's serialization (compact, keys
|
||||
sorted), quoted. `markdownToAdf` emits `attrs`, `content` and `marks` keys only when non-empty;
|
||||
editor-normal ADF reads an empty attrs object, marks array or content array as the absent key
|
||||
(AGENTS.md §2) — the grammar's empty-`{attrs}` omission already collapses the two spellings.
|
||||
|
||||
Marks on a block node ride the reserved attribute key `marks` — the node's marks array as a
|
||||
`json` value: `::::layoutSection {marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"}`.
|
||||
Three child nodes hold inline content rather than blocks (`caption`, `decisionItem`,
|
||||
`taskItem`): their body is at most one paragraph, whose inline content becomes the node's
|
||||
`content`; any other body is a named error.
|
||||
|
||||
A node the sections cannot spell rides the opaque carry: an attrs key its section does not
|
||||
list, a value that is not the section's type, or an arg-slot value that is no bare token. In
|
||||
markdown input the same mismatch is a named error.
|
||||
|
||||
### Panel
|
||||
|
||||
Container; the arg is `panelType` (`custom` `error` `info` `note` `success` `tip` `warning`).
|
||||
Attributes: `localId` (string), `panelColor` (string), `panelIcon` (string), `panelIconId`
|
||||
(string), `panelIconText` (string) — the editor writes the last four for `custom` panels.
|
||||
|
||||
```
|
||||
:::panel warning
|
||||
Check the collation before importing.
|
||||
:::
|
||||
```
|
||||
|
||||
### Expand
|
||||
|
||||
`expand` and `nestedExpand`: containers, no arg — same syntax, two node types, the name picks
|
||||
which. Attributes: `localId` (string), `title` (string).
|
||||
|
||||
```
|
||||
:::expand {title="Full build log"}
|
||||
…
|
||||
:::
|
||||
```
|
||||
|
||||
### The media family
|
||||
|
||||
- `media` — leaf. Attributes: `alt` (string), `collection` (string), `height` (number), `id`
|
||||
(string), `localId` (string), `occurrenceKey` (string), `type` (`external` `file` `link`),
|
||||
`url` (string), `width` (number). `file` and `link` media carry `collection` + `id`;
|
||||
`external` media carry `url`.
|
||||
- `mediaSingle` — container: one `::media`, then optionally one `:::caption`. Attributes:
|
||||
`layout` (`align-end` `align-start` `center` `full-width` `wide` `wrap-left` `wrap-right`),
|
||||
`localId` (string), `width` (number), `widthType` (`percentage` `pixel`).
|
||||
- `caption` — container, inline body. Attributes: `localId` (string).
|
||||
- `mediaGroup` — container of `::media` leaves, no attributes.
|
||||
|
||||
```
|
||||
::::mediaSingle {layout=center width=50}
|
||||
::media {collection=MediaServicesSample id=4478e39c-cf9b-41d1-ba92-68589487cd75 type=file}
|
||||
:::caption
|
||||
The moon, at night.
|
||||
:::
|
||||
::::
|
||||
```
|
||||
|
||||
**The CommonMark image.** A paragraph whose entire inline content is one image `` is
|
||||
a `mediaSingle` with attrs exactly `{"layout":"center"}` holding an `external` `media` — `url`
|
||||
from the destination, `alt` the description's plain-text content when non-empty. `adfToMarkdown` emits the image
|
||||
form for exactly that shape — those attrs and no others, no marks, no caption. An image amid
|
||||
other text, or one carrying a title, is a named error (ADF has a slot for neither).
|
||||
|
||||
### Tables
|
||||
|
||||
One header row plus plain inline cells is a pipe table; anything richer is the directive form
|
||||
(AGENTS.md §4). Precisely: a table emits as a pipe table exactly when the `table`, every row
|
||||
and every cell carry no attrs and no marks, the first row is all `tableHeader` and the rest all
|
||||
`tableCell`, every row has the header's cell count, and every cell holds exactly one attr-less,
|
||||
mark-less paragraph — an empty cell holds one empty paragraph — with no `|` in a code span:
|
||||
backslash escapes are inert there, so pipe form cannot spell that pipe and the table takes the
|
||||
directive form. A pipe table parses back to exactly that shape.
|
||||
|
||||
```
|
||||
| Part | Qty |
|
||||
| --- | --- |
|
||||
| Bolt M8 | 40 |
|
||||
```
|
||||
|
||||
Claiming at block level, symmetric with directives: a line opening with an unescaped `|` is
|
||||
claimed and must parse as part of a pipe table, else it is a named error — escape the pipe
|
||||
(`\|`) to keep it literal text. A pipe table is a header row, a delimiter row whose cells are
|
||||
runs of one or more `-` (canonical `---`), and body rows; rows follow code-fence indentation.
|
||||
Cells split on unescaped `|` before inline parsing — `\|` stays in the cell text, and the
|
||||
inline layer's ordinary CommonMark escaping yields the pipe; each cell is the inline content of
|
||||
one paragraph, trimmed; canonical form pads cells with single spaces and ends rows with `|`
|
||||
(optional in input). Named errors: a delimiter or body row whose cell count differs from the
|
||||
header's, and an alignment colon in the delimiter row — ADF holds no column alignment. In a
|
||||
pipe cell a hard break is `:hardBreak{}`, a literal `|` is `\|`, and `|` inside a quoted
|
||||
attribute value is `\u007c` (JSON has no `\|`).
|
||||
|
||||
The directive form nests cells as containers of block content inside `tableRow` containers:
|
||||
|
||||
```
|
||||
:::::table {isNumberColumnEnabled=true width=760}
|
||||
::::tableRow
|
||||
:::tableHeader {colspan=2 colwidth="[340,420]"}
|
||||
Assembly
|
||||
:::
|
||||
::::
|
||||
::::tableRow
|
||||
:::tableCell {background="#deebff"}
|
||||
Bolt M8
|
||||
:::
|
||||
:::tableCell {valign=top}
|
||||
40
|
||||
:::
|
||||
::::
|
||||
:::::
|
||||
```
|
||||
|
||||
Attributes — `table`: `displayMode` (`default` `fixed`), `isNumberColumnEnabled` (boolean),
|
||||
`layout` (`align-end` `align-start` `center` `default` `full-width` `wide`), `localId`
|
||||
(string), `width` (number, pixels). `tableRow`: `localId` (string). `tableCell` and
|
||||
`tableHeader`: `background` (string), `colspan` (number), `colwidth` (json, one pixel width per
|
||||
spanned column), `localId` (string), `rowspan` (number), `valign` (`bottom` `middle` `top`).
|
||||
|
||||
### Task and decision lists
|
||||
|
||||
- `taskList` — container of `taskItem`, `blockTaskItem` and nested `taskList` directives.
|
||||
Attributes: `localId` (string).
|
||||
- `taskItem` — container, inline body; the arg is the state (`DONE` `TODO`). Attributes:
|
||||
`localId` (string).
|
||||
- `blockTaskItem` — container, block body; arg and attributes as `taskItem`.
|
||||
- `decisionList` — container of `decisionItem` directives. Attributes: `localId` (string).
|
||||
- `decisionItem` — container, inline body. Attributes: `localId` (string), `state` (string —
|
||||
free-form; the editor writes `DECIDED`).
|
||||
|
||||
```
|
||||
::::taskList {localId=0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90}
|
||||
:::taskItem DONE {localId=0198f3a2-8d52-70b1-8c4f-5e9f3d2b7ca1}
|
||||
Write the spec
|
||||
:::
|
||||
:::taskItem TODO {localId=0198f3a2-9e63-7d80-a15b-6fa04e3c8db2}
|
||||
Ship it
|
||||
:::
|
||||
::::
|
||||
```
|
||||
|
||||
### Layout
|
||||
|
||||
`layoutSection` — container of `layoutColumn` containers; attributes: `localId` (string).
|
||||
`layoutColumn` — container, block body; attributes: `localId` (string), `valign` (`bottom`
|
||||
`middle` `top`), `width` (number — percent).
|
||||
|
||||
```
|
||||
::::layoutSection
|
||||
:::layoutColumn {width=50}
|
||||
Left.
|
||||
:::
|
||||
:::layoutColumn {width=50}
|
||||
Right.
|
||||
:::
|
||||
::::
|
||||
```
|
||||
|
||||
### Extensions
|
||||
|
||||
`extension` — leaf. `bodiedExtension` — container, block body. `multiBodiedExtension` —
|
||||
container of `extensionFrame` containers; `extensionFrame` — container, block body, no
|
||||
attributes. The other three share: `extensionKey` (string), `extensionType` (string), `layout`
|
||||
(`default` `full-width` `wide`), `localId` (string), `parameters` (json), `text` (string).
|
||||
|
||||
```
|
||||
::extension {extensionKey=toc extensionType="com.atlassian.confluence.macro.core" parameters="{\"maxLevel\":2}"}
|
||||
```
|
||||
|
||||
### Sync blocks
|
||||
|
||||
`syncBlock` — leaf. `bodiedSyncBlock` — container, block body. Attributes: `localId` (string),
|
||||
`resourceId` (string).
|
||||
|
||||
```
|
||||
::syncBlock {localId=0198f3a2-af74-7e91-b26c-70b15f4d9ec3 resourceId="ari:cloud:confluence:site/page/123"}
|
||||
```
|
||||
|
||||
@@ -11,12 +11,15 @@ detail is settled at its own milestone.
|
||||
- [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
|
||||
- [x] **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; the spelling for text nodes
|
||||
whose whitespace CommonMark cannot hold (literal newlines, leading or trailing spaces).
|
||||
whose whitespace CommonMark cannot hold (literal newlines, leading or trailing spaces) —
|
||||
escape-based, never literal, since pipe cells trim and pad. At `mediaInline`, check real
|
||||
payloads for external-URL support — if it exists, revisit the media section's
|
||||
mid-text-image error and its "no slot" ground.
|
||||
- [ ] **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.
|
||||
|
||||
Reference in New Issue
Block a user