From c9f24f634da3ac78bec7cd103eb46791edbacf64 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 14 Oct 2026 22:24:48 +0200 Subject: [PATCH] =?UTF-8?q?Answer=20architecture=20round=201:=201=20?= =?UTF-8?q?=F0=9F=94=B4,=203=20=F0=9F=9F=A1;=20name=20the=20other-target?= =?UTF-8?q?=20link=20and=20no-alt=20case,=20pin=20both,=20file=20items=201?= =?UTF-8?q?17=20and=20118?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/decisions.md | 5 +++-- .../portable/adf-to-portable-markdown.test.ts | 4 +++- src/markdown/portable/inline-reduction.ts | 11 +++++++---- todo.md | 18 +++++++++++++++++- 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0eda2f2..fed903f 100644 --- a/README.md +++ b/README.md @@ -374,7 +374,7 @@ saving what this pair read replaces mentions, attachments and macros with text. | `backgroundColor` | `==text==` | `==text==` on one line, the text touching both delimiters, bounded outside by whitespace, punctuation or a line edge, or touching a Han, Hangul, Hiragana, Katakana, Thai, Lao, Khmer or Myanmar character on either side, in the editor's default highlight `#f8e6a0` | | `decisionList` | a bullet list | — | | `expand`, `nestedExpand` | Obsidian's folded callout, `> [!NOTE]- Title` | `-` or `+` after any word, the rest of the marker's line the title; a link reads `text (target)`, or its text alone where the text is the target with or without `mailto:`; an expand inside an expand is a `nestedExpand` | -| external `media` | in a block `![alt](url)`, or `[![alt](url)](href)` where a link marks it; inline `[alt](url)`, or `[alt](href) ([image](url))` where a link marks it | — | +| external `media` | in a block `![alt](url)`, or `[![alt](url)](href)` where a link marks it; inline `[alt](url)`, or `[alt](href) ([image](url))` where a link to another target marks it, the link text `href` where `alt` is empty | — | | `inlineCard`, `blockCard`, `embedCard` | a link to the card's URL, else its name | — | | `layoutSection`, `bodiedExtension`, `bodiedSyncBlock`, `multiBodiedExtension`, `extensionFrame`, `caption`, a node this version does not know | its blocks or its text | — | | `mention`, `status`, `emoji`, `date` | their text: `@` kept, a mention with none `@` and its id, an emoji its `shortName` without, a date `2026-09-13` in UTC | — | diff --git a/docs/decisions.md b/docs/decisions.md index 9bd3572..4cbe71f 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -288,8 +288,9 @@ reads that title as the caption; `todo.md` item 100 puts that reading back to a 2026-10-14, a reader panel of the export tool, the LLM pipeline and a support agent, 3 of 3. Goals 6 and 7. Valid while the portable reader reads an image inside a link as the link's text. -Where `adfToPortableMarkdown` writes an external image inline, as in a table cell, a link marking it -keeps its target and the image follows as a link reading `image`. Every reader first wrote +Where `adfToPortableMarkdown` writes an external image inline, as in a table cell, and a link to +another target marks it, the link keeps its target and the image follows as a link reading `image`. +With no alt text, the link text is the link's target, as `[](href)` reads. Every reader first wrote `[![alt](url)](href)`, which reads back as `[alt](href)` and so breaks the byte-for-byte rewrite. Each then picked this over `[alt](url)` and `[alt](href)`, which drop a target. diff --git a/src/markdown/portable/adf-to-portable-markdown.test.ts b/src/markdown/portable/adf-to-portable-markdown.test.ts index 79ff7b7..3d615da 100644 --- a/src/markdown/portable/adf-to-portable-markdown.test.ts +++ b/src/markdown/portable/adf-to-portable-markdown.test.ts @@ -247,8 +247,10 @@ test('keeps both targets of a linked external image standing inline, the image l assert.equal(inCell, '| h |\n| --- |\n| [Moon](https://example.com/docs "Docs") ([image](https://example.com/moon.png)) |\n') const read = portableMarkdownToAdf(inCell) assert.equal(read.ok && portableDocument(read.value), inCell) - assert.equal(portable(table(said('See'), node('mediaSingle', {}, linked('https://example.com/docs')))), '| h |\n| --- |\n| See [https://example.com/moon.png](https://example.com/docs "Docs") ([image](https://example.com/moon.png)) |\n') + assert.equal(portable(table(said('See'), node('mediaSingle', {}, linked('https://example.com/docs')))), '| h |\n| --- |\n| See [https://example.com/docs](https://example.com/docs "Docs") ([image](https://example.com/moon.png)) |\n') assert.equal(portable(paragraph(text('See '), linked('https://example.com/moon.png', 'Moon'))), 'See [Moon](https://example.com/moon.png "Docs")\n') + assert.equal(portable(paragraph(text('See '), linked('https://example.com/moon.png'))), 'See [https://example.com/moon.png](https://example.com/moon.png "Docs")\n') + assert.equal(portable(paragraph(text('See '), linked(''))), 'See [https://example.com/moon.png](<> "Docs") ([image](https://example.com/moon.png))\n') }) test('keeps an external image wherever it stands and spells a stored file as its alt text, else a note', () => { diff --git a/src/markdown/portable/inline-reduction.ts b/src/markdown/portable/inline-reduction.ts index a644c33..0638c09 100644 --- a/src/markdown/portable/inline-reduction.ts +++ b/src/markdown/portable/inline-reduction.ts @@ -80,15 +80,18 @@ function idMention(id: unknown): string | undefined { return typeof id === 'string' && id !== '' ? `@${id}` : undefined } -// An image standing inline is a link to it: CommonMark's inline image reads back as no node. +// An image standing inline is a link to it: CommonMark's inline image reads back as no node, and one inside a link as the link's text. function mediaLeaves(attrs: Readonly, marks: readonly AdfMark[], container: LineContainer): AdfNode[] { const alt = nonEmpty(attrs['alt']) const url = nonEmpty(attrs['url']) if (attrs['type'] !== 'external' || url === undefined) return textLeaves(alt, marks, container, 'image') - const href = nodeAttrs(marks.find((mark) => mark.type === 'link') ?? {})['href'] - if (href === url) return textLeaves(alt ?? url, marks, container) + 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) - return [...textLeaves(alt ?? url, marks, container), textLeaf(' (', []), ...linkedLeaves('image', url, marks, container), textLeaf(')', [])] + if (href === url) return textLeaves(alt ?? url, marks, container) + const linkedAlt = textLeaves(alt ?? nonEmpty(href) ?? url, marks, container) + const image = linkedLeaves('image', url, marks, container) + return [...linkedAlt, textLeaf(' (', []), ...image, textLeaf(')', [])] } function noteLeaves(name: string): AdfNode[] { diff --git a/todo.md b/todo.md index 52fc84d..fadfd54 100644 --- a/todo.md +++ b/todo.md @@ -6,7 +6,7 @@ `Bar = 9` -`Next ID = 117` +`Next ID = 119` | Goal | W | |---|---| @@ -49,6 +49,7 @@ | 81 | 0.4.0 | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 2 | 2 | 5 | 5 | 3 | 16.3 | | 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`.** | 3 | 3 | 5 | 6 | 6 | 4 | 16.3 | | 46 | 0.4.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 3 | 5 | 5 | 3, 8 | 14.8 | +| 117 | 0.4.0 | defect | **Read a GFM pipe-table row holding more cells than its header as GFM does, or name the goal that outranks Goal 4 in refusing it.** | 2 | 2 | 3 | 4 | 6 | 4 | 13.5 | | 72 | 0.4.0 | decision | **Re-create `gitea.larvit.se/larvit/adf-codec` as a read-only pull mirror of GitHub, its issues and Actions off and its description pointing at GitHub.** | 2 | 1 | 1 | 3 | 6 | 3 | 13.1 | | 107 | 0.4.0 | decision | **Spell the block carry's JSON so its length stays linear in the node's JSON.** | 4 | 5 | 7 | 3 | 5 | 1 | 10.8 | | 51 | 0.4.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 4 | 5 | 2 | 7 | 4, 5 | 10.4 | @@ -61,6 +62,7 @@ | 56 | 0.5.0 | principle | **Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes.** | 4 | 5 | 8 | 2 | 4 | 1 | 6.8 | | 76 | 0.5.0 | principle | **State the files the package does not ship once, so the build, the lint ceiling and the coverage exclusions cannot drift apart.** | 2 | 2 | 3 | 1 | 3 | 1 | 5.5 | | 82 | 0.5.0 | principle | **Give the directive tests and fixtures random UUID v7 ids in place of `a-1`, `a`, `x` and `h`.** | 1 | 3 | 1 | 1 | 2 | 1 | 4.2 | +| 118 | 0.5.0 | principle | **Spell a portable link's title through `oneLine`, as the text beside it is.** | 1 | 1 | 1 | 1 | 2 | 6 | 2.5 | | 57 | 0.5.0 | principle | **Make each `ci.sh` leg build what it reads, so one leg run alone tests the current tree.** | 2 | 3 | 3 | 1 | 3 | 8 | 0.6 | | 58 | 0.5.0 | principle | **Port `ci.sh`, `publish.sh` and `docker-runner.sh` to standalone Python scripts.** | 4 | 6 | 4 | 1 | 2 | 8 | -2.6 | | 8 | 0.6.0 | | **Ship a CLI.** | 3 | 7 | 5 | 7 | 6 | 3 | 18.6 | @@ -271,6 +273,14 @@ or report the unminified gzip). The figure lands in README §The package beside dependencies" claim, and in a size column of the comparison table. Measured today, unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped 45.6 kB. +### 117. Read a GFM pipe-table row holding more cells than its header as GFM does, or name the goal that outranks Goal 4 in refusing it. + +`docs/decisions.md` §The portable reader refuses what GFM's reading would drop refuses such a row as +`malformed-pipe-table` by Goal 7, which ranks below Goal 4: a bot whose model writes an over-wide row +gets an error where GitHub renders a table. The maintainer either reads the row as GFM does, cutting +the cells, or states the goal that outranks Goal 4 here and cites it in the entry. Found by the +principles audit of item 99, 2026-10-14. + ### 72. Re-create `gitea.larvit.se/larvit/adf-codec` as a read-only pull mirror of GitHub, its issues and Actions off and its description pointing at GitHub. Lands after every other `0.4.0` item (the maintainer, 2026-10-05). The maintainer does this. Gitea @@ -369,6 +379,12 @@ agree. Found by the technical-principles audit, 2026-10-04. Technical principle: tests use random UUID v7 ids; `portable-markdown-to-adf.test.ts` already does. Found by the technical-principles audit of item 78, 2026-10-05. +### 118. Spell a portable link's title through `oneLine`, as the text beside it is. + +`portableLink` (`src/markdown/portable/inline-reduction.ts`) repeats `oneLine`'s replacements by +hand, so a change to what a portable line drops would miss link titles. Technical principle: DRY. +Found by the principles audit of item 99, 2026-10-14. + ### 57. Make each `ci.sh` leg build what it reads, so one leg run alone tests the current tree. Technical principle "Compose, do not entangle": Deno, Bun and the tests read the install leg's