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 { adfToMarkdown } from '../../index.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 } test('names the node a refusal came from', () => { 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 })), []) }) test('refuses a value that is not an ADF document', () => { assert.equal(code(adfToMarkdown({ type: 'doc', version: Number.NaN })), 'not-an-adf-document') }) 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-destination') 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-title') assert.equal(code(adfToMarkdown(titled('a\\b'))), 'unspellable-link-title') }) 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 whitespace CommonMark cannot hold', () => { assert.equal(code(adfToMarkdown(document(paragraph({ text: '\fa', type: 'text' })))), 'unspellable-whitespace') assert.equal(code(adfToMarkdown(document(paragraph({ text: 'a\rb', type: 'text' })))), 'unspellable-whitespace') }) 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('refuses 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(code(adfToMarkdown(document(list, list))), 'unspellable-adjacent-lists') 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 emitter carries', () => { 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') }) test('refuses a node whose content model the canonical form cannot emit', () => { assert.equal(code(adfToMarkdown(document({ content: [paragraph()], type: 'codeBlock' }))), 'unsupported-node-shape') assert.equal(code(adfToMarkdown(document({ content: [{ content: [{ text: 'lost', type: 'text' }], text: 'x', type: 'text' }], type: 'codeBlock' }))), 'unsupported-node-shape') }) 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: ' \\