mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Open the README with the call to take for each need, and widen the converter comparison
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- The package's description and keywords say it needs no React, Atlaskit or runtime dependency and runs
|
||||
in Node, Forge, Deno, Bun and browsers, and the README opens with the call to take for each need. Its comparison table lists every npm package
|
||||
converting ADF to or from markdown, and other ecosystems' libraries and Atlassian's own paths.
|
||||
- `adfToHtml` writes ADF as HTML: paragraphs, headings, lists, quotes, code blocks, rules, hard
|
||||
breaks, a centered, uncaptioned external image holding no attribute but its URL and alt, and `em`,
|
||||
`strong`, `code` and links as their elements. A paragraph, heading, quote, list, list item, code block or
|
||||
|
||||
@@ -3,30 +3,26 @@
|
||||
[](https://www.npmjs.com/package/@larvit/adf-codec)
|
||||
[](https://github.com/larvit/adf-codec/actions/workflows/ci.yml)
|
||||
|
||||
Convert Atlassian Document Format (ADF), the rich text of Jira and Confluence, to and from markdown, with a lossless round-trip, and to HTML.
|
||||
Convert Atlassian Document Format (ADF), the rich text of Jira and Confluence, to and from markdown,
|
||||
with a lossless round-trip, and to HTML.
|
||||
|
||||
For application developers whose app shows, edits, exports or generates the rich text Atlassian
|
||||
products store as ADF.
|
||||
Pure functions with no React, no Atlaskit and no runtime dependencies, nothing to configure or
|
||||
connect: it runs in Node, Forge, Deno, Bun and browsers. It is for application developers whose app
|
||||
shows, edits, exports or generates the rich text Atlassian products store as ADF.
|
||||
|
||||
- **Lossless round-trip:** `adfToLosslessMarkdown` and `losslessMarkdownToAdf` give back the
|
||||
document they started from — panels, mentions, statuses and node types this version does not know
|
||||
included.
|
||||
- **Markdown in:** `portableMarkdownToAdf` reads a bot's or a model's markdown, including `~~` and
|
||||
GFM's pipe tables whose rows hold as many cells as their header, their column alignment dropped.
|
||||
`commonMarkToAdf` reads strict CommonMark as its spec says: nothing becomes a table, task or
|
||||
panel.
|
||||
- **Portable markdown out, lossy:** `adfToPortableMarkdown` writes the markdown that GitHub, GitLab
|
||||
and Obsidian render — GitHub Flavored Markdown's alerts, task lists and pipe tables, Obsidian
|
||||
Flavored Markdown's callouts and highlights. It keeps the text, images by URL and link targets and
|
||||
drops the rest.
|
||||
- **HTML out and back:** `adfToHtml` writes paragraphs, headings, lists, quotes, code blocks, rules,
|
||||
line breaks, emphasis, inline code, links and a plain external image as their HTML elements. Every
|
||||
other node, an image Atlassian's editor wrote among them, becomes an element holding its JSON and
|
||||
showing its content. `htmlToAdf` reads that HTML back to the document it came from, and refuses
|
||||
any other HTML.
|
||||
- **No throws:** every call returns a result; a refusal carries a code from a closed list.
|
||||
- **Runs anywhere:** pure functions with no runtime dependencies, in Node, Deno, Bun and browsers.
|
||||
TypeScript types ship beside the JavaScript.
|
||||
| You have | You want | Take | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| Markdown from a bot, a model or a person | ADF | `portableMarkdownToAdf` | reads CommonMark plus GFM's tables, task lists and alerts, `~~`, and Obsidian's callouts and `==`; `commonMarkToAdf` reads strict CommonMark alone |
|
||||
| ADF | markdown to edit, then ADF again | `adfToLosslessMarkdown`, then `losslessMarkdownToAdf` | gives back the document it started from, panels, mentions, statuses and node types this version does not know included |
|
||||
| ADF | markdown that GitHub, GitLab and Obsidian render | `adfToPortableMarkdown` | keeps the text, images by URL and link targets and drops the rest |
|
||||
| ADF | HTML | `adfToHtml` | writes the elements a browser shows; `htmlToAdf` reads that HTML back to the same document, and refuses any other HTML |
|
||||
|
||||
Every call returns a result, never a throw. A refusal carries a code from a closed list, and every
|
||||
refusal but `engine-limit` repeats on the same input ([The guarantees](#the-guarantees)).
|
||||
TypeScript types ship beside the JavaScript.
|
||||
|
||||
Atlaskit's markdown transformer converts markdown to ADF and has no ADF to markdown.
|
||||
[Other ADF converters](#other-adf-converters) compares this package with it and the rest.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -248,24 +244,47 @@ place, and on CI failing a change that breaks a guarantee.
|
||||
|
||||
## Other ADF converters
|
||||
|
||||
Serves [Goals](#goals) 3. The table lists every other npm package that converts ADF to or from
|
||||
markdown and has over 1,000 downloads a week. On 2026-10-14,
|
||||
[`converter-comparison/`](https://github.com/larvit/adf-codec/tree/main/converter-comparison) ran
|
||||
each package's published code, at the version the table names, with its default options. A package
|
||||
writing markdown got a document holding marks, a link, a mention, a status, a panel and a list, and
|
||||
a document holding a node type it cannot know. A package reading markdown got markdown holding the
|
||||
same marks and link.
|
||||
Serves [Goals](#goals) 3. The first table lists every npm package that converts ADF to or from
|
||||
markdown, whatever its downloads, unless its own documentation calls it experimental or incomplete.
|
||||
On 2026-10-28, [`converter-comparison/`](https://github.com/larvit/adf-codec/tree/main/converter-comparison)
|
||||
ran each package's published code, at the version the table names, with its default options. A
|
||||
package writing markdown got a document holding marks, a link, a mention, a status, a panel and a
|
||||
list, and a document holding a node type it cannot know. A package reading markdown got markdown
|
||||
holding the same marks and link.
|
||||
|
||||
| Package | Converts | ADF to markdown to ADF | A node type it does not know | Formats | Direct runtime dependencies |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `@larvit/adf-codec` | both ways | the same document | kept | markdown: lossless, portable; CommonMark in; HTML out; its own HTML in | 0 |
|
||||
| `@atlaskit/editor-markdown-transformer` 6.3.1 | markdown to ADF, through `@atlaskit/editor-json-transformer` 10.0.0; ADF to markdown throws "This is not implemented yet" | – | – | markdown | 5 |
|
||||
| `@faulander/md2adf` 1.0.0 | both ways | a different document: the status is gone, the mention's `id` becomes its text and the panel a quote | dropped with its text | markdown | 2 |
|
||||
| `@nthings.dev/adf4j-wasm` 1.0.2 | ADF to markdown | – | replaced by the text `[Unsupported: futureThing]` | GitHub Flavored Markdown, from WebAssembly | 0 |
|
||||
| `adf-md` 0.2.0 | both ways | a different document: the status without its `localId`, the marks in the other order | dropped with its text | markdown with MDX tags for panels, mentions and statuses | 6 |
|
||||
| `adf-to-markdown` 1.0.1 | ADF to markdown | – | dropped with its text | markdown | 0 |
|
||||
| `adf-to-md` 1.2.1 | ADF to markdown | – | dropped with its text, as are mentions, statuses and panels | markdown | 1 |
|
||||
| `adf2markdown` 1.2.0 | ADF to markdown | – | its text kept | markdown | 0 |
|
||||
| `extended-markdown-adf-parser` 2.4.0 | both ways | a different document: the mention comes back as linked text, the status without its `localId` | written as JSON, read back as a paragraph of that JSON's text | markdown | 9 |
|
||||
| `github-markdown-adf` 1.3.0 | both ways | a different document: the mention comes back as text, the status as inline code, the marks in the other order | dropped with its text | markdown | 3 |
|
||||
| `markdown-to-adf` 0.2.20 | markdown to ADF; writes a quote as the node type `blockQuote`, which ADF spells `blockquote` | – | – | markdown | 3 |
|
||||
| `marklassian` 1.2.1 | markdown to ADF | – | – | markdown | 1 |
|
||||
| `md-to-adf` 0.6.4 | markdown to ADF | – | – | markdown | 2 |
|
||||
| `mdast-util-from-adf` 2.3.0 | ADF to an mdast tree, which `mdast-util-to-markdown` writes as markdown; throws on a status | – | throws | markdown | 3 |
|
||||
|
||||
The next table lists converters in other ecosystems and Atlassian's own paths. They are not run
|
||||
here; each row is what the project's documentation says, on 2026-10-28.
|
||||
|
||||
| Converter | Converts | Source |
|
||||
| --- | --- | --- |
|
||||
| `marklas` 0.8.6, Python | GitHub Flavored Markdown to and from ADF | [PyPI](https://pypi.org/project/marklas/) |
|
||||
| `pyadf` 0.5.2, Python | ADF to markdown; a node it cannot render becomes an HTML element holding its attributes | [README](https://github.com/YoungseokCh/pyadf) |
|
||||
| `jc-adf` 0.2.0, Rust | markdown to and from ADF; a node type it does not know goes through an `adf:<type>` fenced block | [crates.io](https://crates.io/crates/jc-adf) |
|
||||
| `atlassian-markdown-converter` 0.2.1, Rust | between markdown, GFM, Jira wiki markup, ADF and Confluence markup | [crates.io](https://crates.io/crates/atlassian-markdown-converter) |
|
||||
| `Meziantou.Framework.AtlassianDataFormat` 1.0.2, .NET | ADF to markdown | [NuGet](https://www.nuget.org/packages/Meziantou.Framework.AtlassianDataFormat) |
|
||||
| `github.com/ajbeck/adf-to-markdown` 1.1.2 and `github.com/ajbeck/goldmark-adf` 2.0.2, Go | ADF to markdown; markdown to ADF through goldmark | [README](https://github.com/ajbeck/adf-to-markdown), [README](https://github.com/ajbeck/goldmark-adf) |
|
||||
| `github.com/rgonek/jira-adf-converter` 1.0.0, Go | GFM to and from ADF | [README](https://github.com/rgonek/jira-adf-converter) |
|
||||
| Jira Cloud REST API v3 | reads and writes ADF in comments and fields such as the description; `expand=renderedFields` returns the fields as HTML; no markdown | [OpenAPI](https://dac-static.atlassian.com/cloud/jira/platform/swagger-v3.v3.json) |
|
||||
| Confluence Cloud REST API | converts ADF to and from storage format, and ADF to rendered HTML views; no markdown | [OpenAPI](https://developer.atlassian.com/cloud/confluence/swagger.v3.json) |
|
||||
| Atlassian Rovo MCP server | its sample skills read and write Confluence pages as markdown | [README](https://github.com/atlassian/atlassian-mcp-server) |
|
||||
| `@atlaskit/editor-wikimarkup-transformer` 12.2.3 | ADF to and from Jira wiki markup, not markdown | [npm](https://www.npmjs.com/package/@atlaskit/editor-wikimarkup-transformer) |
|
||||
|
||||
## The calls
|
||||
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import * as adf2markdown from "adf2markdown";
|
||||
import * as adfCodec from "@larvit/adf-codec";
|
||||
import * as adfMd from "adf-md";
|
||||
import * as adfToMarkdown from "adf-to-markdown";
|
||||
import adfToMd from "adf-to-md";
|
||||
import { loadAdf4j } from "@nthings.dev/adf4j-wasm";
|
||||
import * as atlaskitJson from "@atlaskit/editor-json-transformer";
|
||||
import * as atlaskitMarkdown from "@atlaskit/editor-markdown-transformer";
|
||||
import { createRequire } from "node:module";
|
||||
import * as extendedParser from "extended-markdown-adf-parser";
|
||||
import * as faulander from "@faulander/md2adf";
|
||||
import * as githubMarkdownAdf from "github-markdown-adf";
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
import * as marklassian from "marklassian";
|
||||
import * as markdownToAdf from "markdown-to-adf";
|
||||
import { fromADF } from "mdast-util-from-adf";
|
||||
import mdToAdf from "md-to-adf";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { toMarkdown as mdastToMarkdown } from "mdast-util-to-markdown";
|
||||
|
||||
const { defaultSchema } = createRequire(import.meta.url)("@atlaskit/adf-schema/schema-default");
|
||||
|
||||
@@ -43,6 +50,11 @@ const markdown = "***bold italic*** and [a link](https://example.com) hi @Ann\n\
|
||||
const extended = new extendedParser.Parser();
|
||||
const json = new atlaskitJson.JSONTransformer();
|
||||
const atlaskit = new atlaskitMarkdown.MarkdownTransformer(defaultSchema);
|
||||
const adf4j = await loadAdf4j();
|
||||
const succeeded = (result) => {
|
||||
if (result.type !== "Success") throw new Error(JSON.stringify(result));
|
||||
return result.value;
|
||||
};
|
||||
const converters = {
|
||||
"@atlaskit/editor-markdown-transformer": {
|
||||
toAdf: (md) => json.encode(atlaskit.parse(md)),
|
||||
@@ -52,9 +64,24 @@ const converters = {
|
||||
toAdf: (md) => adfCodec.losslessMarkdownToAdf(md).value,
|
||||
toMarkdown: (doc) => adfCodec.adfToLosslessMarkdown(doc).value,
|
||||
},
|
||||
"@faulander/md2adf": {
|
||||
toAdf: (md) => faulander.markdownToAdf(md),
|
||||
toMarkdown: (doc) => faulander.adfToMarkdown(doc),
|
||||
},
|
||||
"@nthings.dev/adf4j-wasm": { toMarkdown: (doc) => adf4j.convert(JSON.stringify(doc)) },
|
||||
"adf-md": {
|
||||
toAdf: (md) => succeeded(adfMd.markdownToAdf(md)),
|
||||
toMarkdown: (doc) => succeeded(adfMd.adfToMarkdown(doc)),
|
||||
},
|
||||
"adf-to-markdown": { toMarkdown: (doc) => adfToMarkdown.convertADFToMarkdown(doc) },
|
||||
"adf-to-md": { toMarkdown: (doc) => adfToMd.convert(doc).result },
|
||||
adf2markdown: { toMarkdown: (doc) => adf2markdown.adf2markdown(doc) },
|
||||
"github-markdown-adf": {
|
||||
toAdf: (md) => githubMarkdownAdf.mdToAdf(md),
|
||||
toMarkdown: (doc) => githubMarkdownAdf.adfToMd(doc),
|
||||
},
|
||||
"markdown-to-adf": { toAdf: (md) => markdownToAdf.markdownToAdf(md) },
|
||||
"mdast-util-from-adf": { toMarkdown: (doc) => mdastToMarkdown(fromADF(doc)) },
|
||||
"extended-markdown-adf-parser": {
|
||||
toAdf: (md) => extended.markdownToAdf(md),
|
||||
toMarkdown: (doc) => extended.adfToMarkdown(doc),
|
||||
@@ -77,11 +104,11 @@ for (const [name, { toAdf, toMarkdown }] of Object.entries(converters)) {
|
||||
console.log(`\n## ${name} ${manifest.version}`);
|
||||
console.log("direct runtime dependencies:", Object.keys(manifest.dependencies ?? {}).length);
|
||||
const written = toMarkdown && show("ADF to markdown", () => toMarkdown(roundTripDoc));
|
||||
if (written !== undefined) {
|
||||
const readBack = toAdf && show("read back", () => toAdf(written));
|
||||
if (written !== undefined && toAdf) {
|
||||
const readBack = show("read back", () => toAdf(written));
|
||||
if (readBack !== undefined) console.log("round-trip deep-equal:", isDeepStrictEqual(readBack, roundTripDoc));
|
||||
const unknown = show("unknown node to markdown", () => toMarkdown(unknownNodeDoc));
|
||||
if (toAdf && unknown !== undefined) show("unknown node read back", () => toAdf(unknown));
|
||||
}
|
||||
const unknown = toMarkdown && show("unknown node to markdown", () => toMarkdown(unknownNodeDoc));
|
||||
if (toAdf && unknown !== undefined) show("unknown node read back", () => toAdf(unknown));
|
||||
if (toAdf) show("markdown to ADF", () => toAdf(markdown));
|
||||
}
|
||||
|
||||
@@ -3,16 +3,23 @@
|
||||
set -eu
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
node_image=node:26.12.1-alpine3.24
|
||||
docker run --rm --ulimit core=0 -u "$(id -u):$(id -g)" -e HOME=/tmp -v "$here:/comparison:ro" -w /tmp \
|
||||
docker run --rm --memory 2g --memory-swap 2g --ulimit core=0 -u "$(id -u):$(id -g)" -e HOME=/tmp -v "$here:/comparison:ro" -w /tmp \
|
||||
"$node_image" sh -c '
|
||||
npm install --before=2026-10-15 --ignore-scripts --no-audit --no-fund --silent \
|
||||
npm install --before=2026-10-29 --ignore-scripts --no-audit --no-fund --silent \
|
||||
@atlaskit/editor-json-transformer@10.0.0 \
|
||||
@atlaskit/editor-markdown-transformer@6.3.1 \
|
||||
@faulander/md2adf@1.0.0 \
|
||||
@larvit/adf-codec@0.2.0 \
|
||||
@nthings.dev/adf4j-wasm@1.0.2 \
|
||||
adf-md@0.2.0 \
|
||||
adf-to-markdown@1.0.1 \
|
||||
adf-to-md@1.2.1 \
|
||||
adf2markdown@1.2.0 \
|
||||
extended-markdown-adf-parser@2.4.0 \
|
||||
github-markdown-adf@1.3.0 \
|
||||
markdown-to-adf@0.2.20 \
|
||||
marklassian@1.2.1 \
|
||||
md-to-adf@0.6.4 &&
|
||||
md-to-adf@0.6.4 \
|
||||
mdast-util-from-adf@2.3.0 \
|
||||
mdast-util-to-markdown@2.2.1 &&
|
||||
cp /comparison/compare.mjs . && node compare.mjs'
|
||||
|
||||
+6
-2
@@ -1,16 +1,19 @@
|
||||
{
|
||||
"name": "@larvit/adf-codec",
|
||||
"version": "0.2.0",
|
||||
"description": "Convert Atlassian Document Format (ADF), the rich text of Jira and Confluence, to and from markdown, with a lossless round-trip, and to HTML.",
|
||||
"description": "Convert Atlassian Document Format (ADF), the rich text of Jira and Confluence, to and from markdown, with a lossless round-trip, and to HTML. No React, no Atlaskit, no dependencies: runs in Node, Forge, Deno, Bun and browsers.",
|
||||
"keywords": [
|
||||
"adf",
|
||||
"adf-to-html",
|
||||
"adf-to-markdown",
|
||||
"atlassian",
|
||||
"atlassian-document-format",
|
||||
"atlassian-forge",
|
||||
"commonmark",
|
||||
"confluence",
|
||||
"converter",
|
||||
"esm",
|
||||
"forge",
|
||||
"gfm",
|
||||
"html",
|
||||
"jira",
|
||||
@@ -18,7 +21,8 @@
|
||||
"markdown",
|
||||
"markdown-to-adf",
|
||||
"round-trip",
|
||||
"typescript"
|
||||
"typescript",
|
||||
"zero-dependencies"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
Reference in New Issue
Block a user