import assert from 'node:assert/strict' import test from 'node:test' import type { AdfDocument, AdfNode } from '../../adf/document.ts' import type { Result } from '../../result.ts' import { markdownToAdf } from './markdown-to-adf.ts' function code(result: Result): string { return result.ok ? `built ${JSON.stringify(result.value)}` : result.error.code } function content(result: Result): AdfNode[] | string { return result.ok ? (result.value.content ?? []) : `${result.error.code}: ${result.error.message}` } function path(result: Result): readonly (number | string)[] { return result.ok ? ['built'] : result.error.path } function text(value: string): AdfNode { return { text: value, type: 'text' } } function paragraph(value: string): AdfNode { return { content: [text(value)], type: 'paragraph' } } test('builds an empty document from input holding no block', () => { assert.deepEqual(markdownToAdf(''), { ok: true, value: { type: 'doc', version: 1 } }) assert.deepEqual(content(markdownToAdf('\n \n\t\n')), []) }) test('builds one paragraph from the lines a blank line does not part', () => { assert.deepEqual(content(markdownToAdf('One\ntwo.\n\nThree.\n')), [paragraph('One two.'), paragraph('Three.')]) }) test('reads an ATX heading and its level', () => { assert.deepEqual(content(markdownToAdf('# Assembly\n')), [{ attrs: { level: 1 }, content: [text('Assembly')], type: 'heading' }]) assert.deepEqual(content(markdownToAdf('###### M8\n')), [{ attrs: { level: 6 }, content: [text('M8')], type: 'heading' }]) assert.deepEqual(content(markdownToAdf(' ## Parts ##\n')), [{ attrs: { level: 2 }, content: [text('Parts')], type: 'heading' }]) assert.deepEqual(content(markdownToAdf('#\n')), [{ attrs: { level: 1 }, type: 'heading' }]) assert.deepEqual(content(markdownToAdf('####### Seven\n')), [paragraph('####### Seven')]) assert.deepEqual(content(markdownToAdf('#hashtag\n')), [paragraph('#hashtag')]) }) test('reads a setext underline as the heading level it spells', () => { assert.deepEqual(content(markdownToAdf('Assembly\n===\n')), [{ attrs: { level: 1 }, content: [text('Assembly')], type: 'heading' }]) assert.deepEqual(content(markdownToAdf('One\ntwo\n-\n')), [{ attrs: { level: 2 }, content: [text('One two')], type: 'heading' }]) assert.deepEqual(content(markdownToAdf('===\n')), [paragraph('===')]) }) test('reads a thematic break, the dashed one only where no paragraph is open', () => { assert.deepEqual(content(markdownToAdf('---\n')), [{ type: 'rule' }]) assert.deepEqual(content(markdownToAdf('Part.\n\n * * *\n')), [paragraph('Part.'), { type: 'rule' }]) assert.deepEqual(content(markdownToAdf('___\n')), [{ type: 'rule' }]) assert.deepEqual(content(markdownToAdf('Part.\n***\n')), [paragraph('Part.'), { type: 'rule' }]) assert.deepEqual(content(markdownToAdf('Part.\n---\n')), [{ attrs: { level: 2 }, content: [text('Part.')], type: 'heading' }]) }) test('reads a fenced code block, its info string the language', () => { assert.deepEqual(content(markdownToAdf('```sql\nSELECT id\nFROM part\n```\n')), [ { attrs: { language: 'sql' }, content: [text('SELECT id\nFROM part')], type: 'codeBlock' }, ]) assert.deepEqual(content(markdownToAdf('```\nx\n```\n')), [{ content: [text('x')], type: 'codeBlock' }]) assert.deepEqual(content(markdownToAdf('``` rust \nx\n```\n')), [{ attrs: { language: 'rust' }, content: [text('x')], type: 'codeBlock' }]) assert.deepEqual(content(markdownToAdf('```\n```\n')), [{ type: 'codeBlock' }]) assert.deepEqual(content(markdownToAdf('```\nx\n')), [{ content: [text('x')], type: 'codeBlock' }]) assert.deepEqual(content(markdownToAdf('````\n```\n````\n')), [{ content: [text('```')], type: 'codeBlock' }]) assert.deepEqual(content(markdownToAdf('~~~ a`b\n```\n~~~\n')), [{ attrs: { language: 'a`b' }, content: [text('```')], type: 'codeBlock' }]) assert.deepEqual(content(markdownToAdf('``` a`b\n')), [paragraph('``` a`b')]) assert.deepEqual(content(markdownToAdf('```\n``` x\n```\n')), [{ content: [text('``` x')], type: 'codeBlock' }]) }) test('strips the opening fence indentation from the content lines it holds', () => { assert.deepEqual(content(markdownToAdf(' ```\n x\n y\n ```\n')), [{ content: [text(' x\ny')], type: 'codeBlock' }]) assert.deepEqual(content(markdownToAdf(' ```\n\tx\n ```\n')), [{ content: [text(' x')], type: 'codeBlock' }]) }) test('reads an indented code block where no paragraph is open', () => { assert.deepEqual(content(markdownToAdf(' SELECT id\n')), [{ content: [text('SELECT id')], type: 'codeBlock' }]) assert.deepEqual(content(markdownToAdf('\tSELECT id\n')), [{ content: [text('SELECT id')], type: 'codeBlock' }]) assert.deepEqual(content(markdownToAdf(' x\n')), [{ content: [text(' x')], type: 'codeBlock' }]) assert.deepEqual(content(markdownToAdf(' a\n\n b\n\nPart.\n')), [{ content: [text('a\n\nb')], type: 'codeBlock' }, paragraph('Part.')]) assert.deepEqual(content(markdownToAdf('Part.\n more\n')), [paragraph('Part. more')]) }) test('claims a block-level colon run with no directive to parse it', () => { assert.equal(code(markdownToAdf(':::\n')), 'malformed-directive') assert.equal(code(markdownToAdf('::panel\n')), 'malformed-directive') assert.equal(code(markdownToAdf(' :::panel info\nx\n:::\n')), 'malformed-directive') assert.deepEqual(path(markdownToAdf('Part.\n:::x\n')), ['content', 1]) assert.deepEqual(content(markdownToAdf(':10:30\n')), [paragraph(':10:30')]) assert.deepEqual(content(markdownToAdf(':: two\n')), [paragraph(':: two')]) }) test('claims a block-level pipe with no table to parse it', () => { assert.equal(code(markdownToAdf('| Part | Qty |\n')), 'malformed-pipe-table') assert.deepEqual(content(markdownToAdf('\\| Part\n')), [paragraph('\\| Part')]) }) test('refuses the raw HTML no element mapping carries', () => { assert.equal(code(markdownToAdf('\n')), 'unmappable-html') assert.equal(code(markdownToAdf('
\nx\n
\n')), 'unmappable-html') assert.equal(code(markdownToAdf('\n')), 'unmappable-html') assert.equal(code(markdownToAdf('\n')), 'unmappable-html') assert.equal(code(markdownToAdf('\n')), 'unmappable-html') assert.equal(code(markdownToAdf('
\nx\n
\n')), 'unmappable-html') assert.equal(code(markdownToAdf('\n')), 'unmappable-html') assert.deepEqual(path(markdownToAdf('Part.\n\n
\n')), ['content', 1]) assert.equal(code(markdownToAdf('
\nx\n\n:::\n')), 'unmappable-html') }) test('swallows an HTML block ahead of the claim a line inside it would make', () => { assert.equal(code(markdownToAdf('\n')), 'unmappable-html') assert.equal(code(markdownToAdf('
\n| x |\n
\n')), 'unmappable-html') }) test('leaves a tag that opens no HTML block to the paragraph it sits in', () => { assert.deepEqual(content(markdownToAdf('Part.\n\n')), [paragraph('Part. ')]) assert.deepEqual(content(markdownToAdf('3 < 4\n')), [paragraph('3 < 4')]) }) test('gives up the link reference definitions a paragraph opens with', () => { assert.deepEqual(content(markdownToAdf('[a]: /url\n')), []) assert.deepEqual(content(markdownToAdf('[a]: /url\n[b]: /other\nPart.\n')), [paragraph('Part.')]) assert.deepEqual(content(markdownToAdf('[a]: /url\n"Title"\n\nPart.\n')), [paragraph('Part.')]) assert.deepEqual(content(markdownToAdf('[a]: /url and more\n')), [paragraph('[a]: /url and more')]) assert.deepEqual(content(markdownToAdf('Part.\n[a]: /url\n')), [paragraph('Part. [a]: /url')]) assert.deepEqual(content(markdownToAdf('[a]: /url\n===\n')), [paragraph('===')]) }) test('keeps the whitespace CommonMark strips no more of than a space or a tab', () => { assert.deepEqual(content(markdownToAdf('\u00a0Part.\u00a0\n')), [paragraph('\u00a0Part.\u00a0')]) assert.deepEqual(content(markdownToAdf(' \u3000Part.\t\n')), [paragraph('\u3000Part.')]) }) test('normalizes the line endings and the null character CommonMark replaces', () => { assert.deepEqual(content(markdownToAdf('One\r\ntwo.\r\n')), [paragraph('One two.')]) assert.deepEqual(content(markdownToAdf('One\rtwo.\r')), [paragraph('One two.')]) assert.deepEqual(content(markdownToAdf('```\r\nx\r\n```\r\n')), [{ content: [text('x')], type: 'codeBlock' }]) assert.deepEqual(content(markdownToAdf('a\u0000b\n')), [paragraph('a\ufffdb')]) })