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` | `


` | | `text` marked `code`, `em`, `strong` | ``, ``, ``, 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/' }), '

The moon

') 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, '

Moon (image)

') + 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)(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