import assert from 'node:assert/strict' 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 { adfToMarkdown, markdownToAdf } 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, type: 'paragraph' } } function code(result: Result): string { return result.ok ? `emitted ${JSON.stringify(result.value)}` : result.error.code } function markdown(result: Result): string { return result.ok ? result.value : `${result.error.code}: ${result.error.message}` } function path(result: Result): readonly (number | string)[] { return result.ok ? ['emitted'] : result.error.path } function position(result: Result): 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(adfToMarkdown(document(paragraph({ text: 'x', type: 'text' }), list))), ['content', 1, 'content', 1, 'content', 0]) assert.deepEqual(path(adfToMarkdown(document(paragraph({ text: 'x', type: 'text' }, { type: 'text' })))), ['content', 0, 'content', 1]) assert.deepEqual(path(adfToMarkdown({ type: 'doc', version: 2 })), []) assert.equal(position(adfToMarkdown(document(paragraph({ text: 'x', type: 'text' }), list))), undefined) }) test('refuses a value that is not an ADF document, naming the check it failed', () => { assert.equal(markdown(adfToMarkdown({ type: 'doc', version: Number.NaN })), 'not-an-adf-document: an ADF document holds a version number: found NaN') }) test('refuses a document version the markdown cannot carry', () => { assert.equal(code(adfToMarkdown({ type: 'doc', version: 2 })), 'unsupported-document-version') }) test('carries a text node attribute no spelling holds', () => { assert.equal( markdown(adfToMarkdown(document(paragraph({ attrs: { localId: 'a' }, text: 'x', type: 'text' })))), ':adf{json="{\\"attrs\\":{\\"localId\\":\\"a\\"},\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n', ) }) test('spells a CommonMark block as a directive where its own spelling holds neither attribute nor mark', () => { assert.equal(markdown(adfToMarkdown(document({ attrs: { localId: 'a' }, type: 'paragraph' }))), '::paragraph {localId=a}\n') assert.equal(markdown(adfToMarkdown(document({ attrs: { wrap: true }, type: 'codeBlock' }))), ':::codeBlock {wrap=true}\n```\n```\n:::\n') assert.equal(markdown(adfToMarkdown(document(paragraph({ attrs: { localId: 'a' }, type: 'hardBreak' }, { text: 'x', type: 'text' })))), ':hardBreak{localId=a}x\n') assert.equal(markdown(adfToMarkdown(document({ marks: [{ type: 'border' }], type: 'blockquote' }))), ':::blockquote {marks="[{\\"type\\":\\"border\\"}]"}\n:::\n') assert.equal(markdown(adfToMarkdown(document({ type: 'listItem' }))), ':::listItem\n:::\n') }) test('spells an ordered list from the order attribute its first marker is', () => { const items: AdfNode[] = [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }] assert.equal(markdown(adfToMarkdown(document({ content: items, type: 'orderedList' }))), '::::orderedList\n:::listItem\nx\n:::\n::::\n') assert.equal(markdown(adfToMarkdown(document({ attrs: { order: 1 }, content: items, type: 'orderedList' }))), '1. x\n') assert.equal(markdown(adfToMarkdown(document({ attrs: { order: 2 }, content: items, type: 'orderedList' }))), '2. x\n') }) test('spells a code block language no info string holds as an attribute', () => { const language = (value: string): string => markdown(adfToMarkdown(document({ attrs: { language: value }, type: 'codeBlock' }))) assert.equal(language(''), ':::codeBlock {language=""}\n```\n```\n:::\n') assert.equal(language('a`b'), ':::codeBlock {language="a\\u0060b"}\n```\n```\n:::\n') assert.equal(language(' sql'), ':::codeBlock {language=" sql"}\n```\n```\n:::\n') assert.equal(language('adf'), ':::codeBlock {language="\\u0026#97;df"}\n```\n```\n:::\n') assert.equal(language('foo\\+bar'), ':::codeBlock {language="foo\\\\+bar"}\n```\n```\n:::\n') assert.equal(language('a\u0000b'), ':::codeBlock {language="a\\u0000b"}\n```\n```\n:::\n') assert.equal(language('a\tb'), ':::codeBlock {language="a\\tb"}\n```\n```\n:::\n') }) test('refuses a link destination CommonMark cannot spell', () => { const link = (href: string): AdfDocument => document(paragraph({ marks: [{ attrs: { href }, type: 'link' }], text: 't', type: 'text' })) assert.equal(code(adfToMarkdown(link('https://example.com/a b>c'))), 'unspellable-link') assert.equal(code(adfToMarkdown(link(' { const link = (href: string): string => markdown(adfToMarkdown(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]()\n') }) test('escapes the quote a link title holds, and refuses the rest', () => { const titled = (title: string): AdfDocument => document(paragraph({ marks: [{ attrs: { href: 'https://example.com/', title }, type: 'link' }], text: 't', type: 'text' })) assert.equal(markdown(adfToMarkdown(titled('He said "hi"'))), '[t](https://example.com/ "He said \\"hi\\"")\n') assert.equal(code(adfToMarkdown(titled('a\nb'))), 'unspellable-link') assert.equal(code(adfToMarkdown(titled('a\\b'))), 'unspellable-link') }) test('carries a link mark the link spelling cannot write', () => { const carried = (mark: AdfMark): string => markdown(adfToMarkdown(document(paragraph({ marks: [mark], text: 't', type: 'text' })))) assert.equal( carried({ attrs: { href: 'x', id: 'y' }, type: 'link' }), ':adf{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":\\"x\\",\\"id\\":\\"y\\"},\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n', ) assert.equal(carried({ attrs: { href: 4 }, type: 'link' }), ':adf{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":4},\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n') assert.equal(carried({ type: 'link' }), ':adf{json="{\\"marks\\":[{\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n') }) test('carries a mark the canonical spellings cannot nest', () => { const carried = (...marks: AdfMark[]): string => markdown(adfToMarkdown(document(paragraph({ marks, text: 'x', type: 'text' })))) assert.equal(carried({ type: 'annotation' }), ':adf{json="{\\"marks\\":[{\\"type\\":\\"annotation\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n') assert.equal( carried({ type: 'code' }, { type: 'strong' }), ':adf{json="{\\"marks\\":[{\\"type\\":\\"code\\"},{\\"type\\":\\"strong\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n', ) assert.equal( carried({ attrs: { colour: 'red' }, type: 'em' }), ':adf{json="{\\"marks\\":[{\\"attrs\\":{\\"colour\\":\\"red\\"},\\"type\\":\\"em\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n', ) assert.equal( carried({ attrs: { localId: 'x' }, type: 'code' }), ':adf{json="{\\"marks\\":[{\\"attrs\\":{\\"localId\\":\\"x\\"},\\"type\\":\\"code\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n', ) }) test('refuses a line whose start block parsing would claim', () => { assert.equal(code(adfToMarkdown(document(paragraph({ marks: [{ type: 'code' }], text: '```', type: 'text' })))), 'unspellable-line-start') }) test('escapes the delimiter row a hard break leaves opening a pipe table with no leading pipe', () => { const broken = (second: string): string => markdown(adfToMarkdown(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('parts two adjacent lists of the same kind, the marker spelling being what merges', () => { const list: AdfNode = { content: [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }], type: 'bulletList' } assert.equal(markdown(adfToMarkdown(document(list, list))), '- x\n\n::listBreak\n\n- x\n') const ordered: AdfNode = { attrs: { order: 1 }, content: [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }], type: 'orderedList' } assert.equal(markdown(adfToMarkdown(document(ordered, ordered))), '1. x\n\n::listBreak\n\n1. x\n') assert.equal(markdown(adfToMarkdown(document({ attrs: { panelType: 'info' }, content: [list, list], type: 'panel' }))), ':::panel info\n- x\n::listBreak\n- x\n:::\n') const nested: AdfNode = { content: [{ content: [list, list], type: 'listItem' }], type: 'bulletList' } assert.equal(markdown(adfToMarkdown(document(nested))), '- - x\n\n ::listBreak\n\n - x\n') const carried: AdfNode = { ...list, attrs: { unknown: 'x' } } assert.ok(markdown(adfToMarkdown(document(carried, carried))).includes('```\n\n```adf\n')) assert.ok(markdown(adfToMarkdown(document(carried, list))).endsWith('```\n\n- x\n')) assert.ok(markdown(adfToMarkdown(document(list, carried))).startsWith('- x\n\n```adf\n')) }) test('carries a node type no section spells', () => { assert.equal(markdown(adfToMarkdown(document({ type: 'blockCard' }))), '```adf\n{\n "type": "blockCard"\n}\n```\n') assert.equal(markdown(adfToMarkdown(document({ type: 'toString' }))), '```adf\n{\n "type": "toString"\n}\n```\n') assert.equal(markdown(adfToMarkdown(document(paragraph({ type: 'blockCard' })))), ':adf{json="{\\"type\\":\\"blockCard\\"}"}\n') assert.equal(markdown(adfToMarkdown(document({ text: 'x', type: 'text' }))), '```adf\n{\n "text": "x",\n "type": "text"\n}\n```\n') assert.equal(markdown(adfToMarkdown(document({ type: 'hardBreak' }))), '```adf\n{\n "type": "hardBreak"\n}\n```\n') }) test('spells the code block whose language is the reserved info string', () => { assert.equal(markdown(adfToMarkdown(document({ attrs: { language: 'adf' }, type: 'codeBlock' }))), ':::codeBlock {language=adf}\n```\n```\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(adfToMarkdown(document(paragraph({ marks: [strong], text: 'a', type: 'text' }, carried, { marks: [strong], text: 'b', type: 'text' })))), '**a**:adf{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(adfToMarkdown(document(node))), 'unsupported-nesting-depth') assert.equal(code(adfToMarkdown(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(adfToMarkdown(document(shallow)).ok) let quoted: AdfNode = shallow for (let depth = 0; depth < 150; depth += 1) quoted = { content: [quoted], type: 'blockquote' } assert.equal(code(adfToMarkdown(document(quoted))), 'unsupported-nesting-depth') }) test('refuses a node whose content model the canonical form cannot emit', () => { assert.equal( markdown(adfToMarkdown(document({ content: [paragraph()], type: 'codeBlock' }))), 'unsupported-node-shape: a codeBlock holds plain text nodes only: this paragraph node is not one', ) assert.equal( markdown(adfToMarkdown(document({ content: [{ content: [{ text: 'lost', type: 'text' }], text: 'x', type: 'text' }], type: 'codeBlock' }))), 'unsupported-node-shape: a codeBlock holds plain text nodes only: this text node is not one', ) }) test('spells a list its own content shape cannot hold as a directive', () => { assert.equal(markdown(adfToMarkdown(document({ content: [paragraph()], type: 'bulletList' }))), ':::bulletList\n::paragraph\n:::\n') assert.equal(markdown(adfToMarkdown(document({ type: 'bulletList' }))), ':::bulletList\n:::\n') assert.equal(markdown(adfToMarkdown(document({ attrs: { order: 2 }, content: [], type: 'orderedList' }))), ':::orderedList {order=2}\n:::\n') }) test('spells an ordered list no marker fits as a directive', () => { const item: AdfNode = { content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' } const list = (order: number, items: number): AdfDocument => document({ attrs: { order }, content: Array.from({ length: items }, () => item), type: 'orderedList' }) assert.equal(markdown(adfToMarkdown(list(1.5, 1))), '::::orderedList {order="1.5"}\n:::listItem\nx\n:::\n::::\n') assert.equal(markdown(adfToMarkdown(list(999999999, 1))), '999999999. x\n') assert.equal(markdown(adfToMarkdown(list(999999999, 2))), '::::orderedList {order=999999999}\n:::listItem\nx\n:::\n:::listItem\nx\n:::\n::::\n') }) test('carries a code mark over anything but text', () => { assert.equal( markdown(adfToMarkdown(document(paragraph({ marks: [{ type: 'code' }], type: 'hardBreak' })))), ':adf{json="{\\"marks\\":[{\\"type\\":\\"code\\"}],\\"type\\":\\"hardBreak\\"}"}\n', ) }) test('spells a heading level no ATX heading fits as a directive', () => { assert.equal(markdown(adfToMarkdown(document({ attrs: { level: 7 }, content: [{ text: 'x', type: 'text' }], type: 'heading' }))), ':::heading {level=7}\nx\n:::\n') assert.equal(markdown(adfToMarkdown(document({ content: [{ text: 'x', type: 'text' }], type: 'heading' }))), ':::heading\nx\n:::\n') }) test('escapes only text that would otherwise open a construct', () => { const emitted = (text: string): string => markdown(adfToMarkdown(document(paragraph({ text, type: 'text' })))) assert.equal(emitted('
'), '\\
\n') assert.equal(emitted(' e'), 'a \\ e\n') assert.equal(emitted('a c'), 'a \\ c\n') const later = paragraph({ text: 'a', type: 'text' }, { type: 'hardBreak' }, { text: ' \\