From 11529ac16575a3395613730404d65fea8173ee04 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 28 Oct 2026 21:05:05 +0100 Subject: [PATCH] Open the README with the call to take for each need, and widen the converter comparison --- CHANGELOG.md | 3 ++ README.md | 77 ++++++++++++++++++++------------ converter-comparison/compare.mjs | 35 +++++++++++++-- converter-comparison/run.sh | 13 ++++-- package.json | 8 +++- 5 files changed, 98 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08a171f..18567a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index c939c71..e1a4224 100644 --- a/README.md +++ b/README.md @@ -3,30 +3,26 @@ [![npm](https://img.shields.io/npm/v/@larvit/adf-codec)](https://www.npmjs.com/package/@larvit/adf-codec) [![CI](https://github.com/larvit/adf-codec/actions/workflows/ci.yml/badge.svg)](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:` 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 diff --git a/converter-comparison/compare.mjs b/converter-comparison/compare.mjs index d167ae2..78c29e0 100644 --- a/converter-comparison/compare.mjs +++ b/converter-comparison/compare.mjs @@ -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)); } diff --git a/converter-comparison/run.sh b/converter-comparison/run.sh index c756818..2c0ed8b 100755 --- a/converter-comparison/run.sh +++ b/converter-comparison/run.sh @@ -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' diff --git a/package.json b/package.json index 1de77c2..bb968aa 100644 --- a/package.json +++ b/package.json @@ -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": {