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

Read an image alone on an alert's marker line or the lines after it as the image, as a reader panel expected; item 88 filed

This commit is contained in:
2026-10-09 10:23:28 +02:00
committed by Lillem4n
parent ee3086ad1e
commit d677222050
6 changed files with 41 additions and 18 deletions
+1 -1
View File
@@ -243,7 +243,7 @@ Serves Goals 1, 4 and 5.
- An empty link text reads as its destination, `[](/url)` as `/url` linked to `/url`, and
`[]()` as nothing.
- 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 an alert or task marker opens — it reads as its
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
`See [the moon](moon.png).`. To keep an image an image, give it a paragraph of its own: a
blank line between it and any other line.
+6
View File
@@ -217,6 +217,12 @@ reads as that image, with the link marking its `media`. Inside any other link, t
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`, an alert's or callout's marker line stands apart from the lines after
it, so an image alone on either reads as the image: a reader panel of the bot, the LLM pipeline and
a support agent, 3 of 3 on 2026-10-09, expected the picture in every case, a ticked task included.
A task item holds no image in Atlassian's schema, so the paragraph a task marker opens reads its
image as linked alt text, the fallback two of the three named.
## Ids stay site-local
2026-08-23, the maintainer. Goal 1. Valid while ADF ids are minted per site.
+3 -2
View File
@@ -322,8 +322,9 @@ directive form instead.
`mediaInline` carries a media `collection` + `id`, never a URL, so anywhere else an image reads as
its alt text linked to its destination, the link titled by the image's title: beside other
content, in a heading, a table cell or an `!adf:paragraph`, and in the paragraph a portable alert,
callout or task marker opens. An empty alt text reads as the destination, as an empty link text
content, in a heading, a table cell or an `!adf:paragraph`, and in the paragraph a portable task
marker opens. An alert's or callout's marker line stands apart from the lines after it, so an image
alone on either reads as the image. 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` §An image reads as an image only alone in its paragraph).
+15 -10
View File
@@ -20,10 +20,12 @@ import { readBlockDirectiveNode } from './directive-nodes.ts'
import { spellStringAttribute, unsupportedNodeShape } from '../directive-syntax.ts'
import { spellsEmpty } from '../empty-keys.ts'
// A paragraph a marker opens: it shares its markdown paragraph with the marker, so no image there stands alone.
type MarkerParagraph = { kind: 'markerParagraph'; position: SourcePosition; text: string }
type Paragraph = Extract<Block, { kind: 'paragraph' }>
type ReadBlock = Block | MarkerParagraph
// The paragraph a task marker opens: a task item holds no image, so a lone one there reads as its linked alt text.
type TaskParagraph = { kind: 'taskParagraph'; position: SourcePosition; text: string }
type ReadBlock = Block | TaskParagraph
// `inExpand` is whether an expand holds the blocks, which makes a folded callout a nestedExpand.
type Reading = { claims: Claims; definitions: LinkDefinitions; inExpand: boolean; memo: SpellingMemo }
@@ -123,8 +125,6 @@ function readBlock(block: ReadBlock, reading: Reading, path: ConvertErrorPath, d
return contentNode({ attrs: { level: block.level }, type: 'heading' }, block.text, reading, path, 'heading')
case 'html':
return failure('unmappable-html', `no raw HTML converts at this version: ${block.construct}`, path)
case 'markerParagraph':
return contentNode({ type: 'paragraph' }, block.text, reading, path, 'paragraph')
case 'orderedList':
return listNode({ attrs: { order: block.start }, type: 'orderedList' }, block.items, reading, path, depth)
case 'paragraph':
@@ -133,6 +133,8 @@ function readBlock(block: ReadBlock, reading: Reading, path: ConvertErrorPath, d
return success({ type: 'rule' })
case 'table':
return tableNode(block.rows, reading, path)
case 'taskParagraph':
return contentNode({ type: 'paragraph' }, block.text, reading, path, 'paragraph')
}
}
@@ -149,8 +151,9 @@ function markerLine(text: string): { line: string; rest: string } {
return { line: (hardBreak ? line.slice(0, -1) : line).replace(/^[ \t]+/, ''), rest: lineEnd === -1 ? '' : text.slice(lineEnd + 1) }
}
function markerParagraphs(position: SourcePosition, ...texts: string[]): MarkerParagraph[] {
return texts.filter((text) => text !== '').map((text) => ({ kind: 'markerParagraph', position, text }))
// docs/decisions.md, An image reads as an image only alone in its paragraph: an alert's marker line stands apart from the lines after it.
function paragraphsOf(position: SourcePosition, ...texts: string[]): Paragraph[] {
return texts.filter((text) => text !== '').map((text) => ({ kind: 'paragraph', position, text }))
}
function quoteNode(blocks: readonly Block[], reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode> {
@@ -159,12 +162,12 @@ function quoteNode(blocks: readonly Block[], reading: Reading, path: ConvertErro
if (led === undefined) return containerNode({ type: 'blockquote' }, blocks, reading, path, depth)
const { folded, panelType } = led.marker
const { line, rest } = markerLine(led.text)
if (!folded) return filledNode({ attrs: { panelType }, type: 'panel' }, readBlocks([...markerParagraphs(led.position, line, rest), ...body], reading, path, depth + 1))
if (!folded) return filledNode({ attrs: { panelType }, type: 'panel' }, readBlocks([...paragraphsOf(led.position, line, rest), ...body], reading, path, depth + 1))
const title = parseInlineContent(line, reading.definitions, path, 'paragraph', reading.claims)
if (!title.ok) return title
const text = titleText(title.value.nodes)
const type = reading.inExpand ? 'nestedExpand' : 'expand'
return filledNode(text === '' ? { type } : { attrs: { title: text }, type }, readBlocks([...markerParagraphs(led.position, rest), ...body], { ...reading, inExpand: true }, path, depth + 1))
return filledNode(text === '' ? { type } : { attrs: { title: text }, type }, readBlocks([...paragraphsOf(led.position, rest), ...body], { ...reading, inExpand: true }, path, depth + 1))
}
// docs/decisions.md, A callout title keeps its link targets.
@@ -203,7 +206,9 @@ function bulletNode(items: readonly Block[][], reading: Reading, path: ConvertEr
}
const tasks: AdfNode[] = []
for (const [index, { marker, others, position, text }] of led.entries()) {
const read = readBlocks([...markerParagraphs(position, text.replace(/^(?:[ \t\n]|\\\n)+/, '')), ...others], reading, [...path, 'content', index], depth + 1)
const body = text.replace(/^(?:[ \t\n]|\\\n)+/, '')
const marked: TaskParagraph[] = body === '' ? [] : [{ kind: 'taskParagraph', position, text: body }]
const read = readBlocks([...marked, ...others], reading, [...path, 'content', index], depth + 1)
if (!read.ok) return read
let beside = read.value.length
while (read.value[beside - 1]?.type === 'taskList') beside -= 1
@@ -231,12 +231,15 @@ test('reads what CommonMark reads where no row reads, a directive and a carry fe
assert.deepEqual(read('a !adf:carry{json="{}"} \\!adf:x\n'), [said('a !adf:carry{json="{}"} !adf:x')])
assert.deepEqual(read('```adf:blockCard\n{}\n```\n'), [node('codeBlock', { language: 'adf:blockCard' }, text('{}'))])
const linked = paragraph(text('a', { attrs: { href: 'u' }, type: 'link' }))
assert.deepEqual(read('> [!tip] ![a](u)\n'), [panel('tip', linked)])
assert.deepEqual(read('> [!tip]\n> ![a](u)\n'), [panel('tip', linked)])
const image = node('mediaSingle', { layout: 'center' }, { attrs: { alt: 'a', type: 'external', url: 'u' }, type: 'media' })
assert.deepEqual(read('> [!tip] ![a](u)\n'), [panel('tip', image)])
assert.deepEqual(read('> [!tip]\n> ![a](u)\n'), [panel('tip', image)])
assert.deepEqual(read('> [!tip]\n>\n> ![a](u)\n'), [panel('tip', image)])
assert.deepEqual(read('> [!tip]\n> ![a](u) b\n'), [panel('tip', paragraph(...(linked.content ?? []), text(' b')))])
assert.deepEqual(read('> [!NOTE]- t\n> ![a](u)\n'), [node('expand', { title: 't' }, image)])
assert.deepEqual(read('> [!NOTE]- ![a](u)\n'), [node('expand', { title: 'a (u)' }, paragraph())])
assert.deepEqual(read('> [!NOTE]- t\n> ![a](u)\n'), [node('expand', { title: 't' }, linked)])
assert.deepEqual(read('- [x] ![a](u)\n'), [bare('taskList', task('DONE', ...(linked.content ?? [])))])
assert.deepEqual(read('> [!tip]\n>\n> ![a](u)\n'), [panel('tip', node('mediaSingle', { layout: 'center' }, { attrs: { alt: 'a', type: 'external', url: 'u' }, type: 'media' }))])
assert.deepEqual(read('- [x]\n ![a](u)\n'), [bare('taskList', task('DONE', ...(linked.content ?? [])))])
let deep = 'x\n'
for (let level = 0; level < largestNesting; level += 1) deep = `> ${deep}`
assert.equal(typeof read(deep), 'object')
+9 -1
View File
@@ -6,7 +6,7 @@
`Bar = 9`
`Next ID = 88`
`Next ID = 89`
| Goal | W |
|---|---|
@@ -26,6 +26,7 @@
| ID | Release | Exempt | Item | R | S | A | G | Goals | Score |
|---|---|---|---|---|---|---|---|---|---|
| 87 | 0.2.0 | defect | **Build a `hardBreak` carrying no marks where emphasis or a link spans a line break, in every reader.** | 4 | 3 | 6 | 7 | 4 | 16.6 |
| 88 | 0.2.0 | defect | **Build a `blockTaskItem` holding only the paragraphs and extensions Atlassian's schema allows, in `portableMarkdownToAdf`.** | 4 | 4 | 6 | 7 | 4 | 16.4 |
| 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 |
@@ -73,6 +74,13 @@ schema refuses: its `hardBreak_node` holds no `marks` (`spec/adf-schema/full.jso
`commonMarkToAdf` builds a document a site may reject from plain CommonMark. The suite's `em 1/3`
exceptions for 638 and 639 go back to `em 1/2`.
### 88. Build a `blockTaskItem` holding only the paragraphs and extensions Atlassian's schema allows, in `portableMarkdownToAdf`.
`- [x] done\n\n ![a](u)` builds a `blockTaskItem` holding a `mediaSingle`, and `- [x] done\n\n > q`
one holding a `blockquote`; the schema's `blockTaskItem_node` takes paragraphs and extensions only
(`spec/adf-schema/full.json`), so a site may reject the document. The reader needs a reading for
the other blocks, settled by Goals 4 and 7.
### 49. Read a list whose bullet or ordered delimiter changes as two lists, in every reader, and retire `!adf:listBreak`.
Today `- a` then `+ b`, or `1.` then `1)`, reads as one list; CommonMark reads two (spec examples