mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Split the markdown writer's tests into one file per concern
This commit is contained in:
@@ -4,8 +4,7 @@ import test from 'node:test'
|
||||
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { JsonValue } from '../../json-value.ts'
|
||||
import type { Result } from '../../result.ts'
|
||||
import { adfToLosslessMarkdown, losslessMarkdownToAdf } from '../../index.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { adfToLosslessMarkdown } from '../../index.ts'
|
||||
|
||||
function document(...content: AdfNode[]): AdfDocument {
|
||||
return { content, type: 'doc', version: 1 }
|
||||
@@ -15,109 +14,10 @@ function paragraph(...content: AdfNode[]): AdfNode {
|
||||
return content.length === 0 ? { type: 'paragraph' } : { content, type: 'paragraph' }
|
||||
}
|
||||
|
||||
function code(result: Result<string>): string {
|
||||
return result.ok ? `emitted ${JSON.stringify(result.value)}` : result.error.code
|
||||
}
|
||||
|
||||
function markdown(result: Result<string>): string {
|
||||
return result.ok ? result.value : `${result.error.code}: ${result.error.message}`
|
||||
}
|
||||
|
||||
function path(result: Result<string>): readonly (number | string)[] {
|
||||
return result.ok ? ['emitted'] : result.error.path
|
||||
}
|
||||
|
||||
function position(result: Result<string>): unknown {
|
||||
return result.ok ? 'emitted' : result.error.position
|
||||
}
|
||||
|
||||
test('names the node a refusal came from, and no source the emitter never read', () => {
|
||||
const unspellable: AdfNode = { text: 'x', type: 'paragraph' }
|
||||
const list: AdfNode = { content: [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }, { content: [unspellable], type: 'listItem' }], type: 'bulletList' }
|
||||
assert.deepEqual(path(adfToLosslessMarkdown(document(paragraph({ text: 'x', type: 'text' }), list))), ['content', 1, 'content', 1, 'content', 0])
|
||||
assert.deepEqual(path(adfToLosslessMarkdown(document(paragraph({ text: 'x', type: 'text' }, { text: 'x', type: 'status' })))), ['content', 0, 'content', 1])
|
||||
assert.deepEqual(path(adfToLosslessMarkdown({ type: 'doc', version: 2 })), [])
|
||||
assert.equal(position(adfToLosslessMarkdown(document(paragraph({ text: 'x', type: 'text' }), list))), undefined)
|
||||
})
|
||||
|
||||
test('refuses a node whose carry nests too deep ahead of a later node in its line', () => {
|
||||
let id: JsonValue = 'a'
|
||||
for (let depth = 0; depth < largestNesting; depth += 1) id = { id }
|
||||
const twiceStrong: AdfNode = { marks: [{ type: 'strong' }, { type: 'strong' }], text: 'x', type: 'text' }
|
||||
const refused = adfToLosslessMarkdown(document(paragraph({ attrs: { id }, type: 'mention' }, twiceStrong)))
|
||||
assert.equal(code(refused), 'unsupported-nesting-depth')
|
||||
assert.deepEqual(path(refused), ['content', 0, 'content', 0])
|
||||
})
|
||||
|
||||
test('refuses a carried run at the node whose carry nests too deep, among the run\'s own nodes', () => {
|
||||
let deep: JsonValue = 'a'
|
||||
for (let depth = 0; depth < largestNesting - 1; depth += 1) deep = { deep }
|
||||
const carriedEm: AdfNode = { marks: [{ attrs: { deep }, type: 'em' }], text: 'x', type: 'text' }
|
||||
const twiceStrong: AdfNode = { marks: [{ type: 'strong' }, { type: 'strong' }], text: 'y', type: 'text' }
|
||||
const refused = adfToLosslessMarkdown(document(paragraph({ text: 'ok', type: 'text' }, carriedEm, twiceStrong)))
|
||||
assert.equal(code(refused), 'unsupported-nesting-depth')
|
||||
assert.deepEqual(path(refused), ['content', 0, 'content', 1])
|
||||
})
|
||||
|
||||
test('refuses a value that is not an ADF document, naming the check it failed', () => {
|
||||
assert.equal(markdown(adfToLosslessMarkdown({ type: 'doc', version: Number.NaN })), 'not-an-adf-document: an ADF document holds a version number: found NaN')
|
||||
})
|
||||
|
||||
// The code, and the bound the message names.
|
||||
function pastTheBound(result: Result<string>): boolean {
|
||||
return !result.ok && result.error.code === 'unsupported-node-shape' && result.error.message.includes('536870888')
|
||||
}
|
||||
|
||||
// A code block of 300 lines; two of these together fall some 1000 characters short of V8's longest string, so only what their spelling adds passes it.
|
||||
function nearlyHalfTheBound(): AdfNode {
|
||||
return { attrs: {}, content: [{ text: `${'a'.repeat(894_782)}\n`.repeat(300), type: 'text' }], type: 'codeBlock' }
|
||||
}
|
||||
|
||||
// About 556 million characters of carry JSON: past V8's longest string, short of JavaScriptCore's and SpiderMonkey's.
|
||||
test('refuses a carry whose JSON would run past V8\'s longest string, on every engine', () => {
|
||||
let value: JsonValue = new Array<JsonValue>(560_000).fill(0)
|
||||
for (let level = 0; level < 495; level += 1) value = [value]
|
||||
const result = adfToLosslessMarkdown(document({ attrs: { value }, type: 'unknownBlock' }))
|
||||
assert.equal(pastTheBound(result), true)
|
||||
})
|
||||
|
||||
test('refuses a quote whose prefixed lines would pass V8\'s longest string', () => {
|
||||
const code = nearlyHalfTheBound()
|
||||
assert.equal(pastTheBound(adfToLosslessMarkdown(document({ content: [code, code], type: 'blockquote' }))), true)
|
||||
})
|
||||
|
||||
// A code block written as its text plus 53 characters; a blank line parts two blocks, and the markdown ends with a line break.
|
||||
function codeBlockOf(textLength: number): AdfNode {
|
||||
return { attrs: {}, content: [{ text: `${'a'.repeat(textLength - 1)}\n`, type: 'text' }], type: 'codeBlock' }
|
||||
}
|
||||
|
||||
test('refuses markdown one character past V8\'s longest string', () => {
|
||||
const code = codeBlockOf(268_435_390)
|
||||
assert.equal(pastTheBound(adfToLosslessMarkdown(document(code, code))), true)
|
||||
})
|
||||
|
||||
test('writes markdown exactly as long as V8\'s longest string', () => {
|
||||
const code = codeBlockOf(178_956_908)
|
||||
const written = adfToLosslessMarkdown(document(code, code, code))
|
||||
assert.equal(written.ok && written.value.length, 536870888)
|
||||
})
|
||||
|
||||
test('refuses text past V8\'s longest string', () => {
|
||||
const long = 'a'.repeat(2 ** 28)
|
||||
const result = adfToLosslessMarkdown(document(paragraph({ text: long, type: 'text' }, { text: long, type: 'text' }, { text: long, type: 'text' })))
|
||||
assert.equal(pastTheBound(result), true)
|
||||
})
|
||||
|
||||
// Bun stops a test after 5 s by default, and this one writes and reads back a line of 33 million characters.
|
||||
test('escapes more characters in a line than V8\'s Set holds', { timeout: 60000 }, () => {
|
||||
const stars = '*'.repeat(2 ** 24 + 1)
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph({ text: stars, type: 'text' })))), `${'\\*'.repeat(2 ** 24 + 1)}\n`)
|
||||
})
|
||||
|
||||
test('refuses a document version the markdown cannot carry', () => {
|
||||
assert.equal(code(adfToLosslessMarkdown({ type: 'doc', version: 2 })), 'unsupported-document-version')
|
||||
})
|
||||
|
||||
test('carries a text node attribute no spelling holds', () => {
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ attrs: { localId: 'a' }, text: 'x', type: 'text' })))),
|
||||
@@ -151,91 +51,6 @@ test('spells a code block language no info string holds as an attribute', () =>
|
||||
assert.equal(language('a\tb'), '!adf:codeBlock {language="a\\tb"}\n```\n```\n!adf:/codeBlock\n')
|
||||
})
|
||||
|
||||
test('spells a link destination CommonMark cannot as the directive link', () => {
|
||||
const link = (href: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs: { href }, type: 'link' }], text: 't', type: 'text' }))))
|
||||
assert.equal(link('https://example.com/a b>c'), '!adf:link[t]{href="https://example.com/a b>c"}\n')
|
||||
assert.equal(link('<https://example.com/'), '!adf:link[t]{href="\\u003chttps://example.com/"}\n')
|
||||
assert.equal(link('https://example.com/a\\b'), '!adf:link[t]{href="https://example.com/a\\\\b"}\n')
|
||||
assert.equal(link('https://example.com/?a=1&b=2'), '!adf:link[t]{href="https://example.com/?a=1\\u0026amp;b=2"}\n')
|
||||
assert.equal(link('https://example.com/a\nb'), '!adf:link[t]{href="https://example.com/a\\nb"}\n')
|
||||
const entity = 'https://example.com/?a=1&b=2'
|
||||
const autolinkShaped = document(paragraph({ marks: [{ attrs: { href: entity }, type: 'link' }], text: entity, type: 'text' }))
|
||||
assert.equal(markdown(adfToLosslessMarkdown(autolinkShaped)), '!adf:link[https://example.com/?a=1\\&b=2]{href="https://example.com/?a=1\\u0026amp;b=2"}\n')
|
||||
})
|
||||
|
||||
test('spells a link carrying an attribute CommonMark cannot as the directive link, and refuses none', () => {
|
||||
const link = (attrs: Record<string, string>): string =>
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs, type: 'link' }], text: 't', type: 'text' }))))
|
||||
assert.equal(link({ collection: 'c', href: 'https://example.com/' }), '!adf:link[t]{collection=c href="https://example.com/"}\n')
|
||||
assert.equal(link({ href: 'https://example.com/', id: 'i' }), '!adf:link[t]{href="https://example.com/" id=i}\n')
|
||||
assert.equal(link({ href: 'https://example.com/', occurrenceKey: 'k' }), '!adf:link[t]{href="https://example.com/" occurrenceKey=k}\n')
|
||||
assert.equal(link({ href: 'https://example.com/', id: 'i', title: 'a\nb' }), '!adf:link[t]{href="https://example.com/" id=i title="a\\nb"}\n')
|
||||
})
|
||||
|
||||
test('escapes the parenthesis a link destination leaves unbalanced, and no other', () => {
|
||||
const link = (href: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs: { href }, type: 'link' }], text: 't', type: 'text' }))))
|
||||
assert.equal(link('https://en.example.com/a_(b)'), '[t](https://en.example.com/a_(b))\n')
|
||||
assert.equal(link('https://example.com/a)b'), '[t](https://example.com/a\\)b)\n')
|
||||
assert.equal(link('https://example.com/a(b'), '[t](https://example.com/a\\(b)\n')
|
||||
assert.equal(link('https://example.com/)(') , '[t](https://example.com/\\)\\()\n')
|
||||
assert.equal(link('https://example.com/a (b'), '[t](<https://example.com/a (b>)\n')
|
||||
})
|
||||
|
||||
test('escapes the quote a link title holds, and spells the rest as the directive link', () => {
|
||||
const titled = (title: string): string =>
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs: { href: 'https://example.com/', title }, type: 'link' }], text: 't', type: 'text' }))))
|
||||
assert.equal(titled('He said "hi"'), '[t](https://example.com/ "He said \\"hi\\"")\n')
|
||||
assert.equal(titled('a\nb'), '!adf:link[t]{href="https://example.com/" title="a\\nb"}\n')
|
||||
assert.equal(titled('a\\b'), '!adf:link[t]{href="https://example.com/" title="a\\\\b"}\n')
|
||||
assert.equal(titled('a & b'), '!adf:link[t]{href="https://example.com/" title="a \\u0026amp; b"}\n')
|
||||
})
|
||||
|
||||
test('carries a link mark the link spelling cannot write', () => {
|
||||
const carried = (mark: AdfMark): string => markdown(adfToLosslessMarkdown(document(paragraph({ marks: [mark], text: 't', type: 'text' }))))
|
||||
assert.equal(
|
||||
carried({ attrs: { href: 'x', rel: 'y' }, type: 'link' }),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":\\"x\\",\\"rel\\":\\"y\\"},\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
assert.equal(carried({ attrs: { href: 4 }, type: 'link' }), '!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":4},\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n')
|
||||
assert.equal(carried({ type: 'link' }), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n')
|
||||
})
|
||||
|
||||
test('carries a mark the canonical spellings cannot nest', () => {
|
||||
const carried = (...marks: AdfMark[]): string => markdown(adfToLosslessMarkdown(document(paragraph({ marks, text: 'x', type: 'text' }))))
|
||||
assert.equal(carried({ type: 'annotation' }), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"annotation\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n')
|
||||
assert.equal(
|
||||
carried({ type: 'code' }, { type: 'strong' }),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"code\\"},{\\"type\\":\\"strong\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
assert.equal(
|
||||
carried({ attrs: { colour: 'red' }, type: 'em' }),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"colour\\":\\"red\\"},\\"type\\":\\"em\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
assert.equal(
|
||||
carried({ attrs: { localId: 'x' }, type: 'code' }),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"localId\\":\\"x\\"},\\"type\\":\\"code\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
})
|
||||
|
||||
function readsBack(doc: AdfDocument): string {
|
||||
const spelled = markdown(adfToLosslessMarkdown(doc))
|
||||
assert.deepEqual(losslessMarkdownToAdf(spelled), { ok: true, value: doc }, `reading ${JSON.stringify(spelled)}`)
|
||||
return spelled
|
||||
}
|
||||
|
||||
test('spells a code span opening a line, whose backticks open no fence', () => {
|
||||
const fence: AdfNode = { marks: [{ type: 'code' }], text: '``', type: 'text' }
|
||||
assert.equal(readsBack(document(paragraph(fence))), '``` `` ```\n')
|
||||
assert.equal(readsBack(document(paragraph({ text: 'a', type: 'text' }, { type: 'hardBreak' }, fence))), 'a\\\n``` `` ```\n')
|
||||
})
|
||||
|
||||
test('escapes the delimiter row a hard break leaves opening a pipe table with no leading pipe', () => {
|
||||
const broken = (second: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ text: 'a | b', type: 'text' }, { type: 'hardBreak' }, { text: second, type: 'text' }))))
|
||||
assert.equal(broken('--- | ---'), 'a | b\\\n\\--- | ---\n')
|
||||
assert.equal(broken(':--- | ---:'), 'a | b\\\n\\:--- | ---:\n')
|
||||
assert.equal(broken('c | d'), 'a | b\\\nc | d\n')
|
||||
})
|
||||
|
||||
test('alternates the marker between adjacent lists of a kind, the marker change being what parts them', () => {
|
||||
const list: AdfNode = { content: [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }], type: 'bulletList' }
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(list, list, list))), '- x\n\n* x\n\n- x\n')
|
||||
@@ -273,28 +88,6 @@ test('spells the code block whose language opens with the reserved info string p
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { language: 'adf' }, type: 'codeBlock' }))), '```adf\n```\n')
|
||||
})
|
||||
|
||||
test('breaks a mark run at the node it carries', () => {
|
||||
const strong: AdfMark = { type: 'strong' }
|
||||
const carried: AdfNode = { marks: [strong], type: 'placeholder' }
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [strong], text: 'a', type: 'text' }, carried, { marks: [strong], text: 'b', type: 'text' })))),
|
||||
'**a**!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"type\\":\\"placeholder\\"}"}**b**\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('refuses a carried node nested deeper than the levels its position leaves', () => {
|
||||
let node: AdfNode = { type: 'blockCard' }
|
||||
for (let depth = 0; depth < 600; depth += 1) node = { content: [node], type: 'blockCard' }
|
||||
assert.equal(code(adfToLosslessMarkdown(document(node))), 'unsupported-nesting-depth')
|
||||
assert.equal(code(adfToLosslessMarkdown(document(paragraph(node)))), 'unsupported-nesting-depth')
|
||||
let shallow: AdfNode = { type: 'blockCard' }
|
||||
for (let depth = 0; depth < 200; depth += 1) shallow = { content: [shallow], type: 'blockCard' }
|
||||
assert.ok(adfToLosslessMarkdown(document(shallow)).ok)
|
||||
let quoted: AdfNode = shallow
|
||||
for (let depth = 0; depth < 150; depth += 1) quoted = { content: [quoted], type: 'blockquote' }
|
||||
assert.equal(code(adfToLosslessMarkdown(document(quoted))), 'unsupported-nesting-depth')
|
||||
})
|
||||
|
||||
test('carries a code block holding a node no fence holds', () => {
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ content: [paragraph()], type: 'codeBlock' }))), '```adf:codeBlock\n{\n "content": [\n {\n "type": "paragraph"\n }\n ]\n}\n```\n')
|
||||
for (const child of [{ attrs: {}, text: 'x', type: 'text' }, { content: [], text: 'x', type: 'text' }, { marks: [], text: 'x', type: 'text' }, { content: [{ text: 'lost', type: 'text' }], text: 'x', type: 'text' }]) {
|
||||
@@ -317,173 +110,11 @@ test('spells an ordered list no marker fits as a directive', () => {
|
||||
assert.equal(markdown(adfToLosslessMarkdown(list(999999999, 2))), '!adf:orderedList {order=999999999}\n!adf:listItem\nx\n!adf:/listItem\n!adf:listItem\nx\n!adf:/listItem\n!adf:/orderedList\n')
|
||||
})
|
||||
|
||||
test('carries a code mark over anything but text', () => {
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ type: 'code' }], type: 'hardBreak' })))),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"code\\"}],\\"type\\":\\"hardBreak\\"}"}\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('carries every inline node of a line that only the carry spells', () => {
|
||||
const odd: AdfNode = { attrs: { id: 'a', odd: 'b' }, type: 'mention' }
|
||||
const carriedOdd = '!adf:carry{json="{\\"attrs\\":{\\"id\\":\\"a\\",\\"odd\\":\\"b\\"},\\"type\\":\\"mention\\"}"}'
|
||||
const unlinked = '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"link\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}'
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph(odd, { text: ' ', type: 'text' }, { marks: [{ type: 'link' }], text: 'x', type: 'text' }, { text: ' ', type: 'text' }, odd)))),
|
||||
`${carriedOdd} ${unlinked} ${carriedOdd}\n`,
|
||||
)
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ text: 'a', type: 'text' }, { attrs: { foo: 1 }, type: 'hardBreak' }, { text: 'b', type: 'text' })))),
|
||||
'a!adf:carry{json="{\\"attrs\\":{\\"foo\\":1},\\"type\\":\\"hardBreak\\"}"}b\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('carries an inline node holding a mark Atlassian\'s schema withholds from it', () => {
|
||||
const carried = (node: AdfNode): string => markdown(adfToLosslessMarkdown(document(paragraph(node))))
|
||||
assert.equal(carried({ attrs: { id: 'b1c2', text: '@A' }, marks: [{ type: 'strong' }], type: 'mention' }), '!adf:carry{json="{\\"attrs\\":{\\"id\\":\\"b1c2\\",\\"text\\":\\"@A\\"},\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"type\\":\\"mention\\"}"}\n')
|
||||
assert.equal(carried({ attrs: { id: 'm1' }, marks: [{ type: 'em' }], type: 'mediaInline' }), '!adf:carry{json="{\\"attrs\\":{\\"id\\":\\"m1\\"},\\"marks\\":[{\\"type\\":\\"em\\"}],\\"type\\":\\"mediaInline\\"}"}\n')
|
||||
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', () => {
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { level: 7 }, content: [{ text: 'x', type: 'text' }], type: 'heading' }))), '!adf:heading {level=7}\nx\n!adf:/heading\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ content: [{ text: 'x', type: 'text' }], type: 'heading' }))), '!adf:heading\nx\n!adf:/heading\n')
|
||||
})
|
||||
|
||||
test('escapes only text that would otherwise open a construct', () => {
|
||||
const emitted = (text: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ text, type: 'text' }))))
|
||||
assert.equal(emitted('<div>'), '\\<div>\n')
|
||||
assert.equal(emitted('<div'), '\\<div\n')
|
||||
assert.equal(emitted('<div and more'), '\\<div and more\n')
|
||||
assert.equal(emitted('<pre'), '\\<pre\n')
|
||||
assert.equal(emitted('<!x'), '\\<!x\n')
|
||||
assert.equal(emitted('<!-- x'), '\\<!-- x\n')
|
||||
assert.equal(emitted('<?php'), '\\<?php\n')
|
||||
assert.equal(emitted('<![CDATA[x'), '\\<![CDATA[x\n')
|
||||
assert.equal(emitted('<span'), '<span\n')
|
||||
assert.equal(emitted('a < b'), 'a < b\n')
|
||||
assert.equal(emitted('& & x'), '\\& & x\n')
|
||||
assert.equal(emitted('¬areference; x'), '¬areference; x\n')
|
||||
assert.equal(emitted('a <b@c.d> e'), 'a \\<b@c.d> e\n')
|
||||
assert.equal(emitted('a <b 2'), 'a <b 2\n')
|
||||
assert.equal(emitted('a <div b'), 'a <div b\n')
|
||||
assert.equal(emitted('a <!-- b'), 'a <!-- b\n')
|
||||
assert.equal(emitted('a <!-- b --> c'), 'a \\<!-- b --> c\n')
|
||||
const later = paragraph({ text: 'a', type: 'text' }, { type: 'hardBreak' }, { text: '<div', type: 'text' })
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(later))), 'a\\\n\\<div\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ content: [paragraph({ text: '<!-- x', type: 'text' })], type: 'blockquote' }))), '> \\<!-- x\n')
|
||||
assert.equal(emitted('| a | b |'), '\\| a | b |\n')
|
||||
assert.equal(emitted('!adf:mention[@x]{id=1}'), '\\!adf:mention[@x]{id=1}\n')
|
||||
assert.equal(emitted('!adf: completes nothing'), '\\!adf: completes nothing\n')
|
||||
assert.equal(emitted('!adfx and a! alone'), '!adfx and a! alone\n')
|
||||
assert.equal(emitted('!adf:/panel'), '\\!adf:/panel\n')
|
||||
assert.equal(emitted('!adf:rule'), '\\!adf:rule\n')
|
||||
assert.equal(emitted(':::panel info'), ':::panel info\n')
|
||||
assert.equal(emitted('10:30 tomorrow'), '10:30 tomorrow\n')
|
||||
assert.equal(emitted('[a](b)'), '\\[a](b)\n')
|
||||
assert.equal(emitted('[a]: b'), '\\[a]: b\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { level: 1 }, content: [{ text: '[a]: b', type: 'text' }], type: 'heading' }))), '# [a]: b\n')
|
||||
assert.equal(emitted('**bold**'), '\\*\\*bold\\*\\*\n')
|
||||
assert.equal(emitted('a `x` b'), 'a \\`x` b\n')
|
||||
assert.equal(emitted('~~struck~~'), '\\~~struck\\~~\n')
|
||||
assert.equal(emitted('a \\* b'), 'a \\\\\\* b\n')
|
||||
assert.equal(emitted('1. not a list'), '1\\. not a list\n')
|
||||
assert.equal(emitted('*"quoted"*'), '\\*"quoted"\\*\n')
|
||||
assert.equal(emitted('x"_y"'), 'x"\\_y"\n')
|
||||
})
|
||||
|
||||
test('escapes every text bracket in a line where one opens link text and every text backtick where one opens a code span, and none where none does', () => {
|
||||
const emitted = (text: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ text, type: 'text' }))))
|
||||
assert.equal(emitted('[a](b) [c]'), '\\[a](b) \\[c]\n')
|
||||
assert.equal(emitted('see [1] and [docs](url) [2]'), 'see \\[1] and \\[docs](url) \\[2]\n')
|
||||
assert.equal(emitted('[a] b](c'), '[a] b](c\n')
|
||||
assert.equal(emitted('[x]('), '[x](\n')
|
||||
assert.equal(emitted('Use ` or ``code`` syntax'), 'Use \\` or \\`\\`code\\`\\` syntax\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ type: 'code' }], text: 'c', type: 'text' }, { text: 'a ` b', type: 'text' })))), '`c`a ` b\n')
|
||||
})
|
||||
|
||||
test('keeps a text backtick bare in a line where a text bracket opens a link the reader refuses', () => {
|
||||
const refused = paragraph({ text: 'a ` [', type: 'text' }, { attrs: { id: '1' }, type: 'mention' }, { text: '](u)', type: 'text' })
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(refused))), 'a ` \\[!adf:mention{id=1}](u)\n')
|
||||
})
|
||||
|
||||
test('escapes a heading closing sequence', () => {
|
||||
const heading = (text: string): string => markdown(adfToLosslessMarkdown(document({ attrs: { level: 2 }, content: [{ text, type: 'text' }], type: 'heading' })))
|
||||
assert.equal(heading('done #'), '## done \\#\n')
|
||||
assert.equal(heading('#tag first'), '## #tag first\n')
|
||||
})
|
||||
|
||||
test('wraps adjacent nodes carrying one mark once, and a differing mark twice', () => {
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const emitted = (...content: AdfNode[]): string => markdown(adfToLosslessMarkdown(document(paragraph(...content))))
|
||||
assert.equal(emitted(marked('a', { type: 'strong' }), marked('b', { type: 'strong' }, { type: 'em' })), '**a*b***\n')
|
||||
assert.equal(emitted(marked('a', { type: 'strong' }), marked('b', { type: 'em' })), '**a**_b_\n')
|
||||
const link = (href: string): AdfMark => ({ attrs: { href }, type: 'link' })
|
||||
assert.equal(emitted(marked('a', link('http://x')), marked('b', link('http://y'))), '[a](http://x)[b](http://y)\n')
|
||||
})
|
||||
|
||||
test('carries a mark spelling that cannot open or close where it sits', () => {
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const emitted = (...content: AdfNode[]): string => markdown(adfToLosslessMarkdown(document(paragraph(...content))))
|
||||
const strong: AdfMark = { type: 'strong' }
|
||||
assert.equal(
|
||||
emitted({ text: 'un', type: 'text' }, marked('-real', strong), { text: 'istic', type: 'text' }),
|
||||
'un!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"text\\":\\"-real\\",\\"type\\":\\"text\\"}"}istic\n',
|
||||
)
|
||||
assert.equal(
|
||||
emitted(marked('C++', { type: 'em' }), { text: 'ish', type: 'text' }),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"C++\\",\\"type\\":\\"text\\"}"}ish\n',
|
||||
)
|
||||
assert.equal(
|
||||
emitted({ text: 'x', type: 'text' }, marked('.a', strong)),
|
||||
'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"text\\":\\".a\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
assert.equal(emitted({ text: 'un ', type: 'text' }, marked('-real', strong), { text: ' istic', type: 'text' }), 'un **-real** istic\n')
|
||||
const carriedR = '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"text\\":\\"-r\\",\\"type\\":\\"text\\"}"}'
|
||||
assert.equal(emitted({ text: 'un', type: 'text' }, marked('-r', strong), { text: 'un', type: 'text' }, marked('-r', strong), { text: 'un', type: 'text' }), `un${carriedR}un${carriedR}un\n`)
|
||||
assert.equal(
|
||||
emitted({ text: 'x', type: 'text' }, marked('[', { type: 'em' }, { type: 'strike' }), marked('a', { type: 'em' })),
|
||||
'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"},{\\"type\\":\\"strike\\"}],\\"text\\":\\"[\\",\\"type\\":\\"text\\"}"}_a_\n',
|
||||
)
|
||||
assert.equal(emitted(marked('a.', strong)), '**a.**\n')
|
||||
assert.equal(emitted({ text: 'x', type: 'text' }, marked('a', strong), { text: 'y', type: 'text' }), 'x**a**y\n')
|
||||
const em: AdfMark = { type: 'em' }
|
||||
assert.equal(
|
||||
emitted({ text: 'x', type: 'text' }, marked('a.', em), marked('b', strong)),
|
||||
'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"a.\\",\\"type\\":\\"text\\"}"}**b**\n',
|
||||
)
|
||||
assert.equal(emitted({ text: 'x', type: 'text' }, marked('ab', em, strong), { text: 'y', type: 'text' }), 'x***ab***y\n')
|
||||
})
|
||||
|
||||
test('carries the mark spelling that cannot close where it sits, and keeps the later one the reader would pair it with', () => {
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const emitted = (...content: AdfNode[]): string => markdown(adfToLosslessMarkdown(document(paragraph(...content))))
|
||||
const b: AdfNode = { text: 'b', type: 'text' }
|
||||
assert.equal(emitted(marked(':', { type: 'em' }), b, marked('c', { type: 'em' })), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\":\\",\\"type\\":\\"text\\"}"}b*c*\n')
|
||||
assert.equal(emitted(marked(')', { type: 'strong' }), b, marked('c', { type: 'strong' })), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"text\\":\\")\\",\\"type\\":\\"text\\"}"}b**c**\n')
|
||||
assert.equal(emitted(marked('^', { type: 'strike' }), b, marked('c', { type: 'strike' })), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strike\\"}],\\"text\\":\\"^\\",\\"type\\":\\"text\\"}"}b~~c~~\n')
|
||||
})
|
||||
|
||||
test('carries every node holding em, strike or strong, and no other, in a line still misread after 32 passes', () => {
|
||||
const marked = (text: string, ...types: string[]): AdfNode => ({ marks: types.map((type) => ({ type })), text, type: 'text' })
|
||||
const carry = (node: AdfNode): string => `!adf:carry{json="${JSON.stringify(node).replaceAll('"', '\\"')}"}`
|
||||
const repeat = [marked(' ~~ ', 'em', 'strong'), marked(')', 'em', 'strike', 'strong'), { text: 'a', type: 'text' }, marked('.unun', 'strike', 'strong')]
|
||||
const written = repeat.map((node) => (node.marks === undefined ? 'a' : carry(node))).join('')
|
||||
const line = [...Array.from({ length: 40 }, () => repeat).flat(), { text: ' ', type: 'text' }, { marks: [{ attrs: { href: 'u' }, type: 'link' }], text: 'b', type: 'text' }]
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph(...line)))), `${written.repeat(40)} [b](u)\n`)
|
||||
})
|
||||
|
||||
test('refuses a node carrying one mark type twice', () => {
|
||||
const em: AdfMark = { type: 'em' }
|
||||
assert.equal(code(adfToLosslessMarkdown(document(paragraph({ marks: [em, em], text: 'x', type: 'text' })))), 'unsupported-node-shape')
|
||||
})
|
||||
|
||||
test('parts a nested list the tight spelling would swallow from the block above it', () => {
|
||||
const item = (...content: AdfNode[]): AdfNode => (content.length === 0 ? { type: 'listItem' } : { content, type: 'listItem' })
|
||||
const text = (value: string): AdfNode => ({ content: [{ text: value, type: 'text' }], type: 'paragraph' })
|
||||
@@ -499,101 +130,6 @@ test('parts a nested list the tight spelling would swallow from the block above
|
||||
assert.ok(markdown(adfToLosslessMarkdown(outer(text('a'), { ...list, attrs: { unknown: 'x' } }))).startsWith('- a\n\n ```adf:bulletList\n'))
|
||||
})
|
||||
|
||||
test('refuses marks and attributes nested deeper than the emitter carries', () => {
|
||||
const marks: AdfMark[] = Array.from({ length: 600 }, (_, index) => ({ type: index % 2 === 0 ? 'em' : 'strong' }))
|
||||
assert.equal(code(adfToLosslessMarkdown(document(paragraph({ marks, text: 'x', type: 'text' })))), 'unsupported-nesting-depth')
|
||||
const deepest: AdfMark[] = Array.from({ length: 20_000 }, (_, index) => ({ type: index % 2 === 0 ? 'em' : 'strong' }))
|
||||
assert.equal(code(adfToLosslessMarkdown(document(paragraph({ marks: deepest, text: 'x', type: 'text' }, { type: 'hardBreak' }, { marks: deepest, text: 'y', type: 'text' })))), 'unsupported-nesting-depth')
|
||||
let attrs: AdfMark['attrs'] = { depth: 'x' }
|
||||
for (let depth = 0; depth < 600; depth += 1) attrs = { depth: attrs }
|
||||
const deeper = (key: string, type: string): string =>
|
||||
`unsupported-nesting-depth: the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries`
|
||||
const nested = (levels: number): JsonValue => {
|
||||
let value: JsonValue = 1
|
||||
for (let level = 0; level < levels; level += 1) value = [value]
|
||||
return value
|
||||
}
|
||||
const card = (levels: number): AdfNode => ({ attrs: { data: nested(levels), url: 'https://example.com/a' }, type: 'inlineCard' })
|
||||
const marked = (levels: number): AdfNode => ({
|
||||
attrs: { panelType: 'info' },
|
||||
content: [paragraph({ text: 'x', type: 'text' })],
|
||||
marks: [{ attrs: { deep: nested(levels) }, type: 'em' }],
|
||||
type: 'panel',
|
||||
})
|
||||
const roundTrips = (node: AdfNode): void => {
|
||||
const spelled = adfToLosslessMarkdown(document(node))
|
||||
assert.ok(spelled.ok, spelled.ok ? '' : spelled.error.message)
|
||||
const read = losslessMarkdownToAdf(spelled.value)
|
||||
assert.ok(read.ok, read.ok ? '' : read.error.message)
|
||||
assert.deepEqual(read.value, document(node))
|
||||
}
|
||||
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))), deeper('depth', 'em'))
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs: { deep: nested(largestNesting) }, type: 'em' }], text: 'x', type: 'text' })))),
|
||||
`unsupported-nesting-depth: a carried node's JSON nests deeper than the ${largestNesting} levels its position leaves`,
|
||||
)
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph(card(largestNesting + 1))))), deeper('data', 'inlineCard'))
|
||||
assert.deepEqual(path(adfToLosslessMarkdown(document(paragraph(card(largestNesting + 1))))), [])
|
||||
roundTrips(paragraph(card(largestNesting)))
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(marked(largestNesting - 2)))), deeper('marks', 'panel'))
|
||||
assert.deepEqual(path(adfToLosslessMarkdown(document(marked(largestNesting - 2)))), ['content', 0])
|
||||
const markedCode: AdfNode = { content: [{ text: 'x', type: 'text' }], marks: [{ attrs: { deep: nested(largestNesting - 2) }, type: 'em' }], type: 'codeBlock' }
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(markedCode))), deeper('marks', 'codeBlock'))
|
||||
roundTrips(marked(largestNesting - 3))
|
||||
})
|
||||
|
||||
test('escapes a literal delimiter that would merge with an emitted one', () => {
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const emitted = (...content: AdfNode[]): string => markdown(adfToLosslessMarkdown(document(paragraph(...content))))
|
||||
assert.equal(emitted(marked('a_', { type: 'em' })), '_a\\__\n')
|
||||
assert.equal(emitted(marked('_a', { type: 'em' })), '_\\_a_\n')
|
||||
assert.equal(emitted(marked('a*', { type: 'strong' })), '**a\\***\n')
|
||||
assert.equal(emitted(marked('~a', { type: 'strike' })), '~~\\~a~~\n')
|
||||
assert.equal(
|
||||
emitted({ text: 'x', type: 'text' }, marked('~a', { type: 'strike' })),
|
||||
'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strike\\"}],\\"text\\":\\"~a\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
assert.equal(emitted({ text: '`', type: 'text' }, marked('x', { type: 'code' })), '\\``x`\n')
|
||||
assert.equal(emitted(marked('x', { type: 'code' }), { text: '`', type: 'text' }), '`x`\\`\n')
|
||||
assert.equal(emitted({ text: '`a', type: 'text' }, marked('b', { type: 'code' })), '\\`a`b`\n')
|
||||
assert.equal(
|
||||
emitted({ text: '<https://example.com/', type: 'text' }, { attrs: { url: 'x>' }, type: 'inlineCard' }),
|
||||
'\\<https://example.com/!adf:inlineCard{url="x>"}\n',
|
||||
)
|
||||
assert.equal(emitted({ text: '!', type: 'text' }, marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' })), '\\\n')
|
||||
assert.equal(emitted({ text: '[a', type: 'text' }, marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' })), '[a[x](https://example.com/)\n')
|
||||
assert.equal(emitted({ text: '[a](b)', type: 'text' }), '\\[a](b)\n')
|
||||
assert.equal(emitted(marked('x', { type: 'underline' }), { text: '{}', type: 'text' }), '!adf:underline[x]\\{}\n')
|
||||
assert.equal(emitted({ attrs: { text: '' }, type: 'status' }, { text: '{color=red}', type: 'text' }), '!adf:status[]\\{color=red}\n')
|
||||
assert.equal(emitted(marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' }), { text: '{}', type: 'text' }), '[x](https://example.com/){}\n')
|
||||
})
|
||||
|
||||
test('escapes a literal delimiter run that only closes', () => {
|
||||
const emitted = (text: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ text, type: 'text' }))))
|
||||
assert.equal(emitted('a* b'), 'a\\* b\n')
|
||||
assert.equal(emitted('2 * 3'), '2 * 3\n')
|
||||
})
|
||||
|
||||
test("spells a mark run CommonMark's matching pairs as written", () => {
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const emitted = (...content: AdfNode[]): string => markdown(adfToLosslessMarkdown(document(paragraph(...content))))
|
||||
const em: AdfMark = { type: 'em' }
|
||||
const strong: AdfMark = { type: 'strong' }
|
||||
assert.equal(emitted({ text: 'un', type: 'text' }, marked('a', em, strong), { text: 'istic', type: 'text' }), 'un***a***istic\n')
|
||||
assert.equal(emitted({ text: 're', type: 'text' }, marked('structure', strong), { text: ' the code', type: 'text' }), 're**structure** the code\n')
|
||||
assert.equal(
|
||||
emitted({ text: 'un', type: 'text' }, marked('a', em), marked('b', em, strong), marked('c', em), { text: 'istic', type: 'text' }),
|
||||
'un*a**b**c*istic\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('escapes a hyphen underline a hard break would expose', () => {
|
||||
const line = (text: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ text: 'foo', type: 'text' }, { type: 'hardBreak' }, { text, type: 'text' }))))
|
||||
assert.equal(line('--'), 'foo\\\n\\--\n')
|
||||
assert.equal(line('=='), 'foo\\\n\\==\n')
|
||||
})
|
||||
|
||||
test('spells a list item whose marker completes a thematic break as a directive', () => {
|
||||
const item = (...content: AdfNode[]): AdfNode => (content.length === 0 ? { type: 'listItem' } : { content, type: 'listItem' })
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ content: [item({ type: 'rule' })], type: 'bulletList' }))), '!adf:bulletList\n!adf:listItem\n---\n!adf:/listItem\n!adf:/bulletList\n')
|
||||
@@ -611,77 +147,6 @@ test('spells a list item whose marker completes a thematic break as a directive'
|
||||
)
|
||||
})
|
||||
|
||||
test('spells the characters CommonMark rewrites', () => {
|
||||
assert.equal(readsBack(document(paragraph({ text: 'a\r\r\nb', type: 'text' }))), 'a !adf:text{text="\\n"}b\n')
|
||||
assert.equal(readsBack(document(paragraph({ marks: [{ type: 'strong' }], text: '\u0000\u0000a\u0000', type: 'text' }))), '**!adf:text{text="\\u0000\\u0000"}a!adf:text{text="\\u0000"}**\n')
|
||||
assert.equal(readsBack(document(paragraph({ marks: [{ attrs: { href: 'u' }, type: 'link' }], text: 'a\rb\u0000', type: 'text' }))), '[a b!adf:text{text="\\u0000"}](u)\n')
|
||||
assert.equal(readsBack(document({ attrs: { level: 1 }, content: [{ text: '\r', type: 'text' }], type: 'heading' })), '# \n')
|
||||
assert.equal(readsBack(document({ content: [{ text: 'a\rb', type: 'text' }], type: 'codeBlock' })), '```adf:codeBlock\n{\n "content": [\n {\n "text": "a\\rb",\n "type": "text"\n }\n ]\n}\n```\n')
|
||||
readsBack(document({ content: [{ text: 'a\u0000b', type: 'text' }], type: 'codeBlock' }))
|
||||
readsBack(document(paragraph({ marks: [{ type: 'code' }], text: 'a\u0000b', type: 'text' })))
|
||||
})
|
||||
|
||||
test('carries a text node holding no text, or holding content', () => {
|
||||
const nested: AdfNode[] = [{ text: 'lost', type: 'text' }]
|
||||
assert.equal(readsBack(document(paragraph({ text: '', type: 'text' }))), '!adf:carry{json="{\\"text\\":\\"\\",\\"type\\":\\"text\\"}"}\n')
|
||||
assert.equal(readsBack(document(paragraph({ type: 'text' }, { text: 'a', type: 'text' }))), '!adf:carry{json="{\\"type\\":\\"text\\"}"}a\n')
|
||||
readsBack(document(paragraph({ marks: [{ type: 'code' }], text: '', type: 'text' })))
|
||||
readsBack(document(paragraph({ marks: [{ type: 'em' }], text: 'a', type: 'text' }, { marks: [{ type: 'em' }], text: '', type: 'text' })))
|
||||
readsBack(document(paragraph({ content: nested, text: 'x', type: 'text' })))
|
||||
readsBack(document(paragraph({ content: nested, marks: [{ type: 'code' }], text: 'x', type: 'text' })))
|
||||
readsBack(document({ content: [{ text: '', type: 'text' }], type: 'codeBlock' }))
|
||||
readsBack(document({ content: [{ text: 'a', type: 'text' }, { type: 'text' }], type: 'codeBlock' }))
|
||||
})
|
||||
|
||||
test('carries a mark run whose edge holds whitespace CommonMark flanking counts', () => {
|
||||
const em = { type: 'em' }
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [em], text: '\u00a0a', type: 'text' })))),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"\u00a0a\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('pads a code span whose edges CommonMark would strip', () => {
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ type: 'code' }], text: ' \t ', type: 'text' })))), '` \t `\n')
|
||||
})
|
||||
|
||||
test('spells a code span per code-marked node, parted by the text break', () => {
|
||||
const code_ = { type: 'code' }
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [code_], text: 'a', type: 'text' }, { marks: [code_], text: 'b', type: 'text' })))),
|
||||
'`a`!adf:textBreak{}`b`\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('refuses a document nested deeper than the emitter carries', () => {
|
||||
let node: AdfNode = paragraph({ text: 'x', type: 'text' })
|
||||
for (let depth = 0; depth < 600; depth += 1) node = { content: [node], type: 'blockquote' }
|
||||
assert.equal(code(adfToLosslessMarkdown(document(node))), 'unsupported-nesting-depth')
|
||||
let carried: AdfNode = paragraph({ text: 'x', type: 'text' })
|
||||
for (let depth = 0; depth < 500; depth += 1) carried = { content: [carried], type: 'blockquote' }
|
||||
assert.ok(adfToLosslessMarkdown(document(carried)).ok)
|
||||
const listed = (levels: number, first: readonly AdfNode[]): AdfNode => {
|
||||
let list: AdfNode = { content: [{ content: [...first], type: 'listItem' }], type: 'bulletList' }
|
||||
for (let level = 1; level < levels; level += 1) list = { content: [{ content: [...first, list], type: 'listItem' }], type: 'bulletList' }
|
||||
return list
|
||||
}
|
||||
assert.ok(adfToLosslessMarkdown(document(listed(largestNesting, [paragraph({ text: 'x', type: 'text' })]))).ok)
|
||||
assert.equal(code(adfToLosslessMarkdown(document(listed(largestNesting + 1, [paragraph({ text: 'x', type: 'text' })])))), 'unsupported-nesting-depth')
|
||||
const directiveLists = largestNesting / 2
|
||||
const deep = document(listed(directiveLists, [{ type: 'rule' }]))
|
||||
assert.deepEqual(losslessMarkdownToAdf(markdown(adfToLosslessMarkdown(deep))), { ok: true, value: deep })
|
||||
assert.equal(code(adfToLosslessMarkdown(document(listed(directiveLists + 1, [{ type: 'rule' }])))), 'unsupported-nesting-depth')
|
||||
assert.equal(code(adfToLosslessMarkdown(document({ content: [listed(directiveLists, [{ type: 'rule' }])], type: 'panel' }))), 'unsupported-nesting-depth')
|
||||
const chain = (levels: number): AdfNode => {
|
||||
let card: AdfNode = { type: 'blockCard' }
|
||||
for (let level = 0; level < levels; level += 1) card = { content: [card], type: 'blockCard' }
|
||||
return card
|
||||
}
|
||||
const carriedInItem = (levels: number): AdfDocument => document(listed(1, [{ type: 'rule' }, chain(levels)]))
|
||||
assert.deepEqual(losslessMarkdownToAdf(markdown(adfToLosslessMarkdown(carriedInItem(248)))), { ok: true, value: carriedInItem(248) })
|
||||
assert.equal(code(adfToLosslessMarkdown(carriedInItem(249))), 'unsupported-nesting-depth')
|
||||
})
|
||||
|
||||
test('emits an empty list item without trailing whitespace', () => {
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ content: [{ type: 'listItem' }], type: 'bulletList' }))), '-\n')
|
||||
})
|
||||
@@ -770,227 +235,3 @@ test('separates blocks in a container body by a blank line only where a directiv
|
||||
assert.equal(markdown(adfToLosslessMarkdown(panel(caption, text('a')))), '!adf:panel info\n!adf:caption\nc\n!adf:/caption\na\n!adf:/panel\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(panel(paragraph(), text('a')))), '!adf:panel info\n!adf:paragraph\n!adf:/paragraph\na\n!adf:/panel\n')
|
||||
})
|
||||
|
||||
test('spells the image form for exactly the centered external media shape', () => {
|
||||
const url = 'https://example.com/moon.png'
|
||||
const single = (attrs: AdfAttributes, ...content: AdfNode[]): AdfDocument =>
|
||||
document({ attrs: { layout: 'center' }, content: [content.length === 0 ? { attrs, type: 'media' } : { attrs, content, type: 'media' }], type: 'mediaSingle' })
|
||||
assert.equal(markdown(adfToLosslessMarkdown(single({ alt: 'The moon', type: 'external', url }))), `\n`)
|
||||
assert.equal(markdown(adfToLosslessMarkdown(single({ type: 'external', url }))), `\n`)
|
||||
assert.equal(markdown(adfToLosslessMarkdown(single({ alt: 'a [b] c', type: 'external', url }))), `![a \\[b\\] c](${url})\n`)
|
||||
const fallback = (attrs: AdfAttributes): boolean => markdown(adfToLosslessMarkdown(single(attrs))).startsWith('!adf:mediaSingle {layout=center}')
|
||||
assert.ok(fallback({ alt: '', type: 'external', url }))
|
||||
assert.ok(fallback({ alt: 'a\nb', type: 'external', url }))
|
||||
assert.ok(fallback({ alt: 'a\u0000b', type: 'external', url }))
|
||||
assert.ok(fallback({ type: 'external', url: 'https://example.com/a b>c' }))
|
||||
assert.ok(fallback({ alt: ' moon ', type: 'external', url }))
|
||||
assert.ok(fallback({ alt: 4, type: 'external', url }))
|
||||
assert.ok(fallback({ type: 'external', url: 4 }))
|
||||
assert.equal(code(adfToLosslessMarkdown(single({ type: 'external', url }, paragraph()))), 'unsupported-node-shape')
|
||||
const media: AdfNode = { attrs: { type: 'external', url }, type: 'media' }
|
||||
assert.equal(code(adfToLosslessMarkdown(document({ attrs: { layout: 'center' }, content: [media], text: 'x', type: 'mediaSingle' }))), 'unsupported-node-shape')
|
||||
assert.equal(code(adfToLosslessMarkdown(document({ attrs: { layout: 'center' }, content: [{ ...media, text: 'x' }], type: 'mediaSingle' }))), 'unsupported-node-shape')
|
||||
})
|
||||
|
||||
test('spells a media only a link marks as the image the link wraps', () => {
|
||||
const url = 'https://example.com/moon.png'
|
||||
const linked = (alt: string | undefined, ...marks: AdfMark[]): AdfDocument =>
|
||||
document({ attrs: { layout: 'center' }, content: [{ attrs: alt === undefined ? { type: 'external', url } : { alt, type: 'external', url }, marks, type: 'media' }], type: 'mediaSingle' })
|
||||
const link = (attrs: AdfAttributes): AdfMark => ({ attrs, type: 'link' })
|
||||
const spelled = (written: AdfDocument, expected: string): void => {
|
||||
assert.equal(markdown(adfToLosslessMarkdown(written)), expected)
|
||||
assert.deepEqual(losslessMarkdownToAdf(expected), { ok: true, value: written })
|
||||
}
|
||||
spelled(linked('The moon', link({ href: 'https://example.com/docs' })), `[](https://example.com/docs)\n`)
|
||||
spelled(linked(undefined, link({ href: 'https://example.com/docs', title: 'Docs "v2"' })), `[](https://example.com/docs "Docs \\"v2\\"")\n`)
|
||||
spelled(linked('a [b] c', link({ href: '/a b' })), `[![a \\[b\\] c](${url})](</a b>)\n`)
|
||||
spelled(linked('m', link({ href: '' })), `[]()\n`)
|
||||
const fallback = (document: AdfDocument): boolean => markdown(adfToLosslessMarkdown(document)).startsWith('!adf:mediaSingle {layout=center}')
|
||||
assert.ok(fallback(linked('m', link({ href: '/d', id: 'x' }))))
|
||||
assert.ok(fallback(linked('m', link({ href: '/d' }), { attrs: { color: '#000', size: 1 }, type: 'border' })))
|
||||
assert.ok(fallback(linked('m', link({ href: 'a\\b' }))))
|
||||
assert.ok(fallback(linked('m', link({ title: 't' }))))
|
||||
assert.ok(fallback(linked('m', link({ href: '/d', title: 4 }))))
|
||||
const captioned: AdfNode = { attrs: { layout: 'center' }, content: [{ attrs: { alt: 'm', type: 'external', url }, type: 'media' }, { content: [{ text: 'c', type: 'text' }], type: 'caption' }], type: 'mediaSingle' }
|
||||
assert.ok(fallback(document(captioned)))
|
||||
})
|
||||
|
||||
test('spells a mediaSingle the image form does not fit as a directive', () => {
|
||||
const media: AdfNode = { attrs: { type: 'external', url: 'https://example.com/moon.png' }, type: 'media' }
|
||||
const single: AdfNode = { attrs: { layout: 'center' }, content: [media], marks: [{ type: 'border' }], type: 'mediaSingle' }
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(single))),
|
||||
'!adf:mediaSingle {layout=center marks=border}\n!adf:media {type=external url="https://example.com/moon.png"}\n!adf:/mediaSingle\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('spells a table as a pipe table only where every row and cell is plain', () => {
|
||||
const text = (value: string): AdfNode => ({ content: [{ text: value, type: 'text' }], type: 'paragraph' })
|
||||
const cell = (type: string, ...content: AdfNode[]): AdfNode => ({ content, type })
|
||||
const row = (...cells: AdfNode[]): AdfNode => ({ content: cells, type: 'tableRow' })
|
||||
const table = (...rows: AdfNode[]): AdfDocument => document({ content: rows, type: 'table' })
|
||||
const directive = (result: Result<string>): boolean => markdown(result).startsWith('!adf:table')
|
||||
const header = row(cell('tableHeader', text('Part')))
|
||||
assert.equal(markdown(adfToLosslessMarkdown(table(header, row(cell('tableCell', text('Bolt M8')))))), '| Part |\n| --- |\n| Bolt M8 |\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(table(row(cell('tableHeader', text('a|b')))))), '| a\\|b |\n| --- |\n')
|
||||
assert.ok(directive(adfToLosslessMarkdown(table())))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row()))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table({ type: 'tableRow' }))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(header, row()))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row(cell('tableCell', text('Bolt M8')))))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(cell('tableHeader', text('Part'))))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row(cell('tableHeader'))))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row(cell('tableHeader', text('a'), text('b')))))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row(cell('tableHeader', { attrs: { localId: 'a' }, type: 'paragraph' }))))))
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(table(row(cell('tableHeader', { content: [{ attrs: { url: 'a|b' }, type: 'blockCard' }], type: 'paragraph' }))))),
|
||||
'| !adf:carry{json="{\\"attrs\\":{\\"url\\":\\"a\\u007cb\\"},\\"type\\":\\"blockCard\\"}"} |\n| --- |\n',
|
||||
)
|
||||
assert.equal(markdown(adfToLosslessMarkdown(table(row(cell('tableHeader', { content: [{ text: '\fa', type: 'text' }], type: 'paragraph' }))))), '| \fa |\n| --- |\n')
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row(cell('tableHeader', { attrs: { level: 1 }, type: 'heading' }))))))
|
||||
assert.equal(markdown(adfToLosslessMarkdown(table(row(cell('tableHeader', { content: [{ text: ' a', type: 'text' }], type: 'paragraph' }))))), '| !adf:text{text=" "}a |\n| --- |\n')
|
||||
const marked = (mark: AdfMark): AdfDocument => table(row(cell('tableHeader', { content: [{ marks: [mark], text: 'l', type: 'text' }], type: 'paragraph' })))
|
||||
assert.ok(directive(adfToLosslessMarkdown(marked({ attrs: { href: 'https://example.com/?x|y' }, type: 'link' }))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(marked({ attrs: { href: 'https://example.com/', title: 'a|b' }, type: 'link' }))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(marked({ attrs: { href: 'https://example.com/|' }, type: 'link' }))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row(cell('tableHeader', { content: [{ marks: [{ type: 'code' }], text: 'a\\|b', type: 'text' }], type: 'paragraph' }))))))
|
||||
const codeSpan: AdfNode = { marks: [{ type: 'code' }], text: 'a|b', type: 'text' }
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row(cell('tableHeader', { content: [codeSpan], type: 'paragraph' }))))))
|
||||
assert.equal(markdown(adfToLosslessMarkdown(marked({ attrs: { href: 'https://example.com/x' }, type: 'link' }))), '| [l](https://example.com/x) |\n| --- |\n')
|
||||
})
|
||||
|
||||
test('spells an inline node as a directive with its content slot and attributes', () => {
|
||||
const emitted = (node: AdfNode): string => markdown(adfToLosslessMarkdown(document(paragraph(node))))
|
||||
assert.equal(emitted({ attrs: { timestamp: '1756080000000' }, type: 'date' }), '!adf:date{timestamp=1756080000000}\n')
|
||||
assert.equal(emitted({ type: 'mention' }), '!adf:mention{}\n')
|
||||
assert.equal(emitted({ attrs: { text: '' }, type: 'status' }), '!adf:status[]\n')
|
||||
assert.equal(emitted({ attrs: { color: 'yellow', text: 'In review' }, type: 'status' }), '!adf:status[In review]{color=yellow}\n')
|
||||
assert.equal(emitted({ attrs: { id: '1f389', text: 'a]b' }, type: 'emoji' }), '!adf:emoji[a\\]b]{id=1f389}\n')
|
||||
assert.equal(emitted({ attrs: { data: { url: 'https://example.com/' } }, type: 'inlineCard' }), '!adf:inlineCard{data="{\\"url\\":\\"https://example.com/\\"}"}\n')
|
||||
assert.equal(emitted({ attrs: { height: 24 }, type: 'mediaInline' }), '!adf:mediaInline{height=24}\n')
|
||||
assert.equal(emitted({ attrs: { url: 'a`b&c<d|e' }, type: 'inlineCard' }), '!adf:inlineCard{url="a\\u0060b\\u0026c\\u003cd\\u007ce"}\n')
|
||||
})
|
||||
|
||||
test('carries an inline node attribute no section spells', () => {
|
||||
const carried = (node: AdfNode): string => markdown(adfToLosslessMarkdown(document(paragraph(node))))
|
||||
assert.equal(carried({ attrs: { rounded: true }, type: 'status' }), '!adf:carry{json="{\\"attrs\\":{\\"rounded\\":true},\\"type\\":\\"status\\"}"}\n')
|
||||
assert.equal(carried({ attrs: { toString: 'x' }, type: 'status' }), '!adf:carry{json="{\\"attrs\\":{\\"toString\\":\\"x\\"},\\"type\\":\\"status\\"}"}\n')
|
||||
assert.equal(carried({ attrs: { color: 4 }, type: 'status' }), '!adf:carry{json="{\\"attrs\\":{\\"color\\":4},\\"type\\":\\"status\\"}"}\n')
|
||||
assert.equal(carried({ attrs: { width: '2' }, type: 'mediaInline' }), '!adf:carry{json="{\\"attrs\\":{\\"width\\":\\"2\\"},\\"type\\":\\"mediaInline\\"}"}\n')
|
||||
assert.equal(carried({ attrs: { text: 4 }, type: 'status' }), '!adf:carry{json="{\\"attrs\\":{\\"text\\":4},\\"type\\":\\"status\\"}"}\n')
|
||||
})
|
||||
|
||||
test('refuses the content and slot an inline directive has no room for', () => {
|
||||
const refused = (node: AdfNode): string => markdown(adfToLosslessMarkdown(document(paragraph(node, { text: 'y', type: 'text' }))))
|
||||
const neither = (type: string, held: string): string => `unsupported-node-shape: a node of type ${type} holds neither content nor text: this one holds ${held}`
|
||||
assert.equal(refused({ content: [{ text: 'x', type: 'text' }], type: 'status' }), neither('status', 'content'))
|
||||
assert.equal(refused({ text: 'x', type: 'status' }), neither('status', 'text'))
|
||||
assert.equal(refused({ content: [{ text: 'x', type: 'text' }], type: 'hardBreak' }), neither('hardBreak', 'content'))
|
||||
assert.equal(refused({ text: 'x', type: 'hardBreak' }), neither('hardBreak', 'text'))
|
||||
})
|
||||
|
||||
test('carries an inline node whose content slot holds a line ending or a null character', () => {
|
||||
assert.equal(readsBack(document(paragraph({ attrs: { text: 'a\nb' }, type: 'status' }))), '!adf:carry{json="{\\"attrs\\":{\\"text\\":\\"a\\\\nb\\"},\\"type\\":\\"status\\"}"}\n')
|
||||
readsBack(document(paragraph({ attrs: { text: 'a\rb' }, type: 'emoji' })))
|
||||
readsBack(document(paragraph({ attrs: { id: 'x', text: 'a\u0000b' }, type: 'mention' })))
|
||||
})
|
||||
|
||||
test('spells the directive marks around the longest run they cover', () => {
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const emitted = (...content: AdfNode[]): string => markdown(adfToLosslessMarkdown(document(paragraph(...content))))
|
||||
const underline: AdfMark = { type: 'underline' }
|
||||
assert.equal(emitted(marked('x', underline)), '!adf:underline[x]\n')
|
||||
assert.equal(emitted(marked('a', underline), marked('b', underline)), '!adf:underline[a!adf:textBreak{}b]\n')
|
||||
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', { 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')
|
||||
})
|
||||
|
||||
test('spells an emphasis or link run once across the hard breaks between two nodes holding the mark', () => {
|
||||
const em: AdfMark = { type: 'em' }
|
||||
const link: AdfMark = { attrs: { href: '/u' }, type: 'link' }
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const hardBreak: AdfNode = { type: 'hardBreak' }
|
||||
assert.equal(readsBack(document(paragraph(marked('a', em), { marks: [], type: 'hardBreak' }, marked('b', em)))), '_a_!adf:hardBreak{marks=empty}_b_\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', em), { attrs: { id: 'x', text: '@A' }, marks: [], type: 'mention' }, marked('b', em)))), '_a_!adf:mention[@A]{id=x marks=empty}_b_\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('One', em), hardBreak, marked('Two', em)))), '_One\\\nTwo_\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', link), hardBreak, marked('b', link)))), '[a\\\nb](/u)\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', em, link), hardBreak, marked('b', em)))), '_[a](/u)\\\nb_\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', em), hardBreak, hardBreak, marked('b', em)))), '_a\\\n\\\nb_\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', em), hardBreak, { text: 'b', type: 'text' }))), '_a_\\\nb\n')
|
||||
const directive: AdfMark = { attrs: { href: '/u', id: 'x' }, type: 'link' }
|
||||
assert.equal(readsBack(document(paragraph(marked('a', directive), hardBreak, marked('b', directive)))), '!adf:link[a!adf:hardBreak{}b]{href="/u" id=x}\n')
|
||||
readsBack(document(paragraph(marked(']: /x', link, { type: 'code' }), hardBreak, marked('b', link))))
|
||||
assert.equal(readsBack(document(paragraph({ text: 'x', type: 'text' }, marked('.', em), hardBreak, marked('b', em)))), 'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\".\\",\\"type\\":\\"text\\"}"}\\\n_b_\n')
|
||||
})
|
||||
|
||||
test('parts every run on a line that needs a fallback, whichever run needs it', () => {
|
||||
const em: AdfMark = { type: 'em' }
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
assert.equal(
|
||||
readsBack(document(paragraph({ text: 'x', type: 'text' }, marked('.', { type: 'strong' }), { text: 'y ', type: 'text' }, marked('a', em), { type: 'hardBreak' }, marked('b', em)))),
|
||||
'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"text\\":\\".\\",\\"type\\":\\"text\\"}"}y _a_\\\n_b_\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('spells an emphasis run once across the code spans, mentions, dates and inline media between two nodes holding the mark', () => {
|
||||
const strong: AdfMark = { type: 'strong' }
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const mention: AdfNode = { attrs: { id: 'x', text: '@A' }, type: 'mention' }
|
||||
assert.equal(readsBack(document(paragraph(marked('Ping ', strong), mention, marked(' now', strong)))), '**Ping !adf:mention[@A]{id=x} now**\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('Run ', strong), marked('npm ci', { type: 'code' }), marked(' first', strong)))), '**Run `npm ci` first**\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a ', strong), { attrs: { timestamp: '0' }, type: 'date' }, marked(' b', strong)))), '**a !adf:date{timestamp=0} b**\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a ', strong), { attrs: { id: 'm' }, type: 'mediaInline' }, marked(' b', strong)))), '**a !adf:mediaInline{id=m} b**\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a ', strong), marked('c', { type: 'code' }), mention, { type: 'hardBreak' }, marked(' b', strong)))), '**a `c`!adf:mention[@A]{id=x}\\\n!adf:text{text=" "}b**\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', strong), marked('https://x/', { attrs: { href: 'https://x/' }, type: 'link' }, { type: 'code' }), marked('b', strong)))), '**a[`https://x/`](https://x/)b**\n')
|
||||
})
|
||||
|
||||
test('spells a link run once across the mentions and inline cards between two nodes holding it, and parts it at code and inline media that do not hold it', () => {
|
||||
const link: AdfMark = { attrs: { href: '/u' }, type: 'link' }
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
assert.equal(readsBack(document(paragraph(marked('a ', link), { attrs: { id: 'x', text: '@A' }, type: 'mention' }, marked(' b', link)))), '[a !adf:mention[@A]{id=x} b](/u)\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', link), { attrs: { url: 'https://c/' }, type: 'inlineCard' }, marked('b', link)))), '[a!adf:inlineCard{url="https://c/"}b](/u)\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', link), marked('c', { type: 'code' }), marked('b', link)))), '[a](/u)`c`[b](/u)\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', link), { attrs: { id: 'm' }, type: 'mediaInline' }, marked('b', link)))), '[a](/u)!adf:mediaInline{id=m}[b](/u)\n')
|
||||
})
|
||||
|
||||
test('carries a hard break holding marks, which Atlassian\'s schema withholds', () => {
|
||||
const link: AdfMark = { attrs: { href: 'https://example.com/' }, type: 'link' }
|
||||
assert.equal(
|
||||
readsBack(document(paragraph({ marks: [link], text: 'a', type: 'text' }, { marks: [link], type: 'hardBreak' }, { marks: [link], text: 'b', type: 'text' }))),
|
||||
'[a](https://example.com/)!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":\\"https://example.com/\\"},\\"type\\":\\"link\\"}],\\"type\\":\\"hardBreak\\"}"}[b](https://example.com/)\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('carries a mark directive attribute no spelling holds', () => {
|
||||
const carried = (mark: AdfMark): string => markdown(adfToLosslessMarkdown(document(paragraph({ marks: [mark], text: 'x', type: 'text' }))))
|
||||
assert.equal(
|
||||
carried({ attrs: { width: 2 }, type: 'border' }),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"width\\":2},\\"type\\":\\"border\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
assert.equal(
|
||||
carried({ attrs: { size: '2' }, type: 'border' }),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"size\\":\\"2\\"},\\"type\\":\\"border\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('carries whitespace CommonMark strips in the reserved text directive', () => {
|
||||
const emitted = (...content: AdfNode[]): string => markdown(adfToLosslessMarkdown(document(paragraph(...content))))
|
||||
assert.equal(emitted({ text: ' lead', type: 'text' }), '!adf:text{text=" "}lead\n')
|
||||
assert.equal(emitted({ text: 'trail ', type: 'text' }), 'trail!adf:text{text=" "}\n')
|
||||
assert.equal(emitted({ text: 'a\nb', type: 'text' }), 'a!adf:text{text="\\n"}b\n')
|
||||
assert.equal(emitted({ text: '\t', type: 'text' }), '!adf:text{text="\\t"}\n')
|
||||
assert.equal(emitted({ text: 'a ', type: 'text' }, { type: 'hardBreak' }, { text: ' b', type: 'text' }), 'a!adf:text{text=" "}\\\n!adf:text{text=" "}b\n')
|
||||
assert.equal(emitted({ marks: [{ type: 'em' }], text: ' a ', type: 'text' }), '_!adf:text{text=" "}a!adf:text{text=" "}_\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { level: 1 }, content: [{ text: 'x ', type: 'text' }], type: 'heading' }))), '# x!adf:text{text=" "}\n')
|
||||
// CommonMark strips spaces and tabs alone, so the whitespace beside them is plain text.
|
||||
assert.equal(emitted({ text: '\va\f', type: 'text' }), '\va\f\n')
|
||||
})
|
||||
|
||||
test("joins a mark run's segments as a walk rather than as one call's arguments", () => {
|
||||
const run = Array.from({ length: 200000 }, (): AdfNode => ({ marks: [{ type: 'strong' }], text: 'a', type: 'text' }))
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ content: run, type: 'paragraph' }))), `**${Array.from({ length: 200000 }, () => 'a').join('!adf:textBreak{}')}**\n`)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { Result } from '../../result.ts'
|
||||
import { adfToLosslessMarkdown, losslessMarkdownToAdf } from '../../index.ts'
|
||||
|
||||
function document(...content: AdfNode[]): AdfDocument {
|
||||
return { content, type: 'doc', version: 1 }
|
||||
}
|
||||
|
||||
function paragraph(...content: AdfNode[]): AdfNode {
|
||||
return content.length === 0 ? { type: 'paragraph' } : { content, type: 'paragraph' }
|
||||
}
|
||||
|
||||
function markdown(result: Result<string>): string {
|
||||
return result.ok ? result.value : `${result.error.code}: ${result.error.message}`
|
||||
}
|
||||
|
||||
function readsBack(doc: AdfDocument): string {
|
||||
const spelled = markdown(adfToLosslessMarkdown(doc))
|
||||
assert.deepEqual(losslessMarkdownToAdf(spelled), { ok: true, value: doc }, `reading ${JSON.stringify(spelled)}`)
|
||||
return spelled
|
||||
}
|
||||
|
||||
test('spells a code span opening a line, whose backticks open no fence', () => {
|
||||
const fence: AdfNode = { marks: [{ type: 'code' }], text: '``', type: 'text' }
|
||||
assert.equal(readsBack(document(paragraph(fence))), '``` `` ```\n')
|
||||
assert.equal(readsBack(document(paragraph({ text: 'a', type: 'text' }, { type: 'hardBreak' }, fence))), 'a\\\n``` `` ```\n')
|
||||
})
|
||||
|
||||
test('escapes the delimiter row a hard break leaves opening a pipe table with no leading pipe', () => {
|
||||
const broken = (second: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ text: 'a | b', type: 'text' }, { type: 'hardBreak' }, { text: second, type: 'text' }))))
|
||||
assert.equal(broken('--- | ---'), 'a | b\\\n\\--- | ---\n')
|
||||
assert.equal(broken(':--- | ---:'), 'a | b\\\n\\:--- | ---:\n')
|
||||
assert.equal(broken('c | d'), 'a | b\\\nc | d\n')
|
||||
})
|
||||
|
||||
test('escapes only text that would otherwise open a construct', () => {
|
||||
const emitted = (text: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ text, type: 'text' }))))
|
||||
assert.equal(emitted('<div>'), '\\<div>\n')
|
||||
assert.equal(emitted('<div'), '\\<div\n')
|
||||
assert.equal(emitted('<div and more'), '\\<div and more\n')
|
||||
assert.equal(emitted('<pre'), '\\<pre\n')
|
||||
assert.equal(emitted('<!x'), '\\<!x\n')
|
||||
assert.equal(emitted('<!-- x'), '\\<!-- x\n')
|
||||
assert.equal(emitted('<?php'), '\\<?php\n')
|
||||
assert.equal(emitted('<![CDATA[x'), '\\<![CDATA[x\n')
|
||||
assert.equal(emitted('<span'), '<span\n')
|
||||
assert.equal(emitted('a < b'), 'a < b\n')
|
||||
assert.equal(emitted('& & x'), '\\& & x\n')
|
||||
assert.equal(emitted('¬areference; x'), '¬areference; x\n')
|
||||
assert.equal(emitted('a <b@c.d> e'), 'a \\<b@c.d> e\n')
|
||||
assert.equal(emitted('a <b 2'), 'a <b 2\n')
|
||||
assert.equal(emitted('a <div b'), 'a <div b\n')
|
||||
assert.equal(emitted('a <!-- b'), 'a <!-- b\n')
|
||||
assert.equal(emitted('a <!-- b --> c'), 'a \\<!-- b --> c\n')
|
||||
const later = paragraph({ text: 'a', type: 'text' }, { type: 'hardBreak' }, { text: '<div', type: 'text' })
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(later))), 'a\\\n\\<div\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ content: [paragraph({ text: '<!-- x', type: 'text' })], type: 'blockquote' }))), '> \\<!-- x\n')
|
||||
assert.equal(emitted('| a | b |'), '\\| a | b |\n')
|
||||
assert.equal(emitted('!adf:mention[@x]{id=1}'), '\\!adf:mention[@x]{id=1}\n')
|
||||
assert.equal(emitted('!adf: completes nothing'), '\\!adf: completes nothing\n')
|
||||
assert.equal(emitted('!adfx and a! alone'), '!adfx and a! alone\n')
|
||||
assert.equal(emitted('!adf:/panel'), '\\!adf:/panel\n')
|
||||
assert.equal(emitted('!adf:rule'), '\\!adf:rule\n')
|
||||
assert.equal(emitted(':::panel info'), ':::panel info\n')
|
||||
assert.equal(emitted('10:30 tomorrow'), '10:30 tomorrow\n')
|
||||
assert.equal(emitted('[a](b)'), '\\[a](b)\n')
|
||||
assert.equal(emitted('[a]: b'), '\\[a]: b\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { level: 1 }, content: [{ text: '[a]: b', type: 'text' }], type: 'heading' }))), '# [a]: b\n')
|
||||
assert.equal(emitted('**bold**'), '\\*\\*bold\\*\\*\n')
|
||||
assert.equal(emitted('a `x` b'), 'a \\`x` b\n')
|
||||
assert.equal(emitted('~~struck~~'), '\\~~struck\\~~\n')
|
||||
assert.equal(emitted('a \\* b'), 'a \\\\\\* b\n')
|
||||
assert.equal(emitted('1. not a list'), '1\\. not a list\n')
|
||||
assert.equal(emitted('*"quoted"*'), '\\*"quoted"\\*\n')
|
||||
assert.equal(emitted('x"_y"'), 'x"\\_y"\n')
|
||||
})
|
||||
|
||||
test('escapes every text bracket in a line where one opens link text and every text backtick where one opens a code span, and none where none does', () => {
|
||||
const emitted = (text: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ text, type: 'text' }))))
|
||||
assert.equal(emitted('[a](b) [c]'), '\\[a](b) \\[c]\n')
|
||||
assert.equal(emitted('see [1] and [docs](url) [2]'), 'see \\[1] and \\[docs](url) \\[2]\n')
|
||||
assert.equal(emitted('[a] b](c'), '[a] b](c\n')
|
||||
assert.equal(emitted('[x]('), '[x](\n')
|
||||
assert.equal(emitted('Use ` or ``code`` syntax'), 'Use \\` or \\`\\`code\\`\\` syntax\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ type: 'code' }], text: 'c', type: 'text' }, { text: 'a ` b', type: 'text' })))), '`c`a ` b\n')
|
||||
})
|
||||
|
||||
test('keeps a text backtick bare in a line where a text bracket opens a link the reader refuses', () => {
|
||||
const refused = paragraph({ text: 'a ` [', type: 'text' }, { attrs: { id: '1' }, type: 'mention' }, { text: '](u)', type: 'text' })
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(refused))), 'a ` \\[!adf:mention{id=1}](u)\n')
|
||||
})
|
||||
|
||||
test('escapes a heading closing sequence', () => {
|
||||
const heading = (text: string): string => markdown(adfToLosslessMarkdown(document({ attrs: { level: 2 }, content: [{ text, type: 'text' }], type: 'heading' })))
|
||||
assert.equal(heading('done #'), '## done \\#\n')
|
||||
assert.equal(heading('#tag first'), '## #tag first\n')
|
||||
})
|
||||
|
||||
test('escapes a literal delimiter that would merge with an emitted one', () => {
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const emitted = (...content: AdfNode[]): string => markdown(adfToLosslessMarkdown(document(paragraph(...content))))
|
||||
assert.equal(emitted(marked('a_', { type: 'em' })), '_a\\__\n')
|
||||
assert.equal(emitted(marked('_a', { type: 'em' })), '_\\_a_\n')
|
||||
assert.equal(emitted(marked('a*', { type: 'strong' })), '**a\\***\n')
|
||||
assert.equal(emitted(marked('~a', { type: 'strike' })), '~~\\~a~~\n')
|
||||
assert.equal(
|
||||
emitted({ text: 'x', type: 'text' }, marked('~a', { type: 'strike' })),
|
||||
'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strike\\"}],\\"text\\":\\"~a\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
assert.equal(emitted({ text: '`', type: 'text' }, marked('x', { type: 'code' })), '\\``x`\n')
|
||||
assert.equal(emitted(marked('x', { type: 'code' }), { text: '`', type: 'text' }), '`x`\\`\n')
|
||||
assert.equal(emitted({ text: '`a', type: 'text' }, marked('b', { type: 'code' })), '\\`a`b`\n')
|
||||
assert.equal(
|
||||
emitted({ text: '<https://example.com/', type: 'text' }, { attrs: { url: 'x>' }, type: 'inlineCard' }),
|
||||
'\\<https://example.com/!adf:inlineCard{url="x>"}\n',
|
||||
)
|
||||
assert.equal(emitted({ text: '!', type: 'text' }, marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' })), '\\\n')
|
||||
assert.equal(emitted({ text: '[a', type: 'text' }, marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' })), '[a[x](https://example.com/)\n')
|
||||
assert.equal(emitted({ text: '[a](b)', type: 'text' }), '\\[a](b)\n')
|
||||
assert.equal(emitted(marked('x', { type: 'underline' }), { text: '{}', type: 'text' }), '!adf:underline[x]\\{}\n')
|
||||
assert.equal(emitted({ attrs: { text: '' }, type: 'status' }, { text: '{color=red}', type: 'text' }), '!adf:status[]\\{color=red}\n')
|
||||
assert.equal(emitted(marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' }), { text: '{}', type: 'text' }), '[x](https://example.com/){}\n')
|
||||
})
|
||||
|
||||
test('escapes a literal delimiter run that only closes', () => {
|
||||
const emitted = (text: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ text, type: 'text' }))))
|
||||
assert.equal(emitted('a* b'), 'a\\* b\n')
|
||||
assert.equal(emitted('2 * 3'), '2 * 3\n')
|
||||
})
|
||||
|
||||
test('escapes a hyphen underline a hard break would expose', () => {
|
||||
const line = (text: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ text: 'foo', type: 'text' }, { type: 'hardBreak' }, { text, type: 'text' }))))
|
||||
assert.equal(line('--'), 'foo\\\n\\--\n')
|
||||
assert.equal(line('=='), 'foo\\\n\\==\n')
|
||||
})
|
||||
|
||||
test('spells the characters CommonMark rewrites', () => {
|
||||
assert.equal(readsBack(document(paragraph({ text: 'a\r\r\nb', type: 'text' }))), 'a !adf:text{text="\\n"}b\n')
|
||||
assert.equal(readsBack(document(paragraph({ marks: [{ type: 'strong' }], text: '\u0000\u0000a\u0000', type: 'text' }))), '**!adf:text{text="\\u0000\\u0000"}a!adf:text{text="\\u0000"}**\n')
|
||||
assert.equal(readsBack(document(paragraph({ marks: [{ attrs: { href: 'u' }, type: 'link' }], text: 'a\rb\u0000', type: 'text' }))), '[a b!adf:text{text="\\u0000"}](u)\n')
|
||||
assert.equal(readsBack(document({ attrs: { level: 1 }, content: [{ text: '\r', type: 'text' }], type: 'heading' })), '# \n')
|
||||
assert.equal(readsBack(document({ content: [{ text: 'a\rb', type: 'text' }], type: 'codeBlock' })), '```adf:codeBlock\n{\n "content": [\n {\n "text": "a\\rb",\n "type": "text"\n }\n ]\n}\n```\n')
|
||||
readsBack(document({ content: [{ text: 'a\u0000b', type: 'text' }], type: 'codeBlock' }))
|
||||
readsBack(document(paragraph({ marks: [{ type: 'code' }], text: 'a\u0000b', type: 'text' })))
|
||||
})
|
||||
@@ -0,0 +1,125 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { Result } from '../../result.ts'
|
||||
import { adfToLosslessMarkdown, losslessMarkdownToAdf } from '../../index.ts'
|
||||
|
||||
function document(...content: AdfNode[]): AdfDocument {
|
||||
return { content, type: 'doc', version: 1 }
|
||||
}
|
||||
|
||||
function paragraph(...content: AdfNode[]): AdfNode {
|
||||
return content.length === 0 ? { type: 'paragraph' } : { content, type: 'paragraph' }
|
||||
}
|
||||
|
||||
function markdown(result: Result<string>): string {
|
||||
return result.ok ? result.value : `${result.error.code}: ${result.error.message}`
|
||||
}
|
||||
|
||||
function readsBack(doc: AdfDocument): string {
|
||||
const spelled = markdown(adfToLosslessMarkdown(doc))
|
||||
assert.deepEqual(losslessMarkdownToAdf(spelled), { ok: true, value: doc }, `reading ${JSON.stringify(spelled)}`)
|
||||
return spelled
|
||||
}
|
||||
|
||||
test('carries every inline node of a line that only the carry spells', () => {
|
||||
const odd: AdfNode = { attrs: { id: 'a', odd: 'b' }, type: 'mention' }
|
||||
const carriedOdd = '!adf:carry{json="{\\"attrs\\":{\\"id\\":\\"a\\",\\"odd\\":\\"b\\"},\\"type\\":\\"mention\\"}"}'
|
||||
const unlinked = '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"link\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}'
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph(odd, { text: ' ', type: 'text' }, { marks: [{ type: 'link' }], text: 'x', type: 'text' }, { text: ' ', type: 'text' }, odd)))),
|
||||
`${carriedOdd} ${unlinked} ${carriedOdd}\n`,
|
||||
)
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ text: 'a', type: 'text' }, { attrs: { foo: 1 }, type: 'hardBreak' }, { text: 'b', type: 'text' })))),
|
||||
'a!adf:carry{json="{\\"attrs\\":{\\"foo\\":1},\\"type\\":\\"hardBreak\\"}"}b\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('carries an inline node holding a mark Atlassian\'s schema withholds from it', () => {
|
||||
const carried = (node: AdfNode): string => markdown(adfToLosslessMarkdown(document(paragraph(node))))
|
||||
assert.equal(carried({ attrs: { id: 'b1c2', text: '@A' }, marks: [{ type: 'strong' }], type: 'mention' }), '!adf:carry{json="{\\"attrs\\":{\\"id\\":\\"b1c2\\",\\"text\\":\\"@A\\"},\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"type\\":\\"mention\\"}"}\n')
|
||||
assert.equal(carried({ attrs: { id: 'm1' }, marks: [{ type: 'em' }], type: 'mediaInline' }), '!adf:carry{json="{\\"attrs\\":{\\"id\\":\\"m1\\"},\\"marks\\":[{\\"type\\":\\"em\\"}],\\"type\\":\\"mediaInline\\"}"}\n')
|
||||
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('carries a text node holding no text, or holding content', () => {
|
||||
const nested: AdfNode[] = [{ text: 'lost', type: 'text' }]
|
||||
assert.equal(readsBack(document(paragraph({ text: '', type: 'text' }))), '!adf:carry{json="{\\"text\\":\\"\\",\\"type\\":\\"text\\"}"}\n')
|
||||
assert.equal(readsBack(document(paragraph({ type: 'text' }, { text: 'a', type: 'text' }))), '!adf:carry{json="{\\"type\\":\\"text\\"}"}a\n')
|
||||
readsBack(document(paragraph({ marks: [{ type: 'code' }], text: '', type: 'text' })))
|
||||
readsBack(document(paragraph({ marks: [{ type: 'em' }], text: 'a', type: 'text' }, { marks: [{ type: 'em' }], text: '', type: 'text' })))
|
||||
readsBack(document(paragraph({ content: nested, text: 'x', type: 'text' })))
|
||||
readsBack(document(paragraph({ content: nested, marks: [{ type: 'code' }], text: 'x', type: 'text' })))
|
||||
readsBack(document({ content: [{ text: '', type: 'text' }], type: 'codeBlock' }))
|
||||
readsBack(document({ content: [{ text: 'a', type: 'text' }, { type: 'text' }], type: 'codeBlock' }))
|
||||
})
|
||||
|
||||
test('spells an inline node as a directive with its content slot and attributes', () => {
|
||||
const emitted = (node: AdfNode): string => markdown(adfToLosslessMarkdown(document(paragraph(node))))
|
||||
assert.equal(emitted({ attrs: { timestamp: '1756080000000' }, type: 'date' }), '!adf:date{timestamp=1756080000000}\n')
|
||||
assert.equal(emitted({ type: 'mention' }), '!adf:mention{}\n')
|
||||
assert.equal(emitted({ attrs: { text: '' }, type: 'status' }), '!adf:status[]\n')
|
||||
assert.equal(emitted({ attrs: { color: 'yellow', text: 'In review' }, type: 'status' }), '!adf:status[In review]{color=yellow}\n')
|
||||
assert.equal(emitted({ attrs: { id: '1f389', text: 'a]b' }, type: 'emoji' }), '!adf:emoji[a\\]b]{id=1f389}\n')
|
||||
assert.equal(emitted({ attrs: { data: { url: 'https://example.com/' } }, type: 'inlineCard' }), '!adf:inlineCard{data="{\\"url\\":\\"https://example.com/\\"}"}\n')
|
||||
assert.equal(emitted({ attrs: { height: 24 }, type: 'mediaInline' }), '!adf:mediaInline{height=24}\n')
|
||||
assert.equal(emitted({ attrs: { url: 'a`b&c<d|e' }, type: 'inlineCard' }), '!adf:inlineCard{url="a\\u0060b\\u0026c\\u003cd\\u007ce"}\n')
|
||||
})
|
||||
|
||||
test('carries an inline node attribute no section spells', () => {
|
||||
const carried = (node: AdfNode): string => markdown(adfToLosslessMarkdown(document(paragraph(node))))
|
||||
assert.equal(carried({ attrs: { rounded: true }, type: 'status' }), '!adf:carry{json="{\\"attrs\\":{\\"rounded\\":true},\\"type\\":\\"status\\"}"}\n')
|
||||
assert.equal(carried({ attrs: { toString: 'x' }, type: 'status' }), '!adf:carry{json="{\\"attrs\\":{\\"toString\\":\\"x\\"},\\"type\\":\\"status\\"}"}\n')
|
||||
assert.equal(carried({ attrs: { color: 4 }, type: 'status' }), '!adf:carry{json="{\\"attrs\\":{\\"color\\":4},\\"type\\":\\"status\\"}"}\n')
|
||||
assert.equal(carried({ attrs: { width: '2' }, type: 'mediaInline' }), '!adf:carry{json="{\\"attrs\\":{\\"width\\":\\"2\\"},\\"type\\":\\"mediaInline\\"}"}\n')
|
||||
assert.equal(carried({ attrs: { text: 4 }, type: 'status' }), '!adf:carry{json="{\\"attrs\\":{\\"text\\":4},\\"type\\":\\"status\\"}"}\n')
|
||||
})
|
||||
|
||||
test('refuses the content and slot an inline directive has no room for', () => {
|
||||
const refused = (node: AdfNode): string => markdown(adfToLosslessMarkdown(document(paragraph(node, { text: 'y', type: 'text' }))))
|
||||
const neither = (type: string, held: string): string => `unsupported-node-shape: a node of type ${type} holds neither content nor text: this one holds ${held}`
|
||||
assert.equal(refused({ content: [{ text: 'x', type: 'text' }], type: 'status' }), neither('status', 'content'))
|
||||
assert.equal(refused({ text: 'x', type: 'status' }), neither('status', 'text'))
|
||||
assert.equal(refused({ content: [{ text: 'x', type: 'text' }], type: 'hardBreak' }), neither('hardBreak', 'content'))
|
||||
assert.equal(refused({ text: 'x', type: 'hardBreak' }), neither('hardBreak', 'text'))
|
||||
})
|
||||
|
||||
test('carries an inline node whose content slot holds a line ending or a null character', () => {
|
||||
assert.equal(readsBack(document(paragraph({ attrs: { text: 'a\nb' }, type: 'status' }))), '!adf:carry{json="{\\"attrs\\":{\\"text\\":\\"a\\\\nb\\"},\\"type\\":\\"status\\"}"}\n')
|
||||
readsBack(document(paragraph({ attrs: { text: 'a\rb' }, type: 'emoji' })))
|
||||
readsBack(document(paragraph({ attrs: { id: 'x', text: 'a\u0000b' }, type: 'mention' })))
|
||||
})
|
||||
|
||||
test('carries a hard break holding marks, which Atlassian\'s schema withholds', () => {
|
||||
const link: AdfMark = { attrs: { href: 'https://example.com/' }, type: 'link' }
|
||||
assert.equal(
|
||||
readsBack(document(paragraph({ marks: [link], text: 'a', type: 'text' }, { marks: [link], type: 'hardBreak' }, { marks: [link], text: 'b', type: 'text' }))),
|
||||
'[a](https://example.com/)!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":\\"https://example.com/\\"},\\"type\\":\\"link\\"}],\\"type\\":\\"hardBreak\\"}"}[b](https://example.com/)\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('carries whitespace CommonMark strips in the reserved text directive', () => {
|
||||
const emitted = (...content: AdfNode[]): string => markdown(adfToLosslessMarkdown(document(paragraph(...content))))
|
||||
assert.equal(emitted({ text: ' lead', type: 'text' }), '!adf:text{text=" "}lead\n')
|
||||
assert.equal(emitted({ text: 'trail ', type: 'text' }), 'trail!adf:text{text=" "}\n')
|
||||
assert.equal(emitted({ text: 'a\nb', type: 'text' }), 'a!adf:text{text="\\n"}b\n')
|
||||
assert.equal(emitted({ text: '\t', type: 'text' }), '!adf:text{text="\\t"}\n')
|
||||
assert.equal(emitted({ text: 'a ', type: 'text' }, { type: 'hardBreak' }, { text: ' b', type: 'text' }), 'a!adf:text{text=" "}\\\n!adf:text{text=" "}b\n')
|
||||
assert.equal(emitted({ marks: [{ type: 'em' }], text: ' a ', type: 'text' }), '_!adf:text{text=" "}a!adf:text{text=" "}_\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { level: 1 }, content: [{ text: 'x ', type: 'text' }], type: 'heading' }))), '# x!adf:text{text=" "}\n')
|
||||
// CommonMark strips spaces and tabs alone, so the whitespace beside them is plain text.
|
||||
assert.equal(emitted({ text: '\va\f', type: 'text' }), '\va\f\n')
|
||||
})
|
||||
|
||||
test("joins a mark run's segments as a walk rather than as one call's arguments", () => {
|
||||
const run = Array.from({ length: 200000 }, (): AdfNode => ({ marks: [{ type: 'strong' }], text: 'a', type: 'text' }))
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document({ content: run, type: 'paragraph' }))), `**${Array.from({ length: 200000 }, () => 'a').join('!adf:textBreak{}')}**\n`)
|
||||
})
|
||||
@@ -0,0 +1,205 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { JsonValue } from '../../json-value.ts'
|
||||
import type { Result } from '../../result.ts'
|
||||
import { adfToLosslessMarkdown, losslessMarkdownToAdf } from '../../index.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
|
||||
function document(...content: AdfNode[]): AdfDocument {
|
||||
return { content, type: 'doc', version: 1 }
|
||||
}
|
||||
|
||||
function paragraph(...content: AdfNode[]): AdfNode {
|
||||
return content.length === 0 ? { type: 'paragraph' } : { content, type: 'paragraph' }
|
||||
}
|
||||
|
||||
function code(result: Result<string>): string {
|
||||
return result.ok ? `emitted ${JSON.stringify(result.value)}` : result.error.code
|
||||
}
|
||||
|
||||
function markdown(result: Result<string>): string {
|
||||
return result.ok ? result.value : `${result.error.code}: ${result.error.message}`
|
||||
}
|
||||
|
||||
function path(result: Result<string>): readonly (number | string)[] {
|
||||
return result.ok ? ['emitted'] : result.error.path
|
||||
}
|
||||
|
||||
function position(result: Result<string>): unknown {
|
||||
return result.ok ? 'emitted' : result.error.position
|
||||
}
|
||||
|
||||
test('names the node a refusal came from, and no source the emitter never read', () => {
|
||||
const unspellable: AdfNode = { text: 'x', type: 'paragraph' }
|
||||
const list: AdfNode = { content: [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }, { content: [unspellable], type: 'listItem' }], type: 'bulletList' }
|
||||
assert.deepEqual(path(adfToLosslessMarkdown(document(paragraph({ text: 'x', type: 'text' }), list))), ['content', 1, 'content', 1, 'content', 0])
|
||||
assert.deepEqual(path(adfToLosslessMarkdown(document(paragraph({ text: 'x', type: 'text' }, { text: 'x', type: 'status' })))), ['content', 0, 'content', 1])
|
||||
assert.deepEqual(path(adfToLosslessMarkdown({ type: 'doc', version: 2 })), [])
|
||||
assert.equal(position(adfToLosslessMarkdown(document(paragraph({ text: 'x', type: 'text' }), list))), undefined)
|
||||
})
|
||||
|
||||
test('refuses a node whose carry nests too deep ahead of a later node in its line', () => {
|
||||
let id: JsonValue = 'a'
|
||||
for (let depth = 0; depth < largestNesting; depth += 1) id = { id }
|
||||
const twiceStrong: AdfNode = { marks: [{ type: 'strong' }, { type: 'strong' }], text: 'x', type: 'text' }
|
||||
const refused = adfToLosslessMarkdown(document(paragraph({ attrs: { id }, type: 'mention' }, twiceStrong)))
|
||||
assert.equal(code(refused), 'unsupported-nesting-depth')
|
||||
assert.deepEqual(path(refused), ['content', 0, 'content', 0])
|
||||
})
|
||||
|
||||
test('refuses a carried run at the node whose carry nests too deep, among the run\'s own nodes', () => {
|
||||
let deep: JsonValue = 'a'
|
||||
for (let depth = 0; depth < largestNesting - 1; depth += 1) deep = { deep }
|
||||
const carriedEm: AdfNode = { marks: [{ attrs: { deep }, type: 'em' }], text: 'x', type: 'text' }
|
||||
const twiceStrong: AdfNode = { marks: [{ type: 'strong' }, { type: 'strong' }], text: 'y', type: 'text' }
|
||||
const refused = adfToLosslessMarkdown(document(paragraph({ text: 'ok', type: 'text' }, carriedEm, twiceStrong)))
|
||||
assert.equal(code(refused), 'unsupported-nesting-depth')
|
||||
assert.deepEqual(path(refused), ['content', 0, 'content', 1])
|
||||
})
|
||||
|
||||
test('refuses a value that is not an ADF document, naming the check it failed', () => {
|
||||
assert.equal(markdown(adfToLosslessMarkdown({ type: 'doc', version: Number.NaN })), 'not-an-adf-document: an ADF document holds a version number: found NaN')
|
||||
})
|
||||
|
||||
// The code, and the bound the message names.
|
||||
function pastTheBound(result: Result<string>): boolean {
|
||||
return !result.ok && result.error.code === 'unsupported-node-shape' && result.error.message.includes('536870888')
|
||||
}
|
||||
|
||||
// A code block of 300 lines; two of these together fall some 1000 characters short of V8's longest string, so only what their spelling adds passes it.
|
||||
function nearlyHalfTheBound(): AdfNode {
|
||||
return { attrs: {}, content: [{ text: `${'a'.repeat(894_782)}\n`.repeat(300), type: 'text' }], type: 'codeBlock' }
|
||||
}
|
||||
|
||||
// About 556 million characters of carry JSON: past V8's longest string, short of JavaScriptCore's and SpiderMonkey's.
|
||||
test('refuses a carry whose JSON would run past V8\'s longest string, on every engine', () => {
|
||||
let value: JsonValue = new Array<JsonValue>(560_000).fill(0)
|
||||
for (let level = 0; level < 495; level += 1) value = [value]
|
||||
const result = adfToLosslessMarkdown(document({ attrs: { value }, type: 'unknownBlock' }))
|
||||
assert.equal(pastTheBound(result), true)
|
||||
})
|
||||
|
||||
test('refuses a quote whose prefixed lines would pass V8\'s longest string', () => {
|
||||
const code = nearlyHalfTheBound()
|
||||
assert.equal(pastTheBound(adfToLosslessMarkdown(document({ content: [code, code], type: 'blockquote' }))), true)
|
||||
})
|
||||
|
||||
// A code block written as its text plus 53 characters; a blank line parts two blocks, and the markdown ends with a line break.
|
||||
function codeBlockOf(textLength: number): AdfNode {
|
||||
return { attrs: {}, content: [{ text: `${'a'.repeat(textLength - 1)}\n`, type: 'text' }], type: 'codeBlock' }
|
||||
}
|
||||
|
||||
test('refuses markdown one character past V8\'s longest string', () => {
|
||||
const code = codeBlockOf(268_435_390)
|
||||
assert.equal(pastTheBound(adfToLosslessMarkdown(document(code, code))), true)
|
||||
})
|
||||
|
||||
test('writes markdown exactly as long as V8\'s longest string', () => {
|
||||
const code = codeBlockOf(178_956_908)
|
||||
const written = adfToLosslessMarkdown(document(code, code, code))
|
||||
assert.equal(written.ok && written.value.length, 536870888)
|
||||
})
|
||||
|
||||
test('refuses text past V8\'s longest string', () => {
|
||||
const long = 'a'.repeat(2 ** 28)
|
||||
const result = adfToLosslessMarkdown(document(paragraph({ text: long, type: 'text' }, { text: long, type: 'text' }, { text: long, type: 'text' })))
|
||||
assert.equal(pastTheBound(result), true)
|
||||
})
|
||||
|
||||
// Bun stops a test after 5 s by default, and this one writes and reads back a line of 33 million characters.
|
||||
test('escapes more characters in a line than V8\'s Set holds', { timeout: 60000 }, () => {
|
||||
const stars = '*'.repeat(2 ** 24 + 1)
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph({ text: stars, type: 'text' })))), `${'\\*'.repeat(2 ** 24 + 1)}\n`)
|
||||
})
|
||||
|
||||
test('refuses a document version the markdown cannot carry', () => {
|
||||
assert.equal(code(adfToLosslessMarkdown({ type: 'doc', version: 2 })), 'unsupported-document-version')
|
||||
})
|
||||
|
||||
test('refuses a carried node nested deeper than the levels its position leaves', () => {
|
||||
let node: AdfNode = { type: 'blockCard' }
|
||||
for (let depth = 0; depth < 600; depth += 1) node = { content: [node], type: 'blockCard' }
|
||||
assert.equal(code(adfToLosslessMarkdown(document(node))), 'unsupported-nesting-depth')
|
||||
assert.equal(code(adfToLosslessMarkdown(document(paragraph(node)))), 'unsupported-nesting-depth')
|
||||
let shallow: AdfNode = { type: 'blockCard' }
|
||||
for (let depth = 0; depth < 200; depth += 1) shallow = { content: [shallow], type: 'blockCard' }
|
||||
assert.ok(adfToLosslessMarkdown(document(shallow)).ok)
|
||||
let quoted: AdfNode = shallow
|
||||
for (let depth = 0; depth < 150; depth += 1) quoted = { content: [quoted], type: 'blockquote' }
|
||||
assert.equal(code(adfToLosslessMarkdown(document(quoted))), 'unsupported-nesting-depth')
|
||||
})
|
||||
|
||||
test('refuses marks and attributes nested deeper than the emitter carries', () => {
|
||||
const marks: AdfMark[] = Array.from({ length: 600 }, (_, index) => ({ type: index % 2 === 0 ? 'em' : 'strong' }))
|
||||
assert.equal(code(adfToLosslessMarkdown(document(paragraph({ marks, text: 'x', type: 'text' })))), 'unsupported-nesting-depth')
|
||||
const deepest: AdfMark[] = Array.from({ length: 20_000 }, (_, index) => ({ type: index % 2 === 0 ? 'em' : 'strong' }))
|
||||
assert.equal(code(adfToLosslessMarkdown(document(paragraph({ marks: deepest, text: 'x', type: 'text' }, { type: 'hardBreak' }, { marks: deepest, text: 'y', type: 'text' })))), 'unsupported-nesting-depth')
|
||||
let attrs: AdfMark['attrs'] = { depth: 'x' }
|
||||
for (let depth = 0; depth < 600; depth += 1) attrs = { depth: attrs }
|
||||
const deeper = (key: string, type: string): string =>
|
||||
`unsupported-nesting-depth: the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries`
|
||||
const nested = (levels: number): JsonValue => {
|
||||
let value: JsonValue = 1
|
||||
for (let level = 0; level < levels; level += 1) value = [value]
|
||||
return value
|
||||
}
|
||||
const card = (levels: number): AdfNode => ({ attrs: { data: nested(levels), url: 'https://example.com/a' }, type: 'inlineCard' })
|
||||
const marked = (levels: number): AdfNode => ({
|
||||
attrs: { panelType: 'info' },
|
||||
content: [paragraph({ text: 'x', type: 'text' })],
|
||||
marks: [{ attrs: { deep: nested(levels) }, type: 'em' }],
|
||||
type: 'panel',
|
||||
})
|
||||
const roundTrips = (node: AdfNode): void => {
|
||||
const spelled = adfToLosslessMarkdown(document(node))
|
||||
assert.ok(spelled.ok, spelled.ok ? '' : spelled.error.message)
|
||||
const read = losslessMarkdownToAdf(spelled.value)
|
||||
assert.ok(read.ok, read.ok ? '' : read.error.message)
|
||||
assert.deepEqual(read.value, document(node))
|
||||
}
|
||||
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))), deeper('depth', 'em'))
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs: { deep: nested(largestNesting) }, type: 'em' }], text: 'x', type: 'text' })))),
|
||||
`unsupported-nesting-depth: a carried node's JSON nests deeper than the ${largestNesting} levels its position leaves`,
|
||||
)
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph(card(largestNesting + 1))))), deeper('data', 'inlineCard'))
|
||||
assert.deepEqual(path(adfToLosslessMarkdown(document(paragraph(card(largestNesting + 1))))), [])
|
||||
roundTrips(paragraph(card(largestNesting)))
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(marked(largestNesting - 2)))), deeper('marks', 'panel'))
|
||||
assert.deepEqual(path(adfToLosslessMarkdown(document(marked(largestNesting - 2)))), ['content', 0])
|
||||
const markedCode: AdfNode = { content: [{ text: 'x', type: 'text' }], marks: [{ attrs: { deep: nested(largestNesting - 2) }, type: 'em' }], type: 'codeBlock' }
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(markedCode))), deeper('marks', 'codeBlock'))
|
||||
roundTrips(marked(largestNesting - 3))
|
||||
})
|
||||
|
||||
test('refuses a document nested deeper than the emitter carries', () => {
|
||||
let node: AdfNode = paragraph({ text: 'x', type: 'text' })
|
||||
for (let depth = 0; depth < 600; depth += 1) node = { content: [node], type: 'blockquote' }
|
||||
assert.equal(code(adfToLosslessMarkdown(document(node))), 'unsupported-nesting-depth')
|
||||
let carried: AdfNode = paragraph({ text: 'x', type: 'text' })
|
||||
for (let depth = 0; depth < 500; depth += 1) carried = { content: [carried], type: 'blockquote' }
|
||||
assert.ok(adfToLosslessMarkdown(document(carried)).ok)
|
||||
const listed = (levels: number, first: readonly AdfNode[]): AdfNode => {
|
||||
let list: AdfNode = { content: [{ content: [...first], type: 'listItem' }], type: 'bulletList' }
|
||||
for (let level = 1; level < levels; level += 1) list = { content: [{ content: [...first, list], type: 'listItem' }], type: 'bulletList' }
|
||||
return list
|
||||
}
|
||||
assert.ok(adfToLosslessMarkdown(document(listed(largestNesting, [paragraph({ text: 'x', type: 'text' })]))).ok)
|
||||
assert.equal(code(adfToLosslessMarkdown(document(listed(largestNesting + 1, [paragraph({ text: 'x', type: 'text' })])))), 'unsupported-nesting-depth')
|
||||
const directiveLists = largestNesting / 2
|
||||
const deep = document(listed(directiveLists, [{ type: 'rule' }]))
|
||||
assert.deepEqual(losslessMarkdownToAdf(markdown(adfToLosslessMarkdown(deep))), { ok: true, value: deep })
|
||||
assert.equal(code(adfToLosslessMarkdown(document(listed(directiveLists + 1, [{ type: 'rule' }])))), 'unsupported-nesting-depth')
|
||||
assert.equal(code(adfToLosslessMarkdown(document({ content: [listed(directiveLists, [{ type: 'rule' }])], type: 'panel' }))), 'unsupported-nesting-depth')
|
||||
const chain = (levels: number): AdfNode => {
|
||||
let card: AdfNode = { type: 'blockCard' }
|
||||
for (let level = 0; level < levels; level += 1) card = { content: [card], type: 'blockCard' }
|
||||
return card
|
||||
}
|
||||
const carriedInItem = (levels: number): AdfDocument => document(listed(1, [{ type: 'rule' }, chain(levels)]))
|
||||
assert.deepEqual(losslessMarkdownToAdf(markdown(adfToLosslessMarkdown(carriedInItem(248)))), { ok: true, value: carriedInItem(248) })
|
||||
assert.equal(code(adfToLosslessMarkdown(carriedInItem(249))), 'unsupported-nesting-depth')
|
||||
})
|
||||
@@ -0,0 +1,282 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { Result } from '../../result.ts'
|
||||
import { adfToLosslessMarkdown, losslessMarkdownToAdf } from '../../index.ts'
|
||||
|
||||
function document(...content: AdfNode[]): AdfDocument {
|
||||
return { content, type: 'doc', version: 1 }
|
||||
}
|
||||
|
||||
function paragraph(...content: AdfNode[]): AdfNode {
|
||||
return content.length === 0 ? { type: 'paragraph' } : { content, type: 'paragraph' }
|
||||
}
|
||||
|
||||
function code(result: Result<string>): string {
|
||||
return result.ok ? `emitted ${JSON.stringify(result.value)}` : result.error.code
|
||||
}
|
||||
|
||||
function markdown(result: Result<string>): string {
|
||||
return result.ok ? result.value : `${result.error.code}: ${result.error.message}`
|
||||
}
|
||||
|
||||
function readsBack(doc: AdfDocument): string {
|
||||
const spelled = markdown(adfToLosslessMarkdown(doc))
|
||||
assert.deepEqual(losslessMarkdownToAdf(spelled), { ok: true, value: doc }, `reading ${JSON.stringify(spelled)}`)
|
||||
return spelled
|
||||
}
|
||||
|
||||
test('spells a link destination CommonMark cannot as the directive link', () => {
|
||||
const link = (href: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs: { href }, type: 'link' }], text: 't', type: 'text' }))))
|
||||
assert.equal(link('https://example.com/a b>c'), '!adf:link[t]{href="https://example.com/a b>c"}\n')
|
||||
assert.equal(link('<https://example.com/'), '!adf:link[t]{href="\\u003chttps://example.com/"}\n')
|
||||
assert.equal(link('https://example.com/a\\b'), '!adf:link[t]{href="https://example.com/a\\\\b"}\n')
|
||||
assert.equal(link('https://example.com/?a=1&b=2'), '!adf:link[t]{href="https://example.com/?a=1\\u0026amp;b=2"}\n')
|
||||
assert.equal(link('https://example.com/a\nb'), '!adf:link[t]{href="https://example.com/a\\nb"}\n')
|
||||
const entity = 'https://example.com/?a=1&b=2'
|
||||
const autolinkShaped = document(paragraph({ marks: [{ attrs: { href: entity }, type: 'link' }], text: entity, type: 'text' }))
|
||||
assert.equal(markdown(adfToLosslessMarkdown(autolinkShaped)), '!adf:link[https://example.com/?a=1\\&b=2]{href="https://example.com/?a=1\\u0026amp;b=2"}\n')
|
||||
})
|
||||
|
||||
test('spells a link carrying an attribute CommonMark cannot as the directive link, and refuses none', () => {
|
||||
const link = (attrs: Record<string, string>): string =>
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs, type: 'link' }], text: 't', type: 'text' }))))
|
||||
assert.equal(link({ collection: 'c', href: 'https://example.com/' }), '!adf:link[t]{collection=c href="https://example.com/"}\n')
|
||||
assert.equal(link({ href: 'https://example.com/', id: 'i' }), '!adf:link[t]{href="https://example.com/" id=i}\n')
|
||||
assert.equal(link({ href: 'https://example.com/', occurrenceKey: 'k' }), '!adf:link[t]{href="https://example.com/" occurrenceKey=k}\n')
|
||||
assert.equal(link({ href: 'https://example.com/', id: 'i', title: 'a\nb' }), '!adf:link[t]{href="https://example.com/" id=i title="a\\nb"}\n')
|
||||
})
|
||||
|
||||
test('escapes the parenthesis a link destination leaves unbalanced, and no other', () => {
|
||||
const link = (href: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs: { href }, type: 'link' }], text: 't', type: 'text' }))))
|
||||
assert.equal(link('https://en.example.com/a_(b)'), '[t](https://en.example.com/a_(b))\n')
|
||||
assert.equal(link('https://example.com/a)b'), '[t](https://example.com/a\\)b)\n')
|
||||
assert.equal(link('https://example.com/a(b'), '[t](https://example.com/a\\(b)\n')
|
||||
assert.equal(link('https://example.com/)(') , '[t](https://example.com/\\)\\()\n')
|
||||
assert.equal(link('https://example.com/a (b'), '[t](<https://example.com/a (b>)\n')
|
||||
})
|
||||
|
||||
test('escapes the quote a link title holds, and spells the rest as the directive link', () => {
|
||||
const titled = (title: string): string =>
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs: { href: 'https://example.com/', title }, type: 'link' }], text: 't', type: 'text' }))))
|
||||
assert.equal(titled('He said "hi"'), '[t](https://example.com/ "He said \\"hi\\"")\n')
|
||||
assert.equal(titled('a\nb'), '!adf:link[t]{href="https://example.com/" title="a\\nb"}\n')
|
||||
assert.equal(titled('a\\b'), '!adf:link[t]{href="https://example.com/" title="a\\\\b"}\n')
|
||||
assert.equal(titled('a & b'), '!adf:link[t]{href="https://example.com/" title="a \\u0026amp; b"}\n')
|
||||
})
|
||||
|
||||
test('carries a link mark the link spelling cannot write', () => {
|
||||
const carried = (mark: AdfMark): string => markdown(adfToLosslessMarkdown(document(paragraph({ marks: [mark], text: 't', type: 'text' }))))
|
||||
assert.equal(
|
||||
carried({ attrs: { href: 'x', rel: 'y' }, type: 'link' }),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":\\"x\\",\\"rel\\":\\"y\\"},\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
assert.equal(carried({ attrs: { href: 4 }, type: 'link' }), '!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":4},\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n')
|
||||
assert.equal(carried({ type: 'link' }), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n')
|
||||
})
|
||||
|
||||
test('carries a mark the canonical spellings cannot nest', () => {
|
||||
const carried = (...marks: AdfMark[]): string => markdown(adfToLosslessMarkdown(document(paragraph({ marks, text: 'x', type: 'text' }))))
|
||||
assert.equal(carried({ type: 'annotation' }), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"annotation\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n')
|
||||
assert.equal(
|
||||
carried({ type: 'code' }, { type: 'strong' }),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"code\\"},{\\"type\\":\\"strong\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
assert.equal(
|
||||
carried({ attrs: { colour: 'red' }, type: 'em' }),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"colour\\":\\"red\\"},\\"type\\":\\"em\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
assert.equal(
|
||||
carried({ attrs: { localId: 'x' }, type: 'code' }),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"localId\\":\\"x\\"},\\"type\\":\\"code\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('breaks a mark run at the node it carries', () => {
|
||||
const strong: AdfMark = { type: 'strong' }
|
||||
const carried: AdfNode = { marks: [strong], type: 'placeholder' }
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [strong], text: 'a', type: 'text' }, carried, { marks: [strong], text: 'b', type: 'text' })))),
|
||||
'**a**!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"type\\":\\"placeholder\\"}"}**b**\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('carries a code mark over anything but text', () => {
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ type: 'code' }], type: 'hardBreak' })))),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"code\\"}],\\"type\\":\\"hardBreak\\"}"}\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('wraps adjacent nodes carrying one mark once, and a differing mark twice', () => {
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const emitted = (...content: AdfNode[]): string => markdown(adfToLosslessMarkdown(document(paragraph(...content))))
|
||||
assert.equal(emitted(marked('a', { type: 'strong' }), marked('b', { type: 'strong' }, { type: 'em' })), '**a*b***\n')
|
||||
assert.equal(emitted(marked('a', { type: 'strong' }), marked('b', { type: 'em' })), '**a**_b_\n')
|
||||
const link = (href: string): AdfMark => ({ attrs: { href }, type: 'link' })
|
||||
assert.equal(emitted(marked('a', link('http://x')), marked('b', link('http://y'))), '[a](http://x)[b](http://y)\n')
|
||||
})
|
||||
|
||||
test('carries a mark spelling that cannot open or close where it sits', () => {
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const emitted = (...content: AdfNode[]): string => markdown(adfToLosslessMarkdown(document(paragraph(...content))))
|
||||
const strong: AdfMark = { type: 'strong' }
|
||||
assert.equal(
|
||||
emitted({ text: 'un', type: 'text' }, marked('-real', strong), { text: 'istic', type: 'text' }),
|
||||
'un!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"text\\":\\"-real\\",\\"type\\":\\"text\\"}"}istic\n',
|
||||
)
|
||||
assert.equal(
|
||||
emitted(marked('C++', { type: 'em' }), { text: 'ish', type: 'text' }),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"C++\\",\\"type\\":\\"text\\"}"}ish\n',
|
||||
)
|
||||
assert.equal(
|
||||
emitted({ text: 'x', type: 'text' }, marked('.a', strong)),
|
||||
'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"text\\":\\".a\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
assert.equal(emitted({ text: 'un ', type: 'text' }, marked('-real', strong), { text: ' istic', type: 'text' }), 'un **-real** istic\n')
|
||||
const carriedR = '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"text\\":\\"-r\\",\\"type\\":\\"text\\"}"}'
|
||||
assert.equal(emitted({ text: 'un', type: 'text' }, marked('-r', strong), { text: 'un', type: 'text' }, marked('-r', strong), { text: 'un', type: 'text' }), `un${carriedR}un${carriedR}un\n`)
|
||||
assert.equal(
|
||||
emitted({ text: 'x', type: 'text' }, marked('[', { type: 'em' }, { type: 'strike' }), marked('a', { type: 'em' })),
|
||||
'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"},{\\"type\\":\\"strike\\"}],\\"text\\":\\"[\\",\\"type\\":\\"text\\"}"}_a_\n',
|
||||
)
|
||||
assert.equal(emitted(marked('a.', strong)), '**a.**\n')
|
||||
assert.equal(emitted({ text: 'x', type: 'text' }, marked('a', strong), { text: 'y', type: 'text' }), 'x**a**y\n')
|
||||
const em: AdfMark = { type: 'em' }
|
||||
assert.equal(
|
||||
emitted({ text: 'x', type: 'text' }, marked('a.', em), marked('b', strong)),
|
||||
'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"a.\\",\\"type\\":\\"text\\"}"}**b**\n',
|
||||
)
|
||||
assert.equal(emitted({ text: 'x', type: 'text' }, marked('ab', em, strong), { text: 'y', type: 'text' }), 'x***ab***y\n')
|
||||
})
|
||||
|
||||
test('carries the mark spelling that cannot close where it sits, and keeps the later one the reader would pair it with', () => {
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const emitted = (...content: AdfNode[]): string => markdown(adfToLosslessMarkdown(document(paragraph(...content))))
|
||||
const b: AdfNode = { text: 'b', type: 'text' }
|
||||
assert.equal(emitted(marked(':', { type: 'em' }), b, marked('c', { type: 'em' })), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\":\\",\\"type\\":\\"text\\"}"}b*c*\n')
|
||||
assert.equal(emitted(marked(')', { type: 'strong' }), b, marked('c', { type: 'strong' })), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"text\\":\\")\\",\\"type\\":\\"text\\"}"}b**c**\n')
|
||||
assert.equal(emitted(marked('^', { type: 'strike' }), b, marked('c', { type: 'strike' })), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strike\\"}],\\"text\\":\\"^\\",\\"type\\":\\"text\\"}"}b~~c~~\n')
|
||||
})
|
||||
|
||||
test('carries every node holding em, strike or strong, and no other, in a line still misread after 32 passes', () => {
|
||||
const marked = (text: string, ...types: string[]): AdfNode => ({ marks: types.map((type) => ({ type })), text, type: 'text' })
|
||||
const carry = (node: AdfNode): string => `!adf:carry{json="${JSON.stringify(node).replaceAll('"', '\\"')}"}`
|
||||
const repeat = [marked(' ~~ ', 'em', 'strong'), marked(')', 'em', 'strike', 'strong'), { text: 'a', type: 'text' }, marked('.unun', 'strike', 'strong')]
|
||||
const written = repeat.map((node) => (node.marks === undefined ? 'a' : carry(node))).join('')
|
||||
const line = [...Array.from({ length: 40 }, () => repeat).flat(), { text: ' ', type: 'text' }, { marks: [{ attrs: { href: 'u' }, type: 'link' }], text: 'b', type: 'text' }]
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph(...line)))), `${written.repeat(40)} [b](u)\n`)
|
||||
})
|
||||
|
||||
test('refuses a node carrying one mark type twice', () => {
|
||||
const em: AdfMark = { type: 'em' }
|
||||
assert.equal(code(adfToLosslessMarkdown(document(paragraph({ marks: [em, em], text: 'x', type: 'text' })))), 'unsupported-node-shape')
|
||||
})
|
||||
|
||||
test("spells a mark run CommonMark's matching pairs as written", () => {
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const emitted = (...content: AdfNode[]): string => markdown(adfToLosslessMarkdown(document(paragraph(...content))))
|
||||
const em: AdfMark = { type: 'em' }
|
||||
const strong: AdfMark = { type: 'strong' }
|
||||
assert.equal(emitted({ text: 'un', type: 'text' }, marked('a', em, strong), { text: 'istic', type: 'text' }), 'un***a***istic\n')
|
||||
assert.equal(emitted({ text: 're', type: 'text' }, marked('structure', strong), { text: ' the code', type: 'text' }), 're**structure** the code\n')
|
||||
assert.equal(
|
||||
emitted({ text: 'un', type: 'text' }, marked('a', em), marked('b', em, strong), marked('c', em), { text: 'istic', type: 'text' }),
|
||||
'un*a**b**c*istic\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('carries a mark run whose edge holds whitespace CommonMark flanking counts', () => {
|
||||
const em = { type: 'em' }
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [em], text: '\u00a0a', type: 'text' })))),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"\u00a0a\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('pads a code span whose edges CommonMark would strip', () => {
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ type: 'code' }], text: ' \t ', type: 'text' })))), '` \t `\n')
|
||||
})
|
||||
|
||||
test('spells a code span per code-marked node, parted by the text break', () => {
|
||||
const code_ = { type: 'code' }
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [code_], text: 'a', type: 'text' }, { marks: [code_], text: 'b', type: 'text' })))),
|
||||
'`a`!adf:textBreak{}`b`\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('spells the directive marks around the longest run they cover', () => {
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const emitted = (...content: AdfNode[]): string => markdown(adfToLosslessMarkdown(document(paragraph(...content))))
|
||||
const underline: AdfMark = { type: 'underline' }
|
||||
assert.equal(emitted(marked('x', underline)), '!adf:underline[x]\n')
|
||||
assert.equal(emitted(marked('a', underline), marked('b', underline)), '!adf:underline[a!adf:textBreak{}b]\n')
|
||||
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', { 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')
|
||||
})
|
||||
|
||||
test('spells an emphasis or link run once across the hard breaks between two nodes holding the mark', () => {
|
||||
const em: AdfMark = { type: 'em' }
|
||||
const link: AdfMark = { attrs: { href: '/u' }, type: 'link' }
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const hardBreak: AdfNode = { type: 'hardBreak' }
|
||||
assert.equal(readsBack(document(paragraph(marked('a', em), { marks: [], type: 'hardBreak' }, marked('b', em)))), '_a_!adf:hardBreak{marks=empty}_b_\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', em), { attrs: { id: 'x', text: '@A' }, marks: [], type: 'mention' }, marked('b', em)))), '_a_!adf:mention[@A]{id=x marks=empty}_b_\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('One', em), hardBreak, marked('Two', em)))), '_One\\\nTwo_\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', link), hardBreak, marked('b', link)))), '[a\\\nb](/u)\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', em, link), hardBreak, marked('b', em)))), '_[a](/u)\\\nb_\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', em), hardBreak, hardBreak, marked('b', em)))), '_a\\\n\\\nb_\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', em), hardBreak, { text: 'b', type: 'text' }))), '_a_\\\nb\n')
|
||||
const directive: AdfMark = { attrs: { href: '/u', id: 'x' }, type: 'link' }
|
||||
assert.equal(readsBack(document(paragraph(marked('a', directive), hardBreak, marked('b', directive)))), '!adf:link[a!adf:hardBreak{}b]{href="/u" id=x}\n')
|
||||
readsBack(document(paragraph(marked(']: /x', link, { type: 'code' }), hardBreak, marked('b', link))))
|
||||
assert.equal(readsBack(document(paragraph({ text: 'x', type: 'text' }, marked('.', em), hardBreak, marked('b', em)))), 'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\".\\",\\"type\\":\\"text\\"}"}\\\n_b_\n')
|
||||
})
|
||||
|
||||
test('parts every run on a line that needs a fallback, whichever run needs it', () => {
|
||||
const em: AdfMark = { type: 'em' }
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
assert.equal(
|
||||
readsBack(document(paragraph({ text: 'x', type: 'text' }, marked('.', { type: 'strong' }), { text: 'y ', type: 'text' }, marked('a', em), { type: 'hardBreak' }, marked('b', em)))),
|
||||
'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"text\\":\\".\\",\\"type\\":\\"text\\"}"}y _a_\\\n_b_\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('spells an emphasis run once across the code spans, mentions, dates and inline media between two nodes holding the mark', () => {
|
||||
const strong: AdfMark = { type: 'strong' }
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
const mention: AdfNode = { attrs: { id: 'x', text: '@A' }, type: 'mention' }
|
||||
assert.equal(readsBack(document(paragraph(marked('Ping ', strong), mention, marked(' now', strong)))), '**Ping !adf:mention[@A]{id=x} now**\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('Run ', strong), marked('npm ci', { type: 'code' }), marked(' first', strong)))), '**Run `npm ci` first**\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a ', strong), { attrs: { timestamp: '0' }, type: 'date' }, marked(' b', strong)))), '**a !adf:date{timestamp=0} b**\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a ', strong), { attrs: { id: 'm' }, type: 'mediaInline' }, marked(' b', strong)))), '**a !adf:mediaInline{id=m} b**\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a ', strong), marked('c', { type: 'code' }), mention, { type: 'hardBreak' }, marked(' b', strong)))), '**a `c`!adf:mention[@A]{id=x}\\\n!adf:text{text=" "}b**\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', strong), marked('https://x/', { attrs: { href: 'https://x/' }, type: 'link' }, { type: 'code' }), marked('b', strong)))), '**a[`https://x/`](https://x/)b**\n')
|
||||
})
|
||||
|
||||
test('spells a link run once across the mentions and inline cards between two nodes holding it, and parts it at code and inline media that do not hold it', () => {
|
||||
const link: AdfMark = { attrs: { href: '/u' }, type: 'link' }
|
||||
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
|
||||
assert.equal(readsBack(document(paragraph(marked('a ', link), { attrs: { id: 'x', text: '@A' }, type: 'mention' }, marked(' b', link)))), '[a !adf:mention[@A]{id=x} b](/u)\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', link), { attrs: { url: 'https://c/' }, type: 'inlineCard' }, marked('b', link)))), '[a!adf:inlineCard{url="https://c/"}b](/u)\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', link), marked('c', { type: 'code' }), marked('b', link)))), '[a](/u)`c`[b](/u)\n')
|
||||
assert.equal(readsBack(document(paragraph(marked('a', link), { attrs: { id: 'm' }, type: 'mediaInline' }, marked('b', link)))), '[a](/u)!adf:mediaInline{id=m}[b](/u)\n')
|
||||
})
|
||||
|
||||
test('carries a mark directive attribute no spelling holds', () => {
|
||||
const carried = (mark: AdfMark): string => markdown(adfToLosslessMarkdown(document(paragraph({ marks: [mark], text: 'x', type: 'text' }))))
|
||||
assert.equal(
|
||||
carried({ attrs: { width: 2 }, type: 'border' }),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"width\\":2},\\"type\\":\\"border\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
assert.equal(
|
||||
carried({ attrs: { size: '2' }, type: 'border' }),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"size\\":\\"2\\"},\\"type\\":\\"border\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { Result } from '../../result.ts'
|
||||
import { adfToLosslessMarkdown, losslessMarkdownToAdf } from '../../index.ts'
|
||||
|
||||
function document(...content: AdfNode[]): AdfDocument {
|
||||
return { content, type: 'doc', version: 1 }
|
||||
}
|
||||
|
||||
function paragraph(...content: AdfNode[]): AdfNode {
|
||||
return content.length === 0 ? { type: 'paragraph' } : { content, type: 'paragraph' }
|
||||
}
|
||||
|
||||
function code(result: Result<string>): string {
|
||||
return result.ok ? `emitted ${JSON.stringify(result.value)}` : result.error.code
|
||||
}
|
||||
|
||||
function markdown(result: Result<string>): string {
|
||||
return result.ok ? result.value : `${result.error.code}: ${result.error.message}`
|
||||
}
|
||||
|
||||
test('spells the image form for exactly the centered external media shape', () => {
|
||||
const url = 'https://example.com/moon.png'
|
||||
const single = (attrs: AdfAttributes, ...content: AdfNode[]): AdfDocument =>
|
||||
document({ attrs: { layout: 'center' }, content: [content.length === 0 ? { attrs, type: 'media' } : { attrs, content, type: 'media' }], type: 'mediaSingle' })
|
||||
assert.equal(markdown(adfToLosslessMarkdown(single({ alt: 'The moon', type: 'external', url }))), `\n`)
|
||||
assert.equal(markdown(adfToLosslessMarkdown(single({ type: 'external', url }))), `\n`)
|
||||
assert.equal(markdown(adfToLosslessMarkdown(single({ alt: 'a [b] c', type: 'external', url }))), `![a \\[b\\] c](${url})\n`)
|
||||
const fallback = (attrs: AdfAttributes): boolean => markdown(adfToLosslessMarkdown(single(attrs))).startsWith('!adf:mediaSingle {layout=center}')
|
||||
assert.ok(fallback({ alt: '', type: 'external', url }))
|
||||
assert.ok(fallback({ alt: 'a\nb', type: 'external', url }))
|
||||
assert.ok(fallback({ alt: 'a\u0000b', type: 'external', url }))
|
||||
assert.ok(fallback({ type: 'external', url: 'https://example.com/a b>c' }))
|
||||
assert.ok(fallback({ alt: ' moon ', type: 'external', url }))
|
||||
assert.ok(fallback({ alt: 4, type: 'external', url }))
|
||||
assert.ok(fallback({ type: 'external', url: 4 }))
|
||||
assert.equal(code(adfToLosslessMarkdown(single({ type: 'external', url }, paragraph()))), 'unsupported-node-shape')
|
||||
const media: AdfNode = { attrs: { type: 'external', url }, type: 'media' }
|
||||
assert.equal(code(adfToLosslessMarkdown(document({ attrs: { layout: 'center' }, content: [media], text: 'x', type: 'mediaSingle' }))), 'unsupported-node-shape')
|
||||
assert.equal(code(adfToLosslessMarkdown(document({ attrs: { layout: 'center' }, content: [{ ...media, text: 'x' }], type: 'mediaSingle' }))), 'unsupported-node-shape')
|
||||
})
|
||||
|
||||
test('spells a media only a link marks as the image the link wraps', () => {
|
||||
const url = 'https://example.com/moon.png'
|
||||
const linked = (alt: string | undefined, ...marks: AdfMark[]): AdfDocument =>
|
||||
document({ attrs: { layout: 'center' }, content: [{ attrs: alt === undefined ? { type: 'external', url } : { alt, type: 'external', url }, marks, type: 'media' }], type: 'mediaSingle' })
|
||||
const link = (attrs: AdfAttributes): AdfMark => ({ attrs, type: 'link' })
|
||||
const spelled = (written: AdfDocument, expected: string): void => {
|
||||
assert.equal(markdown(adfToLosslessMarkdown(written)), expected)
|
||||
assert.deepEqual(losslessMarkdownToAdf(expected), { ok: true, value: written })
|
||||
}
|
||||
spelled(linked('The moon', link({ href: 'https://example.com/docs' })), `[](https://example.com/docs)\n`)
|
||||
spelled(linked(undefined, link({ href: 'https://example.com/docs', title: 'Docs "v2"' })), `[](https://example.com/docs "Docs \\"v2\\"")\n`)
|
||||
spelled(linked('a [b] c', link({ href: '/a b' })), `[![a \\[b\\] c](${url})](</a b>)\n`)
|
||||
spelled(linked('m', link({ href: '' })), `[]()\n`)
|
||||
const fallback = (document: AdfDocument): boolean => markdown(adfToLosslessMarkdown(document)).startsWith('!adf:mediaSingle {layout=center}')
|
||||
assert.ok(fallback(linked('m', link({ href: '/d', id: 'x' }))))
|
||||
assert.ok(fallback(linked('m', link({ href: '/d' }), { attrs: { color: '#000', size: 1 }, type: 'border' })))
|
||||
assert.ok(fallback(linked('m', link({ href: 'a\\b' }))))
|
||||
assert.ok(fallback(linked('m', link({ title: 't' }))))
|
||||
assert.ok(fallback(linked('m', link({ href: '/d', title: 4 }))))
|
||||
const captioned: AdfNode = { attrs: { layout: 'center' }, content: [{ attrs: { alt: 'm', type: 'external', url }, type: 'media' }, { content: [{ text: 'c', type: 'text' }], type: 'caption' }], type: 'mediaSingle' }
|
||||
assert.ok(fallback(document(captioned)))
|
||||
})
|
||||
|
||||
test('spells a mediaSingle the image form does not fit as a directive', () => {
|
||||
const media: AdfNode = { attrs: { type: 'external', url: 'https://example.com/moon.png' }, type: 'media' }
|
||||
const single: AdfNode = { attrs: { layout: 'center' }, content: [media], marks: [{ type: 'border' }], type: 'mediaSingle' }
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(single))),
|
||||
'!adf:mediaSingle {layout=center marks=border}\n!adf:media {type=external url="https://example.com/moon.png"}\n!adf:/mediaSingle\n',
|
||||
)
|
||||
})
|
||||
|
||||
test('spells a table as a pipe table only where every row and cell is plain', () => {
|
||||
const text = (value: string): AdfNode => ({ content: [{ text: value, type: 'text' }], type: 'paragraph' })
|
||||
const cell = (type: string, ...content: AdfNode[]): AdfNode => ({ content, type })
|
||||
const row = (...cells: AdfNode[]): AdfNode => ({ content: cells, type: 'tableRow' })
|
||||
const table = (...rows: AdfNode[]): AdfDocument => document({ content: rows, type: 'table' })
|
||||
const directive = (result: Result<string>): boolean => markdown(result).startsWith('!adf:table')
|
||||
const header = row(cell('tableHeader', text('Part')))
|
||||
assert.equal(markdown(adfToLosslessMarkdown(table(header, row(cell('tableCell', text('Bolt M8')))))), '| Part |\n| --- |\n| Bolt M8 |\n')
|
||||
assert.equal(markdown(adfToLosslessMarkdown(table(row(cell('tableHeader', text('a|b')))))), '| a\\|b |\n| --- |\n')
|
||||
assert.ok(directive(adfToLosslessMarkdown(table())))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row()))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table({ type: 'tableRow' }))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(header, row()))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row(cell('tableCell', text('Bolt M8')))))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(cell('tableHeader', text('Part'))))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row(cell('tableHeader'))))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row(cell('tableHeader', text('a'), text('b')))))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row(cell('tableHeader', { attrs: { localId: 'a' }, type: 'paragraph' }))))))
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(table(row(cell('tableHeader', { content: [{ attrs: { url: 'a|b' }, type: 'blockCard' }], type: 'paragraph' }))))),
|
||||
'| !adf:carry{json="{\\"attrs\\":{\\"url\\":\\"a\\u007cb\\"},\\"type\\":\\"blockCard\\"}"} |\n| --- |\n',
|
||||
)
|
||||
assert.equal(markdown(adfToLosslessMarkdown(table(row(cell('tableHeader', { content: [{ text: '\fa', type: 'text' }], type: 'paragraph' }))))), '| \fa |\n| --- |\n')
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row(cell('tableHeader', { attrs: { level: 1 }, type: 'heading' }))))))
|
||||
assert.equal(markdown(adfToLosslessMarkdown(table(row(cell('tableHeader', { content: [{ text: ' a', type: 'text' }], type: 'paragraph' }))))), '| !adf:text{text=" "}a |\n| --- |\n')
|
||||
const marked = (mark: AdfMark): AdfDocument => table(row(cell('tableHeader', { content: [{ marks: [mark], text: 'l', type: 'text' }], type: 'paragraph' })))
|
||||
assert.ok(directive(adfToLosslessMarkdown(marked({ attrs: { href: 'https://example.com/?x|y' }, type: 'link' }))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(marked({ attrs: { href: 'https://example.com/', title: 'a|b' }, type: 'link' }))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(marked({ attrs: { href: 'https://example.com/|' }, type: 'link' }))))
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row(cell('tableHeader', { content: [{ marks: [{ type: 'code' }], text: 'a\\|b', type: 'text' }], type: 'paragraph' }))))))
|
||||
const codeSpan: AdfNode = { marks: [{ type: 'code' }], text: 'a|b', type: 'text' }
|
||||
assert.ok(directive(adfToLosslessMarkdown(table(row(cell('tableHeader', { content: [codeSpan], type: 'paragraph' }))))))
|
||||
assert.equal(markdown(adfToLosslessMarkdown(marked({ attrs: { href: 'https://example.com/x' }, type: 'link' }))), '| [l](https://example.com/x) |\n| --- |\n')
|
||||
})
|
||||
Reference in New Issue
Block a user