From 06c225f8f42b21ff2f435da2c13a68f2e07ba0d3 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 9 Oct 2026 11:42:38 +0200 Subject: [PATCH] Keep the hard breaks a link text holds alone, after its destination, and move item 89 to 0.2.1 --- CHANGELOG.md | 2 +- MIGRATION.md | 2 +- README.md | 4 ++-- docs/decisions.md | 18 ++++++++++-------- spec/flavour.md | 5 +++-- src/markdown/parse/inline-content.ts | 4 ++-- src/markdown/parse/markdown-to-adf.test.ts | 3 ++- todo.md | 16 ++++++++-------- 8 files changed, 29 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f5fde6..bf0f9df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,7 +72,7 @@ ([V8 bug](https://issues.chromium.org/issues/521080746)). - **Breaking:** a hard break inside emphasis, strike, a link or a directive mark reads holding no marks, as Atlassian's schema requires, where every reader gave it the marks around it. A link - whose text is only hard breaks reads as an empty link text does, as its destination linked, and + whose text is only hard breaks reads as its destination linked, followed by those breaks, and `losslessMarkdownToAdf` refuses a directive mark wrapping only hard breaks as `unsupported-node-shape`. `adfToLosslessMarkdown` writes a hard break holding marks as `!adf:carry{json="…"}`, and both writers spell an emphasis or link across the hard breaks inside diff --git a/MIGRATION.md b/MIGRATION.md index f7eb9db..e5f32ac 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -72,7 +72,7 @@ Markdown the spelling table leaves alone, which `0.2.0` reads as a different doc | --- | --- | --- | | a link whose text already holds one (`[ab](/v)`) | marks every node the inner link does not, splitting the outer link around it | leaves the outer brackets literal text; write the pieces as separate links to keep them | | markdown holding no block (`""`) | `{ type: 'doc', version: 1 }` | `{ content: [], type: 'doc', version: 1 }`; `!adf:doc {content=none}` reads as the former | -| a hard break inside a mark's spelling (`*a\` then `b*` on the next line) | the hard break holds the mark | the hard break holds no marks, as Atlassian's schema requires, and the recipe above drops them from stored markdown. A link whose text is only hard breaks reads as its destination linked | +| a hard break inside a mark's spelling (`*a\` then `b*` on the next line) | the hard break holds the mark | the hard break holds no marks, as Atlassian's schema requires, and the recipe above drops them from stored markdown. A link whose text is only hard breaks reads as its destination linked, followed by those breaks | | a link whose text is empty (`[](/url)`, `[]()`) | literal text | `/url` linked to `/url`, and nothing where the destination is empty too | | a code fence whose info string opens `adf:` (```` ```adf:x ````) | a `codeBlock` with that language | in `losslessMarkdownToAdf`, the block carry, refusing a body that is not one node's canonical JSON; write `!adf:codeBlock {language="adf:x"}` around a bare fence to keep the code block | diff --git a/README.md b/README.md index 871225c..fdcdaaf 100644 --- a/README.md +++ b/README.md @@ -240,8 +240,8 @@ Serves Goals 1, 4 and 5. canonical spelling, which round-trips byte-identically. - Seven CommonMark spellings parse without an error and build a document the reference implementation renders differently. Four because ADF holds no node for what it renders: - - An empty link text, or one holding only hard breaks, reads as its destination, `[](/url)` as - `/url` linked to `/url`, and `[]()` as nothing. + - An empty link text reads as its destination, `[](/url)` as `/url` linked to `/url`, and `[]()` + as nothing. A link text holding only hard breaks reads the same, followed by its hard breaks. - An image reads as an image only alone in its paragraph. Anywhere else — beside other text, in a heading or a table cell, or in the paragraph a task marker opens — it reads as its alt text linked to its URL: `See ![the moon](moon.png).` reads as diff --git a/docs/decisions.md b/docs/decisions.md index 39ff295..a65f9c3 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -205,18 +205,20 @@ where its reference implementation nests one `` in another. ## An image reads as an image only alone in its paragraph, else as its alt text linked; a link text holding nothing a mark rides reads as its destination 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, by Goal 7. Goals 4, 5, 6 -and 7. Valid while `mediaInline` carries no URL, ADF holds no empty text node and a hard break takes -no marks. +maintainer's plan item asked; a link text of hard breaks alone 2026-10-09, the maintainer, after a +reader panel split 4 to 3. Goals 4, 5, 6 and 7. Valid while `mediaInline` carries no URL, ADF holds +no empty text node and a hard break takes no marks. 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. Anywhere else it reads as its alt text linked to its URL, titled by its title. An empty alt text -reads as the URL, and an empty link text as its destination; so does a link text holding only hard -breaks, which take no marks. `[]()` 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`. Inside any other link, the alt text is plain text the link marks, and the -image's URL and title drop: the panel chose this by Goal 6, which outranks Goal 7. +reads as the URL, and an empty link text as its destination; a link text holding only hard breaks, +which take no marks, reads as its destination followed by those breaks, which four of seven readers +expected over dropping them, and the maintainer kept as an edge case until real reports arrive. +`[]()` 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`. +Inside any other link, the alt text is plain text the link marks, and the image's URL and title +drop: the panel chose this by Goal 6, which outranks Goal 7. 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 diff --git a/spec/flavour.md b/spec/flavour.md index 9a1f28b..007118c 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -522,8 +522,9 @@ reference definition. Every such spelling carries an `href`: a directive link Co is a named error, and so is one spelling none. No link wraps a link at any nesting, which is CommonMark's own rule: a `[text]` already holding one leaves the outer brackets literal text, and the directive form, open to no literal reading, is a named error. ADF holds no empty text node, so a -link whose text is empty or holds only hard breaks reads as its destination linked, `[](/url)` as -`/url`, and one whose destination is empty too, `[]()`, as nothing. +link whose text is empty reads as its destination linked, `[](/url)` as `/url`, and one whose +destination is empty too, `[]()`, as nothing. A link text holding only hard breaks reads the same, +followed by its hard breaks. - `border` — Attributes: `color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 1–3). - `code`, `em`, `strike`, `strong` — Attributes: none. diff --git a/src/markdown/parse/inline-content.ts b/src/markdown/parse/inline-content.ts index 41ae8d8..5fb5f91 100644 --- a/src/markdown/parse/inline-content.ts +++ b/src/markdown/parse/inline-content.ts @@ -325,10 +325,10 @@ function linkMark({ destination, title }: LinkDefinition): AdfMark { return { attrs: title === undefined ? { href: destination } : { href: destination, title }, type: 'link' } } -// ADF holds no empty text node for a link mark to ride, so a link text holding nothing a mark rides reads as its destination, and an empty destination as nothing. +// ADF holds no empty text node for a link mark to ride, so a link text holding nothing a mark rides reads as its destination, then the hard breaks it held, and an empty destination as those breaks alone. function linkTo(nodes: readonly Inline[], definition: LinkDefinition): Inline[] { if (!nodes.every(takesNoMarks)) return applyMark(nodes, linkMark(definition)) - return definition.destination === '' ? [] : [{ marks: [linkMark(definition)], text: definition.destination, type: 'text' }] + return definition.destination === '' ? [...nodes] : [{ marks: [linkMark(definition)], text: definition.destination, type: 'text' }, ...nodes] } // spec/flavour.md, Inline nodes: the leaf builds no node, so only the pair it parts spells it. diff --git a/src/markdown/parse/markdown-to-adf.test.ts b/src/markdown/parse/markdown-to-adf.test.ts index 206b8ab..74ca9ac 100644 --- a/src/markdown/parse/markdown-to-adf.test.ts +++ b/src/markdown/parse/markdown-to-adf.test.ts @@ -840,7 +840,8 @@ test('builds the hard break a mark spelling spans with no marks, in every reader const localId = '01a0431d-201e-7400-a175-9c04d3628f95' assert.deepEqual(content(losslessMarkdownToAdf(`_!adf:hardBreak{localId=${localId}}_\n`)), [{ content: [{ attrs: { localId }, type: 'hardBreak' }], type: 'paragraph' }]) for (const read of [commonMarkToAdf, losslessMarkdownToAdf, portableMarkdownToAdf]) { - assert.deepEqual(content(read('[\\\n](/u)\n')), [{ content: [marked('/u', link('/u'))], type: 'paragraph' }]) + assert.deepEqual(content(read('[\\\n](/u)\n')), [{ content: [marked('/u', link('/u')), hardBreak()], type: 'paragraph' }]) + assert.deepEqual(content(read('a[\\\n]()b\n')), [{ content: [text('a'), hardBreak(), text('b')], type: 'paragraph' }]) } }) diff --git a/todo.md b/todo.md index ddffe07..521104e 100644 --- a/todo.md +++ b/todo.md @@ -30,11 +30,11 @@ | 49 | 0.2.0 | | **Read a list whose bullet or ordered delimiter changes as two lists, in every reader, and retire `!adf:listBreak`.** | 4 | 5 | 5 | 8 | 4, 5 | 15.5 | | 61 | 0.2.0 | decision | **Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it.** | 6 | 8 | 3 | 8 | 1 | 14.0 | | 86 | 0.2.0 | | **Write an image whose `media` only a link marks, and one captioned with plain text, in the CommonMark spelling a writer panel picks, in `adfToLosslessMarkdown`.** | 4 | 4 | 5 | 6 | 4, 6 | 13.0 | -| 89 | 0.2.0 | | **Build a `date`, `emoji`, `inlineCard`, `mention` or `status` holding no mark Atlassian's schema withholds from it, in every reader.** | 4 | 4 | 5 | 6 | 4 | 13.0 | | 85 | 0.2.0 | defect | **Write an external image a link marks as `[![alt](url)](href)` in `adfToPortableMarkdown`, keeping the link target.** | 3 | 3 | 6 | 7 | 7 | 12.9 | | 60 | 0.2.0 | decision | **Collect the questions `parse/` asks `emit/` into one named module.** | 3 | 4 | 2 | 6 | 2 | 10.7 | | 59 | 0.2.0 | decision | **Group the directive grammar into `src/markdown/directive/`, move `Read` to `result.ts`, and move `Flavour` and `flavourClaims` to `markdown/flavour.ts`.** | 3 | 5 | 2 | 6 | 2 | 10.4 | | 62 | 0.2.0 | decision | **Move the portable flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/portable/`, so `emit/` no longer imports `portable/`.** | 4 | 5 | 2 | 6 | 2 | 9.4 | +| 89 | 0.2.1 | | **Build a `date`, `emoji`, `inlineCard`, `mention` or `status` holding no mark Atlassian's schema withholds from it, in every reader.** | 4 | 4 | 5 | 6 | 4 | 13.0 | | 7 | 0.3.0 | decision | **Ship HTML: `adfToHtml`, `htmlToAdf`, and markdown to and from HTML composed through ADF, each call named by its flavour.** | 6 | 9 | 9 | 9 | 2, 4 | 25.8 | | 45 | 0.3.0 | | **Replace `isAdfDocument` with a reader returning `Result`.** | 2 | 3 | 6 | 8 | 1 | 25.2 | | 6 | 0.3.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 4 | 24.7 | @@ -105,13 +105,6 @@ Every reader reads `[![alt](url)](href)` and `![alt](url "caption")` as these sh (`docs/decisions.md` §The formats are API): changing it inside `0.2.0` spares consumers a second breaking change to stored markdown. -### 89. Build a `date`, `emoji`, `inlineCard`, `mention` or `status` holding no mark Atlassian's schema withholds from it, in every reader. - -`*!adf:mention[@a]{id=…}*` and `[!adf:status[Done]{…}](/u)` build a node holding `em` or `link`. -`full.json` gives these five no `marks`, and `stage-0.json` gives them `annotation` alone, so a site -may reject the document. `InlineNodeModel.marks` (`src/adf/inline-nodes.ts`) then names the mark -types a node takes, and the emitter carries a node holding another. - ### 85. Write an external image a link marks as `[![alt](url)](href)` in `adfToPortableMarkdown`, keeping the link target. Today `[![a](/u)](/v)` reads as the image whose `media` the link marks, and `adfToPortableMarkdown` @@ -140,6 +133,13 @@ Writing: `spellPortableBlock`, `quotedUnder`, `tryTaskList` and `taskBlocks` in `flavourClaims`. Every comprehension reader on 2026-10-03 named the portable flavour's spread across three directories. +### 89. Build a `date`, `emoji`, `inlineCard`, `mention` or `status` holding no mark Atlassian's schema withholds from it, in every reader. + +`*!adf:mention[@a]{id=…}*` and `[!adf:status[Done]{…}](/u)` build a node holding `em` or `link`. +`full.json` gives these five no `marks`, and `stage-0.json` gives them `annotation` alone, so a site +may reject the document. `InlineNodeModel.marks` (`src/adf/inline-nodes.ts`) then names the mark +types a node takes, and the emitter carries a node holding another. + ### 7. Ship HTML: `adfToHtml`, `htmlToAdf`, and markdown to and from HTML composed through ADF, each call named by its flavour. Lands after items 6, 59, 60, 61 and 62. Which flavours get a composed call is this item's to settle;