mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Answer product-owner round 1: 0 🔴, 1 🟡; write no (image) after a link to the image itself
This commit is contained in:
+4
-2
@@ -13,8 +13,10 @@
|
||||
around only code spans is `unsupported-node-shape`. `adfToLosslessMarkdown` writes code text
|
||||
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:
|
||||
`(image)` follows the link, linked to the image, so `[look at  tonight](/moon)`
|
||||
reads as `[look at the moon tonight](/moon) ([image](moon.png "Full"))`.
|
||||
`(image)` follows the link, linked to the image, so
|
||||
`[look at  tonight](/moon)` reads as
|
||||
`[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.
|
||||
- `adfToPortableMarkdown` keeps both targets of a linked external image standing inline, as in a
|
||||
table cell: `[alt](href) ([image](url))`, where it wrote `[alt](url)` and dropped the link's target.
|
||||
- `adfToPortableMarkdown` keeps a link title holding a backslash or an entity reference such as
|
||||
|
||||
@@ -556,8 +556,9 @@ Serves [Goals](#goals) 1, 4 and 5.
|
||||
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,
|
||||
linked to the image: `[look at  tonight](/moon)` reads as
|
||||
`[look at the moon tonight](/moon) ([image](moon.png))`. A link alone in its paragraph whose
|
||||
whole text is one image reads as that image, with the link marking it.
|
||||
`[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
|
||||
whose whole text is one image reads as that image, with the link marking it.
|
||||
- An image's title becomes its caption when the image stays an image, and otherwise the title of
|
||||
the link to the image.
|
||||
|
||||
|
||||
+6
-3
@@ -232,8 +232,9 @@ where its reference implementation nests one `<a>` in another.
|
||||
|
||||
2026-10-04, the maintainer and a writer panel, 3 of 3; `[]()` 2026-10-09, by Goals 4 and 7, as the
|
||||
maintainer's plan item asked; a link text of hard breaks alone 2026-10-09, the maintainer; an image
|
||||
inside a link 2026-10-22, a reader panel, 3 of 3 and 5 of 7. Goals 4, 5, 6 and 7. Valid while `mediaInline` carries no URL, ADF holds no empty text node, a hard break
|
||||
takes no marks and no consumer has reported a link of hard breaks alone.
|
||||
inside a link 2026-10-22, a reader panel, 3 of 3 and 5 of 7. Goals 4, 5, 6 and 7. Valid while
|
||||
`mediaInline` carries no URL, ADF holds no empty text node, a hard break takes no marks and no
|
||||
consumer has reported a link of hard breaks alone.
|
||||
|
||||
Every reader reads every CommonMark image and empty link, as `spec/flavour.md` §The CommonMark image
|
||||
and §Marks spell out. An image alone in its paragraph reads as the image, its title as the caption.
|
||||
@@ -248,7 +249,9 @@ text is the image alone, the bot, the LLM pipeline and a support agent picked th
|
||||
link holding words beside the image they split on where `(image)` stands, and four more readers, the
|
||||
bot, the LLM pipeline, a product manager and an engineer, made it 5 of 7 for after the whole link
|
||||
over right after the alt text. Every reader rejected linking the alt text to the image: the words
|
||||
lose the link they were written in.
|
||||
lose the link they were written in. A link to the image's own URL takes no `(image)`, unless the
|
||||
image holds a title the link does not: it would repeat the link, as `adfToPortableMarkdown` never
|
||||
writes it (§A linked image standing inline in portable markdown).
|
||||
|
||||
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
|
||||
|
||||
+3
-2
@@ -331,8 +331,9 @@ does a `+` or `-` callout's title line, so an image alone on the marker line or
|
||||
reads as the image. An empty alt text reads as the destination, as an empty link text does. Inside
|
||||
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
|
||||
each image in the order they stand (`docs/decisions.md` §An image reads as an image only alone in
|
||||
its paragraph).
|
||||
each image in the order they stand. An image whose destination is the link's own takes none, unless
|
||||
it holds a non-empty title the link does not (`docs/decisions.md` §An image reads as an image only
|
||||
alone in its paragraph).
|
||||
|
||||
### Tables
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export function externalMedia(alt: string, url: string, link?: LinkDefinition):
|
||||
return link === undefined ? media : { ...media, marks: [linkMark(link)] }
|
||||
}
|
||||
|
||||
// spec/flavour.md, The CommonMark image: the ` (image)` standing after a link for an image inside it, `image` built by `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`.
|
||||
export function imageAside(link: (text: string) => AdfNode[]): AdfNode[] {
|
||||
return [{ text: ' (', type: 'text' }, ...link('image'), { text: ')', type: 'text' }]
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ type Bracket = { active: boolean; image: boolean; kind: 'open'; start: number }
|
||||
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.
|
||||
type ImageLink = { kind: 'imageLink'; nodes: Inline[] }
|
||||
type ImageLink = { image: LinkDefinition; kind: 'imageLink'; nodes: Inline[] }
|
||||
|
||||
type Nodes = { kind: 'nodes'; nodes: Inline[] }
|
||||
|
||||
@@ -227,7 +227,10 @@ function directiveMarkPieces(scan: Scan, name: string, mark: AdfMark, slot: Slot
|
||||
}
|
||||
const refused = mark.type === 'link' ? refuseLinkDirective(scan, mark, nodes, index) : undefined
|
||||
if (refused !== undefined) return refused
|
||||
return success([...slot.pieces.map((piece) => ({ ...piece, nodes: applyMark(piece.nodes, mark) })), ...slot.imageLinks])
|
||||
const attrs = nodeAttrs(mark)
|
||||
const title = attrs['title']
|
||||
const imageLinks = mark.type === 'link' ? besideLink(slot.imageLinks, linkHref(attrs) ?? '', typeof title === 'string' ? title : undefined) : slot.imageLinks
|
||||
return success([...slot.pieces.map((piece) => ({ ...piece, nodes: applyMark(piece.nodes, mark) })), ...imageLinks])
|
||||
}
|
||||
|
||||
// spec/flavour.md, Marks. A link opening a paragraph may still need the directive form for the line it opens, which `assemble` asks the emitter.
|
||||
@@ -279,7 +282,7 @@ function resolvedPieces(pieces: readonly Piece[], nodes: readonly Inline[][]): R
|
||||
continue
|
||||
}
|
||||
if (run.length > 0) resolved.push({ kind: 'nodes', nodes: run })
|
||||
resolved.push({ kind: 'imageLink', nodes: held })
|
||||
resolved.push({ ...piece, nodes: held })
|
||||
run = []
|
||||
}
|
||||
if (run.length > 0) resolved.push({ kind: 'nodes', nodes: run })
|
||||
@@ -295,7 +298,12 @@ function linkedAlt(piece: Piece, inLink: boolean): Piece[] {
|
||||
if (inLink && piece.kind === 'imageLink') return []
|
||||
if (piece.kind !== 'image' || (inLink && piece.link === undefined)) return [piece]
|
||||
const alt: Piece = { kind: 'nodes', nodes: linkTo(piece.alt === '' ? [] : [{ text: piece.alt, type: 'text' }], piece.link ?? piece.definition) }
|
||||
return piece.link === undefined ? [alt] : [alt, imageLink(piece.definition)]
|
||||
return piece.link === undefined ? [alt] : [alt, ...besideLink([imageLink(piece.definition)], piece.link.destination, piece.link.title)]
|
||||
}
|
||||
|
||||
// As adfToPortableMarkdown writes it: a link to the image itself needs no (image), unless the image holds a title the link does not.
|
||||
function besideLink(imageLinks: readonly ImageLink[], href: string, title: string | undefined): ImageLink[] {
|
||||
return imageLinks.filter(({ image }) => image.destination !== href || (image.title !== undefined && image.title !== '' && image.title !== title))
|
||||
}
|
||||
|
||||
function linkedImages(pieces: readonly Piece[]): ImageLink[] {
|
||||
@@ -306,7 +314,7 @@ function linkedImages(pieces: readonly Piece[]): ImageLink[] {
|
||||
}
|
||||
|
||||
function imageLink(definition: LinkDefinition): ImageLink {
|
||||
return { kind: 'imageLink', nodes: imageAside((text) => [{ marks: [linkMark(definition)], text, type: 'text' }]) }
|
||||
return { image: definition, kind: 'imageLink', nodes: imageAside((text) => [{ marks: [linkMark(definition)], text, type: 'text' }]) }
|
||||
}
|
||||
|
||||
function mediaSingle({ alt, definition, link }: Image): AdfNode {
|
||||
@@ -453,7 +461,7 @@ function linkPieces(scan: Scan, inner: readonly Piece[], definition: LinkDefinit
|
||||
if (!nodes.value.some((node) => marksItem(node, 'link')) && nodes.value.some((node) => isNode(node) && node.type !== 'hardBreak')) {
|
||||
return failure('unsupported-node-shape', unlinkable, scan.path)
|
||||
}
|
||||
return success([{ kind: 'nodes', nodes: linkTo(nodes.value, definition) }, ...linkedImages(inner)])
|
||||
return success([{ kind: 'nodes', nodes: linkTo(nodes.value, definition) }, ...besideLink(linkedImages(inner), definition.destination, definition.title)])
|
||||
}
|
||||
|
||||
// CommonMark: no link nests inside another, though an image's description holds one.
|
||||
|
||||
@@ -1183,6 +1183,15 @@ test('reads an image inside a link not alone in its paragraph as plain alt text
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:link[x [](/v)]{href="a\\nb"}\n')), 'unsupported-node-shape: no link wraps a link: the [content] this one marks already holds one')
|
||||
})
|
||||
|
||||
test('writes no (image) after a link to the image itself, unless the image holds a title the link does not', () => {
|
||||
assert.deepEqual(firstContent('See [](/u).\n'), [text('See '), marked('a', link('/u')), text('.')])
|
||||
assert.deepEqual(firstContent('See [](/u "t").\n'), [text('See '), marked('a', link('/u', 't')), text('.')])
|
||||
assert.deepEqual(firstContent('See [](/u "t").\n'), [text('See '), marked('a', link('/u', 't')), text('.')])
|
||||
assert.deepEqual(firstContent('See [](/u).\n'), [text('See '), marked('a', link('/u')), text(' ('), marked('image', link('/u', 't')), text(').')])
|
||||
assert.deepEqual(firstContent('[b  ](/u)\n'), [marked('b a c', link('/u')), text(' ('), marked('image', link('/w')), text(')')])
|
||||
assert.deepEqual(firstContent('!adf:link[b ]{href="a\\nb"}\n'), [marked('b a', { attrs: { href: 'a\nb' }, type: 'link' })])
|
||||
})
|
||||
|
||||
test('carries the mark a spelling nested inside its own kind names once', () => {
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('*(*a*)*\n')), [{ content: [marked('(a)', em)], type: 'paragraph' }])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf(`${'*'.repeat(600)}a${'*'.repeat(600)}\n`)), [{ content: [marked('a', strong)], type: 'paragraph' }])
|
||||
|
||||
Reference in New Issue
Block a user