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

Answer architecture round 1: the node table says a hard break takes no marks, and a link text of hard breaks alone reads as its destination

This commit is contained in:
2026-10-09 11:04:30 +02:00
committed by Lillem4n
parent 32bee5ab91
commit 2ca5ac41e6
16 changed files with 185 additions and 34 deletions
+11 -7
View File
@@ -260,10 +260,10 @@ function directivePiece(scan: Scan, span: DirectiveSpan, index: number): Result<
}
function directiveMarkPiece(scan: Scan, name: string, mark: AdfMark, slot: SlotContent | undefined, index: number): Result<Piece> {
if (slot === undefined || slot.nodes.length === 0) {
return failure('unsupported-node-shape', `the ${name} mark wraps the [content] it marks: this one wraps none`, scan.path)
if (slot?.nodes.some(holdsOwnMarks) === true) return failure('unsupported-node-shape', ownMarksInMark, scan.path)
if (slot === undefined || slot.nodes.every(takesNoMarks)) {
return failure('unsupported-node-shape', `the ${name} mark wraps the [content] it marks: this one wraps nothing a mark rides`, scan.path)
}
if (slot.nodes.some(holdsOwnMarks)) return failure('unsupported-node-shape', ownMarksInMark, scan.path)
const refused = mark.type === 'link' ? refuseLinkDirective(scan, mark, slot.nodes, index) : undefined
if (refused !== undefined) return refused
return success({ kind: 'nodes', nodes: applyMark(slot.nodes, mark) })
@@ -322,9 +322,9 @@ 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 an empty link text 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, and an empty destination as nothing.
function linkTo(nodes: readonly Inline[], definition: LinkDefinition): Inline[] {
if (nodes.length > 0) return applyMark(nodes, linkMark(definition))
if (!nodes.every(takesNoMarks)) return applyMark(nodes, linkMark(definition))
return definition.destination === '' ? [] : [{ marks: [linkMark(definition)], text: definition.destination, type: 'text' }]
}
@@ -345,6 +345,10 @@ function partText(items: readonly Inline[], scan: Scan): Result<AdfNode[]> {
return success(parted)
}
function takesNoMarks(item: Inline): boolean {
return isNode(item) && inlineNodeModel(item.type)?.marks === false
}
function isNode(item: Inline): item is AdfNode {
return !('kind' in item)
}
@@ -669,10 +673,10 @@ function markType(character: string, used: number): string {
return used === 2 ? 'strong' : 'em'
}
// `*(*a*)*` builds one `em`: a node holds at most one mark of each type. Atlassian's schema gives a hard break no marks.
// `*(*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) || node.type === 'hardBreak') return node
if (!isNode(node) || takesNoMarks(node)) return node
const marks = nodeMarks(node)
return marks.some((carried) => carried.type === mark.type) ? node : { ...node, marks: [mark, ...marks] }
})
+8 -2
View File
@@ -837,7 +837,11 @@ test('builds the hard break a mark spelling spans with no marks, in every reader
}
assert.deepEqual(content(losslessMarkdownToAdf('~~a\\\nb~~\n')), broken(strike))
assert.deepEqual(content(losslessMarkdownToAdf('!adf:underline[a!adf:hardBreak{}b]\n')), broken(underline))
assert.deepEqual(content(losslessMarkdownToAdf('_!adf:hardBreak{localId=x}_\n')), [{ content: [{ attrs: { localId: 'x' }, type: 'hardBreak' }], type: 'paragraph' }])
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' }])
}
})
test('decodes the fenced info string the block walk leaves raw', () => {
@@ -1177,9 +1181,11 @@ test('names the link a directive link wraps, no link holding another', () => {
})
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'
const named = 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps nothing a mark rides'
assert.equal(content(losslessMarkdownToAdf('!adf:underline[]\n')), named)
assert.equal(content(losslessMarkdownToAdf('!adf:underline{}\n')), named)
assert.equal(content(losslessMarkdownToAdf('!adf:underline[!adf:hardBreak{}]\n')), named)
assert.equal(content(losslessMarkdownToAdf('!adf:link[!adf:hardBreak{}]{href="/u" title=t}\n')), named.replace('underline', 'link'))
})
test('reads the text break only between two text nodes CommonMark joins, building no node', () => {