diff --git a/CHANGELOG.md b/CHANGELOG.md index e5ff994..f03d9ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,8 @@ ## Unreleased - `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 + 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 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, diff --git a/README.md b/README.md index 3b884d0..cfa874f 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,9 @@ products store as ADF. 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, 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. + 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. - **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. @@ -483,7 +484,7 @@ content. No stylesheet ships. The complete dialect: | `paragraph` | `
` | | `rule` | `
`, ``, ``, the first mark outermost |
-| 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: `` |
+| an attribute its type holds that 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: `
` |
| any other node, or a node holding what its element cannot | `
its content
`, a `` inline |
- Every URL is written as the ADF holds it, `javascript:` included: a link's, an image's, and a
diff --git a/docs/decisions.md b/docs/decisions.md
index b3de160..f7ea5b0 100644
--- a/docs/decisions.md
+++ b/docs/decisions.md
@@ -435,6 +435,11 @@ is written `[alt](href) ([image](url))`), so both targets stay; Goal 7 counts th
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 `` to that portable spelling.
+Inside a carry element, a card a link marks links to the link's target, and shows its own URL as its
+text where it has one, so both stay. A card showing only its name keeps its URL in `data-adf` alone,
+until `todo.md` item 133 writes inline cards as their own elements, in 0.3.0. Recorded 2026-10-24
+without a panel of its own: it puts the second panel's `` around the text a card shows.
+
## No runtime dependencies
2026-08-23, the maintainer. Goal 8. Valid while ~20 lines of own code, or a vendored table, do each
diff --git a/spec/html.md b/spec/html.md
index 631d13d..99856ec 100644
--- a/spec/html.md
+++ b/spec/html.md
@@ -84,8 +84,8 @@ later release writes it as its own element.
next.
- A hard break is `
`, and so is a line feed in text outside a ``'s code block.
- Text a link marks is `` 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.
+ hold the link's `href` or `title`; a `title` that is no string is left out. 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)`,
diff --git a/src/adf/shown-text.test.ts b/src/adf/shown-text.test.ts
index fd1369b..37dbea7 100644
--- a/src/adf/shown-text.test.ts
+++ b/src/adf/shown-text.test.ts
@@ -1,7 +1,11 @@
import assert from 'node:assert/strict'
import test from 'node:test'
-import { isBlockNodeType, shownText, shownUrl } from './shown-text.ts'
+import type { AdfAttributes, AdfNode } from './document.ts'
+import { isBlockNodeType, shownImage, shownLink, shownText, shownUrl } from './shown-text.ts'
+
+const moon = 'https://example.com/moon.png'
+const target = 'https://example.com/'
test('shows a date as its UTC day, one before 1970 included', () => {
assert.equal(shownText({ attrs: { timestamp: '1700000000000' }, type: 'date' }), '2023-11-14')
@@ -28,6 +32,36 @@ test('links an external image to its URL, and a stored one to nothing', () => {
assert.equal(shownUrl({ attrs: { type: 'file', url: 'https://example.com/a.png' }, type: 'mediaInline' }), undefined)
})
+test('shows an image no link marks by its alt, else its URL, linked to its URL', () => {
+ assert.deepEqual(shownImage({ attrs: { alt: 'The moon', type: 'external', url: moon }, type: 'media' }, undefined), { after: undefined, link: moon, text: 'The moon' })
+ assert.deepEqual(shownImage({ attrs: { alt: ' ', type: 'external', url: moon }, type: 'mediaInline' }, undefined), { after: undefined, link: moon, text: moon })
+})
+
+test('shows an image a link to its own URL marks by the same text, under that link', () => {
+ assert.deepEqual(shownImage({ attrs: { alt: 'The moon', type: 'external', url: moon }, type: 'media' }, moon), { after: undefined, link: undefined, text: 'The moon' })
+ assert.deepEqual(shownImage({ attrs: { type: 'external', url: moon }, type: 'mediaInline' }, moon), { after: undefined, link: undefined, text: moon })
+})
+
+test('shows an image a link to another target marks by its alt, else the target, else its URL, under the link, its URL after it', () => {
+ assert.deepEqual(shownImage({ attrs: { alt: 'The moon', type: 'external', url: moon }, type: 'media' }, target), { after: moon, link: undefined, text: 'The moon' })
+ assert.deepEqual(shownImage({ attrs: { type: 'external', url: moon }, type: 'mediaInline' }, target), { after: moon, link: undefined, text: target })
+ assert.deepEqual(shownImage({ attrs: { type: 'external', url: moon }, type: 'media' }, ' '), { after: moon, link: undefined, text: moon })
+})
+
+test('shows an image with no URL by its alt, else by no text, a note standing in its place', () => {
+ assert.deepEqual(shownImage({ attrs: { alt: 'The moon', type: 'file' }, type: 'media' }, target), { after: undefined, link: undefined, text: 'The moon' })
+ assert.deepEqual(shownImage({ attrs: { type: 'file' }, type: 'mediaInline' }, undefined), { after: undefined, link: undefined, text: undefined })
+ assert.deepEqual(shownImage({ attrs: { alt: 'The moon', type: 'external', url: ' ' }, type: 'media' }, undefined), { after: undefined, link: undefined, text: 'The moon' })
+})
+
+test('follows a link mark holding a string href, keeping its title only where that is a string', () => {
+ const linked = (attrs: AdfAttributes): AdfNode => ({ marks: [{ type: 'strong' }, { attrs, type: 'link' }], text: 'x', type: 'text' })
+ assert.deepEqual(shownLink(linked({ href: target, title: 'Home' })), { href: target, title: 'Home' })
+ assert.deepEqual(shownLink(linked({ href: target, title: 5 })), { href: target })
+ assert.equal(shownLink(linked({ title: 'Home' })), undefined)
+ assert.equal(shownLink({ text: 'x', type: 'text' }), undefined)
+})
+
test('counts a card standing apart as a block, as a block node is', () => {
assert.equal(isBlockNodeType('blockCard'), true)
assert.equal(isBlockNodeType('embedCard'), true)
diff --git a/src/adf/shown-text.ts b/src/adf/shown-text.ts
index 4ea0340..83df9f4 100644
--- a/src/adf/shown-text.ts
+++ b/src/adf/shown-text.ts
@@ -1,7 +1,8 @@
import type { AdfNode } from './document.ts'
import type { JsonValue } from '../json-value.ts'
+import type { LinkAttrs } from './mark-attributes.ts'
import { blockNodeModel } from './block-nodes.ts'
-import { nodeAttrs } from './document.ts'
+import { nodeAttrs, nodeMarks } from './document.ts'
export function isBlockNodeType(type: string): boolean {
return blockNodeModel(type) !== undefined || type === 'blockCard' || type === 'embedCard'
@@ -38,7 +39,24 @@ export function referenceName(node: AdfNode): string | undefined {
}
}
-// The text a node shows in place of itself, read from the attribute holding its content; `undefined` where it holds none.
+// What an image shows in place of itself, `href` the target of the link marking it, if any: `text`, else a note; `link`, its URL, which `text` links to where no link marks it; `after`, its URL, shown after a link going elsewhere.
+export function shownImage(media: AdfNode, href: string | undefined): { after: string | undefined; link: string | undefined; text: string | undefined } {
+ const alt = visibleText(nodeAttrs(media)['alt'])
+ const url = shownUrl(media)
+ if (url === undefined) return { after: undefined, link: undefined, text: alt }
+ if (href === undefined) return { after: undefined, link: url, text: alt ?? url }
+ if (href === url) return { after: undefined, link: undefined, text: alt ?? url }
+ return { after: url, link: undefined, text: alt ?? visibleText(href) ?? url }
+}
+
+// The link a node's first link mark makes: its `href` where that is a string, and its `title` only where that is one.
+export function shownLink(node: AdfNode): LinkAttrs | undefined {
+ const { href, title } = nodeAttrs(nodeMarks(node).find((mark) => mark.type === 'link') ?? {})
+ if (typeof href !== 'string') return undefined
+ return typeof title === 'string' ? { href, title } : { href }
+}
+
+// The text a node shows in place of itself, read from the attribute holding its content; `undefined` where it holds none, and for an image, which `shownImage` answers.
export function shownText(node: AdfNode): string | undefined {
const attrs = nodeAttrs(node)
switch (node.type) {
@@ -56,9 +74,6 @@ export function shownText(node: AdfNode): string | undefined {
case 'extension':
case 'inlineExtension':
return visibleText(attrs['text'])
- case 'media':
- case 'mediaInline':
- return visibleText(attrs['alt']) ?? shownUrl(node)
case 'mention': {
const id = attrs['id']
return visibleText(attrs['text']) ?? (typeof id === 'string' && id !== '' ? `@${id}` : undefined)
diff --git a/src/html/emit/adf-to-html.test.ts b/src/html/emit/adf-to-html.test.ts
index 978cdda..c374785 100644
--- a/src/html/emit/adf-to-html.test.ts
+++ b/src/html/emit/adf-to-html.test.ts
@@ -149,11 +149,12 @@ test('shows a carried node\'s content after a block it holds as a paragraph of i
assert.equal(shown(html(adfToHtml(document({ content: [paragraph(text('a')), text('b')], type: 'panel' })))), 'a
b
')
})
-test('shows text a link marks in a carried node as a link', () => {
+test('shows text a link marks in a carried node as a link, leaving out a title that is no string', () => {
const link: AdfMark = { attrs: { href: 'https://spec.commonmark.org/', title: 'The spec' }, type: 'link' }
const panel: AdfNode = { content: [paragraph(text('Read the '), text('CommonMark spec', link), text(' first.'))], type: 'panel' }
assert.equal(shown(html(adfToHtml(document(panel)))), 'Read the CommonMark spec first.
')
assert.equal(shown(html(adfToHtml(document(paragraph(text('x', { type: 'underline' }, link)))))), 'x')
+ assert.equal(shown(html(adfToHtml(document(paragraph(text('x', { attrs: { href: 'https://spec.commonmark.org/', title: 5 }, type: 'link' })))))), 'x')
})
test('shows a hard break, and a line feed in carried text outside a code block, as a line break', () => {
@@ -219,8 +220,11 @@ test('shows a carried image marked by a link to another target as that link, the
assert.equal(image({ alt: 'The moon', type: 'external', url: 'https://example.com/' }), '')
const blank = shown(html(adfToHtml(document({ attrs: { layout: 'wide' }, content: [{ attrs: { type: 'external', url: 'https://example.com/moon.jpg' }, marks: [{ attrs: { href: ' ' }, type: 'link' }], type: 'media' }], type: 'mediaSingle' }))))
assert.equal(blank, 'https://example.com/moon.jpg (image)
')
- const inline: AdfNode = { attrs: { alt: 'The moon', type: 'external', url: 'https://example.com/moon.jpg' }, marks: [link], type: 'mediaInline' }
- assert.equal(shown(html(adfToHtml(document(paragraph(inline))))), 'The moon (image)')
+ const texted = shown(html(adfToHtml(document({ attrs: { layout: 'wide' }, content: [{ attrs: { alt: 'The moon', type: 'external', url: 'https://example.com/moon.jpg' }, marks: [link], text: 'Moon', type: 'media' }], type: 'mediaSingle' }))))
+ assert.equal(texted, '')
+ const inline = (mark: AdfMark): string => shown(html(adfToHtml(document(paragraph({ attrs: { alt: 'The moon', type: 'external', url: 'https://example.com/moon.jpg' }, marks: [mark], type: 'mediaInline' })))))
+ assert.equal(inline(link), 'The moon (image)')
+ assert.equal(inline({ attrs: { href: 'https://example.com/', title: 5 }, type: 'link' }), 'The moon (image)')
})
test('links a carried card a link marks to that link\'s target alone', () => {
diff --git a/src/html/emit/carry-element.ts b/src/html/emit/carry-element.ts
index e06ffb3..4ef55d4 100644
--- a/src/html/emit/carry-element.ts
+++ b/src/html/emit/carry-element.ts
@@ -1,10 +1,10 @@
import type { AdfNode } from '../../adf/document.ts'
+import type { LinkAttrs } from '../../adf/mark-attributes.ts'
import { anchorOpener, jsonValue, languageClass, shownValue } from './escaping.ts'
import { carriedJson } from '../../adf/carried-json.ts'
import { largestNesting } from '../../nesting.ts'
-import { linkAttrs, type LinkAttrs } from '../../adf/mark-attributes.ts'
-import { nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
-import { partsShownText, referenceName, shownText, shownUrl, visibleText } from '../../adf/shown-text.ts'
+import { nodeAttrs, nodeContent } from '../../adf/document.ts'
+import { partsShownText, referenceName, shownImage, shownLink, shownText, shownUrl } from '../../adf/shown-text.ts'
import { success, type ConvertErrorPath, type Result } from '../../result.ts'
// The content a carry element shows of one block; `code` the code block holding it, if any.
@@ -51,23 +51,25 @@ function shownBlocks(root: AdfNode, codeLineFeed: string): ShownBlock[] {
// The node's own part of what its carry element shows.
function shownPiece(node: AdfNode, lineFeed: string): string {
if (node.type === 'hardBreak') return '
'
- const mark = nodeMarks(node).find((held) => held.type === 'link')
- const target = mark === undefined ? undefined : linkAttrs(nodeAttrs(mark))
- const url = shownUrl(node)
- if (['media', 'mediaInline'].includes(node.type) && target !== undefined && url !== undefined && target.href !== url) return linkedImage(node, target, url, lineFeed)
- const link = target ?? (url === undefined ? undefined : { href: url })
- const text = node.text ?? shownText(node)
+ if (['media', 'mediaInline'].includes(node.type)) return linkedImage(node, lineFeed)
+ return linkedText(node, shownText(node), shownUrl(node), lineFeed)
+}
+
+// As portable markdown writes it inline, except that a link around a note stays (docs/decisions.md §The HTML dialect).
+function linkedImage(media: AdfNode, lineFeed: string): string {
+ const { after, link, text } = shownImage(media, shownLink(media)?.href)
+ const shown = linkedText(media, text, link, lineFeed)
+ return after === undefined ? shown : `${shown} (${linked('image', { href: after })})`
+}
+
+function linkedText(node: AdfNode, standIn: string | undefined, url: string | undefined, lineFeed: string): string {
+ const link = shownLink(node) ?? (url === undefined ? undefined : { href: url })
+ const text = node.text ?? standIn
if (text !== undefined) return linked(shownValue(text, lineFeed), link)
const name = referenceName(node)
return name === undefined ? '' : linked(`(${shownValue(name, lineFeed)})`, link)
}
-// 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 })})`
-}
-
// The shown HTML inside an `` where it links and HTML can hold the link.
function linked(shown: string, link: LinkAttrs | undefined): string {
const anchor = link === undefined ? undefined : anchorOpener(link)
diff --git a/src/markdown/portable/inline-reduction.ts b/src/markdown/portable/inline-reduction.ts
index 47d4c06..396fd36 100644
--- a/src/markdown/portable/inline-reduction.ts
+++ b/src/markdown/portable/inline-reduction.ts
@@ -7,7 +7,7 @@ import { imageAside } from '../image-aside.ts'
import { joinsWhenEditorNormal, sameMarkWhenEditorNormal } from './editor-normal.ts'
import { largestNesting } from '../../nesting.ts'
import { mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
-import { partsShownText, referenceName, shownText, shownUrl, visibleText } from '../../adf/shown-text.ts'
+import { partsShownText, referenceName, shownImage, shownLink, shownText, shownUrl } from '../../adf/shown-text.ts'
import { portableLineFallback, type PortableLineFallback } from '../emit/inline-line.ts'
import { spellDestination } from '../commonmark/link-syntax.ts'
@@ -59,20 +59,15 @@ function cardLeaves(card: AdfNode, marks: readonly AdfMark[], container: LineCon
return linkedLeaves(shownText(card) ?? href, href, marks, container)
}
-function linkedLeaves(text: string, href: string, marks: readonly AdfMark[], container: LineContainer): AdfNode[] {
+function linkedLeaves(text: string | undefined, href: string, marks: readonly AdfMark[], container: LineContainer): AdfNode[] {
return textLeaves(text, [...marks.filter((mark) => mark.type !== 'link'), { attrs: { href }, type: 'link' }], container)
}
// An image standing inline is a link to it: the reader builds no node for an inline image, reading it as its alt text linked, and inside a link as the link's text, then (image).
function mediaLeaves(media: AdfNode, marks: readonly AdfMark[], container: LineContainer): AdfNode[] {
- const alt = visibleText(nodeAttrs(media)['alt'])
- const url = shownUrl(media)
- if (url === undefined) return textLeaves(alt, marks, container, referenceName(media))
- const link = marks.find((mark) => mark.type === 'link')
- const href = link === undefined ? undefined : nodeAttrs(link)['href']
- if (typeof href !== 'string') return linkedLeaves(alt ?? url, url, marks, container)
- if (href === url) return textLeaves(alt ?? url, marks, container)
- return [...textLeaves(alt ?? visibleText(href) ?? url, marks, container), ...imageAside((text) => linkedLeaves(text, url, marks, container))]
+ const { after, link, text } = shownImage(media, shownLink(media)?.href)
+ const shown = link === undefined ? textLeaves(text, marks, container, referenceName(media)) : linkedLeaves(text, link, marks, container)
+ return after === undefined ? shown : [...shown, ...imageAside((word) => linkedLeaves(word, after, marks, container))]
}
function noteLeaves(name: string): AdfNode[] {
diff --git a/todo.md b/todo.md
index 4a46679..f5a163a 100644
--- a/todo.md
+++ b/todo.md
@@ -14,7 +14,7 @@
|---|---|---|---|---|---|---|---|---|---|---|---|
| 142 | 0.3.0 | | security | **Settle in `docs/decisions.md`, weighing the pros and cons with the maintainer, whether `adfToHtml` writes a `javascript:`, `vbscript:`, `file:` or non-image `data:` URL as the ADF holds it, in a link's `href`, an image's `src` and each link the carry element shows.** | 2 | 2 | 2 | 4 | 3 | 6 by 3 | 4.5 |
| 131 | 0.3.0 | | | **Ship `htmlToAdf`, reading back exactly what `adfToHtml` writes and refusing any other HTML as `unmappable-html`.** | 4 | 1 | 2 | 5 | 7 | 3 by 3 | 0.6 |
-| 133 | 0.3.0 | 131 | | **Write mentions, emoji, dates, statuses and inline cards as semantic HTML in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.** | 2 | 2 | 2 | 4 | 4 | 6 by 3 | 3.0 |
+| 133 | 0.3.0 | 131 | decision | **Write mentions, emoji, dates, statuses and inline cards as semantic HTML in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.** | 2 | 2 | 2 | 4 | 4 | 6 by 3 | 3.0 |
| 132 | 0.3.0 | 131 | | **Write tables as `` in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.** | 2 | 2 | 2 | 4 | 5 | 6 by 4 | 2.4 |
| 128 | 0.4.0 | | | **Pin the portable flavour's block reading and writing with tests: callout titles past one line, a link beside other marks in a title, the nesting limit of callouts and task lists, refusals inside tasks and their paths, and empty or non-paragraph tasks.** | 2 | 1 | 1 | 2 | 3 | code health by 3 | 19.6 |
| 140 | 0.4.0 | | defect | **Read an inline link's target in time linear in the inline markdown, in every reader.** | 3 | 1 | 2 | 4 | 3 | 9 by 3 | 19.6 |
@@ -28,8 +28,8 @@
| 45 | 0.4.0 | | defect | **Replace `isAdfDocument` with a reader returning `Result`.** | 2 | 1 | 3 | 4 | 3 | 1 by 2 | 13.1 |
| 68 | 0.4.0 | | defect | **Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.** | 2 | 1 | 2 | 3 | 3 | 1 by 2 | 13.1 |
| 127 | 0.4.0 | | principle | **Hold what `entity-references.test.ts`, `json-text.test.ts` and `result.test.ts` check by asserting outcomes, so none reads source text.** | 2 | 2 | 1 | 3 | 3 | code health by 2 | 13.1 |
-| 143 | 0.4.0 | | defect | **Keep the link around a note in `adfToPortableMarkdown`, so a linked stored image with no alt writes its note linked.** | 1 | 1 | 2 | 2 | 2 | 7 by 1 | 8.7 |
| 144 | 0.4.0 | | | **Give every container `docker-runner.sh` and `publish.sh` start a memory limit and an equal swap limit, sized from the largest a gate leg measurably needs.** | 1 | 1 | 1 | 1 | 2 | code health by 1 | 8.7 |
+| 143 | 0.4.0 | | defect | **Keep the link around a note in `adfToPortableMarkdown`, so a linked stored image with no alt writes its note linked.** | 2 | 1 | 2 | 3 | 3 | 7 by 1 | 6.5 |
| 112 | 0.4.0 | | | **Pad a GFM pipe table's short rows with empty cells, read its body rows holding no pipe, and read a one-column table without leading pipes in `portableMarkdownToAdf`.** | 2 | 1 | 2 | 3 | 3 | 4 by 4 | 6.2 |
| 80 | 0.4.0 | | | **Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.** | 2 | 1 | 2 | 3 | 3 | 6 by 4 | 6.0 |
| 81 | 0.4.0 | | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 1 | 1 | 1 | 2 | 3 by 2 | 4.2 |
@@ -93,7 +93,9 @@ takes).
### 133. Write mentions, emoji, dates, statuses and inline cards as semantic HTML in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.
-A writer panel picks the elements, classes and `data-*` attributes.
+A writer panel picks the elements, classes and `data-*` attributes. Where a link marks a card, both
+the link's target and the card's URL stay in sight, a card showing only its name included
+(`docs/decisions.md` §The HTML dialect).
### 132. Write tables as `` in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.
@@ -212,12 +214,6 @@ Three tests read source text:
Item 81 changes `result.test.ts` too. Found by the technical-principles audit of
item 60, 2026-10-22.
-### 143. Keep the link around a note in `adfToPortableMarkdown`, so a linked stored image with no alt writes its note linked.
-
-`noteLeaves` (`src/markdown/portable/inline-reduction.ts`) drops the node's marks, so a stored
-image a link marks writes `_(image not included)_` without the link's target, which Goal 7 counts as
-content. `adfToHtml`'s carry element keeps it, `(image)`.
-
### 144. Give every container `docker-runner.sh` and `publish.sh` start a memory limit and an equal swap limit, sized from the largest a gate leg measurably needs.
`in_image`, `with_firefox` and `publish.sh` run `docker run` with neither `--memory` nor
@@ -225,6 +221,19 @@ content. `adfToHtml`'s carry element keeps it, `(image)
strings near V8's longest, about 512 MB each, so measure each leg's peak before picking the limit:
one too small fails the gate.
+### 143. Keep the link around a note in `adfToPortableMarkdown`, so a linked stored image with no alt writes its note linked.
+
+`noteLeaves` (`src/markdown/portable/inline-reduction.ts`) drops the node's marks, so a stored
+image a link marks writes `_(image not included)_` without the link's target, which Goal 7 counts as
+content. `adfToHtml`'s carry element keeps it, `(image)`.
+
+`cardLeaves`, in the same file, links a card to its own URL, so a card a link to another target
+marks writes a link to the card's URL and drops the mark's target. The carry element keeps both: it
+links the card's URL, as its text, to the mark's target (`docs/decisions.md` §The HTML dialect).
+
+Move the choice of where a card's text and a note link into `src/adf/shown-text.ts`, beside
+`shownImage`'s for an image, so both writers render one answer.
+
### 112. Pad a GFM pipe table's short rows with empty cells, read its body rows holding no pipe, and read a one-column table without leading pipes in `portableMarkdownToAdf`.
GFM pads a short row with empty cells, reads a line holding no `|` after a table as a row, and