diff --git a/AGENTS.md b/AGENTS.md index 1a2a24e..4d36961 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ In `docs/decisions.md`: - CommonMark is a subset - Tables - Links -- What ADF holds no node for reads as a link +- Every CommonMark image and empty link reads: the image where it stands alone, else a link - Ids stay site-local - Portable task ids come from position - A callout title keeps its link targets diff --git a/README.md b/README.md index 3a36c28..30a4d1b 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ products store as ADF. started from — panels, mentions, statuses and node types this version does not know included. - **Markdown in:** `portableMarkdownToAdf` reads a bot's or a model's markdown, including `~~` and pipe tables whose every row opens with a pipe. `commonMarkToAdf` reads strict CommonMark as its - spec says: nothing becomes a table, task or panel. Both refuse raw HTML, and - `portableMarkdownToAdf` a claimed shape that does not complete - ([The guarantees](#the-guarantees)). + spec says: nothing becomes a table, task or panel. Both refuse raw HTML and nesting past 500 + levels, and `portableMarkdownToAdf` a claimed shape that does not complete + ([The errors](#the-errors)). - **Portable markdown, lossy:** `adfToPortableMarkdown` and `portableMarkdownToAdf` convert to and from the markdown that GitHub, GitLab and Obsidian render — GitHub Flavored Markdown's alerts, task lists and pipe tables, Obsidian Flavored Markdown's callouts and highlights. @@ -230,7 +230,8 @@ Serves Goals 1, 4 and 5. - Markdown this library reads, and markdown it writes, means what the CommonMark spec says; from `0.3.0`, well-formed HTML means what the HTML standard says, read or written. The bullets below name every exception. -- CommonMark is valid input to `commonMarkToAdf`, apart from the raw HTML `unmappable-html` names. +- CommonMark is valid input to `commonMarkToAdf`, apart from the raw HTML `unmappable-html` names + and nesting past 500 levels. - The other readers claim shapes CommonMark reads as text or code. `losslessMarkdownToAdf` claims four: literal text matching directive, pipe-table or strikethrough syntax, and a code fence whose info string opens `adf:`. `portableMarkdownToAdf` claims what [Portable @@ -245,8 +246,8 @@ Serves Goals 1, 4 and 5. - An image anywhere but alone in a plain paragraph reads as its alt text linked to its URL, `See ![the moon](moon.png).` as `See [the moon](moon.png).`, and inside a link as plain text the link marks. - - An image's title reads as its caption where the image stands alone in a plain paragraph, and - as its link's title where it reads as linked alt text. + - An image's title reads as its caption where the image stands alone in a plain paragraph, as + its link's title where it reads as alt text linked to its URL, and inside a link it drops. And three more: a list continuing past a marker change stays one list against CommonMark's two, a shortcut reference matching its definition only under Unicode case folding stays unresolved, diff --git a/docs/decisions.md b/docs/decisions.md index 813438e..3e20cdd 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -202,10 +202,11 @@ link reference definition — and a directive link CommonMark could spell is ref link — the bracket form goes literal, the directive form refused — which is CommonMark's prose where its reference implementation nests one `` in another. -## What ADF holds no node for reads as a link +## Every CommonMark image and empty link reads: the image where it stands alone, else a link -2026-10-04, the maintainer and a writer panel, 3 of 3; `[]()` 2026-10-09, by Goals 4 and 7. Goals 4, -5 and 6. Valid while `mediaInline` carries no URL and ADF holds no empty text node. +2026-10-04, the maintainer and a writer panel, 3 of 3; `[]()` 2026-10-09, settled by Goals 4 and 7 +as the maintainer's plan item directed. Goals 4, 5, 6 and 7. Valid while `mediaInline` carries no +URL and ADF holds no empty text node. Every reader reads every CommonMark image and empty link. An image anywhere but alone in a plain paragraph reads as its alt text linked to its URL, titled by its title; an empty alt text reads as @@ -213,7 +214,7 @@ the URL, and an empty link text as its destination. `[]()` reads as nothing: it Goal 7 loses no content. A titled image alone in a plain paragraph reads as the image captioned with its title. A link alone in a plain paragraph whose whole text is an image reads as the image with the link marking its `media`. Inside a link otherwise, the alt text is plain text the link marks and -the image's URL drops: the panel chose this by Goal 6, which outranks Goal 7. +the image's URL and title drop: the panel chose this by Goal 6, which outranks Goal 7. ## Ids stay site-local diff --git a/spec/flavour.md b/spec/flavour.md index 9569fbf..b757532 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -324,8 +324,8 @@ Anywhere else — beside other content, in a heading, a table cell or an `!adf:p reads as its alt text linked to its destination, the link titled by the image's title, since `mediaInline` carries a media `collection` + `id`, never a URL. An empty alt text reads as the destination, as an empty link text does. Inside a link's text, the alt text is plain text the link -marks, and the image's destination and title drop (`docs/decisions.md` §What ADF holds no node for -reads as a link). +marks, and the image's destination and title drop (`docs/decisions.md` §Every CommonMark image and +empty link reads). ### Tables diff --git a/src/markdown/emit/image.ts b/src/markdown/emit/image.ts index 6058bac..c0b577c 100644 --- a/src/markdown/emit/image.ts +++ b/src/markdown/emit/image.ts @@ -4,11 +4,10 @@ import { nodeAttrs, nodeContent } from '../../adf/document.ts' import { serializeCanonicalJson } from '../../canonical-json.ts' import { tryImageLine } from './inline-line.ts' -// An empty `alt` takes the directive form: `![](url)` reads back with no `alt`. export function tryImage(node: AdfNode): string | undefined { const [media] = nodeContent(node) const { alt, url } = media === undefined ? {} : nodeAttrs(media) - if (typeof url !== 'string' || (alt !== undefined && typeof alt !== 'string') || alt === '') return undefined + if (typeof url !== 'string' || (alt !== undefined && typeof alt !== 'string')) return undefined const read = centeredImage([externalMedia(alt ?? '', url)]) return serializeCanonicalJson(node, 'compact') === serializeCanonicalJson(read, 'compact') ? tryImageLine(alt, url) : undefined } diff --git a/src/markdown/external-image.ts b/src/markdown/external-image.ts index 921c324..348e0d2 100644 --- a/src/markdown/external-image.ts +++ b/src/markdown/external-image.ts @@ -1,10 +1,11 @@ import type { AdfNode } from '../adf/document.ts' -// spec/flavour.md, The CommonMark image: the shape `![alt](url)` reads to, and the only one written back as it. +// spec/flavour.md, The CommonMark image: `![alt](url)` reads to `centeredImage([externalMedia(alt, url)])`, the only shape written back as it. export function centeredImage(content: AdfNode[]): AdfNode { return { attrs: { layout: 'center' }, content, type: 'mediaSingle' } } +// `![](url)` builds no `alt`, so a media holding an empty one takes the directive form. export function externalMedia(alt: string, url: string): AdfNode { return { attrs: alt === '' ? { type: 'external', url } : { alt, type: 'external', url }, type: 'media' } }