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

Build text holding no border mark in losslessMarkdownToAdf, and carry text holding one

This commit is contained in:
2026-10-31 02:40:51 +01:00
parent de0033c083
commit 7ce296c455
13 changed files with 47 additions and 28 deletions
+3 -2
View File
@@ -3,6 +3,7 @@ import type { AttributeVocabulary } from './attribute-vocabulary.ts'
import { nodeMarks } from './document.ts'
export const codeTextMarkTypes: readonly string[] = ['annotation', 'code', 'link']
export const formattedTextMarkTypes: readonly string[] = ['annotation', 'backgroundColor', 'em', 'link', 'strike', 'strong', 'subsup', 'textColor', 'underline']
export type InlineNodeModel = {
attributes: AttributeVocabulary
@@ -37,9 +38,9 @@ export function inlineNodeModel(type: string): InlineNodeModel | undefined {
return Object.hasOwn(inlineNodes, type) ? inlineNodes[type] : undefined
}
// Undefined means every mark: the table lists only types the schema restricts, and it restricts text only when marked code.
// Undefined means every mark: the table lists only types the schema restricts. Text is code text, or formatted text, which withholds `border`.
export function takenMarkTypes(node: AdfNode): readonly string[] | undefined {
if (node.type === 'text') return nodeMarks(node).some((mark) => mark.type === 'code') ? codeTextMarkTypes : undefined
if (node.type === 'text') return nodeMarks(node).some((mark) => mark.type === 'code') ? codeTextMarkTypes : formattedTextMarkTypes
return inlineNodeModel(node.type)?.markTypes
}
+11 -1
View File
@@ -8,7 +8,7 @@ import test from 'node:test'
import type { AttributeKind, AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
import { blockArgument } from '../markdown/directive/block.ts'
import { blockNodes } from '../adf/block-nodes.ts'
import { codeTextMarkTypes, inlineNodes } from '../adf/inline-nodes.ts'
import { codeTextMarkTypes, formattedTextMarkTypes, inlineNodes } from '../adf/inline-nodes.ts'
import { markVocabularies } from '../adf/mark-attributes.ts'
type Held = Map<string, Set<AttributeKind>>
@@ -92,6 +92,16 @@ test('code text takes exactly the mark types the ADF JSON Schemas give a code_in
}
})
test('formatted text takes exactly the mark types the ADF JSON Schemas give a formatted_text_inline_node', () => {
for (const file of schemaFiles) {
const definitions = schemaObject(schemaObject(JSON.parse(readFileSync(join(root, file), 'utf8')), file)['definitions'], `${file} definitions`)
const where = `${file} formatted_text_inline_node`
const marks = alternatives(schemaObject(definitions['formatted_text_inline_node'], where), definitions, where).flatMap((properties) => properties.get('marks') ?? [])
const narrowed = marks.map((schema) => heldMarkTypes(schema, definitions, `${where}.marks`)).filter((types) => !types.includes('every mark type'))
assert.deepEqual(narrowed.flat().sort(), formattedTextMarkTypes, file)
}
})
function spelled(): Spelled[] {
return [
...Object.entries(blockNodes).map(([type, model]) => spelledType(type, model.attributes, blockArgument(type))),
+4 -1
View File
@@ -335,6 +335,10 @@ test('carries an inline node holding a mark Atlassian\'s schema withholds from i
assert.equal(carried({ attrs: { id: 'm1' }, marks: [{ attrs: { href: '/u' }, type: 'link' }], type: 'mediaInline' }), '[!adf:mediaInline{id=m1}](/u)\n')
assert.equal(carried({ marks: [{ type: 'strong' }, { type: 'code' }], text: 'x', type: 'text' }), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"},{\\"type\\":\\"code\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n')
assert.equal(carried({ marks: [{ attrs: { href: '/u' }, type: 'link' }, { type: 'code' }], text: 'x', type: 'text' }), '[`x`](/u)\n')
const border: AdfMark = { attrs: { color: '#091e42', size: 2 }, type: 'border' }
const bordered = '!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"color\\":\\"#091e42\\",\\"size\\":2},\\"type\\":\\"border\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}'
assert.equal(carried({ marks: [border], text: 'x', type: 'text' }), `${bordered}\n`)
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ type: 'em' }], text: 'a', type: 'text' }, { marks: [border], text: 'x', type: 'text' })))), `_a_${bordered}\n`)
})
test('spells a heading level no ATX heading fits as a directive', () => {
@@ -888,7 +892,6 @@ test('spells the directive marks around the longest run they cover', () => {
assert.equal(emitted(marked('a', { attrs: {}, type: 'underline' }), marked('b', underline)), '!adf:underline[a]{attrs=empty}!adf:underline[b]\n')
assert.equal(emitted(marked('x', { attrs: { type: 'sub' }, type: 'subsup' })), '!adf:subsup[x]{type=sub}\n')
assert.equal(emitted(marked('x', { attrs: { color: '#ae2e24' }, type: 'textColor' })), '!adf:textColor[x]{color="#ae2e24"}\n')
assert.equal(emitted(marked('x', { attrs: { color: '#091e42', size: 2 }, type: 'border' })), '!adf:border[x]{color="#091e42" size=2}\n')
assert.equal(emitted(marked('x', { type: 'em' }, underline)), '_!adf:underline[x]_\n')
assert.equal(emitted(marked('x', underline, { type: 'em' })), '!adf:underline[_x_]\n')
assert.equal(emitted(marked('a', underline), { type: 'hardBreak' }, marked('b', underline)), '!adf:underline[a]\\\n!adf:underline[b]\n')
+5 -1
View File
@@ -926,6 +926,11 @@ test('gives an inline node no mark Atlassian\'s schema withholds from it', () =>
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.deepEqual(read('!adf:border[a !adf:mediaInline{id=m1}]{color="#091e42" size=2}\n'), [{ content: [{ text: 'a ', type: 'text' }, { ...media, marks: [border] }], type: 'paragraph' }])
const unborderable = 'unsupported-node-shape: the border mark wraps the [content] it marks: this one wraps only nodes that take no border mark: write the border mark around text beside them'
assert.equal(read('!adf:border[a]{color="#091e42" size=2}\n'), unborderable)
assert.equal(read('_!adf:border[a]{color="#091e42" size=2}_\n'), unborderable)
assert.equal(read('!adf:underline[!adf:border[a]{color="#091e42" size=2}]\n'), unborderable)
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: write the underline mark around text beside them')
assert.equal(read('!adf:underline[!adf:mediaInline{id=m1}]\n'), 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps only nodes that take no underline mark: write the underline mark around text beside them')
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: write the link mark around text beside them')
@@ -1289,7 +1294,6 @@ test('reads the directive marks, the nesting outermost first', () => {
assert.deepEqual(content(losslessMarkdownToAdf('!adf:underline[!adf:underline[a]]\n')), wrapped(underline))
assert.deepEqual(content(losslessMarkdownToAdf('!adf:textColor[a]{color="#ae2e24"}\n')), wrapped({ attrs: { color: '#ae2e24' }, type: 'textColor' }))
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' }, type: 'date' }], type: 'paragraph' },
])
@@ -416,7 +416,7 @@ test('gives up every highlight run whose own delimiter cannot flank in one fallb
})
test('refuses a portable line holding a node only the carry spells, at the line', () => {
const refused = portableLineFallback([text('x', { type: 'odd' })], 'paragraph', ['content', 0])
const refused = portableLineFallback([text('x', { attrs: { a: 1 }, type: 'em' })], 'paragraph', ['content', 0])
assert.deepEqual(refused.ok ? refused : [refused.error.code, refused.error.path], ['unsupported-node-shape', ['content', 0]])
})