14 KiB
Todo
The plan, in order. Nothing is built. Design questions are settled in AGENTS.md; remaining spec
detail is settled at its own milestone.
Milestones
- 0 — Scaffold.
package.jsonper §6,tsconfig.json,.npmrc(save-exact=true), the Docker tooling,renovate.json(§9), and.gitea/workflows/ci.ymlgating branches:runs-on: docker-host, actions pinned to semver tags. - 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; the spelling for text nodes whose whitespace CommonMark cannot hold (literal newlines, leading or trailing spaces) — escape-based, never literal, since pipe cells trim and pad. AtmediaInline, 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 fixtures per spec'd node, in
corpus/, one directory per contract kind (corpus/README.md). Blocked on the maintainer (§15), not to be guessed: Canonical form has no totality guard. Per@atlaskit/adf-schema57.1.0 every block node it spells —blockquote,bulletList,codeBlock,heading,listItem,orderedList,paragraph,rule— carries alocalIdwith no spelling,codeBlockalsohideLineNumbers,uniqueIdandwrap,blockquotealso marks, andhardBreaktextandlocalIdwith no section for the carry fallback to reach. Picking one (directive sections for those nodes, or the opaque carry) is a permanent format decision (§8). Two collision sites wait incorpus/unspellable/meanwhile, each a choice between the absent attribute and the empty value: acodeBlockwhose info string is empty, and anorderedListstarting at 1, independent of the totality answer sinceorder: 9keeps the markdown form either way. Neither has a second spelling to fall back to, which is what settled the third — amediawith an emptyalttakes the directive form (spec/flavour.md, the CommonMark image). Also blocked: the link rule covers destination spaces only, so two shapes have no spelling and are refused meanwhile — hrefhttps://example.com/a)band titleHe said "hi", both incorpus/unspellable/. Two defensible spellings each — angle brackets or a backslash escape, and for titles'…'or(…)besides — so §8 leaves the pick here. Also blocked: block separation is unstated for a CommonMark block beside a directive block in a container body — anexpandwhose content isparagraph"A" then apanel(panelTypewarning) holding "B" spellsAand:::panel warningeither on consecutive lines or with a blank line between. Two defensible spellings, so §8 leaves the pick here;unspelled-block-separationrefuses the pair meanwhile, an empty paragraph's::paragraphbeside a CommonMark block included — and, since amediaSingle's spelling now follows whether CommonMark can spell its URL, two sibling images differing only by an&land in the same refusal.- 1d1 — The CommonMark subset: blockquote, bulletList, codeBlock, heading, orderedList,
paragraph, rule, listItem, hardBreak, text, code spans, and the
code,em,link,strikeandstrongmarks — one mark per text node; nesting is 1d3's. - 1d2 — Block nodes: panel, expand/nestedExpand, the media family and the CommonMark
image shape, both table forms, task and decision lists, layout, extensions, syncBlock —
with the reserved
marksattribute and the fence lengths nesting forces. - 1d3 — Inline nodes and marks: date, emoji, inlineCard, mediaInline, mention, status;
border, subsup, textColor, underline; the content slot's
textattribute and the:text{text="…"}whitespace spelling.
- 1d1 — The CommonMark subset: blockquote, bulletList, codeBlock, heading, orderedList,
paragraph, rule, listItem, hardBreak, text, code spans, and the
- 2 —
adfToMarkdown. First real code. Each sub-item turns one corpus directory green; the two that have no fixtures yet write them in the same chunk, tests first (§10).- 2a — The runner and the CommonMark subset. The corpus runner: walk
corpus/round-trip/, assertadfToMarkdownemits each.mdbyte for byte. Decide here where §10's coverage check lives, and gate that everycorpus/**/*.jsonre-serializes to itself under the library's own canonical serializer — one implementation, keys sorted, two spellings: two-space indent for the corpus files and the block carry's body, compact for the inline carry.commonmark-subset/green. - 2b — Block nodes.
block-nodes/green. A nested list that cannot interrupt the block above it is refused meanwhile, not spelled: the maintainer's answer on tight-versus-blank separation turns that refusal into an emission. Block separation becomesseparationBetween(previous, next, container)here — a boolean cannot hold the third casespec/flavour.mdstates for two directive blocks in a container body, and the maintainer's answer on a CommonMark block beside a directive block (1d) drops into the same seam. Give the emitter's refusals a corpus home while the directories grow:corpus/unspellable/, a.jsonbeside theConvertErrorCodeit must return, the emitter half ofcorpus/errors/. - 2c — Inline nodes and marks.
inline-nodes/green.InlineSegmentsplits into its two axes — escapability (attributefor:text{text="…"},backslash,bracketed,none) and the emphasis role. A lone surrogate in a text node emits verbatim and becomes U+FFFD on any UTF-8 encode, a §2 break plain text still holds open — attribute values already escape it. The pipe form's fallback reads the emitted segments rather than naming the nodes whose attribute values spell a pipe as syntax, so 2e's\u007cnarrows it in one place. - 2d — The opaque carry (§3). Fixtures and emitter together, into
corpus/round-trip/opaque-carry/: an unknown node in both positions, the reservedadfinfo string, and thecodeBlockwhose language isadf. - 2e — Carve-outs and combinations. Fixtures and emitter together, into
corpus/round-trip/combinations/: the three carve-outs and their escapes, mark runs — the longest-run rule, attributes included — and the runs a carry breaks, a mark spelling that cannot open where it sits (un**-real**istic; the spec owes the carry a trigger), attribute canonicalization, a pipe cell's whitespace edges and\u007cfor a|inside a quoted attribute value, documents combining nodes rather than isolating one, and a paragraph line inside a container body shaped like a closing fence (:::,::: x). GuardfenceNestingFault's bare-run pop here too — a run shorter than the open fence is a fault, not a close — which today's emitter cannot reach. Two moves land before the attribute spelling changes. One mark vocabulary:emphasisSpellings,linkAttributesand thecode/linknames joininline-directives.ts, which holds four of the nine marks while the rest are branch literals in the emitter — and the parser (3) needs every name to make:em[x]the named errorspec/flavour.mdpromises. Andescaping: 'attribute'earns its keep at the\u007crule or collapses intonone: nothing the escaper does tells the two apart today, since a carried segment holds only spaces, tabs and newlines. The gate gains the collision property here: no two corpus documents may emit the same bytes — one spelling for two documents is a round-trip break no parser can undo, and it is provable without one. It also settles the emitter's one known approximation: delimiter flanking is exact, but CommonMark's matching — the multiple-of-3 rule and the way a run splits across several openers — is not modelled. No reachable violation has been found by hand; the property test is what decides it.
- 2a — The runner and the CommonMark subset. The corpus runner: walk
- 3 —
markdownToAdf. The CommonMark parser is the largest single component; split it into sub-items before starting (§15). Fixtures land with the code that reads them:corpus/normalization/(setext, indented code, loose lists,*/+bullets, entity references, soft wraps — one-way, the markdown not canonical) andcorpus/errors/(a markdown input per named error — malformed directives, the image gap, a claimed pipe-table line that does not parse, the content slot, raw HTML with no mapping — each with the error it must return). The raw-HTML element mapping is empty until milestone 6, so at0.1.0every raw-HTML construct in input is an error result. The CommonMark spec suite runs against it from here (§10). The parser owes~the samecan_open/can_closethe emitter assumes — CommonMark flanking, as for*— whichspec/flavour.mddoes not yet pin.src/gets its hierarchy at the same split —adf/,markdown/,html/, the grammar module shared insidemarkdown/— while the rename is still mechanical. Three files do not move whole:block-directives.tsandinline-directives.tseach hold a node table milestones 6-7 need inadf/beside a markdown spelling that belongs inmarkdown/, anddirective-attributes.tsfuses the format-neutral conformance walk (vocabularyPairs) with the markdown value spelling HTML has no use for.spellDestination,spellTitleandbalancedleavemarkdown-inline.tshere too — CommonMark destination spellingemitLinkandtryImageLineshare, and the six concerns that file carries are one fewer for it.AttributeKindandAttributeVocabularystay above all of it — the vocabulary a string-typed attribute grammar needs, which is why HTML will want them too, not a markdown spelling. Both node tables are a second copy ofspec/flavour.md's prose with no drift guard, and a mistyped attribute name degrades into a false refusal no test catches. - 4 — Round-trip property tests over the corpus, both ways — the thing that proves 2 and
3. Editor-normal (§2) gets its implementation here —
toEditorNormal(doc)and the equality the round-trip asserts, which over normalized input is the canonical serializer's compact spelling — rather than staying spelled inline as?? []at every reader. The reading half isnodeContent/nodeAttrs/nodeMarksover the ~28 sites spelling it inline today, which also lifts the branch floor §10 keeps below 100 for exactly those halves. Generators emit editor-normal ADF (§2). Real sanitized ADF from live Atlassian APIs lands here too (§10), incorpus/real-payloads/: an ADF→markdown→ADF check with no expected markdown, the payloads supplied by the maintainer. - 5 — Release pipeline, ship
0.1.0. Publish-on-version-change (§9),NPM_TOKENsecret, the repo made public first (§6). TheConvertErrorCodefreeze (§8) is checkable here: everycorpus/unspellable/document is one of 1d's decisions, so the directory empties as they land and whatever survives is permanent.0.1.0is the markdown round-trip: both markdown directions, the types,isAdfDocument. The build lands here: a build tsconfig emitting JS and.d.tstodist/(the dev config'sallowImportingTsExtensionsforcesnoEmit, so the build config needsrewriteRelativeImportExtensions), plusexports/filesinpackage.json. The maintainer's bump PR also removesprivate: true, the guard against any earlier publish. - 6 — The HTML dialect spec. Element-by-element mapping, the
data-*fidelity scheme, the opaque-carry form, and the documented foreign-element sethtmlToAdfaccepts. - 7 — HTML, ship
0.2.0.adfToHtml,htmlToAdf, the composedmarkdownToHtml/htmlToMarkdown. CommonMark spec suite runs againstmarkdownToHtmlfrom here (§10). - 8 — CLI. A later goal, shaped around the personas once the library exists.
The ADF inventory to cover
From Atlassian's structure
reference — not the
whole schema: real payloads also carry taskList/taskItem, decisionList/decisionItem,
layoutSection/layoutColumn, blockCard/embedCard, extension/bodiedExtension/inlineExtension
and placeholder, none documented there. The documented set is the floor: the floor gets designed
syntax, the rest rides the opaque carry (§3) until it does too.
| Top-level block | blockquote bodiedSyncBlock bulletList codeBlock expand heading mediaGroup mediaSingle multiBodiedExtension orderedList panel paragraph rule syncBlock table |
| Child block | blockTaskItem extensionFrame listItem media nestedExpand tableCell tableHeader tableRow |
| Inline | date emoji hardBreak inlineCard mediaInline mention status text |
| 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 and strong
marks; strike is the flavour's ~~ carve-out. Everything else is what the flavour is for.