15 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; the answer governs every unknown node type too, the block carry counting as a CommonMark block since its spelling is a fenced code block.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— carried whole ahead of the attribute fallback 2e owes, since the reservation leaves that node no other spelling whatever 1d decides for itslocalId. - 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 carry's fallback triggers land here too:spec/flavour.mdcarries a node its section cannot spell — an attrs key no section lists, a value that is not the section's type, an arg slot holding no bare token, marks no nesting spells — where the emitter still refuses, which leaves 1d's blocked CommonMark nodes as the only refusal. The\u007crule covers the inline carry'sjsonvalue with them: the carry spellsescaping: 'none'today so thattryPipeCellrefuses the pipe form for a carry whose JSON holds a|. 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, and the precedence between a directive and a code span that overlap, which the emitter already spells leftmost-first (`a`:adf{json="…`…"}`b`).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.