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

Answer prose round 1: 0 🔴, 8 🟡; split the image-in-link ruling out, place the (image) plainly, credit only the same-URL rule to the portable writer

This commit is contained in:
2026-10-23 03:39:36 +02:00
parent ad6a4da94e
commit 8e7da0dc8c
7 changed files with 34 additions and 32 deletions
+1 -1
View File
@@ -13,7 +13,7 @@
around only code spans is `unsupported-node-shape`. `adfToLosslessMarkdown` writes code text around only code spans is `unsupported-node-shape`. `adfToLosslessMarkdown` writes code text
holding another mark as `!adf:carry{json="…"}`, and `adfToPortableMarkdown` drops that mark. holding another mark as `!adf:carry{json="…"}`, and `adfToPortableMarkdown` drops that mark.
- Every reader keeps the URL and title of an image inside a link's text, where it dropped them: - Every reader keeps the URL and title of an image inside a link's text, where it dropped them:
`(image)` follows the link, linked to the image, so after the link comes `(image)`, which links to the image, so
`[look at ![the moon](moon.png "Full") tonight](/moon)` reads as `[look at ![the moon](moon.png "Full") tonight](/moon)` reads as
`[look at the moon tonight](/moon) ([image](moon.png "Full"))`. A link to the image's own URL `[look at the moon tonight](/moon) ([image](moon.png "Full"))`. A link to the image's own URL
takes no `(image)`, unless the image's title is one the link lacks. takes no `(image)`, unless the image's title is one the link lacks.
+2 -2
View File
@@ -554,8 +554,8 @@ Serves [Goals](#goals) 1, 4 and 5.
blank line between it and any other line. blank line between it and any other line.
- A code span inside emphasis reads as code without the emphasis, since ADF gives code text no - A code span inside emphasis reads as code without the emphasis, since ADF gives code text no
mark but a link or an annotation: ``**`npm ci`**`` reads as `` `npm ci` ``. mark but a link or an annotation: ``**`npm ci`**`` reads as `` `npm ci` ``.
- Inside a link, an image reads as plain text the link marks, and `(image)` follows the link, - Inside a link, an image reads as plain text the link marks. After the link comes `(image)`,
linked to the image: `[look at ![the moon](moon.png) tonight](/moon)` reads as which links to the image: `[look at ![the moon](moon.png) tonight](/moon)` reads as
`[look at the moon tonight](/moon) ([image](moon.png))`. A link to the image's own URL takes `[look at the moon tonight](/moon) ([image](moon.png))`. A link to the image's own URL takes
no `(image)`, unless the image's title is one the link lacks. A link alone in its paragraph no `(image)`, unless the image's title is one the link lacks. A link alone in its paragraph
whose whole text is one image reads as that image, with the link marking it. whose whole text is one image reads as that image, with the link marking it.
+13 -11
View File
@@ -243,15 +243,17 @@ reads as the URL, and an empty link text as its destination. A link text holding
reads as its destination, then those breaks: four of seven readers expected that over dropping them. reads as its destination, then those breaks: four of seven readers expected that over dropping them.
`[]()` reads as nothing: it renders nothing, so Goal 7 loses no content. A link alone in its `[]()` reads as nothing: it renders nothing, so Goal 7 loses no content. A link alone in its
paragraph whose whole text is one image reads as that image, with the link marking its `media`. paragraph whose whole text is one image reads as that image, with the link marking its `media`.
Inside any other link, the alt text is plain text the link marks, and ` (image)` follows the link,
`image` linked to the image's URL and titled by its title, so Goal 7 keeps both. For a link whose Inside any other link, the alt text is plain text the link marks. After the link comes ` (image)`,
text is the image alone, the bot, the LLM pipeline and a support agent picked this 3 of 3. For a where `image` links to the image's URL and carries its title, so Goal 7 keeps both. Three readers,
link holding words beside the image they split on where `(image)` stands, and four more readers, the the bot, the LLM pipeline and a support agent, picked this 3 of 3 for a link whose text is the image
bot, the LLM pipeline, a product manager and an engineer, made it 5 of 7 for after the whole link alone. For a link with words beside the image, the three split on where `(image)` goes. Four fresh
over right after the alt text. Every reader rejected linking the alt text to the image: the words readers joined: a second bot, a second LLM pipeline, a product manager and an engineer. Five of the
lose the link they were written in. A link to the image's own URL takes no `(image)`, unless the seven put `(image)` after the whole link rather than right after the alt text. Every reader rejected
image holds a title the link does not: it would repeat the link, as `adfToPortableMarkdown` never linking the alt text to the image, since the words would lose the link they were written in.
writes it (§A linked image standing inline in portable markdown).
A link to the image's own URL takes no `(image)`, since the link already reaches the image. The
exception is an image whose title the link lacks: there `(image)` keeps the title.
In `portableMarkdownToAdf`, a `[!WORD]` marker with no `+` or `-` stands apart from the lines after In `portableMarkdownToAdf`, a `[!WORD]` marker with no `+` or `-` stands apart from the lines after
it, and so does a `+` or `-` callout's title line, so an image alone on the marker line or after the it, and so does a `+` or `-` callout's title line, so an image alone on the marker line or after the
@@ -315,8 +317,8 @@ linked text.
Where `adfToPortableMarkdown` writes an external image inline, as in a table cell, and a link to 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`. 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, which is how the reader reads `[](href)`, so With no alt text, the link text is the link's target, which is how the reader reads `[](href)`, so
the rewrite holds. Every seat first wrote `[![alt](url)](href)`, which reads back as linked text the rewrite holds. Every seat first wrote `[![alt](url)](href)`, but the reader reads that as
written in another spelling, and so breaks the byte-for-byte rewrite. Each then picked this over `[alt](href) ([image](url))`, so writing it breaks the byte-for-byte rewrite. Each then picked this over
`[alt](url)` and `[alt](href)`, which drop a target. `[alt](url)` and `[alt](href)`, which drop a target.
## Ids stay site-local ## Ids stay site-local
+7 -6
View File
@@ -328,12 +328,13 @@ its alt text linked to its destination, the link titled by the image's title: be
content, in a heading, a table cell or an `!adf:paragraph`, and in the paragraph a portable task content, in a heading, a table cell or an `!adf:paragraph`, and in the paragraph a portable task
marker opens. A `[!WORD]` marker with no `+` or `-` stands apart from the lines after it, and so marker opens. A `[!WORD]` marker with no `+` or `-` stands apart from the lines after it, and so
does a `+` or `-` callout's title line, so an image alone on the marker line or after the title does a `+` or `-` callout's title line, so an image alone on the marker line or after the title
reads as the image. An empty alt text reads as the destination, as an empty link text does. Inside reads as the image. An empty alt text reads as the destination, as an empty link text does.
a link's text, an `!adf:link`'s included, the alt text is plain text the link marks, and after the
link stands ` (image)`, `image` linked to the image's destination and titled by its title, one for Inside a link's text, the alt text is plain text the link marks. This holds for an `!adf:link`'s
each image in the order they stand. An image whose destination is the link's own takes none, unless text too. After the link comes ` (image)` for each image, in the order the images stand. Each
it holds a non-empty title the link does not (`docs/decisions.md` §An image reads as an image only `image` links to its image's destination and carries the image's title. An image whose destination
alone in its paragraph). is the link's own takes no ` (image)`, unless the image has a non-empty title the link lacks
(`docs/decisions.md` §An image reads as an image only alone in its paragraph).
### Tables ### Tables
+1 -1
View File
@@ -13,7 +13,7 @@ export function externalMedia(alt: string, url: string, link?: LinkDefinition):
return link === undefined ? media : { ...media, marks: [linkMark(link)] } return link === undefined ? media : { ...media, marks: [linkMark(link)] }
} }
// The ` (image)` after a link for an image inside it, as the readers build it (spec/flavour.md, The CommonMark image) and `adfToPortableMarkdown` writes it; `link` builds `image`. // The ` (image)` after a link for an image inside it, as the readers build it (spec/flavour.md, The CommonMark image) and `adfToPortableMarkdown` writes it; `link` builds the linked word `image`.
export function imageAside(link: (text: string) => AdfNode[]): AdfNode[] { export function imageAside(link: (text: string) => AdfNode[]): AdfNode[] {
return [{ text: ' (', type: 'text' }, ...link('image'), { text: ')', type: 'text' }] return [{ text: ' (', type: 'text' }, ...link('image'), { text: ')', type: 'text' }]
} }
+6 -6
View File
@@ -34,7 +34,7 @@ type OwnMarks = { kind: 'ownMarks'; node: AdfNode }
type Inline = AdfNode | OwnMarks | TextBreak type Inline = AdfNode | OwnMarks | TextBreak
// `imageLinks` are the (image)s an `!adf:link` slot's link stands after itself. // `imageLinks` are the (image)s an `!adf:link` slot holds, which the link places after itself.
type Scanned = { image?: AdfNode; imageLinks: ImageLink[]; pieces: Resolved[] } type Scanned = { image?: AdfNode; imageLinks: ImageLink[]; pieces: Resolved[] }
export type LinkDefinitions = ReadonlyMap<string, LinkDefinition> export type LinkDefinitions = ReadonlyMap<string, LinkDefinition>
@@ -44,7 +44,7 @@ type Bracket = { active: boolean; image: boolean; kind: 'open'; start: number }
// `link` is the link whose text is this image alone. // `link` is the link whose text is this image alone.
type Image = { alt: string; definition: LinkDefinition; kind: 'image'; link?: LinkDefinition } type Image = { alt: string; definition: LinkDefinition; kind: 'image'; link?: LinkDefinition }
// The (image) after a link for one image its text holds: running text reads it, alt text drops it. // The (image) after a link for one image its text holds: running text keeps it, alt text drops it.
type ImageLink = { image: LinkDefinition; kind: 'imageLink'; nodes: Inline[] } type ImageLink = { image: LinkDefinition; kind: 'imageLink'; nodes: Inline[] }
type Nodes = { kind: 'nodes'; nodes: Inline[] } type Nodes = { kind: 'nodes'; nodes: Inline[] }
@@ -74,7 +74,7 @@ type Scan = {
// Pieces below this have been walked for openers to deactivate: an image close folds the link-marked piece into alt text, leaving this the only record that the brackets around it are doomed. // Pieces below this have been walked for openers to deactivate: an image close folds the link-marked piece into alt text, leaving this the only record that the brackets around it are doomed.
deactivatedBefore: number deactivatedBefore: number
definitions: LinkDefinitions definitions: LinkDefinitions
// Whether an `!adf:link` slot holds this content through mark slots alone, where an image reads as its plain alt text and the link stands its (image) after itself. // Whether an `!adf:link` slot holds this content through mark slots alone, where an image reads as its plain alt text and the link places its (image) after itself.
inLink: boolean inLink: boolean
openingSpellableLink: boolean openingSpellableLink: boolean
path: ConvertErrorPath path: ConvertErrorPath
@@ -270,7 +270,7 @@ function assemble(scan: Scan): Result<Scanned> {
return success(scan.pieces.length === 1 && only?.kind === 'image' ? { ...resolved, image: mediaSingle(only) } : resolved) return success(scan.pieces.length === 1 && only?.kind === 'image' ? { ...resolved, image: mediaSingle(only) } : resolved)
} }
// A content slot is read before the scan holding it knows whether an image description holds it, so each (image) stays a piece of its own. // A slot is read before its holder knows whether it sits in an image description, which drops every (image), so each (image) stays a piece of its own.
function resolvedPieces(pieces: readonly Piece[], nodes: readonly Inline[][]): Resolved[] { function resolvedPieces(pieces: readonly Piece[], nodes: readonly Inline[][]): Resolved[] {
const resolved: Resolved[] = [] const resolved: Resolved[] = []
let run: Inline[] = [] let run: Inline[] = []
@@ -292,7 +292,7 @@ function contentNodes(pieces: readonly Resolved[]): Inline[] {
return mergeReadText(pieces.flatMap((piece) => piece.nodes)) return mergeReadText(pieces.flatMap((piece) => piece.nodes))
} }
// spec/flavour.md, The CommonMark image: an image no paragraph holds alone reads as its alt text, linked, and inside a link as the link's text, then (image) linked to it. // spec/flavour.md, The CommonMark image: an image no paragraph holds alone reads as its alt text, linked, and inside a link as the link's text, then (image) linked to the image.
function linkedAlt(piece: Piece, inLink: boolean): Piece[] { function linkedAlt(piece: Piece, inLink: boolean): Piece[] {
if (inLink && piece.kind === 'imageLink') return [] if (inLink && piece.kind === 'imageLink') return []
if (piece.kind !== 'image' || (inLink && piece.link === undefined)) return [piece] if (piece.kind !== 'image' || (inLink && piece.link === undefined)) return [piece]
@@ -300,7 +300,7 @@ function linkedAlt(piece: Piece, inLink: boolean): Piece[] {
return piece.link === undefined ? [alt] : [alt, ...besideLink([imageLink(piece.definition)], piece.link)] return piece.link === undefined ? [alt] : [alt, ...besideLink([imageLink(piece.definition)], piece.link)]
} }
// As adfToPortableMarkdown writes it: a link to the image itself needs no (image), unless the image holds a title the link does not. // A link to the image itself needs no (image), as `adfToPortableMarkdown` writes none; one stays to keep an image title the link lacks.
function besideLink(imageLinks: readonly ImageLink[], link: LinkDefinition): ImageLink[] { function besideLink(imageLinks: readonly ImageLink[], link: LinkDefinition): ImageLink[] {
return imageLinks.filter(({ image }) => image.destination !== link.destination || (image.title !== undefined && image.title !== '' && image.title !== link.title)) return imageLinks.filter(({ image }) => image.destination !== link.destination || (image.title !== undefined && image.title !== '' && image.title !== link.title))
} }
+4 -5
View File
@@ -64,12 +64,11 @@ operations (§Cost fixes are measured, never timed), 4× per doubling: about 255
- `markPairings`, called through `markedNodes`, re-marks every piece each emphasis pairing spans: - `markPairings`, called through `markedNodes`, re-marks every piece each emphasis pairing spans:
`'*'.repeat(n) + '[b a](/v)'.repeat(n) + '*'.repeat(n)`. Pair by range instead, outermost `'*'.repeat(n) + '[b a](/v)'.repeat(n) + '*'.repeat(n)`. Pair by range instead, outermost
pairing first, marking each piece once. pairing first, marking each piece once.
- `lastBracket`, called by `closeBracket`, walks back over every piece a bracket that went literal - `lastBracket`, called by `closeBracket`, walks back over the pieces each literal bracket leaves:
left: `'['.repeat(n) + '[b a](/v)' + '](/w)'.repeat(n)`. Drop such a bracket from the search, as `'['.repeat(n) + '[b a](/v)' + '](/w)'.repeat(n)`. Drop a bracket from the search once it goes
CommonMark's bracket stack does. literal, as CommonMark's bracket stack does.
The seats found `closeBracket` and `resolveNodes` hard to read; leave both easier to read. Found by The seats found `closeBracket` and `resolveNodes` hard to read; leave both easier to read.
the stability review of item 122, 2026-10-23.
### 95. Find the code-span closers an escape forms, the mark runs a line cannot spell and the text `[` that opens link text by reading the line back, in place of `escapeClosedRuns`, `unspellableRuns` and `lastLinkClose` predicting them. ### 95. Find the code-span closers an escape forms, the mark runs a line cannot spell and the text `[` that opens link text by reading the line back, in place of `escapeClosedRuns`, `unspellableRuns` and `lastLinkClose` predicting them.