3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-15 21:10:25 +01:00

Give an inline node no mark Atlassian's schema withholds from it, in every reader

This commit is contained in:
2026-10-14 13:27:53 +02:00
committed by Lillem4n
parent dcbdf3683d
commit 1ae7ba6992
21 changed files with 122 additions and 102 deletions
+8 -7
View File
@@ -210,8 +210,8 @@ function directiveMarkPiece(scan: Scan, name: string, mark: AdfMark, slot: SlotC
return failure('unsupported-node-shape', `the ${name} mark wraps the [content] it marks: this one wraps none`, scan.path)
}
if (slot.nodes.some(holdsOwnMarks)) return failure('unsupported-node-shape', ownMarksInMark, scan.path)
if (slot.nodes.every(takesNoMarks)) {
return failure('unsupported-node-shape', `the ${name} mark wraps the [content] it marks: this one wraps only hard breaks, which take no marks`, scan.path)
if (!slot.nodes.some((node) => takesMark(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)
}
const refused = mark.type === 'link' ? refuseLinkDirective(scan, mark, slot.nodes, index) : undefined
if (refused !== undefined) return refused
@@ -266,9 +266,9 @@ function mediaSingle({ alt, definition, link }: Image): AdfNode {
return centeredImage([externalMedia(alt, definition.destination, link), ...caption])
}
// ADF holds no empty text node and a hard break takes no marks, so the destination text carries the link ahead of the text's hard breaks; an empty destination leaves the breaks alone.
// ADF holds no empty text node and some nodes take no link, so the destination text carries the link ahead of the text's nodes; an empty destination leaves them alone.
function linkTo(nodes: readonly Inline[], definition: LinkDefinition): Inline[] {
if (!nodes.every(takesNoMarks)) return applyMark(nodes, linkMark(definition))
if (nodes.some((node) => takesMark(node, 'link'))) return applyMark(nodes, linkMark(definition))
return definition.destination === '' ? [...nodes] : [{ marks: [linkMark(definition)], text: definition.destination, type: 'text' }, ...nodes]
}
@@ -289,8 +289,9 @@ function partText(items: readonly Inline[], scan: Scan): Result<AdfNode[]> {
return success(parted)
}
function takesNoMarks(item: Inline): boolean {
return isNode(item) && inlineNodeModel(item.type)?.takesMarks === false
// spec/flavour.md, Marks: a spelling skips a node Atlassian's schema withholds its mark from.
function takesMark(item: Inline, type: string): boolean {
return !isNode(item) || (inlineNodeModel(item.type)?.marks.includes(type) ?? true)
}
function isNode(item: Inline): item is AdfNode {
@@ -590,7 +591,7 @@ function markType(character: string, used: number): string {
// `*(*a*)*` builds one `em`: a node holds at most one mark of each type.
function applyMark(nodes: readonly Inline[], mark: AdfMark): Inline[] {
return nodes.map((node) => {
if (!isNode(node) || takesNoMarks(node)) return node
if (!isNode(node) || !takesMark(node, mark.type)) return node
const marks = nodeMarks(node)
return marks.some((carried) => carried.type === mark.type) ? node : { ...node, marks: [mark, ...marks] }
})
+25 -8
View File
@@ -909,6 +909,26 @@ test('gives the hard break inside a mark spelling no marks, and reads a link of
}
})
test('gives an inline node no mark Atlassian\'s schema withholds from it', () => {
const mention: AdfNode = { attrs: { id: 'b1c2', text: '@A' }, type: 'mention' }
const status: AdfNode = { attrs: { color: 'green', text: 'Done' }, type: 'status' }
const media: AdfNode = { attrs: { id: 'm1' }, type: 'mediaInline' }
const border: AdfMark = { attrs: { color: '#091e42', size: 2 }, type: 'border' }
const read = (markdown: string): AdfNode[] | string => content(losslessMarkdownToAdf(markdown))
assert.deepEqual(read('**!adf:mention[@A]{id=b1c2}**\n'), [{ content: [mention], type: 'paragraph' }])
assert.deepEqual(read('*a !adf:date{timestamp=1}*\n'), [{ content: [marked('a ', em), { attrs: { timestamp: '1' }, type: 'date' }], type: 'paragraph' }])
assert.deepEqual(read('!adf:underline[a !adf:emoji[🎉]{shortName=":tada:"}]\n'), [
{ content: [marked('a ', underline), { attrs: { shortName: ':tada:', text: '🎉' }, type: 'emoji' }], type: 'paragraph' },
])
assert.deepEqual(read('[!adf:status[Done]{color=green}](/u)\n'), [{ content: [marked('/u', link('/u')), status], type: 'paragraph' }])
assert.deepEqual(read('[a !adf:inlineCard{url="https://x/"}](/u)\n'), [{ content: [marked('a ', link('/u')), { attrs: { url: 'https://x/' }, type: 'inlineCard' }], type: 'paragraph' }])
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: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')
})
test('decodes the fenced info string the block walk leaves raw', () => {
assert.deepEqual(content(losslessMarkdownToAdf('```java&#8203;script\nx\n```\n')), [
{ attrs: { language: 'java\u200bscript' }, content: [text('x')], type: 'codeBlock' },
@@ -1141,9 +1161,6 @@ test('reads the content slot as the text attribute the node spells there', () =>
assert.deepEqual(content(losslessMarkdownToAdf('!adf:status[ In review ]{color=yellow}\n')), status({ color: 'yellow', text: ' In review ' }))
assert.deepEqual(content(losslessMarkdownToAdf('!adf:status[In!adf:text{text=" "}review]{color=yellow}\n')), status({ color: 'yellow', text: 'In review' }))
assert.deepEqual(content(losslessMarkdownToAdf('!adf:status[a\\]b]{color=yellow}\n')), status({ color: 'yellow', text: 'a]b' }))
assert.deepEqual(content(losslessMarkdownToAdf('**!adf:mention[@A]{id=b1c2}**\n')), [
{ content: [{ attrs: { id: 'b1c2', text: '@A' }, marks: [strong], type: 'mention' }], type: 'paragraph' },
])
})
test('names the content slot no lone plain text node reads back from', () => {
@@ -1198,10 +1215,10 @@ test('reads the directive marks, the nesting outermost first', () => {
assert.deepEqual(content(losslessMarkdownToAdf('!adf:subsup[a]{type=sub}\n')), wrapped({ attrs: { type: 'sub' }, type: 'subsup' }))
assert.deepEqual(content(losslessMarkdownToAdf('!adf:border[a]{color="#091e42" size=2}\n')), wrapped({ attrs: { color: '#091e42', size: 2 }, type: 'border' }))
assert.deepEqual(content(losslessMarkdownToAdf('!adf:underline[a!adf:date{timestamp=1}]\n')), [
{ content: [marked('a', underline), { attrs: { timestamp: '1' }, marks: [underline], type: 'date' }], type: 'paragraph' },
{ content: [marked('a', underline), { attrs: { timestamp: '1' }, type: 'date' }], type: 'paragraph' },
])
assert.deepEqual(content(losslessMarkdownToAdf('_!adf:underline[a!adf:date{timestamp=1}]_\n')), [
{ content: [marked('a', em, underline), { attrs: { timestamp: '1' }, marks: [em, underline], type: 'date' }], type: 'paragraph' },
{ content: [marked('a', em, underline), { attrs: { timestamp: '1' }, type: 'date' }], type: 'paragraph' },
])
assert.equal(content(losslessMarkdownToAdf('!adf:border[a]{color="#091e42" size=x}\n')), 'unsupported-node-shape: the size attribute of border is no number')
})
@@ -1249,9 +1266,9 @@ 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 = 'the [content] it marks: this one wraps only hard breaks, which take no marks'
assert.equal(content(losslessMarkdownToAdf('!adf:underline[!adf:hardBreak{}]\n')), `unsupported-node-shape: the underline mark wraps ${breaks}`)
assert.equal(content(losslessMarkdownToAdf('!adf:link[!adf:hardBreak{}]{href="/u" title=t}\n')), `unsupported-node-shape: the link mark wraps ${breaks}`)
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`
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'))
})
test('reads the text break only between two text nodes CommonMark joins, building no node', () => {