import assert from 'node:assert/strict' import test from 'node:test' import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts' import type { ParseError, Result, SourcePosition } from '../../result.ts' import { largestNesting } from '../../nesting.ts' import { markdownToAdf } from './markdown-to-adf.ts' const em: AdfMark = { type: 'em' } const strike: AdfMark = { type: 'strike' } const strong: AdfMark = { type: 'strong' } const underline: AdfMark = { type: 'underline' } 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 position(result: Result): SourcePosition | string { return result.ok ? 'built' : result.error.position } function text(value: string): AdfNode { return { text: value, type: 'text' } } function paragraph(value: string): AdfNode { return { content: [text(value)], type: 'paragraph' } } function codeSpan(value: string): AdfNode { return { marks: [{ type: 'code' }], text: value, type: 'text' } } function hardBreak(): AdfNode { return { type: 'hardBreak' } } function item(...content: AdfNode[]): AdfNode { return content.length === 0 ? { type: 'listItem' } : { content, type: 'listItem' } } function bulletList(...content: AdfNode[]): AdfNode { return { content, type: 'bulletList' } } function orderedList(order: number, ...content: AdfNode[]): AdfNode { return { attrs: { order }, content, type: 'orderedList' } } function quote(...content: AdfNode[]): AdfNode { return content.length === 0 ? { type: 'blockquote' } : { content, type: 'blockquote' } } function marked(value: string, ...marks: AdfMark[]): AdfNode { return { marks, text: value, type: 'text' } } function link(href: string, title?: string): AdfMark { return { attrs: title === undefined ? { href } : { href, title }, type: 'link' } } function image(url: string, alt?: string): AdfNode { const media: AdfNode = { attrs: alt === undefined ? { type: 'external', url } : { alt, type: 'external', url }, type: 'media' } return { attrs: { layout: 'center' }, content: [media], type: 'mediaSingle' } } function cell(type: string, ...content: AdfNode[]): AdfNode { return { content: [content.length === 0 ? { type: 'paragraph' } : { content, type: 'paragraph' }], type } } function row(...cells: AdfNode[]): AdfNode { return { content: cells, type: 'tableRow' } } function table(...rows: AdfNode[]): AdfNode { return { content: rows, type: 'table' } } 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' }]) assert.deepEqual(content(markdownToAdf('```\n- x\n> y\n```\n')), [{ content: [text('- x\n> y')], type: 'codeBlock' }]) }) test('reads the codeBlock directive body as the node content, the info string its language', () => { const fenced = ':::codeBlock {wrap=true}\n```rust\nfn main() {}\n```\n:::\n' assert.deepEqual(content(markdownToAdf(fenced)), [ { attrs: { language: 'rust', wrap: true }, content: [text('fn main() {}')], type: 'codeBlock' }, ]) assert.deepEqual(content(markdownToAdf(':::codeBlock {wrap=true}\n```\n```\n:::\n')), [{ attrs: { wrap: true }, type: 'codeBlock' }]) assert.deepEqual(content(markdownToAdf(':::codeBlock {language=""}\n```\nx\n```\n:::\n')), [ { attrs: { language: '' }, content: [text('x')], type: 'codeBlock' }, ]) assert.deepEqual(content(markdownToAdf(':::codeBlock {wrap=true}\n fn()\n:::\n')), [ { attrs: { wrap: true }, content: [text('fn()')], type: 'codeBlock' }, ]) // The body is a CommonMark fence, so its info string decodes escapes the way any other fence's does. assert.deepEqual(content(markdownToAdf(':::codeBlock {wrap=true}\n```\\#c\nx\n```\n:::\n')), [ { attrs: { language: '#c', wrap: true }, content: [text('x')], type: 'codeBlock' }, ]) }) test('names the slot a codeBlock spells its language outside of', () => { const slot = 'unsupported-node-shape: codeBlock spells its language in the fence info string, or in the attribute where no info string carries it back' assert.equal(content(markdownToAdf(':::codeBlock {language=rust wrap=true}\n```\nx\n```\n:::\n')), slot) assert.equal(content(markdownToAdf(':::codeBlock {language=rust}\n```sql\nx\n```\n:::\n')), slot) assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\n```adf\nx\n```\n:::\n')), slot) assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\n```a\\b\nx\n```\n:::\n')), slot) assert.equal(content(markdownToAdf('::codeBlock {wrap=true}\n')), 'unsupported-node-shape: codeBlock spells its body in the container form, :::, never the leaf form') }) test('reads a pipe table into the header row and the body rows under it', () => { const pipes = '| Part | Note |\n| --- | --- |\n| Nut \\| washer | `8.8` |\n| Spare | |\n' assert.deepEqual(content(markdownToAdf(pipes)), [ table( row(cell('tableHeader', text('Part')), cell('tableHeader', text('Note'))), row(cell('tableCell', text('Nut | washer')), cell('tableCell', codeSpan('8.8'))), row(cell('tableCell', text('Spare')), cell('tableCell')), ), ]) assert.deepEqual(content(markdownToAdf('| Part\n| -\n')), [table(row(cell('tableHeader', text('Part'))))]) assert.deepEqual(content(markdownToAdf(' | Part |\n | --- |\n')), [table(row(cell('tableHeader', text('Part'))))]) }) test('claims the line a pipe opens and gives the rest back to the block walk', () => { const header = table(row(cell('tableHeader', text('a')))) assert.deepEqual(content(markdownToAdf('Part.\n| a |\n| --- |\n')), [paragraph('Part.'), header]) assert.deepEqual(content(markdownToAdf('| a |\n| --- |\nPart.\n')), [header, paragraph('Part.')]) assert.deepEqual(content(markdownToAdf('> | a |\n> | --- |\n')), [quote(header)]) assert.deepEqual(content(markdownToAdf('- | a |\n | --- |\n')), [bulletList(item(header))]) assert.deepEqual(content(markdownToAdf('| a |\n| --- |\n x\n')), [header, { content: [text('x')], type: 'codeBlock' }]) assert.deepEqual(content(markdownToAdf('\\| a |\n')), [paragraph('| a |')]) }) test('names the pipe table a claimed line does not spell', () => { assert.equal(content(markdownToAdf('| a | b |\n')), 'malformed-pipe-table: a pipe table underlines its header with a row of `-` runs: this one has none; \\| at the start of every row keeps them literal text') assert.equal(content(markdownToAdf('| a |\n| x |\n')), 'malformed-pipe-table: a pipe table underlines its header with a row of `-` runs: this one has none; \\| at the start of every row keeps them literal text') assert.equal(content(markdownToAdf('| a | b |\n| :--- | ---: |\n')), 'malformed-pipe-table: a pipe table carries no column alignment ADF could hold: this delimiter row holds an alignment colon') assert.equal(content(markdownToAdf('| a | b |\n| --- |\n')), 'malformed-pipe-table: a pipe table row holds 1 cell where its header holds 2 cells') assert.equal(content(markdownToAdf('| a |\n| --- |\n| b | c |\n')), 'malformed-pipe-table: a pipe table row holds 2 cells where its header holds 1 cell') assert.deepEqual(path(markdownToAdf('Part.\n\n| a |\n')), ['content', 1]) }) test('names the pipe table whose rows open with no pipe', () => { const bare = 'malformed-pipe-table: a pipe table opens every row with `|`: this one does not; \\| keeps a pipe literal text' assert.equal(content(markdownToAdf('a | b\n--- | ---\n')), bare) assert.equal(content(markdownToAdf('Intro.\na | b\n--- | ---\n')), bare) assert.equal(content(markdownToAdf('a | b\n--- | ---\n===\n')), bare) assert.equal(content(markdownToAdf('a | b\n:--- | ---:\n')), bare) assert.deepEqual(content(markdownToAdf('a | b\nc | d\n')), [paragraph('a | b c | d')]) assert.deepEqual(content(markdownToAdf('a | b\n--- | --- | ---\n')), [paragraph('a | b --- | --- | ---')]) assert.deepEqual(content(markdownToAdf('a \\| b\n--- | ---\n')), [paragraph('a | b --- | ---')]) assert.deepEqual(content(markdownToAdf('a\n---\n')), [{ attrs: { level: 2 }, content: [text('a')], type: 'heading' }]) assert.deepEqual(path(markdownToAdf('Part.\n\na | b\n--- | ---\n')), ['content', 1]) }) test('gives back the refusal an inline body holds, never the shape check above it', () => { const bare = 'malformed-pipe-table: a pipe table opens every row with `|`: this one does not; \\| keeps a pipe literal text' assert.equal(content(markdownToAdf(':::caption\na | b\n--- | ---\n:::\n')), bare) assert.deepEqual(position(markdownToAdf(':::caption\na | b\n--- | ---\n:::\n')), { line: 2, offset: 11 }) assert.equal(code(markdownToAdf(':::caption\n| a |\n:::\n')), 'malformed-pipe-table') assert.equal(content(markdownToAdf(':::caption\n- a\n:::\n')), 'unsupported-node-shape: caption takes one paragraph as its body: this body is not one') }) test('reads the separator that parts two adjacent lists of one kind', () => { const parted = [bulletList(item(paragraph('a'))), bulletList(item(paragraph('b')))] assert.deepEqual(content(markdownToAdf('- a\n\n::listBreak\n\n- b\n')), parted) assert.deepEqual(content(markdownToAdf('- a\n::listBreak\n- b\n')), parted) assert.deepEqual(content(markdownToAdf('1. a\n\n::listBreak\n\n1. b\n')), [orderedList(1, item(paragraph('a'))), orderedList(1, item(paragraph('b')))]) assert.deepEqual(content(markdownToAdf('> - a\n> ::listBreak\n> - b\n')), [quote(...parted)]) assert.deepEqual(path(markdownToAdf('- a\n\n::listBreak\n\n- b\n\n| x |\n')), ['content', 2]) }) test('refuses the list separator that parts anything else', () => { const parts = 'unsupported-node-shape: listBreak parts two adjacent lists of one type: this one parts something else' assert.equal(content(markdownToAdf('::listBreak\n')), parts) assert.equal(content(markdownToAdf('- a\n\n::listBreak\n')), parts) assert.equal(content(markdownToAdf('- a\n\n::listBreak\n\n1. b\n')), parts) assert.equal(content(markdownToAdf('Part.\n\n::listBreak\n\n- b\n')), parts) const bare = 'unsupported-node-shape: listBreak spells the bare leaf form, ::listBreak: this one spells more' assert.equal(content(markdownToAdf('- a\n\n::listBreak x\n\n- b\n')), bare) assert.equal(content(markdownToAdf('- a\n\n::listBreak {id=x}\n\n- b\n')), bare) assert.equal(content(markdownToAdf(':::listBreak\n- a\n:::\n')), bare) assert.equal(content(markdownToAdf(':listBreak{}\n')), 'unsupported-node-shape: listBreak takes the block form, ::listBreak, never the inline form') assert.deepEqual(path(markdownToAdf('Part.\n\n::listBreak\n')), ['content', 1]) }) test('refuses the image a pipe cell holds no ADF node for', () => { assert.equal(content(markdownToAdf('| a |\n| --- |\n| ![x](/u) |\n')), 'unmappable-image: no ADF node carries an image inside a paragraph') assert.deepEqual(path(markdownToAdf('| a |\n| --- |\n| ![x](/u) |\n')), ['content', 0, 'content', 1, 'content', 0, 'content', 0]) }) 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 {a=1 a=2}\n')), 'malformed-directive') assert.deepEqual(content(markdownToAdf(':10:30\n')), [paragraph(':10:30')]) assert.deepEqual(content(markdownToAdf(':: two\n')), [paragraph(':: two')]) }) test('reads the three directive forms into the nodes the tables name', () => { assert.deepEqual(content(markdownToAdf('::rule {localId=a-1}\n')), [{ attrs: { localId: 'a-1' }, type: 'rule' }]) assert.deepEqual(content(markdownToAdf('::paragraph\n')), [{ type: 'paragraph' }]) assert.deepEqual(content(markdownToAdf(' :::panel info\nPart.\n:::\n')), [ { attrs: { panelType: 'info' }, content: [paragraph('Part.')], type: 'panel' }, ]) assert.deepEqual(content(markdownToAdf(':::blockquote {localId=a-1}\n:::\n')), [{ attrs: { localId: 'a-1' }, type: 'blockquote' }]) assert.deepEqual(content(markdownToAdf(':::heading {level=2 localId=a-1}\nPart.\n:::\n')), [ { attrs: { level: 2, localId: 'a-1' }, content: [text('Part.')], type: 'heading' }, ]) assert.deepEqual(content(markdownToAdf('Part:hardBreak{}.\n')), [{ content: [text('Part'), hardBreak(), text('.')], type: 'paragraph' }]) }) test('names the directive form a node CommonMark spells refuses', () => { const named = (type: string): string => `unsupported-node-shape: ${type} takes the CommonMark spelling, not the directive form` assert.equal(content(markdownToAdf('::rule\n')), named('rule')) assert.equal(content(markdownToAdf(':::blockquote\nPart.\n:::\n')), named('blockquote')) assert.equal(content(markdownToAdf(':::heading {level=2}\nPart.\n:::\n')), named('heading')) assert.equal(content(markdownToAdf(':::paragraph\nPart.\n:::\n')), named('paragraph')) assert.equal(content(markdownToAdf('::::bulletList\n:::listItem\nPart.\n:::\n::::\n')), named('bulletList')) // The item whose first line reads back as a thematic break keeps the directive form the emitter falls back to. assert.deepEqual(content(markdownToAdf('::::bulletList\n:::listItem\n---\n:::\n::::\n')), [bulletList(item({ type: 'rule' }))]) }) // The spelling the emitter refuses gives the emitter's own error, never a second name for it. test('gives back the refusal the CommonMark spelling itself raises', () => { const destination = ':::blockquote\n[t](https://example.com/a\\b)\n:::\n' assert.equal(content(markdownToAdf(destination)), 'unspellable-link: no canonical escape spells a backslash in a link destination') }) test('names the directive name no node reads back to', () => { assert.equal(code(markdownToAdf(':::widget info\nx\n:::\n')), 'unknown-directive-name') assert.equal(content(markdownToAdf('::widget\n')), 'unknown-directive-name: the directive name widget reads back to no node; \\::: keeps the line literal text') assert.equal(content(markdownToAdf(':widget[x]\n')), 'unknown-directive-name: the directive name widget reads back to no node; \\: keeps the colon literal') assert.equal(content(markdownToAdf('ratio a:b[c]{d}\n')), 'malformed-directive: an attribute reads key=value, the value bare or double-quoted: this one does not; \\: keeps the colon literal') assert.deepEqual(path(markdownToAdf('Part.\n\n::widget\n')), ['content', 1]) assert.equal(content(markdownToAdf('Part.\n:::x\n')), 'malformed-directive: a container fenced with 3 colons is unclosed') assert.deepEqual(path(markdownToAdf('Part.\n:::x\n')), ['content', 1]) }) test('names the position a directive name the other one spells belongs to', () => { assert.equal(content(markdownToAdf(':::em\na\n:::\n')), 'unsupported-node-shape: em is spelled _x_, never as a block directive') assert.equal(content(markdownToAdf('::underline\n')), 'unsupported-node-shape: underline is spelled :underline[…], never as a block directive') assert.equal(content(markdownToAdf('::text {text=" "}\n')), 'unsupported-node-shape: text takes the inline form, :text{…}, never the block form') assert.equal(content(markdownToAdf('::date {timestamp=1}\n')), 'unsupported-node-shape: date takes the inline form, :date{…}, never the block form') assert.equal(content(markdownToAdf(':paragraph[a]\n')), 'unsupported-node-shape: paragraph takes the block form, :::paragraph, never the inline form') assert.equal(content(markdownToAdf(':rule[a]\n')), 'unsupported-node-shape: rule takes the block form, ::rule, never the inline form') assert.equal(code(markdownToAdf(':::widget\na\n:::\n')), 'unknown-directive-name') assert.equal(code(markdownToAdf(':widget[a]\n')), 'unknown-directive-name') }) test('names the reserved carry name a block directive spells', () => { const reserved = 'malformed-directive: the name adf is reserved for the opaque carry, whose block form is the fence' assert.equal(content(markdownToAdf('::adf\n')), reserved) assert.equal(content(markdownToAdf(':::adf\nx\n:::\n')), reserved) assert.deepEqual(content(markdownToAdf('```adfx\nx\n```\n')), [{ attrs: { language: 'adfx' }, content: [text('x')], type: 'codeBlock' }]) }) const carried = ':adf{json="{\\"type\\":\\"placeholder\\"}"}' test('reads the adf fence back to the node its JSON holds', () => { assert.deepEqual(content(markdownToAdf('```adf\n{\n "attrs": {\n "url": "https://example.com/x"\n },\n "type": "blockCard"\n}\n```\n')), [ { attrs: { url: 'https://example.com/x' }, type: 'blockCard' }, ]) }) test('reads the inline carry back to the node its json attribute holds', () => { assert.deepEqual(content(markdownToAdf(`a ${carried} b\n`)), [ { content: [text('a '), { type: 'placeholder' }, text(' b')], type: 'paragraph' }, ]) }) test('names the invalid JSON no opaque carry holds', () => { const invalid = 'malformed-directive: the opaque carry holds invalid JSON' assert.equal(content(markdownToAdf('```adf\n{"type":\n```\n')), invalid) assert.equal(content(markdownToAdf('```adf\n```\n')), invalid) assert.equal(content(markdownToAdf(':adf{json="{"}\n')), invalid) assert.equal(content(markdownToAdf(':adf{json=abc}\n')), invalid) }) test('names the canonical spelling a carried JSON reads alone', () => { const canonically = "unsupported-node-shape: the opaque carry spells its node's JSON canonically: " assert.equal(content(markdownToAdf('```adf\n{"type":"blockCard"}\n```\n')), `${canonically}two-space indent, keys sorted`) assert.equal(content(markdownToAdf(':adf{json="{\\"type\\": \\"blockCard\\"}"}\n')), `${canonically}compact, keys sorted`) assert.equal(content(markdownToAdf(':adf{json="{\\"type\\":\\"blockCard\\",\\"attrs\\":{}}"}\n')), `${canonically}compact, keys sorted`) }) test('names the node JSON an opaque carry restores alone', () => { const node = "unsupported-node-shape: the opaque carry holds one ADF node's JSON: this JSON is no ADF node" assert.equal(content(markdownToAdf('```adf\n[]\n```\n')), node) assert.equal(content(markdownToAdf(':adf{json=null}\n')), node) assert.equal(content(markdownToAdf(':adf{json="{\\"kind\\":\\"x\\"}"}\n')), node) }) test('names the shape the inline carry reads alone', () => { assert.equal(content(markdownToAdf(':adf[x]{json="{}"}\n')), 'unsupported-node-shape: adf takes no content: this one holds some') assert.equal(content(markdownToAdf(':adf{}\n')), 'unsupported-node-shape: adf holds one json attribute alone: this one does not') assert.equal(content(markdownToAdf(':adf{json="{}" localId=x}\n')), 'unsupported-node-shape: adf holds one json attribute alone: this one does not') assert.equal(content(markdownToAdf(':adf{json="null"}\n')), 'unsupported-node-shape: adf spells its json attribute as json=null') }) test('holds a carried JSON value to the nesting its position leaves', () => { const nested = (levels: number): string => `${'['.repeat(levels)}${']'.repeat(levels)}` const fence = (prefix: string, levels: number): string => `${prefix}\`\`\`adf\n${prefix}${nested(levels)}\n${prefix}\`\`\`\n` const deeper = (levels: number): string => `unsupported-nesting-depth: a carried node's JSON nests deeper than the ${levels} levels its position leaves` assert.equal(content(markdownToAdf(`:adf{json="${nested(largestNesting + 2)}"}\n`)), deeper(largestNesting)) assert.equal( content(markdownToAdf(fence('', largestNesting + 1))), "unsupported-node-shape: the opaque carry spells its node's JSON canonically: two-space indent, keys sorted", ) assert.equal(content(markdownToAdf(fence('> ', largestNesting + 1))), deeper(largestNesting - 1)) }) test('names the number no JSON spelling carries in an opaque carry', () => { const named = 'unsupported-node-shape: the opaque carry holds a number JSON cannot spell' assert.equal(content(markdownToAdf(':adf{json="{\\"attrs\\":{\\"width\\":1e999},\\"type\\":\\"blockCard\\"}"}\n')), named) assert.equal(content(markdownToAdf('```adf\n1e999\n```\n')), named) }) test('names the mark spelling no opaque carry sits inside', () => { const named = 'unsupported-node-shape: no mark spelling wraps an opaque carry: the carried node restores exactly, marks included' assert.equal(content(markdownToAdf(`_a ${carried} b_\n`)), named) assert.equal(content(markdownToAdf(`**${carried}**\n`)), named) assert.equal(content(markdownToAdf(`~~a ${carried}~~\n`)), named) assert.equal(content(markdownToAdf(`[a ${carried} b](https://example.com/x)\n`)), named) assert.equal(content(markdownToAdf(`:underline[${carried}]\n`)), named) assert.equal(content(markdownToAdf(`:textColor[a ${carried}]{color="#ae2e24"}\n`)), named) assert.equal(content(markdownToAdf(`![_a ${carried}_](https://example.com/i)\n`)), named) }) test('keeps the carry a mark spelling does not wrap', () => { assert.deepEqual(content(markdownToAdf(`[a ${carried} b]\n`)), [ { content: [text('[a '), { type: 'placeholder' }, text(' b]')], type: 'paragraph' }, ]) assert.deepEqual(content(markdownToAdf(`**a**${carried}**b**\n`)), [ { content: [marked('a', strong), { type: 'placeholder' }, marked('b', strong)], type: 'paragraph' }, ]) assert.deepEqual(content(markdownToAdf(`![a ${carried} b](https://example.com/i)\n`)), [image('https://example.com/i', 'a b')]) }) test('reads each attribute value as the type its section assigns', () => { assert.deepEqual(content(markdownToAdf('::media {height=10 id=a-1 type=file url="/x y" width="20.5"}\n')), [ { attrs: { height: 10, id: 'a-1', type: 'file', url: '/x y', width: 20.5 }, type: 'media' }, ]) assert.deepEqual(content(markdownToAdf(':::table {isNumberColumnEnabled=true}\n:::\n')), [{ attrs: { isNumberColumnEnabled: true }, type: 'table' }]) assert.deepEqual(content(markdownToAdf(':::tableCell {colwidth="[340,420]"}\n:::\n')), [{ attrs: { colwidth: [340, 420] }, type: 'tableCell' }]) assert.deepEqual(content(markdownToAdf('::rule {localId=a-1}\n')), [{ attrs: { localId: 'a-1' }, type: 'rule' }]) }) test('reads the reserved marks key as the node array it spells', () => { assert.deepEqual(content(markdownToAdf('::rule {marks="[{\\"type\\":\\"em\\"}]"}\n')), [{ marks: [em], type: 'rule' }]) assert.deepEqual(content(markdownToAdf('::rule {localId=a-1 marks="[{\\"attrs\\":{\\"mode\\":\\"wide\\"},\\"type\\":\\"breakout\\"}]"}\n')), [ { attrs: { localId: 'a-1' }, marks: [{ attrs: { mode: 'wide' }, type: 'breakout' }], type: 'rule' }, ]) }) test('names the marks key no marks array reads back from', () => { const named = 'unsupported-node-shape: the marks attribute of rule is its marks array in canonical JSON: this one is not' assert.equal(content(markdownToAdf('::rule {marks="[]"}\n')), named) assert.equal(content(markdownToAdf('::rule {marks="[1]"}\n')), named) assert.equal(content(markdownToAdf('::rule {marks="{}"}\n')), named) assert.equal(content(markdownToAdf('::rule {marks=x}\n')), named) assert.equal(content(markdownToAdf('::rule {marks="[{\\"attrs\\":{},\\"type\\":\\"em\\"}]"}\n')), named) }) test('names the attribute a node holds no reading for', () => { assert.equal(content(markdownToAdf('::rule {bogus=1}\n')), 'unsupported-node-shape: rule holds no bogus attribute: this one spells it') assert.equal(content(markdownToAdf('::media {width=wide}\n')), 'unsupported-node-shape: the width attribute of media is no number') assert.equal(content(markdownToAdf(':::table {isNumberColumnEnabled=yes}\n:::\n')), 'unsupported-node-shape: the isNumberColumnEnabled attribute of table is no boolean') assert.equal(content(markdownToAdf('::media {width=true}\n')), 'unsupported-node-shape: the width attribute of media is no number') assert.equal(content(markdownToAdf(':::tableCell {colwidth="[340,"}\n:::\n')), 'unsupported-node-shape: the colwidth attribute of tableCell is no json') assert.equal(content(markdownToAdf(':::panel info {panelType=note}\nx\n:::\n')), 'unsupported-node-shape: panel spells its panelType attribute as the directive argument, never in {attrs}') assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot, never in {attrs}') }) test('names the depth an attribute value nests past, never the kind the JSON reads as', () => { const nested = (levels: number): string => `${'['.repeat(levels)}1${']'.repeat(levels)}` const deeper = (key: string, type: string): string => `unsupported-nesting-depth: the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries` assert.equal(content(markdownToAdf(`:::tableCell {colwidth="${nested(largestNesting + 1)}"}\n:::\n`)), deeper('colwidth', 'tableCell')) assert.equal(content(markdownToAdf(`::rule {marks="${nested(largestNesting + 1)}"}\n`)), deeper('marks', 'rule')) assert.equal(content(markdownToAdf(`::rule {marks="[{\\"attrs\\":{\\"deep\\":${nested(largestNesting - 2)}},\\"type\\":\\"em\\"}]"}\n`)), deeper('marks', 'rule')) assert.equal(content(markdownToAdf(`::media {width="${nested(largestNesting + 1)}"}\n`)), 'unsupported-node-shape: the width attribute of media is no number') }) test('names the attribute value spelled outside the canonical form', () => { assert.equal(content(markdownToAdf('::rule {localId="a-1"}\n')), 'unsupported-node-shape: rule spells its localId attribute as localId=a-1') assert.equal(content(markdownToAdf('::media {width="20.0"}\n')), 'unsupported-node-shape: media spells its width attribute as width=20') assert.equal(content(markdownToAdf(':::tableCell {colwidth="[340, 420]"}\n:::\n')), 'unsupported-node-shape: tableCell spells its colwidth attribute as colwidth="[340,420]"') }) test('names the argument and the body a node takes no reading for', () => { assert.equal(content(markdownToAdf('::rule x\n')), 'unsupported-node-shape: rule takes no argument: this one spells one') assert.equal(content(markdownToAdf(':::rule\nPart.\n:::\n')), 'unsupported-node-shape: rule holds no content: this one holds some') assert.equal(content(markdownToAdf('::bulletList\n')), 'unsupported-node-shape: bulletList spells its body in the container form, :::, never the leaf form') assert.equal(content(markdownToAdf(':::paragraph\n:::\n')), 'unsupported-node-shape: an empty paragraph takes the leaf form, ::, never an empty container') assert.equal(content(markdownToAdf(':::paragraph\nOne.\n\nTwo.\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body: this body is not one') assert.equal(content(markdownToAdf(':::paragraph\n---\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body: this body is not one') assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\nx\n:::\n')), 'unsupported-node-shape: codeBlock takes one code block as its body: this body is not one') assert.equal(content(markdownToAdf(':::paragraph\n![a](/u)\n:::\n')), 'unmappable-image: no ADF node carries an image inside a paragraph') assert.equal(content(markdownToAdf('Part :date[now]{timestamp=1}.\n')), 'unsupported-node-shape: date takes no content: this one holds some') }) test('leaves the colon that opens no directive the text it is', () => { assert.deepEqual(content(markdownToAdf('At 10:30 :smile: today.\n')), [paragraph('At 10:30 :smile: today.')]) assert.deepEqual(content(markdownToAdf('\\:mention[@A]\n')), [paragraph(':mention[@A]')]) assert.deepEqual(content(markdownToAdf('`:mention[@A]`\n')), [{ content: [codeSpan(':mention[@A]')], type: 'paragraph' }]) }) test('names the inline directive left unclosed at the end of its line', () => { assert.equal(content(markdownToAdf('Part :mention[@A\n')), 'malformed-directive: an inline directive [content] is unclosed; \\: keeps the colon literal') assert.equal(code(markdownToAdf('Part :mention[@A]{id=\n')), 'malformed-directive') assert.deepEqual(path(markdownToAdf('> Part :mention[@A\n')), ['content', 0, 'content', 0]) }) 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.equal(code(markdownToAdf('\n')), ['content', 1]) assert.equal(code(markdownToAdf('
\nx\n\n:::\n')), 'unmappable-html') assert.equal(code(markdownToAdf('
\n- x\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('names the line and the offset in the input a refusal sits at, the innermost block winning', () => { assert.deepEqual(position(markdownToAdf('
\n')), { line: 1, offset: 0 }) assert.deepEqual(position(markdownToAdf('Part.\n\n
\n')), { line: 3, offset: 7 }) assert.deepEqual(position(markdownToAdf('> Part.\n>\n> a b\n')), { line: 3, offset: 10 }) assert.deepEqual(position(markdownToAdf('- Part.\n- a b\n')), { line: 2, offset: 8 }) assert.deepEqual(position(markdownToAdf('Part.\n\n:::panel info\nMore.\n')), { line: 3, offset: 7 }) assert.deepEqual(position(markdownToAdf('x\n\na b\n===\n')), { line: 3, offset: 3 }) assert.deepEqual(position(markdownToAdf('x\n\n```adf\n{\n```\n')), { line: 3, offset: 3 }) assert.deepEqual(position(markdownToAdf('x\n\n| a |\n')), { line: 3, offset: 3 }) assert.deepEqual(position(markdownToAdf('a\nb c\n')), { line: 1, offset: 0 }) assert.deepEqual(position(markdownToAdf('Part.\r\n\r\n
\r\n')), { line: 3, offset: 9 }) assert.deepEqual(position(markdownToAdf('a\u0000b\n\n
\n')), { line: 3, offset: 5 }) assert.deepEqual(position(markdownToAdf(':::caption\na b\n:::\n')), { line: 2, offset: 11 }) assert.deepEqual(position(markdownToAdf('x\n\n:::caption\n- a\n:::\n')), { line: 3, offset: 3 }) }) test('names the line the text a paragraph keeps starts on, never a definition line it gave up', () => { assert.deepEqual(position(markdownToAdf('[a]: /url\nb\n')), { line: 2, offset: 10 }) assert.deepEqual(position(markdownToAdf('[a]: /a\n[b]: /b\n[c]: /c\n[d]: /d\nx\n')), { line: 5, offset: 32 }) assert.deepEqual(position(markdownToAdf('[a]:\n\n"Title"\nb\n')), { line: 4, offset: 23 }) assert.deepEqual(position(markdownToAdf('> [a]: /url\n> b\n')), { line: 2, offset: 12 }) assert.deepEqual(position(markdownToAdf(':::caption\n[a]: /url\nb\n:::\n')), { line: 3, offset: 21 }) }) 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')]) }) test('reads a blockquote and the blocks its prefix carries', () => { assert.deepEqual(content(markdownToAdf('> Ship it.\n>\n> Then tell them.\n')), [quote(paragraph('Ship it.'), paragraph('Then tell them.'))]) assert.deepEqual(content(markdownToAdf('>Ship it.\n')), [quote(paragraph('Ship it.'))]) assert.deepEqual(content(markdownToAdf(' > > Deep.\n')), [quote(quote(paragraph('Deep.')))]) assert.deepEqual(content(markdownToAdf('>\n')), [quote()]) assert.deepEqual(content(markdownToAdf('> One.\n\n> Two.\n')), [quote(paragraph('One.')), quote(paragraph('Two.'))]) assert.deepEqual(content(markdownToAdf('Part.\n> Ship it.\n')), [paragraph('Part.'), quote(paragraph('Ship it.'))]) assert.deepEqual(content(markdownToAdf(' > Code.\n')), [{ content: [text('> Code.')], type: 'codeBlock' }]) }) test('reads a bullet list, the marker width setting the continuation', () => { assert.deepEqual(content(markdownToAdf('- Bolt M8\n- Nut M8\n')), [bulletList(item(paragraph('Bolt M8')), item(paragraph('Nut M8')))]) assert.deepEqual(content(markdownToAdf('- Washer M8\n - Fibre\n')), [bulletList(item(paragraph('Washer M8'), bulletList(item(paragraph('Fibre')))))]) assert.deepEqual(content(markdownToAdf('-\n')), [bulletList(item())]) assert.deepEqual(content(markdownToAdf('- One\n\n Two.\n')), [bulletList(item(paragraph('One'), paragraph('Two.')))]) assert.deepEqual(content(markdownToAdf('- Code.\n')), [bulletList(item({ content: [text('Code.')], type: 'codeBlock' }))]) assert.deepEqual(content(markdownToAdf('- a\n* b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))]) assert.deepEqual(content(markdownToAdf('- a\n\n+ b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))]) assert.deepEqual(content(markdownToAdf('- a\n-\n\n- c\n')), [bulletList(item(paragraph('a')), item(), item(paragraph('c')))]) assert.deepEqual(content(markdownToAdf('- a\n1. b\n')), [bulletList(item(paragraph('a'))), orderedList(1, item(paragraph('b')))]) assert.deepEqual(content(markdownToAdf('- a\n\n[r]: /u\n\n- b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))]) assert.deepEqual(content(markdownToAdf('-\n\n Part.\n')), [bulletList(item()), paragraph('Part.')]) }) test('reads an ordered list, its first marker the order attribute', () => { assert.deepEqual(content(markdownToAdf('9. Bolt M8\n10. Nut M8\n')), [orderedList(9, item(paragraph('Bolt M8')), item(paragraph('Nut M8')))]) assert.deepEqual(content(markdownToAdf('1) Loosen the clamp\n')), [orderedList(1, item(paragraph('Loosen the clamp')))]) assert.deepEqual(content(markdownToAdf('1. a\n1) b\n')), [orderedList(1, item(paragraph('a')), item(paragraph('b')))]) assert.deepEqual(content(markdownToAdf('0. Zero\n')), [orderedList(0, item(paragraph('Zero')))]) }) test('measures a tab from the column the containers cut it to', () => { assert.deepEqual(content(markdownToAdf('>\t\tfoo\n')), [quote({ content: [text(' foo')], type: 'codeBlock' })]) assert.deepEqual(content(markdownToAdf('-\t\tfoo\n')), [bulletList(item({ content: [text(' foo')], type: 'codeBlock' }))]) assert.deepEqual(content(markdownToAdf('- foo\n\n\t\tbar\n')), [bulletList(item(paragraph('foo'), { content: [text(' bar')], type: 'codeBlock' }))]) assert.deepEqual(content(markdownToAdf('-\t foo\n')), [bulletList(item(paragraph('foo')))]) assert.deepEqual(content(markdownToAdf(' - foo\n - bar\n\t - baz\n')), [ bulletList(item(paragraph('foo'), bulletList(item(paragraph('bar'), bulletList(item(paragraph('baz'))))))), ]) }) test('drops the tightness ADF does not record', () => { assert.deepEqual(content(markdownToAdf('- a\n\n- b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))]) assert.deepEqual(content(markdownToAdf('- a\n\n 2. b\n')), [bulletList(item(paragraph('a'), orderedList(2, item(paragraph('b')))))]) assert.deepEqual(content(markdownToAdf('- a\n\n -\n')), [bulletList(item(paragraph('a'), bulletList(item())))]) }) test('opens a list beside a paragraph only where the marker interrupts it', () => { assert.deepEqual(content(markdownToAdf('Part.\n- a\n')), [paragraph('Part.'), bulletList(item(paragraph('a')))]) assert.deepEqual(content(markdownToAdf('Part.\n1. a\n')), [paragraph('Part.'), orderedList(1, item(paragraph('a')))]) assert.deepEqual(content(markdownToAdf('Part.\n2. a\n')), [paragraph('Part. 2. a')]) assert.deepEqual(content(markdownToAdf('Part.\n*\n')), [paragraph('Part. *')]) 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' }]) assert.deepEqual(content(markdownToAdf('- a\n 2. b\n')), [bulletList(item(paragraph('a 2. b')))]) assert.deepEqual(content(markdownToAdf('- a\n 1. b\n')), [bulletList(item(paragraph('a'), orderedList(1, item(paragraph('b')))))]) }) test('folds a lazy continuation into the paragraph the container holds', () => { assert.deepEqual(content(markdownToAdf('> One\ntwo.\n')), [quote(paragraph('One two.'))]) assert.deepEqual(content(markdownToAdf('- One\ntwo.\n')), [bulletList(item(paragraph('One two.')))]) assert.deepEqual(content(markdownToAdf('> One\n two.\n')), [quote(paragraph('One two.'))]) assert.deepEqual(content(markdownToAdf('> One\n\ntwo.\n')), [quote(paragraph('One')), paragraph('two.')]) assert.deepEqual(content(markdownToAdf('> One\n# Two\n')), [quote(paragraph('One')), { attrs: { level: 1 }, content: [text('Two')], type: 'heading' }]) assert.deepEqual(content(markdownToAdf('> One\n---\n')), [quote(paragraph('One')), { type: 'rule' }]) assert.deepEqual(content(markdownToAdf('> One\n```\n')), [quote(paragraph('One')), { type: 'codeBlock' }]) assert.equal(code(markdownToAdf('> One\n
\n')), 'unmappable-html') assert.deepEqual(path(markdownToAdf('> One\n
\n')), ['content', 1]) assert.deepEqual(path(markdownToAdf('> One\n\n')), ['content', 0, 'content', 0]) }) test('ends a lazy continuation at a claimed line', () => { assert.equal(code(markdownToAdf('> Part.\n:::\n')), 'malformed-directive') assert.deepEqual(path(markdownToAdf('> Part.\n:::\n')), ['content', 1]) assert.equal(code(markdownToAdf('- Part.\n| x |\n')), 'malformed-pipe-table') }) test('names the block the claim inside a container opens', () => { assert.deepEqual(path(markdownToAdf('> Part.\n>\n> :::x\n')), ['content', 0, 'content', 1]) assert.deepEqual(path(markdownToAdf('- Part.\n- | x |\n')), ['content', 0, 'content', 1, 'content', 0]) }) test('refuses input nested deeper than the parser carries', () => { assert.equal(code(markdownToAdf('> '.repeat(501))), 'unsupported-nesting-depth') assert.ok(markdownToAdf('> '.repeat(500)).ok) const marks = (levels: number): string => `${':underline['.repeat(levels)}a${']'.repeat(levels)}\n` assert.equal(code(markdownToAdf(marks(largestNesting + 1))), 'unsupported-nesting-depth') assert.deepEqual(content(markdownToAdf(marks(largestNesting))), [{ content: [marked('a', underline)], type: 'paragraph' }]) }) test('decodes the backslash escapes CommonMark spells, and keeps the rest literal', () => { assert.deepEqual(content(markdownToAdf('\\*not emphasis\\*\n')), [paragraph('*not emphasis*')]) assert.deepEqual(content(markdownToAdf('\\\\\n')), [paragraph('\\')]) assert.deepEqual(content(markdownToAdf('\\a \\\u00a0\n')), [paragraph('\\a \\\u00a0')]) assert.deepEqual(content(markdownToAdf('Part\\\n')), [paragraph('Part\\')]) assert.deepEqual(content(markdownToAdf('a\\`b`\n')), [paragraph('a`b`')]) }) test('decodes the entity references HTML5 names, and the numeric ones', () => { assert.deepEqual(content(markdownToAdf('& © ≧̸ ‌ Æ\n')), [paragraph('& \u00a9 \u2267\u0338 \u200c \u00c6')]) assert.deepEqual(content(markdownToAdf('# " ♥\n')), [paragraph('# " \u2665')]) assert.deepEqual(content(markdownToAdf('� � �\n')), [paragraph('\ufffd \ufffd \ufffd')]) assert.deepEqual(content(markdownToAdf('&zzz; & &#; &\n')), [paragraph('&zzz; & &#; &')]) assert.deepEqual(content(markdownToAdf('`not code`\n')), [paragraph('`not code`')]) assert.deepEqual(content(markdownToAdf('a b c d𝔸e|f\n')), [paragraph('a\tb\nc d\u{1d538}e|f')]) }) test('reads a code span, its content literal', () => { assert.deepEqual(content(markdownToAdf('Run `npm test` now.\n')), [ { content: [text('Run '), codeSpan('npm test'), text(' now.')], type: 'paragraph' }, ]) assert.deepEqual(content(markdownToAdf('``a`b``\n')), [{ content: [codeSpan('a`b')], type: 'paragraph' }]) assert.deepEqual(content(markdownToAdf('` `` `\n')), [{ content: [codeSpan('``')], type: 'paragraph' }]) assert.deepEqual(content(markdownToAdf('` `\n')), [{ content: [codeSpan(' ')], type: 'paragraph' }]) assert.deepEqual(content(markdownToAdf('`a\nb`\n')), [{ content: [codeSpan('a b')], type: 'paragraph' }]) assert.deepEqual(content(markdownToAdf('`foo``bar`\n')), [{ content: [codeSpan('foo``bar')], type: 'paragraph' }]) assert.deepEqual(content(markdownToAdf('`:::panel` `~~x~~` `\\*` `&`\n')), [ { content: [codeSpan(':::panel'), text(' '), codeSpan('~~x~~'), text(' '), codeSpan('\\*'), text(' '), codeSpan('&')], type: 'paragraph', }, ]) assert.deepEqual(content(markdownToAdf('`foo\n')), [paragraph('`foo')]) assert.deepEqual(content(markdownToAdf('``foo`\n')), [paragraph('``foo`')]) }) test('reads a hard break from a trailing backslash and from two trailing spaces alike', () => { assert.deepEqual(content(markdownToAdf('One\\\ntwo.\n')), [{ content: [text('One'), hardBreak(), text('two.')], type: 'paragraph' }]) assert.deepEqual(content(markdownToAdf('One \ntwo.\n')), [{ content: [text('One'), hardBreak(), text('two.')], type: 'paragraph' }]) assert.deepEqual(content(markdownToAdf('One\\ \ntwo.\n')), [{ content: [text('One\\'), hardBreak(), text('two.')], type: 'paragraph' }]) assert.deepEqual(content(markdownToAdf('One \\\ntwo.\n')), [{ content: [text('One '), hardBreak(), text('two.')], type: 'paragraph' }]) assert.deepEqual(content(markdownToAdf('One \ntwo.\n')), [paragraph('One two.')]) assert.deepEqual(content(markdownToAdf('One \t\ntwo.\n')), [paragraph('One two.')]) assert.deepEqual(content(markdownToAdf('One \n')), [paragraph('One')]) assert.deepEqual(content(markdownToAdf('> One\\\n> two.\n')), [quote({ content: [text('One'), hardBreak(), text('two.')], type: 'paragraph' })]) }) test('decodes the fenced info string the block walk leaves raw', () => { assert.deepEqual(content(markdownToAdf('```java​script\nx\n```\n')), [ { attrs: { language: 'java\u200bscript' }, content: [text('x')], type: 'codeBlock' }, ]) assert.deepEqual(content(markdownToAdf('```\\#c\nx\n```\n')), [{ attrs: { language: '#c' }, content: [text('x')], type: 'codeBlock' }]) }) test('refuses the raw inline HTML no element mapping carries, naming it', () => { assert.equal(content(markdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: ') assert.equal(content(markdownToAdf('Part
here.\n')), 'unmappable-html: no raw HTML converts at this version:
') assert.equal(content(markdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML comment') assert.equal(content(markdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML processing instruction') assert.equal(content(markdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML declaration') assert.equal(content(markdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: a CDATA section') assert.equal(content(markdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML comment') assert.equal(content(markdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML comment') assert.equal(code(markdownToAdf('A b\n')), 'unmappable-html') assert.equal(code(markdownToAdf('Part.\n\n')), 'unmappable-html') assert.deepEqual(path(markdownToAdf('Part.\n\nA b.\n')), ['content', 1]) }) test('leaves the angle bracket that opens no HTML construct to the text it sits in', () => { assert.deepEqual(content(markdownToAdf('3 < 4 and 5 d\n')), [paragraph('a d')]) assert.deepEqual(content(markdownToAdf('a