From 2aa246898d3284215a36fa95ce6a353670ceb5a4 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 9 Nov 2026 09:14:03 +0100 Subject: [PATCH] Keep the link around a note in adfToPortableMarkdown, chosen in shown-text beside the image's --- CHANGELOG.md | 3 +++ README.md | 2 +- docs/reading-cost.md | 14 ++++++++++++++ src/adf/shown-text.ts | 11 +++++++++-- src/html/carry-content.ts | 11 +++++------ .../portable/adf-to-portable-markdown.test.ts | 9 +++++++++ src/markdown/portable/inline-reduction.ts | 18 ++++++++++-------- todo.md | 14 -------------- 8 files changed, 51 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e76d48..cbb5194 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- `adfToPortableMarkdown` keeps the link marking a stored image, an extension or a card with no + text around the note standing in its place, so its target is no longer dropped: + `[_(image not included)_](https://example.com/a)`. - **Breaking:** `readAdfDocument` replaces `isAdfDocument`: it returns a `Result` whose error names the branch that refused, and `MIGRATION.md` shows the change. - `readAdfDocument`, `adfToHtml`, `adfToLosslessMarkdown` and `adfToPortableMarkdown` read the diff --git a/README.md b/README.md index 632bb26..0e1c2d6 100644 --- a/README.md +++ b/README.md @@ -486,7 +486,7 @@ saving what this pair read replaces mentions, attachments and macros with text. naming it where it stood: `_(image not included)_` for stored media with no `alt`, `_(link card not included)_` for a card with neither URL nor name, an extension with no `text` its key, `_(jira-issues-table not included)_`, or `_(extension not included)_` without one, and - `_(synced block not included)_` for a `syncBlock`. + `_(synced block not included)_` for a `syncBlock`. A link marking the node stays around its note. - `backgroundColor`, `code`, `em`, `link`, `strike` and `strong` stay; every other mark drops, keeping its text, and so does a mark the flavour cannot spell where it stands. A line still read back wrong after 32 tries drops every `em`, `strike` and `strong` at once. Code text keeps diff --git a/docs/reading-cost.md b/docs/reading-cost.md index 019993f..6e9d6c5 100644 --- a/docs/reading-cost.md +++ b/docs/reading-cost.md @@ -936,3 +936,17 @@ Read most (opens and searches): - `src/markdown/portable/adf-to-portable-markdown.ts` 2 - `src/result.ts` 2 - `AGENTS.md` 1 + +## Reading cost, chunk 93989f5f1745, finished 2026-11-09T08:12:20Z + +89422 new input tokens before the first commit. + +Read most (opens and searches): + +- `ci.sh` 4 +- `todo.md` 4 +- `docker-runner.sh` 2 +- `docs/decisions.md` 2 +- `AGENTS.md` 1 +- `README.md` 1 +- `publish.sh` 1 diff --git a/src/adf/shown-text.ts b/src/adf/shown-text.ts index e7b1637..469ee20 100644 --- a/src/adf/shown-text.ts +++ b/src/adf/shown-text.ts @@ -17,8 +17,15 @@ export function oneLine(text: string): string { return text.replace(/[\r\u0000]/g, '').replace(/\n/g, ' ') } -// The name of what a node references where it shows no text of its own, for a note standing in its place. -export function referenceName(node: AdfNode): string | undefined { +export type ShownNote = { link: LinkAttrs | undefined; name: string } + +// The note standing in place of a node that shows no text of its own: `name` what it references, and `link` the node's link, which both writers keep around the note. `undefined` where the node references nothing. +export function shownNote(node: AdfNode): ShownNote | undefined { + const name = referenceName(node) + return name === undefined ? undefined : { link: shownLink(node), name } +} + +function referenceName(node: AdfNode): string | undefined { switch (node.type) { case 'blockCard': case 'embedCard': diff --git a/src/html/carry-content.ts b/src/html/carry-content.ts index 716fb0e..3a0a8c4 100644 --- a/src/html/carry-content.ts +++ b/src/html/carry-content.ts @@ -3,7 +3,7 @@ import type { LinkAttrs } from '../adf/mark-attributes.ts' import { anchorOpener } from './anchor-element.ts' import { languageClass } from './language-class.ts' import { nodeAttrs, nodeContent } from '../adf/document.ts' -import { partsShownText, referenceName, shownImage, shownLink, shownText, shownUrl } from '../adf/shown-text.ts' +import { partsShownText, shownImage, shownLink, shownNote, shownText, shownUrl } from '../adf/shown-text.ts' import { shownValue } from './escaping.ts' // Compared by reference: each block makes one, so its content after a nested block starts a new shown block. `code`: this block, where it is a code block. @@ -40,7 +40,7 @@ function shownPiece(node: AdfNode, lineFeed: string): string { 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). +// As portable markdown writes it inline (docs/decisions.md §The HTML dialect). function linkedImage(media: AdfNode, lineFeed: string): string { const image = shownImage(media, shownLink(media)?.href) const shown = linkedText(media, image?.text, image?.link, lineFeed) @@ -48,11 +48,10 @@ function linkedImage(media: AdfNode, lineFeed: string): string { } 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) + if (text !== undefined) return linked(shownValue(text, lineFeed), shownLink(node) ?? (url === undefined ? undefined : { href: url })) + const note = shownNote(node) + return note === undefined ? '' : linked(`(${shownValue(note.name, lineFeed)})`, note.link) } // The shown HTML inside an `` where it links and HTML can hold the link. diff --git a/src/markdown/portable/adf-to-portable-markdown.test.ts b/src/markdown/portable/adf-to-portable-markdown.test.ts index 0c2add9..ce1c6e7 100644 --- a/src/markdown/portable/adf-to-portable-markdown.test.ts +++ b/src/markdown/portable/adf-to-portable-markdown.test.ts @@ -296,6 +296,15 @@ test('keeps an external image wherever it stands and spells a stored file as its assert.equal(portable(caption), 'The moon.\n') }) +test('keeps the link marking a node around the note standing in its place', () => { + const linked = (stored: AdfNode, href: string, title?: string): AdfNode => ({ ...stored, marks: [{ attrs: title === undefined ? { href } : { href, title }, type: 'link' }, { type: 'strong' }] }) + assert.equal(portable(paragraph(text('a '), linked(node('mediaInline', { type: 'file' }), 'https://example.com/a'))), 'a [_(image not included)_](https://example.com/a)\n') + assert.equal(portable(paragraph(linked(node('mediaInline', { type: 'file' }), 'https://example.com/a', 'T'))), '[_(image not included)_](https://example.com/a "T")\n') + assert.equal(portable(paragraph(linked(node('inlineExtension', { extensionKey: 'macro' }), 'https://example.com/b'))), '[_(macro not included)_](https://example.com/b)\n') + assert.equal(portable(paragraph(linked(node('mediaInline', { type: 'file' }), 'a|b'))), '[_(image not included)_](a|b)\n') + assert.equal(portable(node('table', {}, node('tableRow', {}, node('tableCell', {}, paragraph(linked(node('mediaInline', { type: 'file' }), 'a|b')))))), '| [_(image not included)_](a%7Cb) |\n| --- |\n') +}) + test('spells an extension as its text attribute, else a note naming it, and a placeholder as nothing', () => { assert.equal(portable(node('extension', { extensionKey: 'toc', text: 'Contents' }), node('extension', { extensionKey: 'jira-issues-table' })), 'Contents\n\n_(jira-issues-table not included)_\n') assert.equal(portable(node('syncBlock', { resourceId: 'r' })), '_(synced block not included)_\n') diff --git a/src/markdown/portable/inline-reduction.ts b/src/markdown/portable/inline-reduction.ts index 434807c..868efea 100644 --- a/src/markdown/portable/inline-reduction.ts +++ b/src/markdown/portable/inline-reduction.ts @@ -2,6 +2,7 @@ import type { AdfMark, AdfNode } from '../../adf/document.ts' import type { LineContainer } from '../line-container.ts' import type { MarkRun } from '../emit/line-escaping.ts' import type { RunGrouping } from '../emit/inline-runs.ts' +import type { ShownNote } from '../../adf/shown-text.ts' import { codeTextMarkTypes } from '../../adf/inline-nodes.ts' import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts' import { identicalMark, mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts' @@ -10,7 +11,7 @@ import { insideRun, runsSharedWithNext } from '../emit/inline-runs.ts' import { isEmphasis } from '../mark-spellings.ts' import { joinsWhenEditorNormal, sourceIndex, type SourceIndexes } from './editor-normal.ts' import { largestNesting } from '../../nesting.ts' -import { partsShownText, referenceName, shownImage, shownLink, shownText, shownUrl } from '../../adf/shown-text.ts' +import { partsShownText, shownImage, shownLink, shownNote, shownText, shownUrl } from '../../adf/shown-text.ts' import { portableLineFallback, settledLine, type LineJudgement, type PortableLineFallback } from '../emit/inline-line.ts' import { spellDestination } from '../commonmark/link-syntax.ts' @@ -72,7 +73,7 @@ function nodeLeaves(node: AdfNode, container: LineContainer, place: ReductionPla if (node.type === 'hardBreak') return success([lineBreak(container)]) if (['date', 'emoji', 'mention', 'status'].includes(node.type)) return success(textLeaves(shownText(node), marks, container)) if (node.type === 'placeholder') return success([]) - if (['extension', 'inlineExtension', 'syncBlock'].includes(node.type)) return success(textLeaves(shownText(node), marks, container, referenceName(node))) + if (['extension', 'inlineExtension', 'syncBlock'].includes(node.type)) return success(textLeaves(shownText(node), marks, container, shownNote(node))) if (['media', 'mediaInline'].includes(node.type)) return success(mediaLeaves(node, marks, container)) if (['blockCard', 'embedCard', 'inlineCard'].includes(node.type)) return success(cardLeaves(node, marks, container)) const own = textLeaves(node.text ?? shownText(node), marks, container) @@ -83,7 +84,7 @@ function nodeLeaves(node: AdfNode, container: LineContainer, place: ReductionPla function cardLeaves(card: AdfNode, marks: readonly AdfMark[], container: LineContainer): AdfNode[] { const href = shownUrl(card) - if (href === undefined) return textLeaves(shownText(card), marks, container, referenceName(card)) + if (href === undefined) return textLeaves(shownText(card), marks, container, shownNote(card)) return linkedLeaves(shownText(card) ?? href, href, marks, container) } @@ -94,22 +95,23 @@ function linkedLeaves(text: string, href: string, marks: readonly AdfMark[], con // 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 image = shownImage(media, shownLink(media)?.href) - if (image === undefined) return textLeaves(undefined, marks, container, referenceName(media)) + if (image === undefined) return textLeaves(undefined, marks, container, shownNote(media)) const { after, link, text } = image const shown = link === undefined ? textLeaves(text, marks, container) : linkedLeaves(text, link, marks, container) return after === undefined ? shown : [...shown, ...imageAside((word) => linkedLeaves(word, after, marks, container))] } -function noteLeaves(name: string): AdfNode[] { - return [textLeaf(`(${name} not included)`, [{ type: 'em' }])] +function noteLeaves({ link, name }: ShownNote, container: LineContainer): AdfNode[] { + const kept = link === undefined ? undefined : portableLink({ attrs: link, type: 'link' }, container) + return [textLeaf(`(${name} not included)`, kept === undefined ? [{ type: 'em' }] : [kept, { type: 'em' }])] } function lineBreak(container: LineContainer): AdfNode { return container === 'paragraph' ? { type: 'hardBreak' } : textLeaf(' ', []) } -function textLeaves(value: unknown, marks: readonly AdfMark[], container: LineContainer, note?: string): AdfNode[] { - if (typeof value !== 'string') return note === undefined ? [] : noteLeaves(note) +function textLeaves(value: unknown, marks: readonly AdfMark[], container: LineContainer, note?: ShownNote): AdfNode[] { + if (typeof value !== 'string') return note === undefined ? [] : noteLeaves(note, container) const text = value.replace(/[\r\u0000]/g, '') const kept = portableMarks(marks, container, text) const leaves: AdfNode[] = [] diff --git a/todo.md b/todo.md index 3977c25..84291fc 100644 --- a/todo.md +++ b/todo.md @@ -13,7 +13,6 @@ | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | |---|---|---|---|---|---|---|---|---|---|---|---| | 153 | 0.4.0 | | question | **Decide whether `src/markdown/` root may import `emit/`, `parse/` or `portable/`, and hold the answer in `.oxlintrc.json` and §The source parts by ADF and format.** | 1 | 2 | 2 | 3 | 2 | code health by 3 | 26.2 | -| 143 | 0.4.0 | | defect | **Keep, in `adfToPortableMarkdown`, the link around a note, so a linked stored image writes every target.** | 2 | 1 | 3 | 4 | 3 | 7 by 1 | 6.5 | | 169 | 0.4.0 | | owner | **Investigate `kovetskiy/mark` in depth and record in a report how its formats differ from ours, where the two projects overlap and how they compare.** | 1 | 1 | 1 | 1 | 3 | 3 by 4 | 6.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`.** | 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 | @@ -64,19 +63,6 @@ a paragraph opening with a spellable `!adf:link` (§Nothing recurses unbounded). `src/markdown/**` bans only `html/`; banning `emit/`, `parse/` and `portable/` for the root's own files, with its `ci.sh` probe, extends the decision. -### 143. Keep, in `adfToPortableMarkdown`, the link around a note, so a linked stored image writes every target. - -`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)`. - -Move the choice of where a note's link goes into `src/adf/shown-text.ts`, beside `shownImage`'s for -an image, so both writers render one answer. - -Leave what an HTML carry element shows unchanged, or ship the change as a breaking HTML change with -a `MIGRATION.md` entry: `htmlToAdf` refuses any other content, so HTML stored by an earlier release -would no longer read (`docs/decisions.md` §The formats are API). - ### 169. Investigate `kovetskiy/mark` in depth and record in a report how its formats differ from ours, where the two projects overlap and how they compare. `https://github.com/kovetskiy/mark` publishes markdown files as Confluence pages. Read its