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

Answer prose round 1: 0 🔴, 8 🟡; say which marks a node refuses, and name the fix in the directive mark refusal

This commit is contained in:
2026-10-14 14:41:57 +02:00
committed by Lillem4n
parent 1ece44c882
commit 6c389b9c83
10 changed files with 50 additions and 49 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ In `docs/decisions.md`:
- Tables
- Links
- An image reads as an image only alone in its paragraph, else as its alt text linked; a link text of hard breaks alone reads as its destination, then the breaks
- A mark skips a node that cannot hold it, and a link wrapping only such nodes is refused
- A mark skips a node that cannot hold it, and a mark wrapping only such nodes is refused, but for a link around hard breaks alone
- A linked image is written `[![alt](url)](href)`, a captioned image as its directive
- Ids stay site-local
- Portable task ids come from position
+6 -7
View File
@@ -2,15 +2,14 @@
## Unreleased
- **Breaking:** `losslessMarkdownToAdf` gives a `date`, `emoji`, `inlineCard`, `mention`, `status`
or `mediaInline` only the marks Atlassian's schema lets it hold: `**!adf:mention[@A]{id=…}**` now
reads as a plain mention. A link or directive mark around only nodes that cannot take it is
`unsupported-node-shape`, even in markdown `adfToLosslessMarkdown` wrote. A link around hard
breaks alone still reads as before. `adfToLosslessMarkdown` writes a node holding a mark its type
cannot take as `!adf:carry{json="…"}`. `MIGRATION.md` says how to convert stored markdown.
- `losslessMarkdownToAdf` names the CommonMark spelling to write when it refuses a directive whose
node CommonMark spells, such as `[![alt](url)](href)` for a linked `!adf:mediaSingle`.
- **Breaking:** a mark spelling skips a `date`, `emoji`, `inlineCard`, `mention`, `status` or
`mediaInline` Atlassian's schema withholds the mark from, where `losslessMarkdownToAdf` gave it
the marks around it: `**!adf:mention[@A]{id=…}**` reads as a plain mention. A link around only
such nodes, or a directive mark around nodes none of which takes it, is `unsupported-node-shape`,
markdown `adfToLosslessMarkdown` wrote before included.
`adfToLosslessMarkdown` writes a node holding such a mark as `!adf:carry{json="…"}`. See
`MIGRATION.md`.
## 0.2.0
+9 -5
View File
@@ -7,12 +7,16 @@ A mark spelling skips a node Atlassian's schema withholds the mark from:
| Input | `0.2.0` | `0.3.0` |
| --- | --- | --- |
| a `date`, `emoji`, `inlineCard`, `mention`, `status` or `mediaInline` inside a mark's spelling (`**!adf:mention[@A]{id=x}**`) | the node holds the mark | the node holds only marks its type takes; text beside it keeps the mark |
| a link or directive mark around such nodes alone (`[!adf:status[Done]{color=green}](/u)`) | the nodes hold the mark | `unsupported-node-shape`; write the mark around text beside them |
| a link or directive mark around only nodes that cannot take it, hard breaks under a link aside (`[!adf:status[Done]{color=green}](/u)`) | the nodes hold the mark | `unsupported-node-shape`; write the mark around text beside them |
`0.2.0` wrote these spellings for a node holding such a mark, which `0.3.0` writes as
`!adf:carry{json="…"}`. Markdown stored from such a document reads back without the mark, or, where
the mark wrapped such nodes alone, is refused: to keep it, read the markdown with `0.2.0` and write
it with `0.3.0`, installed side by side.
`0.2.0` wrote these spellings for a node holding such a mark; `0.3.0` writes the node as
`!adf:carry{json="…"}`. `0.3.0` reads markdown stored from such a document without the mark, or
refuses it where the mark wrapped such nodes alone. To keep the mark, read the markdown with `0.2.0`
and write it with `0.3.0`, installed side by side:
```sh
npm install @larvit/adf-codec@0.3.0 adf-codec-0.2@npm:@larvit/adf-codec@0.2.0
```
## From `0.1.0` to `0.2.0`
+2 -2
View File
@@ -463,8 +463,8 @@ Every call can raise these:
replace a line ending with a space, or, for either, write the node as `!adf:carry`;
- wraps a directive mark around nothing, or only around nodes that take no such mark, such as a
hard break or a mention;
- writes a link around nodes none of which takes a link, more than hard breaks alone, such as
`[!adf:status[Done]{color=green}](/u)`: write the link around text beside them;
- writes a link whose text holds something besides hard breaks, but no node that takes a link,
such as `[!adf:status[Done]{color=green}](/u)`: write the link around text beside them;
- writes as a directive a node or mark the lossless flavour spells as CommonMark;
- puts `!adf:textBreak{}` where it parts nothing, or `!adf:doc` anywhere but as the whole
document.
+9 -10
View File
@@ -256,18 +256,18 @@ The same panel confirmed the other readings on 2026-10-09: a title as the captio
text as linked alt text, and an empty link text as its URL, each 3 of 3; `[]()` as nothing, 5 of 7.
Their CommonMark suite exceptions are `node-model` (§The CommonMark suite checks three ways).
## A mark skips a node that cannot hold it, and a link wrapping only such nodes is refused
## A mark skips a node that cannot hold it, and a mark wrapping only such nodes is refused, but for a link around hard breaks alone
2026-10-14, two reader panels of the viewer/editor app, the LLM pipeline and a support agent, 3 of
3 each. Goals 1, 4 and 6. Valid while Atlassian's schema withholds marks from inline nodes.
`src/adf/inline-nodes.ts` holds the mark types Atlassian's schema gives each inline node. A mark
spelling skips a node that takes none of its type, so `**Ping !adf:mention[@A]{id=x} now**` reads
as bold text around a plain mention. A link whose text holds no node that takes a link, and more
than hard breaks, is `unsupported-node-shape` in `losslessMarkdownToAdf`: the panel refused to have
`[!adf:status[Done]{color=green}](/u)` read as `/u` linked beside the status. Hard breaks alone keep
§An image reads as an image only alone in its paragraph's reading. `adfToLosslessMarkdown` writes a
node holding a mark its type does not take as `!adf:carry`, so the round-trip holds.
A mark spelling skips a node that takes none of its type, so `**Ping !adf:mention[@A]{id=x} now**`
reads as bold text around a plain mention. A link whose text holds something besides hard breaks,
but no node that takes a link, is `unsupported-node-shape` in `losslessMarkdownToAdf`: the panel
refused to have `[!adf:status[Done]{color=green}](/u)` read as `/u` linked beside the status. A link
text of hard breaks alone still reads as its destination, then the breaks (§An image reads as an
image only alone in its paragraph). `adfToLosslessMarkdown` writes a node holding a mark its type
does not take as `!adf:carry`, so the round-trip holds.
## A linked image is written `[![alt](url)](href)`, a captioned image as its directive
@@ -702,8 +702,7 @@ holds.
For every node and mark the tables spell, the attribute names and kinds equal what `full.json` and
`stage-0.json` (§Standards ship as data) hold between them, and so do the mark types each inline
node takes. Value sets stay documentation, since
any value round-trips. What the schema holds and the tables do not spell is pinned by name — an
node takes. Value sets stay documentation, since any value round-trips. What the schema holds and the tables do not spell is pinned by name — an
attribute as a gap, a type as carried — so a re-pin adding either goes red until someone spells it
or pins it.
+7 -8
View File
@@ -531,8 +531,8 @@ nesting, which is CommonMark's own rule: a `[text]` already holding one leaves t
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 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; one holding other nodes, none of which takes a
link, is a named error.
breaks reads the same, followed by its hard breaks. A link text holding something else, but no node
that takes a link, is a named error.
- `border` — Attributes: `color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 1–3).
- `code`, `em`, `strike`, `strong` — Attributes: none.
@@ -550,18 +550,17 @@ A block's `marks` key also holds these, which no inline spelling wraps:
- `indentation` — Attributes: `level` (number, 1–6).
A spelling adds its mark to every text node it wraps, and to every other inline node Atlassian's
schema gives that mark: `hardBreak`, `date`, `emoji`, `inlineCard`, `mention` and `status` take none
a spelling makes, and `mediaInline` only `border` and `link`. Nesting is the marks array in order, outermost first:
`_!adf:underline[x]_` gives marks `[em, underline]`, `!adf:underline[_x_]` the reverse. `adfToLosslessMarkdown` nests in
the order the array holds rather than sorting it — `docs/decisions.md` §Equality is deep restores
schema gives that mark. Of the marks a spelling makes, `date`, `emoji`, `hardBreak`, `inlineCard`,
`mention` and `status` take none, and `mediaInline` takes only `border` and `link`. Nesting is the
marks array in order, outermost first: `_!adf:underline[x]_` gives marks `[em, underline]`,
`!adf:underline[_x_]` the reverse. `adfToLosslessMarkdown` nests in the order the array holds rather than sorting it — `docs/decisions.md` §Equality is deep restores
the array, not a set — and opens each spelling once over the longest run of adjacent inline nodes
carrying an identical mark, attributes included, at that depth: `attrs: {}` differs from no `attrs`,
and a directive spells it `{attrs=empty}`. A run breaks at every node the emitter carries, so no
emitted carry sits inside a mark spelling.
An inline node whose marks no nesting spells — a node holding a mark Atlassian's schema withholds
from it, a mark type the first
list above does not hold, an attrs key its spelling does not list, a value that is not the
from it, a mark type the first list above does not hold, an attrs key its spelling does not list, a value that is not the
spelling's type, an attribute the spelling needs and the mark lacks, an empty `attrs` on a mark CommonMark spells, an order
putting a code span outside another mark, `code` over anything but a text node holding text, or over
text holding a line ending or a null character, or a spelling CommonMark's flanking rules cannot
+1 -1
View File
@@ -33,7 +33,7 @@ export function inlineNodeModel(type: string): InlineNodeModel | undefined {
return Object.hasOwn(inlineNodes, type) ? inlineNodes[type] : undefined
}
// An unknown type is no inline leaf the table holds, so the table withholds nothing from it.
// The table withholds nothing from a type it does not list, such as text.
export function takesMark(type: string, markType: string): boolean {
return inlineNodeModel(type)?.markTypes.includes(markType) ?? true
}
+1 -1
View File
@@ -212,7 +212,7 @@ function directiveMarkPiece(scan: Scan, name: string, mark: AdfMark, slot: SlotC
}
if (slot.nodes.some(holdsOwnMarks)) return failure('unsupported-node-shape', ownMarksInMark, scan.path)
if (!slot.nodes.some((node) => marksItem(node, mark.type))) {
return failure('unsupported-node-shape', `the ${name} mark wraps the [content] it marks: this one wraps only nodes that take no ${name} mark`, scan.path)
return failure('unsupported-node-shape', `the ${name} mark wraps the [content] it marks: this one wraps only nodes that take no ${name} mark: write the ${name} mark around text beside them`, scan.path)
}
const refused = mark.type === 'link' ? refuseLinkDirective(scan, mark, slot.nodes, index) : undefined
if (refused !== undefined) return refused
+4 -4
View File
@@ -926,9 +926,9 @@ test('gives an inline node no mark Atlassian\'s schema withholds from it', () =>
assert.deepEqual(read('_!adf:mediaInline{id=m1}_\n'), [{ content: [media], type: 'paragraph' }])
assert.deepEqual(read('!adf:border[!adf:mediaInline{id=m1}]{color="#091e42" size=2}\n'), [{ content: [{ ...media, marks: [border] }], type: 'paragraph' }])
assert.deepEqual(read('[!adf:mediaInline{id=m1}](/u)\n'), [{ content: [{ ...media, marks: [link('/u')] }], type: 'paragraph' }])
assert.equal(read('!adf:underline[!adf:mention[@A]{id=b1c2}]\n'), 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps only nodes that take no underline mark')
assert.equal(read('!adf:underline[!adf:mediaInline{id=m1}]\n'), 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps only nodes that take no underline mark')
assert.equal(read('!adf:link[!adf:status[Done]{color=green}]{collection=c href="/u"}\n'), 'unsupported-node-shape: the link mark wraps the [content] it marks: this one wraps only nodes that take no link mark')
assert.equal(read('!adf:underline[!adf:mention[@A]{id=b1c2}]\n'), 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps only nodes that take no underline mark: write the underline mark around text beside them')
assert.equal(read('!adf:underline[!adf:mediaInline{id=m1}]\n'), 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps only nodes that take no underline mark: write the underline mark around text beside them')
assert.equal(read('!adf:link[!adf:status[Done]{color=green}]{collection=c href="/u"}\n'), 'unsupported-node-shape: the link mark wraps the [content] it marks: this one wraps only nodes that take no link mark: write the link mark around text beside them')
})
test('decodes the fenced info string the block walk leaves raw', () => {
@@ -1268,7 +1268,7 @@ test('names the directive mark left without the content it wraps', () => {
const named = 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps none'
assert.equal(content(losslessMarkdownToAdf('!adf:underline[]\n')), named)
assert.equal(content(losslessMarkdownToAdf('!adf:underline{}\n')), named)
const breaks = (name: string): string => `unsupported-node-shape: the ${name} mark wraps the [content] it marks: this one wraps only nodes that take no ${name} mark`
const breaks = (name: string): string => `unsupported-node-shape: the ${name} mark wraps the [content] it marks: this one wraps only nodes that take no ${name} mark: write the ${name} mark around text beside them`
assert.equal(content(losslessMarkdownToAdf('!adf:underline[!adf:hardBreak{}]\n')), breaks('underline'))
assert.equal(content(losslessMarkdownToAdf('!adf:link[!adf:hardBreak{}]{href="/u" title=t}\n')), breaks('link'))
})
+10 -10
View File
@@ -25,7 +25,7 @@
| ID | Release | Exempt | Item | R | S | C | A | G | Goals | Score |
|---|---|---|---|---|---|---|---|---|---|---|
| 114 | 0.3.0 | defect | **Build text marked `code` holding no mark but `link` and `annotation`, in every reader, and carry any other in `adfToLosslessMarkdown`.** | 3 | 4 | 5 | 5 | 7 | 4 | 15.4 |
| 114 | 0.3.0 | defect | **Build code-marked text holding no mark besides `link` and `annotation`, in every reader, and have `adfToLosslessMarkdown` carry code text holding any other mark.** | 3 | 4 | 5 | 5 | 7 | 4 | 15.4 |
| 99 | 0.3.0 | defect | **Write a linked external image standing inline or in a table cell in the spelling a reader panel picks, in `adfToPortableMarkdown`.** | 3 | 3 | 5 | 6 | 7 | 7 | 12.9 |
| 100 | 0.3.0 | decision | **Put the reading of an image's title as its caption back to a reader panel, now that a writer panel reads a title as hover text.** | 3 | 3 | 3 | 4 | 5 | 4, 6 | 10.9 |
| 60 | 0.3.0 | decision | **Collect the questions `parse/` asks `emit/` into one named module.** | 3 | 4 | 4 | 2 | 6 | 2 | 10.7 |
@@ -52,7 +52,7 @@
| 107 | 0.4.0 | decision | **Spell the block carry's JSON so its length stays linear in the node's JSON.** | 4 | 5 | 7 | 3 | 5 | 1 | 10.8 |
| 51 | 0.4.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 4 | 5 | 2 | 7 | 4, 5 | 10.4 |
| 95 | 0.4.0 | decision | **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.** | 5 | 6 | 9 | 2 | 6 | 1 | 9.5 |
| 91 | 0.4.0 | | **Spell an emphasis or link once across the hard breaks, dates, emoji, inline cards, mentions, statuses and inline media between two nodes it marks, in both writers.** | 5 | 5 | 8 | 5 | 6 | 6 | 9.0 |
| 91 | 0.4.0 | | **Spell an emphasis or link once across the hard breaks, dates, emoji, inline cards, mentions and statuses between two nodes it marks, and an emphasis across inline media, in both writers.** | 5 | 5 | 8 | 5 | 6 | 6 | 9.0 |
| 73 | 0.5.0 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 1 | 6 | 8 | 3 | 23.0 |
| 9 | 0.5.0 | | **Ship an online sandbox: a web page with two textboxes converting between ADF and markdown on the library's browser build.** | 2 | 6 | 4 | 6 | 8 | 3 | 21.0 |
| 38 | 0.5.0 | | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 2 | 2 | 5 | 4 | 7 | 1 | 19.5 |
@@ -69,12 +69,12 @@
## Details
### 114. Build text marked `code` holding no mark but `link` and `annotation`, in every reader, and carry any other in `adfToLosslessMarkdown`.
### 114. Build code-marked text holding no mark besides `link` and `annotation`, in every reader, and have `adfToLosslessMarkdown` carry code text holding any other mark.
`**`npm ci`**` builds text holding `[strong, code]`; `code_inline_node` (`spec/adf-schema/full.json`)
``**`npm ci`**`` builds text holding `[strong, code]`; `code_inline_node` (`spec/adf-schema/full.json`)
gives code text `code`, `link` and `annotation` alone, so a site may reject the document. The `==`
highlight already skips code (`highlighted`, `src/markdown/parse/inline-content.ts`). Moves the
reading of stored markdown, so it ships where semver allows a breaking change.
highlight already skips code (`highlighted`, `src/markdown/parse/inline-content.ts`). It changes
how stored markdown reads, so it ships in a minor.
### 99. Write a linked external image standing inline or in a table cell in the spelling a reader panel picks, in `adfToPortableMarkdown`.
@@ -298,14 +298,14 @@ text. Only the property tests catch drift between these and the reader, and it h
under the comprehension floor. Reading the line back costs one parse each time a line is tried. The
run it carries may differ from today's, which changes lossless output (§The formats are API).
### 91. Spell an emphasis or link once across the hard breaks, dates, emoji, inline cards, mentions, statuses and inline media between two nodes it marks, in both writers.
### 91. Spell an emphasis or link once across the hard breaks, dates, emoji, inline cards, mentions and statuses between two nodes it marks, and an emphasis across inline media, in both writers.
Lands after item 95, which lets the joined spelling be tried and kept only where it reads back. No
reader marks a hard break, so `*One\` then `Two*` writes back as `_One_\` then `_Two_`, and a link
typed across a line as two links. No reader gives those other nodes a mark Atlassian's schema
withholds from them, so `**Ping !adf:mention[@A]{id=x} now**` writes back with each strong run's
inner space spelled `!adf:text{text=" "}`. Joining the run across the break in `inline-line.ts` ruled worse
with two comprehension panels (PR #22): a joined run that cannot be spelled needs its own fallback,
withholds from them, so `**Ping !adf:mention[@A]{id=x} now**` writes back with the space beside
the mention in each strong run spelled `!adf:text{text=" "}`. Joining the run across the break in
`inline-line.ts` ruled worse with two comprehension panels (PR #22): a joined run that cannot be spelled needs its own fallback,
first as a set of parted breaks, then as a flag the first fallback clears.
### 73. Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.