From 11ed9fe999490aefd59af2f65482605fcaf6bb11 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 23 Oct 2026 12:50:48 +0200 Subject: [PATCH] Ship adfToHtml: semantic HTML for the CommonMark blocks and marks, an adf-carry element for the rest --- AGENTS.md | 3 +- CHANGELOG.md | 4 + README.md | 63 ++++- docs/decisions.md | 30 ++- docs/reading-cost.md | 4 + package-tests/consumer.ts | 5 +- package.json | 4 +- spec/html.md | 78 ++++++ src/adf/carried-json.ts | 17 ++ src/adf/external-image.ts | 29 ++ src/adf/mark-attributes.ts | 15 ++ src/adf/shown-text.ts | 60 +++++ src/conformance/adf-property.test.ts | 11 + src/conformance/property-harness.ts | 5 +- src/html/emit/adf-to-html.test.ts | 191 ++++++++++++++ src/html/emit/adf-to-html.ts | 249 ++++++++++++++++++ src/index.ts | 1 + src/{markdown => }/length-bound.test.ts | 4 +- src/{markdown => }/length-bound.ts | 14 +- src/markdown/emit/adf-to-markdown.test.ts | 2 +- src/markdown/emit/adf-to-markdown.ts | 4 +- src/markdown/emit/image.ts | 18 +- src/markdown/external-image.ts | 19 -- src/markdown/image-aside.ts | 6 + src/markdown/mark-spellings.ts | 21 +- src/markdown/opaque-carry.ts | 18 +- src/markdown/parse/inline-content.ts | 7 +- src/markdown/parse/markdown-to-adf.test.ts | 2 +- src/markdown/parse/markdown-to-adf.ts | 2 +- .../portable/adf-to-portable-markdown.ts | 10 +- src/markdown/portable/inline-reduction.ts | 51 +--- .../portable/portable-markdown-to-adf.ts | 2 +- todo.md | 24 +- 33 files changed, 826 insertions(+), 147 deletions(-) create mode 100644 spec/html.md create mode 100644 src/adf/carried-json.ts create mode 100644 src/adf/external-image.ts create mode 100644 src/adf/shown-text.ts create mode 100644 src/html/emit/adf-to-html.test.ts create mode 100644 src/html/emit/adf-to-html.ts rename src/{markdown => }/length-bound.test.ts (96%) rename src/{markdown => }/length-bound.ts (76%) delete mode 100644 src/markdown/external-image.ts create mode 100644 src/markdown/image-aside.ts diff --git a/AGENTS.md b/AGENTS.md index f3c0fa3..fa24a39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,7 +66,8 @@ In `docs/decisions.md`: - The CommonMark suite checks three ways - The flavour spec is read as a source - The node tables answer to Atlassian's schema -- Markdown past V8's longest string is refused on every engine +- Output past V8's longest string is refused on every engine +- A URL a browser would run is carried, never written - Nothing recurses unbounded - JSON text is read by `parseJsonText` - No value `JSON.parse` or `structuredClone` builds loops a call diff --git a/CHANGELOG.md b/CHANGELOG.md index 8873ff6..34d3906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- `adfToHtml` writes ADF as HTML: paragraphs, headings, lists, quotes, code blocks, rules, hard + breaks, external images, and `em`, `strong`, `code` and links as their elements. Every other node + is an element of class `adf-carry` holding the node's JSON in `data-adf` and showing its text. A + link or image whose URL a browser would run, such as `javascript:`, rides that element too. - **Breaking:** `losslessMarkdownToAdf` gives a `date`, `emoji`, `inlineCard`, `mention`, `status` or `mediaInline` only the marks Atlassian's schema lets it hold: `**!adf:mention[@A]{id=…}**` now reads as a plain mention. A link or directive mark around only nodes that cannot take it is diff --git a/README.md b/README.md index a489831..3c7c375 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# @larvit/adf-codec — ADF to markdown, markdown to ADF +# @larvit/adf-codec — ADF to markdown and HTML, markdown to ADF [![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. +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. @@ -19,6 +19,9 @@ products store as ADF. 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:** `adfToHtml` writes paragraphs, headings, lists, quotes, code blocks, images and + links as their HTML elements, and every other node as an element holding its JSON and showing its + text. - **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. @@ -67,6 +70,18 @@ if (portable.ok) console.log(portable.value) // > Check the collation before importing. ``` +### ADF to HTML + +Full reference: [HTML](#html). + +```ts +import { adfToHtml } from '@larvit/adf-codec' + +const html = adfToHtml(doc) // the panel above +if (html.ok) console.log(html.value) +//
Check the collation before importing.
+``` + ### Markdown to ADF Full reference: [The calls](#the-calls) and [Portable markdown](#portable-markdown). @@ -150,6 +165,7 @@ Nothing throws. `code` is from a closed list, so a `switch` with no `default` co - [The calls](#the-calls) - [The lossless flavour](#the-lossless-flavour) - [Portable markdown](#portable-markdown) +- [HTML](#html) - [The errors](#the-errors) - [The guarantees](#the-guarantees) - [The package](#the-package) @@ -217,7 +233,7 @@ 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 | 0 | +| `@larvit/adf-codec` | both ways | the same document | kept | markdown: lossless, portable; CommonMark in; HTML out | 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 | | `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 | @@ -240,12 +256,15 @@ commonMarkToAdf(markdown: string): Result adfToPortableMarkdown(doc: AdfDocument): Result portableMarkdownToAdf(markdown: string): Result + +adfToHtml(doc: AdfDocument): Result ``` -Each call's name says which markdown it reads or writes: +Each call's name says which format it reads or writes: -| Call | Markdown | Refuses with | +| Call | Reads or writes | Refuses with | | --- | --- | --- | +| `adfToHtml` | writes HTML: the elements [HTML](#html) lists, and an `adf-carry` element holding everything else | `engine-limit`, `not-an-adf-document`, `unsupported-document-version`, `unsupported-nesting-depth`, `unsupported-node-shape` | | `adfToLosslessMarkdown` | writes the lossless flavour: CommonMark plus GFM's pipe tables and `~~`, with `!adf:` directives and `adf:` fences for everything else | `engine-limit`, `not-an-adf-document`, `unsupported-document-version`, `unsupported-nesting-depth`, `unsupported-node-shape` | | `adfToPortableMarkdown` | writes markdown GitHub, GitLab and Obsidian render; drops what they cannot show, and writes no directive | `engine-limit`, `not-an-adf-document`, `unsupported-document-version`, `unsupported-nesting-depth`, `unsupported-node-shape` | | `commonMarkToAdf` | reads strict CommonMark as its spec says: a directive, a pipe table, `~~`, `==` and an `adf:` fence read as text and code | `engine-limit`, `not-a-string`, `unmappable-html`, `unsupported-nesting-depth`, `unsupported-node-shape` | @@ -425,6 +444,34 @@ saving what this pair read replaces mentions, attachments and macros with text. position, the same on every read. Join markdown bound for one document and read it once: the same markdown read twice into one document repeats its ids. +## HTML + +Serves [Goals](#goals) 1, 6 and 7. `adfToHtml` writes the HTML element for each node HTML has one +for, and keeps every other node whole: an element of class `adf-carry` holds the node's JSON in +`data-adf` and shows the node's text, so a reader of the page loses no content. No stylesheet +ships. The complete dialect: +[`spec/html.md`](https://github.com/larvit/adf-codec/blob/main/spec/html.md). + +| ADF | HTML | +| --- | --- | +| `blockquote` | `
` | +| `bulletList`, `orderedList`, `listItem` | `