3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 11:48:29 +01:00

Answer architecture round 4: 0 🔴, 1 🟡 (0 misread); one image rule in shown-text for both writers, a link mark's href kept past a bad title, the card rule recorded

This commit is contained in:
2026-10-24 11:58:23 +02:00
parent 76d524298f
commit 0373b7bcf8
10 changed files with 115 additions and 50 deletions
+2 -2
View File
@@ -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,
+4 -3
View File
@@ -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` | `<p>` |
| `rule` | `<hr>` |
| `text` marked `code`, `em`, `strong` | `<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: `<p data-local-id="a1f3">` |
| 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: `<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
+5
View File
@@ -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 `<a>` 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 `<a>` 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
+2 -2
View File
@@ -84,8 +84,8 @@ later release writes it as its own element.
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, 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)`,
+35 -1
View File
@@ -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)
+20 -5
View File
@@ -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)
+7 -3
View File
@@ -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' })))), '<p>a</p><p>b</p>')
})
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)))), '<p>Read the <a href="https://spec.commonmark.org/" title="The spec">CommonMark spec</a> first.</p>')
assert.equal(shown(html(adfToHtml(document(paragraph(text('x', { type: 'underline' }, link)))))), '<a href="https://spec.commonmark.org/" title="The spec">x</a>')
assert.equal(shown(html(adfToHtml(document(paragraph(text('x', { attrs: { href: 'https://spec.commonmark.org/', title: 5 }, type: 'link' })))))), '<a href="https://spec.commonmark.org/">x</a>')
})
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/' }), '<p><a href="https://example.com/">The moon</a></p>')
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, '<p><a href=" ">https://example.com/moon.jpg</a> (<a href="https://example.com/moon.jpg">image</a>)</p>')
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))))), '<a href="https://example.com/">The moon</a> (<a href="https://example.com/moon.jpg">image</a>)')
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, '<p><a href="https://example.com/">Moon</a> (<a href="https://example.com/moon.jpg">image</a>)</p>')
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), '<a href="https://example.com/">The moon</a> (<a href="https://example.com/moon.jpg">image</a>)')
assert.equal(inline({ attrs: { href: 'https://example.com/', title: 5 }, type: 'link' }), '<a href="https://example.com/">The moon</a> (<a href="https://example.com/moon.jpg">image</a>)')
})
test('links a carried card a link marks to that link\'s target alone', () => {
+17 -15
View File
@@ -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 '<br>'
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(`<em>(${shownValue(name, lineFeed)})</em>`, 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 `<a>` where it links and HTML can hold the link.
function linked(shown: string, link: LinkAttrs | undefined): string {
const anchor = link === undefined ? undefined : anchorOpener(link)
+5 -10
View File
@@ -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[] {
+18 -9
View File
@@ -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 `<table>` 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<AdfDocument>`.** | 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 `<table>` 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, `<a href="…"><em>(image)</em></a>`.
### 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, `<a href="…"><em>(image)</em></
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, `<a href="…"><em>(image)</em></a>`.
`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