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

Keep the hard breaks a link text holds alone, after its destination, and move item 89 to 0.2.1

This commit is contained in:
2026-10-09 11:42:38 +02:00
parent b4f8c042e6
commit 1f8df75917
8 changed files with 29 additions and 25 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 (`[a<https://example.com/>b](/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 |
+2 -2
View File
@@ -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
+10 -8
View File
@@ -205,18 +205,20 @@ where its reference implementation nests one `<a>` 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
+3 -2
View File
@@ -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.
+2 -2
View File
@@ -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.
+2 -1
View File
@@ -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' }])
}
})
+8 -8
View File
@@ -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<T>` 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<AdfDocument>`.** | 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;