diff --git a/CHANGELOG.md b/CHANGELOG.md index d0cba89..b227b2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ - `losslessMarkdownToAdf` names the CommonMark spelling to write when it refuses a directive whose node CommonMark spells, such as `[![alt](url)](href)` for a linked `!adf:mediaSingle`. +- The README compares the package with seven other ADF converters, each checked against its + published version. + ## 0.2.0 - **Breaking:** each call names the markdown it reads or writes. `adfToMarkdown` and `markdownToAdf` diff --git a/README.md b/README.md index dd9e988..1ee4426 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ Nothing throws. `code` is from a closed list, so a `switch` with no `default` co - [Goals](#goals) - [Audience](#audience) +- [Other ADF converters](#other-adf-converters) - [The calls](#the-calls) - [The lossless flavour](#the-lossless-flavour) - [Portable markdown](#portable-markdown) @@ -191,6 +192,24 @@ Behind those apps, the people who read and write the markdown, and later the HTM managers, engineers and support agents working in Atlassian products through a plugin or another UI. They know markdown and not ADF, and rely on every spelling saying what it means to them. +## Other ADF converters + +Serves [Goals](#goals) 3. Each package at the version named, checked 2026-10-14 by running its +published code on one document holding marks, a link, a mention, a status, a panel and a list, and +on one holding a node type it cannot know: +[`converter-comparison/`](https://github.com/larvit/adf-codec/tree/main/converter-comparison). + +| Package | Converts | ADF to markdown to ADF | A node type it does not know | Formats | Runtime dependencies | +| --- | --- | --- | --- | --- | --- | +| `@larvit/adf-codec` 0.2.0 | both ways | the same document | kept | markdown: lossless, portable, CommonMark | 0 | +| `@atlaskit/editor-markdown-transformer` 6.3.1 | markdown to ADF, through `@atlaskit/editor-json-transformer`; ADF to markdown throws "not implemented" | – | – | markdown | 5 | +| `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 | +| `marklassian` 1.2.1 | markdown to ADF | – | – | markdown | 1 | +| `md-to-adf` 0.6.4 | markdown to ADF | – | – | markdown | 2 | + ## The calls Serves [Goals](#goals) 1, 2 and 8. Pure functions, each taking a whole document and returning a diff --git a/converter-comparison/compare.mjs b/converter-comparison/compare.mjs new file mode 100644 index 0000000..66c4e46 --- /dev/null +++ b/converter-comparison/compare.mjs @@ -0,0 +1,86 @@ +import { createRequire } from "node:module"; +import { readFileSync } from "node:fs"; +import { isDeepStrictEqual } from "node:util"; +import * as atlaskitJson from "@atlaskit/editor-json-transformer"; +import * as atlaskitMarkdown from "@atlaskit/editor-markdown-transformer"; +import * as adfCodec from "@larvit/adf-codec"; +import * as adfToMarkdown from "adf-to-markdown"; +import adfToMd from "adf-to-md"; +import * as adf2markdown from "adf2markdown"; +import * as extendedParser from "extended-markdown-adf-parser"; +import * as marklassian from "marklassian"; +import mdToAdf from "md-to-adf"; + +const { defaultSchema } = createRequire(import.meta.url)("@atlaskit/adf-schema/schema-default"); + +const roundTripDoc = { + type: "doc", version: 1, content: [ + { type: "paragraph", content: [ + { type: "text", text: "bold italic", marks: [{ type: "strong" }, { type: "em" }] }, + { type: "text", text: " and " }, + { type: "text", text: "a link", marks: [{ type: "link", attrs: { href: "https://example.com" } }] }, + { type: "text", text: " hi " }, + { type: "mention", attrs: { id: "abc", text: "@Ann" } }, + { type: "text", text: " state " }, + { type: "status", attrs: { color: "green", localId: "s1", text: "DONE" } }, + ] }, + { type: "panel", attrs: { panelType: "info" }, content: [{ type: "paragraph", content: [{ type: "text", text: "note" }] }] }, + { type: "bulletList", content: [ + { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "one" }] }] }, + { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "two" }] }] }, + ] }, + ], +}; +const unknownNodeDoc = { + type: "doc", version: 1, content: [ + { type: "paragraph", content: [{ type: "text", text: "before" }] }, + { type: "futureThing", attrs: { x: 1 }, content: [{ type: "paragraph", content: [{ type: "text", text: "inside" }] }] }, + { type: "paragraph", content: [{ type: "text", text: "after" }] }, + ], +}; +const markdown = "***bold italic*** and [a link](https://example.com) hi @Ann\n\n> note\n\n- one\n- two\n"; + +const extended = new extendedParser.Parser(); +const json = new atlaskitJson.JSONTransformer(); +const atlaskit = new atlaskitMarkdown.MarkdownTransformer(defaultSchema); +const converters = { + "@atlaskit/editor-markdown-transformer": { + toAdf: (md) => json.encode(atlaskit.parse(md)), + toMarkdown: (doc) => atlaskit.encode(json.parse(doc)), + }, + "@larvit/adf-codec": { + toAdf: (md) => adfCodec.losslessMarkdownToAdf(md).value, + toMarkdown: (doc) => adfCodec.adfToLosslessMarkdown(doc).value, + }, + "adf-to-markdown": { toMarkdown: (doc) => adfToMarkdown.convertADFToMarkdown(doc) }, + "adf-to-md": { toMarkdown: (doc) => adfToMd.convert(doc).result }, + adf2markdown: { toMarkdown: (doc) => adf2markdown.adf2markdown(doc) }, + "extended-markdown-adf-parser": { + toAdf: (md) => extended.markdownToAdf(md), + toMarkdown: (doc) => extended.adfToMarkdown(doc), + }, + marklassian: { toAdf: (md) => marklassian.markdownToAdf(md) }, + "md-to-adf": { toAdf: (md) => mdToAdf(md) }, +}; + +const show = (label, call) => { + try { + const value = call(); + console.log(`${label}:`, JSON.stringify(value)); + return value; + } catch (error) { + console.log(`${label} threw:`, error.message); + } +}; +for (const [name, { toAdf, toMarkdown }] of Object.entries(converters)) { + const manifest = JSON.parse(readFileSync(`node_modules/${name}/package.json`, "utf8")); + console.log(`\n## ${name} ${manifest.version}`); + console.log("runtime dependencies:", Object.keys(manifest.dependencies ?? {}).length); + const written = toMarkdown && show("ADF to markdown", () => toMarkdown(roundTripDoc)); + if (written !== undefined) { + if (toAdf) show("round-trip deep-equal", () => isDeepStrictEqual(toAdf(written), roundTripDoc)); + const unknown = 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 new file mode 100755 index 0000000..8541913 --- /dev/null +++ b/converter-comparison/run.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# Re-checks the README's comparison table against each package's published version. +set -eu +here=$(cd "$(dirname "$0")" && pwd) +docker run --rm --ulimit core=0 -u "$(id -u):$(id -g)" -e HOME=/tmp -v "$here:/comparison:ro" -w /tmp \ + node:24.21.0-alpine3.24 sh -c ' + npm install --ignore-scripts --no-audit --no-fund --silent \ + @atlaskit/editor-json-transformer@10.0.0 \ + @atlaskit/editor-markdown-transformer@6.3.1 \ + @larvit/adf-codec@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 \ + marklassian@1.2.1 \ + md-to-adf@0.6.4 && + cp /comparison/compare.mjs . && node compare.mjs' diff --git a/todo.md b/todo.md index ed31ada..cb9d590 100644 --- a/todo.md +++ b/todo.md @@ -26,7 +26,6 @@ | ID | Release | Exempt | Item | R | S | C | A | G | Goals | Score | |---|---|---|---|---|---|---|---|---|---|---| | 73 | 0.3.0 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 1 | 6 | 8 | 3 | 23.0 | -| 71 | 0.3.0 | | **Add a README table comparing the package with the other ADF converters, each cell checked against that package's own docs or behaviour.** | 1 | 3 | 2 | 6 | 7 | 3 | 21.2 | | 114 | 0.3.0 | defect | **Build code-marked text holding no mark besides `link` and `annotation`, in every reader, and have `adfToLosslessMarkdown` carry code text holding any other mark.** | 3 | 4 | 5 | 5 | 7 | 4 | 15.4 | | 99 | 0.3.0 | defect | **Write a linked external image standing inline or in a table cell in the spelling a reader panel picks, in `adfToPortableMarkdown`.** | 3 | 3 | 5 | 6 | 7 | 7 | 12.9 | | 100 | 0.3.0 | decision | **Put the reading of an image's title as its caption back to a reader panel, now that a writer panel reads a title as hover text.** | 3 | 3 | 3 | 4 | 5 | 4, 6 | 10.9 | @@ -71,20 +70,9 @@ ### 73. Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions. -Lands after item 71, so each post links the GitHub repository, the README's comparison table and a -README that opens with what the package is. We draft each post for the place it goes; the maintainer -posts them once `0.3.0` is published. Found while planning discoverability, 2026-10-04. - -### 71. Add a README table comparing the package with the other ADF converters, each cell checked against that package's own docs or behaviour. - -Lands before items 7 and 46, which add HTML and a size column to the table. Its columns: which -directions each converts, whether the round-trip holds, what an unknown node does, the formats and -runtime dependencies. Candidates are the packages an npm search for ADF and markdown returns. Every -cell cites that package's README or a run against its published version, and the table names each -version and the date it was checked. Found while planning discoverability, 2026-10-04. Checked -2026-10-05: no other package converts HTML or holds the round-trip; the candidates are -`@atlaskit/editor-markdown-transformer`, `adf-to-markdown`, `adf-to-md`, `adf2markdown`, -`extended-markdown-adf-parser`, `marklassian` and `md-to-adf`. +Each post links the GitHub repository, the README's comparison table and a README that opens with +what the package is. We draft each post for the place it goes; the maintainer posts them once +`0.3.0` is published. Found while planning discoverability, 2026-10-04. ### 114. Build code-marked text holding no mark besides `link` and `annotation`, in every reader, and have `adfToLosslessMarkdown` carry code text holding any other mark. @@ -185,7 +173,7 @@ on such a document. Found while shipping item 106, 2026-10-11. Lands after items 6, 59, 60 and 62. Which flavours get a composed call is this item's to settle; the CommonMark spec suite also runs against the one reading CommonMark. The README documents HTML as it -documents markdown, its comparison table (item 71) names HTML, and its tagline and `package.json`'s +documents markdown, its comparison table names HTML, and its tagline and `package.json`'s description and keywords name HTML again. `htmlToAdf` refuses a value that is not a string as `not-a-string` (`docs/decisions.md` §Which code a cause takes). @@ -266,7 +254,7 @@ tarball `npm pack` produces, its unpacked `dist`, and the built JavaScript minif figure the competitors advertise (marklassian's "12kb") and the only apples-to-apples one, since ours ships tsc's unminified output and no minifier yet (decide here whether to minify for the build or report the unminified gzip). The figure lands in README §The package beside the "no runtime -dependencies" claim, and in a size column of the comparison table (item 71). Measured today, +dependencies" claim, and in a size column of the comparison table. Measured today, unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped 45.6 kB. ### 72. Re-create `gitea.larvit.se/larvit/adf-codec` as a read-only pull mirror of GitHub, its issues and Actions off and its description pointing at GitHub.