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:
@@ -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 .` 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.
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
|
||||
@@ -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] \n'), [panel('tip', linked)])
|
||||
assert.deepEqual(read('> [!tip]\n> \n'), [panel('tip', linked)])
|
||||
const image = node('mediaSingle', { layout: 'center' }, { attrs: { alt: 'a', type: 'external', url: 'u' }, type: 'media' })
|
||||
assert.deepEqual(read('> [!tip] \n'), [panel('tip', image)])
|
||||
assert.deepEqual(read('> [!tip]\n> \n'), [panel('tip', image)])
|
||||
assert.deepEqual(read('> [!tip]\n>\n> \n'), [panel('tip', image)])
|
||||
assert.deepEqual(read('> [!tip]\n>  b\n'), [panel('tip', paragraph(...(linked.content ?? []), text(' b')))])
|
||||
assert.deepEqual(read('> [!NOTE]- t\n> \n'), [node('expand', { title: 't' }, image)])
|
||||
assert.deepEqual(read('> [!NOTE]- \n'), [node('expand', { title: 'a (u)' }, paragraph())])
|
||||
assert.deepEqual(read('> [!NOTE]- t\n> \n'), [node('expand', { title: 't' }, linked)])
|
||||
assert.deepEqual(read('- [x] \n'), [bare('taskList', task('DONE', ...(linked.content ?? [])))])
|
||||
assert.deepEqual(read('> [!tip]\n>\n> \n'), [panel('tip', node('mediaSingle', { layout: 'center' }, { attrs: { alt: 'a', type: 'external', url: 'u' }, type: 'media' }))])
|
||||
assert.deepEqual(read('- [x]\n \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')
|
||||
|
||||
@@ -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 ` 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
|
||||
|
||||
Reference in New Issue
Block a user