@larvit/atlassian-adf-converter
Lossless conversion between Atlassian Document Format (ADF), an extended markdown flavour, and an HTML dialect.
Status: pre-release — adfToMarkdown only, and not yet every document.
Plan: todo.md. Decisions: AGENTS.md. The flavour's grammar:
spec/flavour.md.
What it is for
Atlassian Cloud REST APIs hand out rich text — issue descriptions, comments, pages — as ADF, a
ProseMirror-shaped JSON tree, and take it back the same way. No Atlassian endpoint converts it
(pf-editor-service/convert is decommissioned,
JRACLOUD-77436 open), and the npm ecosystem is
one-directional and lossy. A consumer that shows a document and lets someone edit it needs both
directions lossless — otherwise saving destroys the panels, mentions and attachments it could not
represent.
The shape
Pure functions, no I/O, no configuration. ADF is the hub: markdown↔HTML compose through it.
adfToMarkdown(doc: AdfDocument): Result<string>
markdownToAdf(markdown: string): Result<AdfDocument>
adfToHtml(doc: AdfDocument): Result<string>
htmlToAdf(html: string): Result<AdfDocument>
markdownToHtml(markdown: string): Result<string> // via ADF
htmlToMarkdown(html: string): Result<string> // via ADF
isAdfDocument(v: unknown): v is AdfDocument
Result<T> is { ok: true; value: T } | { ok: false; error: ConvertError } — nothing throws.
ConvertError is { code, message, path }: a code from a closed set, and the path of the node it
names, from the document root.
The guarantees
markdownToAdf(adfToMarkdown(doc))equalsdoc— unknown node types included, carried opaquely (AGENTS.md §3).htmlToAdf(adfToHtml(doc))equalsdoc— fidelity HTML cannot express ridesdata-*attributes.- Plain CommonMark is valid input to
markdownToAdf, with three carve-outs — literal text matching directive, pipe-table or strikethrough 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.
- A document nested deeper than 500 levels is an error result, not a stack overflow.
- The emitted formats are semver surface (AGENTS.md §8).
Who it is for
Personas, never named consumers (AGENTS.md §7):
- Viewer/editor app — shows a document, lets a human edit, posts back. Losslessness above all.
- Bot posting content — converts generated markdown to ADF; needs the CommonMark promise.
- Export/indexing tool — bulk ADF→markdown/HTML; needs readable output.
- LLM/agent pipeline — documents to a model as markdown, edits back; needs the round-trip and markdown legible to a reader that half-knows the flavour.
The package
ESM only, no runtime dependencies, public npmjs. Built JavaScript with .d.ts beside it.
Pure ECMAScript at an ES2022 baseline, reaching for no host API, so a browser runs it as readily
as a server; the test suite runs under Node, Deno and Bun. Contract: AGENTS.md §5–6.