mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-13 12:18:28 +01:00
Answer architecture round 1: 2 🔴, 4 🟡; refuse a link around only nodes that take none, mark the change breaking, file code-text marks
This commit is contained in:
@@ -11,7 +11,7 @@ import { decodeTextEscapes, trimTrailingSpace } from '../commonmark/grammar.ts'
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { highlightDelimiter } from '../portable/conventions.ts'
|
||||
import { identicalMarks, mergeAdjacentText, nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
||||
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||
import { inlineNodeModel, takesMark } from '../../adf/inline-nodes.ts'
|
||||
import { joinsWhenRead, textBreakName, textBreakSpelling } from '../adjacent-text.ts'
|
||||
import { matchEmphasis } from '../commonmark/emphasis-matching.ts'
|
||||
import { normalizeLabel, readInlineTarget, readLabel } from '../commonmark/link-syntax.ts'
|
||||
@@ -84,6 +84,7 @@ const ownMarksInMark = 'move the opaque carry, or the node spelling marks=empty,
|
||||
const editorHighlight: AdfMark = { attrs: { color: '#f8e6a0' }, type: 'backgroundColor' }
|
||||
const hreflessLink = 'the link mark spells its href: this one spells none'
|
||||
const linkInLink = 'no link wraps a link: the [content] this one marks already holds one'
|
||||
const unlinkable = 'the text of this link holds only nodes that take no link: write the link around text beside them'
|
||||
const spellableLink = 'link takes the directive form only where CommonMark cannot spell it: this one it can, as [text](url "title") or <url>'
|
||||
const textBreak: TextBreak = { kind: 'textBreak' }
|
||||
|
||||
@@ -210,7 +211,7 @@ 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.some((node) => takesMark(node, mark.type))) {
|
||||
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)
|
||||
}
|
||||
const refused = mark.type === 'link' ? refuseLinkDirective(scan, mark, slot.nodes, index) : undefined
|
||||
@@ -266,9 +267,9 @@ function mediaSingle({ alt, definition, link }: Image): AdfNode {
|
||||
return centeredImage([externalMedia(alt, definition.destination, link), ...caption])
|
||||
}
|
||||
|
||||
// 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.
|
||||
// ADF holds no empty text node and a hard break takes no link, so the destination text carries the link ahead of the text's hard breaks; an empty destination leaves the breaks alone.
|
||||
function linkTo(nodes: readonly Inline[], definition: LinkDefinition): Inline[] {
|
||||
if (nodes.some((node) => takesMark(node, 'link'))) return applyMark(nodes, linkMark(definition))
|
||||
if (nodes.some((node) => marksItem(node, 'link'))) return applyMark(nodes, linkMark(definition))
|
||||
return definition.destination === '' ? [...nodes] : [{ marks: [linkMark(definition)], text: definition.destination, type: 'text' }, ...nodes]
|
||||
}
|
||||
|
||||
@@ -290,8 +291,8 @@ function partText(items: readonly Inline[], scan: Scan): Result<AdfNode[]> {
|
||||
}
|
||||
|
||||
// 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 marksItem(item: Inline, type: string): boolean {
|
||||
return !isNode(item) || takesMark(item.type, type)
|
||||
}
|
||||
|
||||
function isNode(item: Inline): item is AdfNode {
|
||||
@@ -400,7 +401,11 @@ function linkPiece(scan: Scan, inner: readonly Piece[], definition: LinkDefiniti
|
||||
const [only] = inner
|
||||
if (inner.length === 1 && only?.kind === 'image') return success({ ...only, link: definition })
|
||||
const nodes = resolveNodes(inner, scan, true)
|
||||
return nodes.ok ? success({ kind: 'nodes', nodes: linkTo(nodes.value, definition) }) : nodes
|
||||
if (!nodes.ok) return nodes
|
||||
if (!nodes.value.some((node) => marksItem(node, 'link')) && nodes.value.some((node) => isNode(node) && node.type !== 'hardBreak')) {
|
||||
return failure('unsupported-node-shape', unlinkable, scan.path)
|
||||
}
|
||||
return success({ kind: 'nodes', nodes: linkTo(nodes.value, definition) })
|
||||
}
|
||||
|
||||
// CommonMark: no link nests inside another, though an image's description holds one.
|
||||
@@ -591,7 +596,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) || !takesMark(node, mark.type)) return node
|
||||
if (!isNode(node) || !marksItem(node, mark.type)) return node
|
||||
const marks = nodeMarks(node)
|
||||
return marks.some((carried) => carried.type === mark.type) ? node : { ...node, marks: [mark, ...marks] }
|
||||
})
|
||||
|
||||
@@ -911,7 +911,6 @@ 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))
|
||||
@@ -920,7 +919,9 @@ test('gives an inline node no mark Atlassian\'s schema withholds from it', () =>
|
||||
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' }])
|
||||
const unlinkable = 'unsupported-node-shape: the text of this link holds only nodes that take no link: write the link around text beside them'
|
||||
assert.equal(read('[!adf:status[Done]{color=green}](/u)\n'), unlinkable)
|
||||
assert.equal(read('[!adf:status[Done]{color=green}\\\n](/u)\n'), unlinkable)
|
||||
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' }])
|
||||
|
||||
Reference in New Issue
Block a user