mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Answer architecture round 3: 0 🔴, 1 🟡 (0 misread); the data-* and carry contract names its elements, an item's first paragraph holding an attribute on the item's line (panel 6 of 7)
This commit is contained in:
@@ -466,15 +466,15 @@ 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, an attribute HTML has none for as `data-*`, 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:
|
||||
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 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).
|
||||
|
||||
| ADF | HTML |
|
||||
| --- | --- |
|
||||
| `blockquote` | `<blockquote>` |
|
||||
| `bulletList`, `orderedList`, `listItem` | `<ul>`, `<ol>`, `<ol start="3">` for an `order` of 3, and `<li>`; an item's first paragraph stands bare, `<li>Bolt</li>` |
|
||||
| `bulletList`, `orderedList`, `listItem` | `<ul>`, `<ol>`, `<ol start="3">` for an `order` of 3, and `<li>`; an item's first paragraph stands bare, `<li>Bolt</li>`, or as `<li><p data-local-id="p1">Bolt</p></li>` where it holds an attribute |
|
||||
| `codeBlock` | `<pre><code class="language-js">`, no `class` without a language |
|
||||
| `hardBreak` | `<br>` |
|
||||
| `heading` | `<h1>` to `<h6>` |
|
||||
@@ -483,14 +483,15 @@ included, so a reader of the page loses no content. No stylesheet ships. The com
|
||||
| `paragraph` | `<p>` |
|
||||
| `rule` | `<hr>` |
|
||||
| `text` marked `code`, `em`, `strong` | `<code>`, `<em>`, `<strong>`, the first mark outermost |
|
||||
| any other attribute the node's type holds | `data-*` named as HTML's `dataset` names it: `<p data-local-id="a1f3">` |
|
||||
| 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">` |
|
||||
| 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 |
|
||||
|
||||
- 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.
|
||||
- The carry element shows each block's content as a `<p>`, 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 image as
|
||||
a link to it, and an expand by its title. What it cannot show leaves a note, `<em>(image)</em>`.
|
||||
- The carry element shows each block's content as a `<p>` in a `<div>`, parted by `<br>` in a
|
||||
`<span>`. 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. What it cannot show leaves a note, `<em>(image)</em>`.
|
||||
- A link's or image's URL is written as the ADF holds it, `javascript:` included: sanitize the
|
||||
HTML before showing a document from an author you do not trust.
|
||||
|
||||
|
||||
@@ -412,6 +412,12 @@ block and a block carried for it would show no element at all; inside a carry el
|
||||
line feed, and media it 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
|
||||
`<li data-local-id="i1"><p data-local-id="p1">Bolt</p></li>`, 6 of 7, over the same elements with
|
||||
the `<p>` on a line of its own, 1 of 7, and over
|
||||
`<li data-local-id="i1" data-paragraph-local-id="p1">Bolt</li>`, 0 of 7.
|
||||
|
||||
## No runtime dependencies
|
||||
|
||||
2026-08-23, the maintainer. Goal 8. Valid while ~20 lines of own code, or a vendored table, do each
|
||||
|
||||
+8
-5
@@ -16,7 +16,7 @@ document whose `content` is an empty array is the empty string.
|
||||
| `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` | `<li>…</li>`: a first paragraph holding content and no attribute bare, its inline content straight after `<li>`, then each other block after a line feed and a line feed before `</li>`; with no bare paragraph, `<li>`, a line feed, its blocks, a line feed, `</li>` |
|
||||
| `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 whitespace; no `class` where it holds no `language` |
|
||||
| `rule` | `<hr>` |
|
||||
| `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 |
|
||||
@@ -24,9 +24,10 @@ document whose `content` is an empty array is the empty string.
|
||||
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>`.
|
||||
|
||||
Every other attribute its type holds (`spec/flavour.md` §Block nodes) is a `data-*` attribute on the
|
||||
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:
|
||||
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>`.
|
||||
|
||||
@@ -91,10 +92,12 @@ later release writes it as its own element.
|
||||
- an unmarked text node straight after another, 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 whitespace;
|
||||
- a rule or hard break holding `content`;
|
||||
- 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;
|
||||
- a `listItem` outside a list, an inline node in block position and a block node inline.
|
||||
|
||||
@@ -92,10 +92,13 @@ test('writes lists tight, an item holding more than one paragraph or a list in e
|
||||
assert.equal(html(adfToHtml(document({ content: [item(paragraph())], type: 'bulletList' }))), '<ul>\n<li>\n<p></p>\n</li>\n</ul>\n')
|
||||
})
|
||||
|
||||
test('writes an item\'s first paragraph as its element where it holds an attribute, and as its carry where it holds a mark', () => {
|
||||
const first = (node: AdfNode): string => html(adfToHtml(document({ content: [item(node)], type: 'bulletList' })))
|
||||
assert.equal(first({ attrs: { localId: ids.paragraph }, content: [text('Bolt')], type: 'paragraph' }), `<ul>\n<li>\n<p data-local-id="${ids.paragraph}">Bolt</p>\n</li>\n</ul>\n`)
|
||||
assert.equal(first({ content: [text('Bolt')], marks: [{ attrs: { align: 'center' }, type: 'alignment' }], type: 'paragraph' }).startsWith('<ul>\n<li>\n<div class="adf-carry"'), true)
|
||||
test('writes an item\'s first paragraph as its element on the item\'s line where it holds an attribute, and as its carry where it holds a mark', () => {
|
||||
const listed = (...blocks: AdfNode[]): string => html(adfToHtml(document({ content: [item(...blocks)], type: 'bulletList' })))
|
||||
const held: AdfNode = { attrs: { localId: ids.paragraph }, content: [text('Bolt')], type: 'paragraph' }
|
||||
assert.equal(listed(held), `<ul>\n<li><p data-local-id="${ids.paragraph}">Bolt</p></li>\n</ul>\n`)
|
||||
assert.equal(listed(held, { content: [item(paragraph(text('M8')))], type: 'bulletList' }), `<ul>\n<li><p data-local-id="${ids.paragraph}">Bolt</p>\n<ul>\n<li>M8</li>\n</ul>\n</li>\n</ul>\n`)
|
||||
assert.equal(listed({ attrs: { localId: ids.paragraph }, type: 'paragraph' }), `<ul>\n<li><p data-local-id="${ids.paragraph}"></p></li>\n</ul>\n`)
|
||||
assert.equal(listed({ content: [text('Bolt')], marks: [{ attrs: { align: 'center' }, type: 'alignment' }], type: 'paragraph' }).startsWith('<ul>\n<li>\n<div class="adf-carry"'), true)
|
||||
})
|
||||
|
||||
test('writes the attributes an element cannot express as data-* attributes on it, sorted, a value that is no string as JSON', () => {
|
||||
@@ -104,7 +107,7 @@ test('writes the attributes an element cannot express as data-* attributes on it
|
||||
assert.equal(html(adfToHtml(document({ attrs: { level: 2, ...localId }, content: [text('x')], type: 'heading' }))), `<h2 data-local-id="${ids.paragraph}">x</h2>\n`)
|
||||
assert.equal(html(adfToHtml(document({ attrs: { localId: ids.quote }, content: [paragraph(text('x'))], type: 'blockquote' }))), `<blockquote data-local-id="${ids.quote}">\n<p>x</p>\n</blockquote>\n`)
|
||||
const listed: AdfNode = { attrs: { localId: ids.list, order: 3 }, content: [{ attrs: { localId: ids.item }, content: [{ attrs: localId, content: [text('Bolt')], type: 'paragraph' }], type: 'listItem' }], type: 'orderedList' }
|
||||
assert.equal(html(adfToHtml(document(listed))), `<ol start="3" data-local-id="${ids.list}">\n<li data-local-id="${ids.item}">\n<p data-local-id="${ids.paragraph}">Bolt</p>\n</li>\n</ol>\n`)
|
||||
assert.equal(html(adfToHtml(document(listed))), `<ol start="3" data-local-id="${ids.list}">\n<li data-local-id="${ids.item}"><p data-local-id="${ids.paragraph}">Bolt</p></li>\n</ol>\n`)
|
||||
const code: AdfNode = { attrs: { wrap: true, language: 'js', hideLineNumbers: false, ...localId }, content: [text('x')], type: 'codeBlock' }
|
||||
assert.equal(html(adfToHtml(document(code))), `<pre data-hide-line-numbers="false" data-local-id="${ids.paragraph}" data-wrap="true"><code class="language-js">x</code></pre>\n`)
|
||||
assert.equal(html(adfToHtml(document({ attrs: { weight: -0, color: '#ccc', style: 'a"b' }, type: 'rule' }))), '<hr data-color="#ccc" data-style="a"b" data-weight="-0">\n')
|
||||
@@ -259,6 +262,7 @@ test('refuses a carried block whose JSON nests past the levels its depth leaves,
|
||||
assert.equal(refusal(adfToHtml(document(paragraph(deep)))), 'unsupported-nesting-depth at /content/0/content/0')
|
||||
assert.equal(refusal(adfToHtml(document({ attrs: { level: 1 }, content: [deep], type: 'heading' }))), 'unsupported-nesting-depth at /content/0/content/0')
|
||||
assert.equal(refusal(adfToHtml(document({ content: [item(paragraph(deep))], type: 'bulletList' }))), 'unsupported-nesting-depth at /content/0/content/0/content/0/content/0')
|
||||
assert.equal(refusal(adfToHtml(document({ content: [item({ attrs: { localId: ids.paragraph }, content: [deep], type: 'paragraph' })], type: 'bulletList' }))), 'unsupported-nesting-depth at /content/0/content/0/content/0/content/0')
|
||||
assert.equal(refusal(adfToHtml(document({ content: [item(paragraph(text('a')), paragraph(deep)), item(paragraph(deep))], type: 'bulletList' }))), 'unsupported-nesting-depth at /content/0/content/0/content/1/content/0')
|
||||
})
|
||||
|
||||
|
||||
@@ -112,16 +112,23 @@ 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 bare where it holds content and no attribute, panel pick, docs/decisions.md §The HTML dialect.
|
||||
// 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.
|
||||
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)
|
||||
const [first] = node.content
|
||||
if (first === undefined || first.type !== 'paragraph' || dataAttributes(first, []) !== '' || nodeContent(first).length === 0) return success([`<li${attributes}>\n`, ...blocks, '\n</li>'])
|
||||
const bare = inlineHtml(nodeContent(first), [...path, 'content', 0])
|
||||
if (!bare.ok) return bare
|
||||
// The bare paragraph's inline content stands in for its block.
|
||||
return success(blocks.length === 1 ? [`<li${attributes}>${bare.value}</li>`] : [`<li${attributes}>${bare.value}`, ...blocks.slice(1), '\n</li>'])
|
||||
const line = first?.type === 'paragraph' ? tryFirstParagraph(first, [...path, 'content', 0]) : undefined
|
||||
if (line === undefined) return success([`<li${attributes}>\n`, ...blocks, '\n</li>'])
|
||||
if (!line.ok) return line
|
||||
// The first paragraph, written on the item's line, stands in for its block.
|
||||
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.
|
||||
function tryFirstParagraph(paragraph: AdfNode, path: ConvertErrorPath): Result<string> | undefined {
|
||||
const attributes = dataAttributes(paragraph, [])
|
||||
if (attributes !== '') return inlineElement('p', attributes, nodeContent(paragraph), path)
|
||||
return nodeContent(paragraph).length === 0 ? undefined : inlineHtml(nodeContent(paragraph), path)
|
||||
}
|
||||
|
||||
function tryLeafBlock(node: AdfNode, path: ConvertErrorPath): Result<string> | undefined {
|
||||
|
||||
Reference in New Issue
Block a user