mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Answer prose round 1: 0 🔴, 11 🟡 (0 misread); claims true to the code, one pick per line in the dialect's panels, plainer comments and test names
This commit is contained in:
@@ -225,7 +225,8 @@ The comprehension panel's fill-ins:
|
||||
|
||||
- Language: TypeScript.
|
||||
- Kind: a pure-function document converter with hand-written parsers and emitters.
|
||||
- Domain: Atlassian Document Format and CommonMark parsing.
|
||||
- Domain docs: the CommonMark spec, ADF's JSON schema and `spec/flavour.md`.
|
||||
- Domain: Atlassian Document Format, CommonMark and HTML.
|
||||
- Domain docs: the CommonMark spec, the WHATWG HTML standard, ADF's JSON schema, `spec/flavour.md` and
|
||||
`spec/html.md`.
|
||||
- 3am question: a viewer/editor app reports that a document it saved comes back with two text
|
||||
nodes merged and a mark gone after `losslessMarkdownToAdf(adfToLosslessMarkdown(doc))`.
|
||||
|
||||
+3
-2
@@ -5,8 +5,9 @@
|
||||
- `adfToHtml` writes ADF as HTML: paragraphs, headings, lists, quotes, code blocks, rules, hard
|
||||
breaks, a centered 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
|
||||
rule writes an attribute such as `localId` as `data-local-id`. Every other node is an element of class `adf-carry` holding the node's JSON in
|
||||
`data-adf` and showing its content, links included. Every URL is written as the ADF holds it,
|
||||
rule writes its other attributes as `data-*` attributes, `localId` as `data-local-id`. Every other
|
||||
node is an element of class `adf-carry` holding the node's JSON in `data-adf` and showing its
|
||||
content, links included. Every URL is written as the ADF holds it,
|
||||
`javascript:` included: sanitize HTML from an author you do not trust before showing it.
|
||||
- **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
|
||||
|
||||
@@ -19,9 +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 and links as
|
||||
their HTML elements, and every other node, an image Atlassian's editor wrote among them, as an
|
||||
element holding its JSON and showing its content.
|
||||
- **HTML out:** `adfToHtml` writes paragraphs, headings, lists, quotes, code blocks, rules, line
|
||||
breaks, emphasis, inline code and links 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.
|
||||
- **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.
|
||||
@@ -70,37 +70,6 @@ if (portable.ok) console.log(portable.value)
|
||||
// > Check the collation before importing.
|
||||
```
|
||||
|
||||
### ADF to HTML
|
||||
|
||||
Full reference: [HTML](#html).
|
||||
|
||||
```ts
|
||||
import { adfToHtml, type AdfDocument } from '@larvit/adf-codec'
|
||||
|
||||
const page: AdfDocument = {
|
||||
content: [
|
||||
{ attrs: { level: 2, localId: 'a1f3' }, content: [{ text: 'Release notes', type: 'text' }], type: 'heading' },
|
||||
{
|
||||
content: [
|
||||
{ text: 'Read the ', type: 'text' },
|
||||
{ marks: [{ attrs: { href: 'https://spec.commonmark.org/' }, type: 'link' }], text: 'spec', type: 'text' },
|
||||
{ text: ', then ask ', type: 'text' },
|
||||
{ attrs: { id: '5b10a2', text: '@Mikael' }, type: 'mention' },
|
||||
],
|
||||
type: 'paragraph',
|
||||
},
|
||||
],
|
||||
type: 'doc',
|
||||
version: 1,
|
||||
}
|
||||
|
||||
// URLs are written as the ADF holds them, javascript: included: sanitize HTML from an author you do not trust before showing it.
|
||||
const html = adfToHtml(page)
|
||||
if (html.ok) console.log(html.value)
|
||||
// <h2 data-local-id="a1f3">Release notes</h2>
|
||||
// <p>Read the <a href="https://spec.commonmark.org/">spec</a>, then ask <span class="adf-carry" data-adf="{"attrs":{"id":"5b10a2","text":"@Mikael"},"type":"mention"}">@Mikael</span></p>
|
||||
```
|
||||
|
||||
### Markdown to ADF
|
||||
|
||||
Full reference: [The calls](#the-calls) and [Portable markdown](#portable-markdown).
|
||||
@@ -156,6 +125,37 @@ if (!markdown.ok) {
|
||||
Unedited, the markdown reads back to a document that deep-equals `doc`, node types this version
|
||||
does not know included.
|
||||
|
||||
### ADF to HTML
|
||||
|
||||
Full reference: [HTML](#html).
|
||||
|
||||
```ts
|
||||
import { adfToHtml, type AdfDocument } from '@larvit/adf-codec'
|
||||
|
||||
const page: AdfDocument = {
|
||||
content: [
|
||||
{ attrs: { level: 2, localId: 'a1f3' }, content: [{ text: 'Release notes', type: 'text' }], type: 'heading' },
|
||||
{
|
||||
content: [
|
||||
{ text: 'Read the ', type: 'text' },
|
||||
{ marks: [{ attrs: { href: 'https://spec.commonmark.org/' }, type: 'link' }], text: 'spec', type: 'text' },
|
||||
{ text: ', then ask ', type: 'text' },
|
||||
{ attrs: { id: '5b10a2', text: '@Mikael' }, type: 'mention' },
|
||||
],
|
||||
type: 'paragraph',
|
||||
},
|
||||
],
|
||||
type: 'doc',
|
||||
version: 1,
|
||||
}
|
||||
|
||||
// URLs are written as the ADF holds them, javascript: included: sanitize HTML from an author you do not trust before showing it.
|
||||
const html = adfToHtml(page)
|
||||
if (html.ok) console.log(html.value)
|
||||
// <h2 data-local-id="a1f3">Release notes</h2>
|
||||
// <p>Read the <a href="https://spec.commonmark.org/">spec</a>, then ask <span class="adf-carry" data-adf="{"attrs":{"id":"5b10a2","text":"@Mikael"},"type":"mention"}">@Mikael</span></p>
|
||||
```
|
||||
|
||||
### Handle a result's error
|
||||
|
||||
Full reference: [The errors](#the-errors).
|
||||
@@ -465,8 +465,8 @@ saving what this pair read replaces mentions, attachments and macros with text.
|
||||
|
||||
## 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
|
||||
Serves [Goals](#goals) 1, 6 and 7. `adfToHtml` writes the nodes and marks the table lists as their
|
||||
HTML elements, 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 content, its links included, 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).
|
||||
@@ -483,21 +483,21 @@ content. No stylesheet ships. The complete dialect:
|
||||
| `paragraph` | `<p>` |
|
||||
| `rule` | `<hr>` |
|
||||
| `text` marked `code`, `em`, `strong` | `<code>`, `<em>`, `<strong>`, the first mark outermost |
|
||||
| any other attribute the node's type holds, on a paragraph, heading, quote, list, list item, code block or rule | `data-*` named as HTML's `dataset` names it: `<p data-local-id="a1f3">` |
|
||||
| an attribute its element does not write, such as `localId`, on a paragraph, heading, quote, list, list item, code block or rule | `data-*` named as HTML's `dataset` names it: `<p data-local-id="a1f3">` |
|
||||
| any other node, or a node holding what its element cannot | `<div class="adf-carry" data-adf="{…}"><p>its content</p></div>`, a `<span>` inline |
|
||||
|
||||
- Every URL is written as the ADF holds it, `javascript:` included: a link's, an image's, and a
|
||||
card's or a carried link's in the carry element. Sanitize the HTML before showing a document from
|
||||
an author you do not trust.
|
||||
- Text and attribute values are escaped: `&`, `<`, `>` and `"`. A node holding what HTML cannot
|
||||
hold, such as a carriage return or a control character, rides the carry element.
|
||||
- In a `<div>`, the carry element shows each block's content as a `<p>`, and a code block's as
|
||||
`<pre><code>` keeping its indentation; in a `<span>`, it parts blocks with `<br>`. It shows text a
|
||||
link marks as a link, a mention, status or emoji by its text, a date as `2026-09-13` in UTC, a
|
||||
card as a link to its URL, an external image as a link to it, a stored one by its `alt`, and an
|
||||
expand by its title. An image a link to another target marks shows that link, then `(image)`
|
||||
linking to the image. What it cannot show leaves a note, `<em>(image)</em>`, inside the node's
|
||||
link where it has one.
|
||||
- Every URL is written as the ADF holds it, `javascript:` included: a link's, an image's, and a
|
||||
card's or a carried link's in the carry element. Sanitize the HTML before showing a document from
|
||||
an author you do not trust.
|
||||
card as a link to its URL, or to the target of a link marking it, an external image as a link to
|
||||
it, a stored one by its `alt`, and an expand by its title. An image marked by a link to another
|
||||
target shows that link, then `(image)` linking to the image. A node it cannot show leaves a note
|
||||
instead, such as `<em>(image)</em>`; a link marking the node goes around the note.
|
||||
|
||||
## The errors
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ leg "install ($node_image)" in_image "$node_image" npm ci
|
||||
leg "typecheck ($node_image)" in_image "$node_image" npm run typecheck
|
||||
leg "lint ($node_image)" in_image "$node_image" npm run lint
|
||||
|
||||
# The boundaries pass by finding nothing, so probe that each still fires: oxlint bumps automerge on green. Each line is a probe and the import a boundary refuses it.
|
||||
# The boundaries pass by finding nothing, so probe that each still fires: oxlint bumps automerge on green. Each line names a probe file and an import its boundary refuses.
|
||||
boundary_probes="src/adf/boundary-probe.ts ../markdown/flavour.ts
|
||||
src/adf/boundary-probe-html.ts ../html/emit/adf-to-html.ts
|
||||
src/html/boundary-probe.ts ../markdown/flavour.ts
|
||||
|
||||
+31
-24
@@ -398,19 +398,25 @@ themselves.
|
||||
The HTML dialect mirrors the markdown flavour: semantic elements, stable `adf-*` classes, `data-*`
|
||||
for what HTML cannot express, text always escaped. No stylesheet ships.
|
||||
|
||||
The spellings, `spec/html.md`, are a writer panel's (2026-10-23): a product manager, an engineer and
|
||||
a support agent picked, 3 of 3, an image standing alone as `<p><img …></p>` over `<figure>` and a
|
||||
bare `<img>`, a list item's first paragraph bare, `<li>Bolt</li>`, over `<li><p>Bolt</p></li>`,
|
||||
`class="language-js"` over `lang-js`, and `<br>` over `<br />`. For the carry element, seven readers
|
||||
picked `data-adf` 6 of 7 over `data-adf-node`. Four of them wanted the class `adf-carry` and three
|
||||
no class; this entry's stable `adf-*` classes settle it.
|
||||
The spellings, `spec/html.md`, are four writer panels' (2026-10-23 and -24), each of a product
|
||||
manager, an engineer and a support agent. The first picked each of these 3 of 3:
|
||||
|
||||
A second panel of the same three roles picked, 3 of 3: an attribute the element cannot express as
|
||||
`data-local-id` over `data-adf-local-id` and `id`, since Atlassian's editor puts a `localId` on every
|
||||
block and a block carried for it would show no element at all; inside a carry element a link as
|
||||
`<a>` over `text (url)` and over its text alone, a carried node's blocks as `<p>` over `<br>` and a
|
||||
line feed, and media it cannot show as `<em>(image)</em>` over `<em>(image not shown)</em>` and
|
||||
nothing.
|
||||
- an image standing alone as `<p><img …></p>`, over `<figure>` and a bare `<img>`;
|
||||
- a list item's first paragraph bare, `<li>Bolt</li>`, over `<li><p>Bolt</p></li>`;
|
||||
- `class="language-js"`, over `lang-js`;
|
||||
- `<br>`, over `<br />`.
|
||||
|
||||
Seven readers picked `data-adf` for the carry element, 6 of 7, over `data-adf-node`. Four wanted the
|
||||
class `adf-carry` and three no class; this entry's stable `adf-*` classes settle it.
|
||||
|
||||
Atlassian's editor puts a `localId` on every block, so carrying each block holding one would leave
|
||||
no element at all. The second panel picked each of these 3 of 3:
|
||||
|
||||
- an attribute the element cannot express as `data-local-id`, over `data-adf-local-id` and `id`;
|
||||
- a link inside a carry element as `<a>`, over `text (url)` and its text alone;
|
||||
- a carried node's blocks as `<p>`, over `<br>` and a line feed;
|
||||
- media a carry element cannot show as `<em>(image)</em>`, over `<em>(image not shown)</em>` and
|
||||
nothing.
|
||||
|
||||
A third panel, seven readers in the same three roles, was asked how to write a list item whose first
|
||||
paragraph holds an attribute, the shape of every item Atlassian's editor writes. It picked
|
||||
@@ -420,13 +426,14 @@ the `<p>` on a line of its own, 1 of 7, and over
|
||||
|
||||
A fourth panel of the same three roles picked, 3 of 3, a code block inside a carry element as
|
||||
`<pre><code>…</code></pre>` over `<pre>…</pre>` and over a `<p>` whose lines part at `<br>`, which
|
||||
loses the indentation. All three wrote `<code class="language-python">`, which no option held.
|
||||
loses the indentation. All three wrote `<code class="language-python">`, which no option held, so
|
||||
the shown code keeps the class its code block's own element takes.
|
||||
|
||||
Inside a carry element (2026-10-24, no panel of its own: the second panel's `<a>` joined to a
|
||||
portable spelling), an image a link to another target marks takes the spelling portable markdown
|
||||
gives it inline (§A linked image standing inline in portable markdown, its link to another
|
||||
target, is written `[alt](href) ([image](url))`), keeping both targets, which Goal 7 counts as
|
||||
content. A link marking a node that shows a note goes around the note.
|
||||
Inside a carry element, an image marked by a link to another target shows as portable markdown
|
||||
writes it inline (§A linked image standing inline in portable markdown, its link to another target,
|
||||
is written `[alt](href) ([image](url))`), so both targets stay; Goal 7 counts them as content. A link
|
||||
marking a node that shows a note goes around the note. Recorded 2026-10-24 without a panel of its
|
||||
own: it joins the second panel's `<a>` to that portable spelling.
|
||||
|
||||
## No runtime dependencies
|
||||
|
||||
@@ -573,8 +580,8 @@ handles one cause alike whichever node, attribute or direction raised it.
|
||||
emitter carries such a node (2026-10-14).
|
||||
- A non-finite number takes two codes: `unsupported-node-shape` parsing, `not-an-adf-document`
|
||||
emitting — no document holds one, so no round-trip crosses them (2026-09-23).
|
||||
- A document whose markdown would be longer than V8's longest string is `unsupported-node-shape`
|
||||
at the document's path, in either writer, and so is a directive `losslessMarkdownToAdf` reads
|
||||
- A document whose output would be longer than V8's longest string is `unsupported-node-shape`
|
||||
at the document's path, in every writer, and so is a directive `losslessMarkdownToAdf` reads
|
||||
whose CommonMark spelling would be: the document does not convert, and no spelling in the input
|
||||
causes the bound (§The code list). A carried node passes it on about 1 MB of deeply nested JSON,
|
||||
since the block carry indents two spaces a level; `todo.md` item 107 respells the carry in a
|
||||
@@ -831,10 +838,10 @@ that, JavaScriptCore throws `Out of memory`, which is `engine-limit` (§Which co
|
||||
`adfToHtml` writes every URL exactly as the ADF holds it, escaped as every attribute value is,
|
||||
`javascript:`, `vbscript:`, `file:` and `data:` included. That covers a link's `href`, an image's
|
||||
`src`, and the `href` of each link the carry element shows: a carried node's link mark, a card's
|
||||
`url` or `data.url`, and an external image's `url`. Carrying such a link or image in the carry
|
||||
element, as cmark's default leaves these URLs out of its HTML, was proposed and not taken: item 142
|
||||
weighs that against writing the URL. Carrying alone no longer keeps the URL out of the HTML, since
|
||||
the carry element links what it shows, so that remedy would also drop the shown `href`.
|
||||
`url` or `data.url`, and an external image's `url`. Carrying such a link or image, to leave the URL
|
||||
out of the HTML as cmark does by default, was proposed and not taken; item 142 weighs it against
|
||||
writing the URL. Carrying alone would not keep the URL out, since the carry element links what it
|
||||
shows: the carry would also have to drop the shown `href`.
|
||||
|
||||
## Nothing recurses unbounded
|
||||
|
||||
|
||||
+38
-32
@@ -6,39 +6,39 @@ for every other node (`docs/decisions.md` §The HTML dialect). The written form
|
||||
|
||||
## Blocks
|
||||
|
||||
One block a line: blocks are separated by a line feed, and a document holding any ends with one. A
|
||||
document whose `content` is an empty array is the empty string.
|
||||
Blocks are separated by a line feed, and a document holding any ends with one. A document whose
|
||||
`content` is an empty array is the empty string.
|
||||
|
||||
| ADF | HTML |
|
||||
| --- | --- |
|
||||
| `paragraph` | `<p>…</p>` |
|
||||
| `heading`, `level` 1 to 6 | `<h1>…</h1>` to `<h6>…</h6>` |
|
||||
| `blockquote` | `<blockquote>`, a line feed, its blocks, a line feed, `</blockquote>` |
|
||||
| `bulletList` | `<ul>`, a line feed, its items a line each, a line feed, `</ul>` |
|
||||
| `orderedList` | as `bulletList` with `<ol>`; `<ol start="3">` for an `order` of 3, a safe integer other than `-0`; no `start` where the list holds no `order` |
|
||||
| `listItem` | its first paragraph straight after `<li>`: bare where it holds content and no attribute, `<li>Bolt</li>`, and as its `<p>` element where it holds an attribute, `<li><p data-local-id="p1">Bolt</p></li>`; where other blocks follow, each after a line feed, and a line feed before `</li>`. Otherwise `<li>`, a line feed, its blocks, a line feed, `</li>` |
|
||||
| `codeBlock` | `<pre><code class="language-js">…</code></pre>`, the language one token holding no ASCII whitespace (a tab, line feed, form feed, carriage return or space); no `class` where it holds no `language` |
|
||||
| `rule` | `<hr>` |
|
||||
| `heading`, `level` 1 to 6 | `<h1>…</h1>` to `<h6>…</h6>` |
|
||||
| `listItem` | its first paragraph straight after `<li>`: bare where it holds content and no attribute, `<li>Bolt</li>`, and as its `<p>` element where it holds an attribute, `<li><p data-local-id="p1">Bolt</p></li>`; where other blocks follow, each after a line feed, and a line feed before `</li>`. Where the first block is no paragraph, holds neither content nor an attribute, or rides the carry: `<li>`, a line feed, its blocks, a line feed, `</li>` |
|
||||
| `mediaSingle`, centered, holding one external `media` | `<p><img src="…" alt="…"></p>`, no `alt` where the media holds none; `<p><a href="…"><img …></a></p>` where a link marks it |
|
||||
| `orderedList` | as `bulletList` with `<ol>`; `<ol start="3">` for an `order` of 3, a safe integer other than `-0`; no `start` where the list holds no `order` |
|
||||
| `paragraph` | `<p>…</p>` |
|
||||
| `rule` | `<hr>` |
|
||||
|
||||
A container holding no `content` key is its element holding nothing: `<blockquote></blockquote>`,
|
||||
`<ul></ul>`, `<li></li>`; so is a paragraph or heading, `<p></p>`.
|
||||
|
||||
For a paragraph, heading, quote, list, list item, code block or rule, every other attribute its type
|
||||
holds (`spec/flavour.md` §Block nodes) is a `data-*` attribute on its element, named as HTML's
|
||||
`dataset` names it, `localId` as `data-local-id`, after the element's own attributes and sorted by
|
||||
name. A string is its value; any other value is its JSON, compact:
|
||||
`<ol start="3" data-local-id="91177a27-b0ff">`, `<pre data-wrap="true"><code>`. A code block's
|
||||
`data-*` attributes stand on its `<pre>`.
|
||||
On a paragraph, heading, quote, list, list item, code block or rule, each attribute its type holds
|
||||
(`spec/flavour.md` §Block nodes) that the element does not write as `level`, `order` or `language`
|
||||
is a `data-*` attribute, named as HTML's `dataset` names it, `localId` as `data-local-id`, after the
|
||||
element's own attributes and sorted by name. A string is its value; any other value is its JSON,
|
||||
compact: `<ol start="3" data-local-id="91177a27-b0ff">`, `<pre data-wrap="true"><code>`. A code
|
||||
block's `data-*` attributes stand on its `<pre>`.
|
||||
|
||||
## Inline
|
||||
|
||||
| ADF | HTML |
|
||||
| --- | --- |
|
||||
| `text` | its text, escaped |
|
||||
| `em`, `strong`, `code` | `<em>`, `<strong>`, `<code>` around the text |
|
||||
| `link`, holding `href` and an optional `title` | `<a href="…" title="…">` around the text |
|
||||
| `hardBreak` | `<br>` |
|
||||
| `link`, holding `href` and an optional `title` | `<a href="…" title="…">` around the text |
|
||||
| `text` | its text, escaped |
|
||||
|
||||
A text node's marks nest in the order its `marks` array lists them, the first outermost:
|
||||
`<strong><em>manual</em></strong>`. Each text node writes its own elements, so two text nodes a link
|
||||
@@ -51,10 +51,10 @@ Attribute values are double-quoted. Every URL is written as the ADF holds it, wh
|
||||
(`docs/decisions.md` §HTML writes a URL as the ADF holds it, whatever its scheme): a link's `href`,
|
||||
an image's `src`, and the `href` of each link a carry element shows.
|
||||
|
||||
HTML text and attribute values cannot hold a carriage return, which a parser reads as a line feed,
|
||||
a control other than a tab, line feed or form feed, a noncharacter, or a lone surrogate, which
|
||||
UTF-8 cannot encode (WHATWG HTML §13.1.3). A node holding one in a value its element writes rides
|
||||
the carry.
|
||||
HTML text and attribute values cannot hold these (WHATWG HTML §13.1.3): any control but a tab, line
|
||||
feed or form feed, so a carriage return too, which a parser reads as a line feed; a noncharacter;
|
||||
and a lone surrogate, which UTF-8 cannot encode. A node holding one in a value its element writes
|
||||
rides the carry.
|
||||
|
||||
## The carry element
|
||||
|
||||
@@ -73,19 +73,24 @@ later release writes it as its own element.
|
||||
included, each character HTML cannot hold spelled as a JSON escape, `\u0085`. A document holding
|
||||
no `content` key is the block carry of `{"type":"doc","version":1}`.
|
||||
- The content shown is every text node's text below the node, and what a node holding no text shows
|
||||
in its place: a mention's text, else `@` and its id where it has one; an emoji's text, else its `shortName`; a
|
||||
status's text; a date as `2026-09-13` in UTC; a card's URL, else its name, else the URL its data
|
||||
holds; media's `alt`, else an external one's URL; an extension's text; an expand's title.
|
||||
in its place: a mention's text, else `@` and its id where it has one; an emoji's text, else its
|
||||
`shortName`; a status's text; a date as `2026-09-13` in UTC; a card's URL, else its name, else the
|
||||
URL its data holds; media's `alt`, else an external one's URL; an extension's text; an expand's
|
||||
title.
|
||||
- In a `<div>`, each block's content is a `<p>`, and a code block's is
|
||||
`<pre><code class="language-js">…</code></pre>`, keeping its line feeds and spaces. Its class
|
||||
is the one the code block's element takes, and it has none where the language is no class
|
||||
token. In a `<span>`, `<br>` parts two blocks'.
|
||||
`<pre><code class="language-js">…</code></pre>`, keeping its line feeds and spaces. The `<code>`
|
||||
takes the class the code block's own element takes, and none where the language is not one
|
||||
token free of ASCII whitespace. In a `<span>`, a `<br>` parts each block's content from the
|
||||
next.
|
||||
- A hard break is `<br>`, and so is a line feed in text outside a `<div>`'s code block.
|
||||
- Text a link marks is `<a href="…" title="…">` around it, and a card or an external image is a
|
||||
link to its URL.
|
||||
- An image a link to another target marks shows as portable markdown writes it inline, the link
|
||||
around its `alt`, else the link's target, else its URL, then `image` linking to the image in
|
||||
parentheses: `<a href="https://example.com/">The moon</a> (<a href="https://example.com/moon.jpg">image</a>)`.
|
||||
- Text a link marks is `<a href="…" title="…">` around it, or the text alone where HTML cannot
|
||||
hold the link's `href` or `title`. A card is a link to the target of a link marking it, else to
|
||||
its URL.
|
||||
- An external image with no link to another target is a link to its URL. An image marked by a
|
||||
link to another target shows as portable markdown writes it inline. First comes the link,
|
||||
around the image's `alt`, else the link's target, else the image's URL. Then comes ` (image)`,
|
||||
the word `image` linking to the image:
|
||||
`<a href="https://example.com/">The moon</a> (<a href="https://example.com/moon.jpg">image</a>)`.
|
||||
- A node referencing what it cannot show is a note naming it, `<em>(image)</em>`: `image` for
|
||||
media with no `alt` or URL, `link card` for a card with neither URL nor name, an extension's
|
||||
key, else `extension`, for an extension with no text, and `synced block` for a `syncBlock`. A
|
||||
@@ -96,14 +101,15 @@ later release writes it as its own element.
|
||||
element cannot hold:
|
||||
- text holding no character, or a character HTML cannot hold; text holding a mark twice, or a
|
||||
mark with attributes besides a link's `href` and `title`;
|
||||
- an unmarked text node straight after another, which would read back as one;
|
||||
- an unmarked text node straight after one written as plain text, which would read back as one;
|
||||
- a link whose `href` or `title` holds a character HTML cannot hold, and an image whose `url` or
|
||||
`alt` holds one or whose link's `href` or `title` does;
|
||||
- an image holding an attribute besides its `center` layout and its media's `type`, `url` and
|
||||
non-empty `alt`;
|
||||
- a heading whose `level` is not 1 to 6, and an ordered list whose `order` is not a safe integer
|
||||
or is `-0`;
|
||||
- a code block holding more than one text node, a marked one, or a language holding ASCII whitespace;
|
||||
- a code block holding more than one text node, a marked one, or a language holding ASCII
|
||||
whitespace;
|
||||
- a rule or hard break holding `content`, and a hard break holding an attribute;
|
||||
- a list holding a node that is no `listItem`, or an item holding a mark, text, an empty key or
|
||||
an attribute its type does not hold: the whole list rides the carry;
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ export function writableContent(document: AdfDocument): Result<readonly AdfNode[
|
||||
if (fault !== undefined) return faulted(fault, [])
|
||||
if (document.version !== 1) return failure('unsupported-document-version', `no format this library writes carries ADF version ${document.version}`, [])
|
||||
if (document.content === undefined) return success(undefined)
|
||||
// Every character of text is written at least once, so this much text cannot fit, and none of it is written.
|
||||
// Text alone past the bound is refused before anything is written (docs/decisions.md §Output past V8's longest string is refused on every engine).
|
||||
return heldTextLength(document.content) > longestOutput ? tooLong() : success(document.content)
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ for (const name of names(realPayloadsRoot, '.json')) {
|
||||
})
|
||||
}
|
||||
|
||||
// The element each block an editor writes with no attribute but `localId`, and `level` or `order`, opens with; every other block is carried.
|
||||
// The opening tag of each block type whose only attributes are `localId`, `level` and `order`, as the editor writes them.
|
||||
const blockElements: Readonly<Record<string, string>> = { blockquote: '<blockquote', bulletList: '<ul', heading: '<h', orderedList: '<ol', paragraph: '<p', rule: '<hr' }
|
||||
|
||||
for (const name of names(realPayloadsRoot, '.json')) {
|
||||
|
||||
@@ -59,7 +59,7 @@ function shown(written: string): string {
|
||||
return written.slice(written.indexOf('">') + 2).replace(/<\/(?:div|span)>(?:<\/p>)?\n$/, '')
|
||||
}
|
||||
|
||||
test('writes paragraphs, headings and rules as their elements, one block a line', () => {
|
||||
test('writes paragraphs, headings and rules as their elements, separated by line feeds', () => {
|
||||
const heading = (level: number): AdfNode => ({ attrs: { level }, content: [text(`Level ${level}`)], type: 'heading' })
|
||||
assert.equal(html(adfToHtml(document(heading(1), paragraph(text('Read the manual.')), { type: 'rule' }, heading(6)))), '<h1>Level 1</h1>\n<p>Read the manual.</p>\n<hr>\n<h6>Level 6</h6>\n')
|
||||
assert.equal(html(adfToHtml(document(paragraph(), { attrs: { level: 2 }, type: 'heading' }))), '<p></p>\n<h2></h2>\n')
|
||||
@@ -75,7 +75,7 @@ test('writes the marks CommonMark spells, the first mark outermost', () => {
|
||||
assert.equal(html(adfToHtml(document(paragraph(text('Line one'), { type: 'hardBreak' }, text('Line two'))))), '<p>Line one<br>Line two</p>\n')
|
||||
})
|
||||
|
||||
test('escapes text and attribute values, and keeps tabs, line feeds and form feeds', () => {
|
||||
test('escapes text, and keeps tabs, line feeds and form feeds', () => {
|
||||
assert.equal(html(adfToHtml(document(paragraph(text('a < b && c > "d" \'e\'\tf\ng\fh'))))), '<p>a < b && c > "d" \'e\'\tf\ng\fh</p>\n')
|
||||
})
|
||||
|
||||
@@ -88,7 +88,7 @@ test('carries text holding a character HTML text cannot hold, escaping it in the
|
||||
assert.equal(html(adfToHtml(document(paragraph(text('a\u{1fffe}b', { type: 'underline' }))))).includes('"text":"a\\ud83f\\udffeb"'), true)
|
||||
})
|
||||
|
||||
test('writes lists tight, an item holding more than one paragraph or a list in elements after the first', () => {
|
||||
test('writes lists tight, and an item\'s blocks after its first paragraph as elements on lines of their own', () => {
|
||||
assert.equal(html(adfToHtml(document({ content: [item(paragraph(text('Bolt'))), item(paragraph(text('Nut')))], type: 'bulletList' }))), '<ul>\n<li>Bolt</li>\n<li>Nut</li>\n</ul>\n')
|
||||
const listed: AdfNode = { attrs: { order: 3 }, content: [item(paragraph(text('Bolt')), { content: [item(paragraph(text('M8')))], type: 'bulletList' }), item(paragraph(text('a')), paragraph(text('b')))], type: 'orderedList' }
|
||||
assert.equal(html(adfToHtml(document(listed))), '<ol start="3">\n<li>Bolt\n<ul>\n<li>M8</li>\n</ul>\n</li>\n<li>a\n<p>b</p>\n</li>\n</ol>\n')
|
||||
@@ -195,7 +195,7 @@ test('shows a code block in a span carry as text, its line feeds line breaks', (
|
||||
assert.equal(shown(html(adfToHtml(document(paragraph({ attrs: { language: 'python' }, content: [text('if ready:\n deploy()')], type: 'codeBlock' }))))), 'if ready:<br> deploy()')
|
||||
})
|
||||
|
||||
test('shows the content a carried node holds in an attribute, a link to what it links to', () => {
|
||||
test('shows the content a carried node holds in an attribute, and links a card or an external image to its URL', () => {
|
||||
const inline = (node: AdfNode): string => shown(html(adfToHtml(document(paragraph(node)))))
|
||||
assert.equal(inline({ attrs: { timestamp: '1700000000000' }, type: 'date' }), '2023-11-14')
|
||||
assert.equal(inline({ attrs: { shortName: ':smile:', text: '😄' }, type: 'emoji' }), '😄')
|
||||
@@ -210,7 +210,7 @@ test('shows the content a carried node holds in an attribute, a link to what it
|
||||
assert.equal(image({ attrs: { alt: 'The moon', type: 'external', url: 'https://example.com/moon.jpg' }, marks: [{ attrs: { title: 'No target' }, type: 'link' }], type: 'media' }), '<p><a href="https://example.com/moon.jpg">The moon</a></p>')
|
||||
})
|
||||
|
||||
test('shows a carried image a link to another target marks as that link, then a link to the image reading image', () => {
|
||||
test('shows a carried image marked by a link to another target as that link, then a link to the image reading image', () => {
|
||||
const link: AdfMark = { attrs: { href: 'https://example.com/' }, type: 'link' }
|
||||
const image = (attrs: AdfAttributes): string => shown(html(adfToHtml(document({ attrs: { layout: 'wide' }, content: [{ attrs, marks: [link], type: 'media' }], type: 'mediaSingle' }))))
|
||||
assert.equal(image({ alt: 'The moon', type: 'external', url: 'https://example.com/moon.jpg' }), '<p><a href="https://example.com/">The moon</a> (<a href="https://example.com/moon.jpg">image</a>)</p>')
|
||||
@@ -269,7 +269,7 @@ test('carries a node holding what its element cannot', () => {
|
||||
assert.equal(carriedBlock({ attrs: { layout: 'center' }, content: [{ attrs: { alt: '', type: 'external', url: 'https://example.com/a.png' }, type: 'media' }], type: 'mediaSingle' }), true)
|
||||
})
|
||||
|
||||
test('carries inline what its element cannot hold, and a text node an unmarked one before it would join', () => {
|
||||
test('carries inline what its element cannot hold, and a text node that would join the unmarked one before it', () => {
|
||||
assert.equal(carriedSpans(text('a'), text('b')), 1)
|
||||
assert.equal(carriedSpans(text('a'), text('b'), text('c')), 1)
|
||||
assert.equal(carriedSpans(text('a'), text('b', { type: 'em' }), text('c')), 0)
|
||||
|
||||
@@ -11,7 +11,7 @@ import { longestOutput, tooLong, withinEngineLimits } from '../../length-bound.t
|
||||
import { plainImage } from '../../adf/plain-image.ts'
|
||||
import { serializeCanonicalJson } from '../../canonical-json.ts'
|
||||
|
||||
// depth: the nesting level of the block list holding the node, as `largestNesting` counts it; an item's attributes are its element's, checked by its list.
|
||||
// depth: the nesting level of the block list holding the node, as `largestNesting` counts it; `attributes`: the `<li>`'s `data-*` attributes, built when its list checked the item.
|
||||
type Block = { depth: number; kind: 'block'; node: AdfNode; path: ConvertErrorPath }
|
||||
type Item = { attributes: string; depth: number; kind: 'item'; node: AdfNode; path: ConvertErrorPath }
|
||||
type Pending = Block | Item | string
|
||||
@@ -99,7 +99,7 @@ function containerPieces(tag: string, attributes: string, children: Pending[] |
|
||||
return [`<${tag}${attributes}>\n`, ...children, `\n</${tag}>`]
|
||||
}
|
||||
|
||||
// An item's blocks sit at its own level, one below its list; its first paragraph stands on the item's line, panel picks, docs/decisions.md §The HTML dialect.
|
||||
// An item's first paragraph stands on the item's line, as the writer panels picked (docs/decisions.md §The HTML dialect).
|
||||
function writeItem({ attributes, depth, node, path }: Item): Result<Pending[]> {
|
||||
if (node.content === undefined) return success([`<li${attributes}></li>`])
|
||||
const blocks = blockPieces(node.content, path, depth)
|
||||
@@ -111,7 +111,7 @@ function writeItem({ attributes, depth, node, path }: Item): Result<Pending[]> {
|
||||
return success(blocks.length === 1 ? [`<li${attributes}>${line.value}</li>`] : [`<li${attributes}>${line.value}`, ...blocks.slice(1), '\n</li>'])
|
||||
}
|
||||
|
||||
// `undefined` where the paragraph takes a line of its own: where it rides the carry, or holds neither content nor an attribute, which bare would read back as no paragraph.
|
||||
// `undefined` where the paragraph takes a line of its own: where it rides the carry, or holds neither content nor an attribute: written bare, it would read back as no paragraph.
|
||||
function tryFirstParagraph(paragraph: AdfNode, path: ConvertErrorPath): Result<string> | undefined {
|
||||
const attributes = dataAttributes(paragraph, blockNodes.paragraph.attributes, [])
|
||||
if (attributes !== '') return inlineElement('p', attributes, nodeContent(paragraph), path)
|
||||
@@ -126,7 +126,7 @@ function tryLeafBlock(node: AdfNode, path: ConvertErrorPath): Result<string> | u
|
||||
return html === undefined ? undefined : success(html)
|
||||
}
|
||||
|
||||
// `undefined` where the attributes are, so the node rides the carry.
|
||||
// `undefined` where `attributes` is undefined, so the node rides the carry.
|
||||
function inlineElement(tag: string, attributes: string | undefined, nodes: readonly AdfNode[], path: ConvertErrorPath): Result<string> | undefined {
|
||||
if (attributes === undefined) return undefined
|
||||
const inline = inlineHtml(nodes, path)
|
||||
|
||||
@@ -30,7 +30,7 @@ export function carryElement(tag: string, json: string, content: string): string
|
||||
function shownBlocks(root: AdfNode, codeLineFeed: string): ShownBlock[] {
|
||||
const shown: ShownBlock[] = []
|
||||
let current: ShownBlock = { code: undefined, html: '' }
|
||||
// Each entry holds the code block it stands in, none where another block stands between them; a `null` node ends a block, after which the content stands in `code` again.
|
||||
// Each entry holds the code block it stands in, none where another block stands between them; a `null` node ends a block, and what follows stands in that entry's `code`, the code block around the block that ended.
|
||||
const pending: { code: AdfNode | undefined; node: AdfNode | null }[] = [{ code: undefined, node: root }]
|
||||
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
||||
const { node } = next
|
||||
@@ -48,7 +48,7 @@ function shownBlocks(root: AdfNode, codeLineFeed: string): ShownBlock[] {
|
||||
return shown
|
||||
}
|
||||
|
||||
// The node's own part of what its carry element shows: its text, what it shows in its place, or a note naming what it references, linked to its link mark's target, else to the URL a card or an image links to; an image a link to another target marks is followed by a link to it.
|
||||
// The node's own part of what its carry element shows.
|
||||
function shownPiece(node: AdfNode, lineFeed: string): string {
|
||||
if (node.type === 'hardBreak') return '<br>'
|
||||
const mark = nodeMarks(node).find((held) => held.type === 'link')
|
||||
@@ -62,7 +62,7 @@ function shownPiece(node: AdfNode, lineFeed: string): string {
|
||||
return name === undefined ? '' : linked(`<em>(${shownValue(name, lineFeed)})</em>`, link)
|
||||
}
|
||||
|
||||
// The image's alt, else the link's target, else its URL, linked to the target, then a link to the image, as portable markdown writes it inline.
|
||||
// As portable markdown writes it inline (docs/decisions.md §The HTML dialect).
|
||||
function linkedImage(media: AdfNode, target: LinkAttrs, url: string, lineFeed: string): string {
|
||||
const text = visibleText(nodeAttrs(media)['alt']) ?? visibleText(target.href) ?? url
|
||||
return `${linked(shownValue(text, lineFeed), target)} (${linked('image', { href: url })})`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { JsonValue } from '../../json-value.ts'
|
||||
import type { LinkAttrs } from '../../adf/mark-attributes.ts'
|
||||
|
||||
// What HTML text and attribute values cannot hold (WHATWG HTML §13.1.3): a control other than ASCII whitespace or a carriage return, which a parser reads as a line feed; a noncharacter; and a lone surrogate, which UTF-8 cannot encode.
|
||||
// What HTML text and attribute values cannot hold (WHATWG HTML §13.1.3): any control but a tab, line feed or form feed, a carriage return among them, since a parser reads it as a line feed; a noncharacter; and a lone surrogate, which UTF-8 cannot encode.
|
||||
const unwritable = /(?![\t\n\f ])[\p{Cc}\p{Cs}\p{Noncharacter_Code_Point}]/u
|
||||
const unwritables = new RegExp(unwritable.source, 'gu')
|
||||
|
||||
@@ -29,7 +29,6 @@ export function optionalAttribute(name: string, value: string | undefined): stri
|
||||
return escaped === undefined ? undefined : ` ${name}="${escaped}"`
|
||||
}
|
||||
|
||||
// The text a carry element shows, dropping what HTML cannot hold, each line feed spelled `lineFeed`.
|
||||
export function shownValue(value: string, lineFeed: string): string {
|
||||
return escapeHtml(value.replace(unwritables, '')).replaceAll('\n', lineFeed)
|
||||
}
|
||||
|
||||
@@ -242,14 +242,14 @@ the union. Found by the technical-principles audit of item 78, 2026-10-05.
|
||||
|
||||
### 141. Write `strike` as HTML in `adfToHtml` and read it back in `htmlToAdf`, in place of the carry element.
|
||||
|
||||
GFM spells `strike`, and no other HTML item writes it: item 130 wrote the marks CommonMark spells.
|
||||
A writer panel picks `<s>` or `<del>`.
|
||||
GFM spells `strike`, and no other HTML item writes it: `adfToHtml` writes only the marks CommonMark
|
||||
spells. A writer panel picks `<s>` or `<del>`.
|
||||
|
||||
### 135. Write media, media groups and captions as semantic HTML in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.
|
||||
|
||||
A writer panel picks the spellings; an external image already has one from item 130. Atlassian's
|
||||
editor puts `localId`, `width` and `widthType` on every `mediaSingle`, so every editor-written
|
||||
image rides the carry until this item.
|
||||
A writer panel picks the spellings; a plain external image already has one, `<p><img …></p>`.
|
||||
Atlassian's editor puts `localId`, `width` and `widthType` on every `mediaSingle`, so every
|
||||
editor-written image rides the carry until this item.
|
||||
|
||||
### 136. Write underline, text and background colours, subscript, superscript, alignment and indentation as HTML in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user