import assert from 'node:assert/strict' import test from 'node:test' import vm from 'node:vm' import type { AdfDocument, AdfNode } from './document.ts' import type { JsonValue } from '../json-value.ts' import { adfToHtml, adfToLosslessMarkdown, adfToPortableMarkdown } from '../index.ts' import { adfDocumentFault, isAdfDocument, writableContent } from './document.ts' import { largestNesting } from '../nesting.ts' function fault(value: unknown): string { return adfDocumentFault(value)?.message ?? 'accepted' } function faultCode(value: unknown): string { return adfDocumentFault(value)?.code ?? 'accepted' } function nested(levels: number): JsonValue { let value: JsonValue = 1 for (let level = 0; level < levels; level += 1) value = [value] return value } function withAttribute(value: JsonValue): unknown { return { content: [{ attrs: { a: value }, type: 'paragraph' }], type: 'doc', version: 1 } } test('accepts an editor-normal document', () => { assert.equal(isAdfDocument({ content: [{ content: [{ text: 'x', type: 'text' }], type: 'paragraph' }], type: 'doc', version: 1 }), true) assert.equal(isAdfDocument({ type: 'doc', version: 1 }), true) assert.equal(fault({ content: [{ content: [{ text: 'x', type: 'text' }], type: 'paragraph' }], type: 'doc', version: 1 }), 'accepted') }) test('names the check anything that is not a doc node failed', () => { assert.equal(isAdfDocument(null), false) assert.equal(fault(null), 'an ADF document is an object: found null') assert.equal(fault(undefined), 'an ADF document is an object: found undefined') assert.equal(fault([]), 'an ADF document is an object: found an array') assert.equal(fault('doc'), 'an ADF document is an object: found "doc"') assert.equal(fault('x'.repeat(200000)), `an ADF document is an object: found "${'x'.repeat(40)}…"`) assert.equal(fault(function named(first: number, second: number) { return first + second }), 'an ADF document is an object: found a function') assert.equal(fault({ fields: { description: { type: 'doc', version: 1 } } }), 'an ADF document holds content, type and version alone: found the key fields') assert.equal(fault({ extra: 1, type: 'doc', version: 1 }), 'an ADF document holds content, type and version alone: found the key extra') assert.equal(fault({ version: 1 }), 'an ADF document holds type "doc": found no type field') assert.equal(fault({ type: 'document', version: 1 }), 'an ADF document holds type "doc": found "document"') assert.equal(fault({ type: 'paragraph', version: 1 }), 'an ADF document holds type "doc": found "paragraph"') assert.equal(fault({ type: 1, version: 1 }), 'an ADF document holds type "doc": found 1') assert.equal(fault({ type: 'doc' }), 'an ADF document holds a version number: found no version field') assert.equal(fault({ type: 'doc', version: '1' }), 'an ADF document holds a version number: found "1"') assert.equal(fault({ type: 'doc', version: Number.NaN }), 'an ADF document holds a version number: found NaN') }) test('rejects a node whose shape ProseMirror JSON cannot hold', () => { assert.equal(fault({ content: {}, type: 'doc', version: 1 }), "an ADF document's content is an array: found an object") assert.equal(fault({ content: [{ type: 1 }], type: 'doc', version: 1 }), "an ADF document's content holds ADF nodes: one of them is not") assert.equal(isAdfDocument({ content: [{ type: 1 }], type: 'doc', version: 1 }), false) assert.equal(isAdfDocument({ content: [{ text: 1, type: 'text' }], type: 'doc', version: 1 }), false) assert.equal(isAdfDocument({ content: [{ node: 'x', type: 'paragraph' }], type: 'doc', version: 1 }), false) assert.equal(isAdfDocument({ content: [{ content: {}, type: 'paragraph' }], type: 'doc', version: 1 }), false) assert.equal(isAdfDocument({ content: [{ marks: [{ type: 1 }], text: 'x', type: 'text' }], type: 'doc', version: 1 }), false) assert.equal(isAdfDocument({ content: [{ attrs: { a: Number.POSITIVE_INFINITY }, type: 'paragraph' }], type: 'doc', version: 1 }), false) assert.equal(isAdfDocument({ content: [{ attrs: [], type: 'paragraph' }], type: 'doc', version: 1 }), false) }) test('names the attribute nesting past the levels the parser reads one at, and still calls the value a document', () => { const deeper = (key: string, type: string): string => `the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries` assert.equal(fault(withAttribute(nested(largestNesting))), 'accepted') assert.equal(fault(withAttribute(nested(largestNesting + 1))), deeper('a', 'paragraph')) assert.equal(faultCode(withAttribute(nested(largestNesting + 1))), 'unsupported-nesting-depth') assert.equal(isAdfDocument(withAttribute(nested(largestNesting + 1))), true) const marked = (levels: number): unknown => ({ content: [{ marks: [{ attrs: { a: nested(levels) }, type: 'link' }], text: 'x', type: 'text' }], type: 'doc', version: 1 }) assert.equal(fault(marked(largestNesting)), 'accepted') assert.equal(fault(marked(largestNesting + 1)), deeper('a', 'link')) assert.equal(isAdfDocument(marked(largestNesting + 1)), true) }) test('accepts the JSON values an attribute may hold', () => { assert.equal(isAdfDocument({ content: [{ attrs: { a: [1, 'x', null, true, { b: 2 }] }, type: 'paragraph' }], type: 'doc', version: 1 }), true) assert.equal(isAdfDocument({ content: [{ attrs: { a: [() => 1] }, type: 'paragraph' }], type: 'doc', version: 1 }), false) assert.equal(isAdfDocument({ content: [{ attrs: { a: Object.assign(Object.create(null), { b: 1 }) }, type: 'paragraph' }], type: 'doc', version: 1 }), true) assert.equal(isAdfDocument(vm.runInNewContext("({ content: [{ attrs: { a: { b: [1] } }, type: 'paragraph' }], type: 'doc', version: 1 })")), true) }) test('refuses an attribute value that is an object JSON does not hold', () => { class Point { x = 1 } for (const value of [new Date(0), new Map([['b', 1]]), new Set([1]), new Uint8Array([1, 2]), new Point(), [new Map()], { b: new Set() }]) { assert.equal(isAdfDocument({ content: [{ attrs: { a: value }, type: 'paragraph' }], type: 'doc', version: 1 }), false, String(value)) } assert.equal(isAdfDocument({ content: [{ attrs: new Map(), type: 'paragraph' }], type: 'doc', version: 1 }), false) assert.equal(isAdfDocument({ content: [{ marks: [{ attrs: { href: new URL('https://x.test') }, type: 'link' }], text: 'x', type: 'text' }], type: 'doc', version: 1 }), false) }) test("reads a node's siblings as a walk rather than as one call's arguments", () => { const wide = { content: [{ content: Array.from({ length: 200000 }, () => ({ type: 'rule' })), type: 'blockquote' }], type: 'doc', version: 1 } assert.equal(isAdfDocument(wide), true) assert.equal(fault(wide), 'accepted') }) test('refuses an object holding itself, and accepts one shared', () => { const holdsItself = 'an ADF document is a tree: an object in it holds itself' const paragraph: { content: unknown[]; type: string } = { content: [], type: 'paragraph' } paragraph.content.push(paragraph) assert.equal(fault({ content: [paragraph], type: 'doc', version: 1 }), holdsItself) const document: { content: unknown[]; type: string; version: number } = { content: [], type: 'doc', version: 1 } document.content.push(document) assert.equal(fault(document), holdsItself) const attrs: Record = {} attrs['a'] = [attrs] assert.equal(fault({ content: [{ attrs, type: 'paragraph' }], type: 'doc', version: 1 }), holdsItself) assert.equal(fault({ content: [{ marks: [{ attrs, type: 'link' }], text: 'x', type: 'text' }], type: 'doc', version: 1 }), holdsItself) assert.equal(isAdfDocument({ content: [{ attrs, type: 'paragraph' }], type: 'doc', version: 1 }), false) const bold = { type: 'strong' } const text = { marks: [bold], text: 'x', type: 'text' } const shared = { content: [text, text], type: 'paragraph' } const values = { b: 1 } assert.equal(fault({ content: [shared, shared, { attrs: { a: [values, values] }, type: 'paragraph' }], type: 'doc', version: 1 }), 'accepted') }) test('opens every writer with the same checks: a document, version 1, and no more text than the longest output holds', () => { const checked = (document: AdfDocument): string => { const result = writableContent(document) return result.ok ? JSON.stringify(result.value) ?? 'no content key' : result.error.code } assert.equal(checked({ type: 'doc', version: Number.NaN }), 'not-an-adf-document') assert.equal(checked({ type: 'doc', version: 2 }), 'unsupported-document-version') assert.equal(checked({ type: 'doc', version: 1 }), 'no content key') assert.equal(checked({ content: [{ type: 'rule' }], type: 'doc', version: 1 }), '[{"type":"rule"}]') const long = 'a'.repeat(2 ** 28) const text = { content: [{ text: long, type: 'text' }, { text: long, type: 'text' }, { text: long, type: 'text' }], type: 'paragraph' } assert.equal(checked({ content: [text], type: 'doc', version: 1 }), 'unsupported-node-shape') }) test('refuses an inline card a link marks, wherever the document holds it and whichever call writes it', () => { const card = { attrs: { url: 'https://example.com/roadmap' }, marks: [{ attrs: { href: 'https://example.com/' }, type: 'link' }], type: 'inlineCard' } const paragraph = { content: [{ text: 'See ', type: 'text' }, card], type: 'paragraph' } const holders = [ { holder: paragraph, path: ['content', 0, 'content', 1] }, { holder: { content: [{ content: [paragraph], type: 'listItem' }], type: 'bulletList' }, path: ['content', 0, 'content', 0, 'content', 0, 'content', 1] }, { holder: { content: [paragraph], type: 'panel' }, path: ['content', 0, 'content', 0, 'content', 1] }, { holder: { content: [card, { text: 'x', type: 'text' }], marks: [{ attrs: { align: 'center' }, type: 'alignment' }], type: 'paragraph' }, path: ['content', 0, 'content', 0] }, { holder: { content: [{ content: [card], type: 'futureNode' }], type: 'futureBlock' }, path: ['content', 0, 'content', 0, 'content', 0] }, ] const writers = [adfToHtml, adfToLosslessMarkdown, adfToPortableMarkdown] for (const { holder, path } of holders) { for (const write of writers) { const refused = write({ content: [holder], type: 'doc', version: 1 }) assert.equal(!refused.ok && refused.error.code, 'unsupported-node-shape') assert.deepEqual(!refused.ok && refused.error.path, path) assert.match(!refused.ok ? refused.error.message : '', /inlineCard.*link/) } } }) test('refuses what Atlassian\'s schema gives no node, wherever the document holds it and whichever call writes it', () => { const shapes: { message: string; node: AdfNode }[] = [ { message: 'a panel carries no text: this one holds text', node: { text: 'x', type: 'panel' } }, { message: 'a paragraph carries no text: this one holds text', node: { text: 'x', type: 'paragraph' } }, { message: 'a media holds no content: this one holds some', node: { content: [{ type: 'paragraph' }], type: 'media' } }, { message: 'a rule holds no content: this one holds some', node: { content: [{ type: 'paragraph' }], type: 'rule' } }, { message: 'a status node holds neither content nor text: this one holds content', node: { content: [{ text: 'x', type: 'text' }], type: 'status' } }, { message: 'a status node holds neither content nor text: this one holds text', node: { text: 'x', type: 'status' } }, { message: 'a hardBreak node holds neither content nor text: this one holds text', node: { text: 'x', type: 'hardBreak' } }, ] for (const { message, node } of shapes) { const holders = [ { content: [node], type: 'doc' }, { content: [{ content: [node], type: 'paragraph' }], type: 'doc' }, { content: [{ content: [node], type: 'futureBlock' }], type: 'doc' }, ] const paths = [['content', 0], ['content', 0, 'content', 0], ['content', 0, 'content', 0]] for (const [index, { content }] of holders.entries()) { for (const write of [adfToHtml, adfToLosslessMarkdown, adfToPortableMarkdown]) { const refused = write({ content, type: 'doc', version: 1 }) assert.deepEqual(refused.ok ? 'written' : refused.error, { code: 'unsupported-node-shape', message, path: paths[index] }) } } } }) test('writes what the schema refusals leave to the carry', () => { const carried: AdfNode[] = [ { content: [], type: 'rule' }, { content: [{ text: 'x', type: 'text' }], text: 'y', type: 'futureBlock' }, { content: [{ content: [{ type: 'paragraph' }], type: 'text' }], type: 'paragraph' }, ] for (const node of carried) for (const write of [adfToHtml, adfToLosslessMarkdown, adfToPortableMarkdown]) assert.equal(write({ content: [node], type: 'doc', version: 1 }).ok, true) }) test('names the first link-marked card in document order', () => { const card = (url: string): AdfNode => ({ attrs: { url }, marks: [{ attrs: { href: 'https://example.com/' }, type: 'link' }], type: 'inlineCard' }) const content = [{ content: [card('https://a.example/'), card('https://b.example/')], type: 'paragraph' }, { content: [card('https://c.example/')], type: 'paragraph' }] const refused = writableContent({ content, type: 'doc', version: 1 }) assert.deepEqual(!refused.ok && refused.error.path, ['content', 0, 'content', 0]) }) test('writes a card holding no link mark, and a node type it does not know holding one', () => { const comment = { attrs: { annotationType: 'inlineComment', id: 'c1' }, type: 'annotation' } const link = { attrs: { href: 'https://example.com/' }, type: 'link' } const marked = { content: [{ attrs: { url: 'https://example.com/roadmap' }, marks: [comment], type: 'inlineCard' }, { marks: [link], type: 'futureInline' }], type: 'paragraph' } for (const write of [adfToHtml, adfToLosslessMarkdown, adfToPortableMarkdown]) assert.equal(write({ content: [marked], type: 'doc', version: 1 }).ok, true) }) test('names a link-marked card at any depth by its path alone, with no step quadratic in the depth', () => { const depth = 100_000 let node: AdfNode = { attrs: { url: 'https://example.com/' }, marks: [{ attrs: { href: 'https://example.com/' }, type: 'link' }], type: 'inlineCard' } for (let level = 0; level < depth; level += 1) node = { content: [node], type: 'panel' } const refused = writableContent({ content: [node], type: 'doc', version: 1 }) assert.equal(!refused.ok && refused.error.path.length, 2 * (depth + 1)) assert.deepEqual(!refused.ok && refused.error.path.slice(0, 4), ['content', 0, 'content', 0]) })