880 lines
60 KiB
TypeScript
880 lines
60 KiB
TypeScript
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<AdfDocument>): string {
|
||
return result.ok ? `built ${JSON.stringify(result.value)}` : result.error.code
|
||
}
|
||
|
||
function content(result: Result<AdfDocument>): AdfNode[] | string {
|
||
return result.ok ? (result.value.content ?? []) : `${result.error.code}: ${result.error.message}`
|
||
}
|
||
|
||
function path(result: Result<AdfDocument>): readonly (number | string)[] {
|
||
return result.ok ? ['built'] : result.error.path
|
||
}
|
||
|
||
function position(result: Result<AdfDocument, ParseError>): 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, :::')
|
||
})
|
||
|
||
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')
|
||
assert.equal(content(markdownToAdf('| a |\n| x |\n')), 'malformed-pipe-table: a pipe table underlines its header with a row of `-` runs')
|
||
assert.equal(content(markdownToAdf('| a | b |\n| :--- | ---: |\n')), 'malformed-pipe-table: a pipe table carries no column alignment ADF could hold')
|
||
assert.equal(content(markdownToAdf('| a | b |\n| --- |\n')), 'malformed-pipe-table: a pipe table row holds 1 cells where its header holds 2')
|
||
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')
|
||
assert.deepEqual(path(markdownToAdf('Part.\n\n| a |\n')), ['content', 1])
|
||
})
|
||
|
||
test('refuses the image a pipe cell holds no ADF node for', () => {
|
||
assert.equal(content(markdownToAdf('| a |\n| --- |\n|  |\n')), 'unmappable-image: no ADF node carries an image inside a paragraph')
|
||
assert.deepEqual(path(markdownToAdf('| a |\n| --- |\n|  |\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-destination: 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')
|
||
assert.equal(code(markdownToAdf(':widget[x]\n')), 'unknown-directive-name')
|
||
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{…}')
|
||
assert.equal(content(markdownToAdf('::date {timestamp=1}\n')), 'unsupported-node-shape: date takes the inline form, :date{…}')
|
||
assert.equal(content(markdownToAdf(':paragraph[a]\n')), 'unsupported-node-shape: paragraph takes the block form, :::paragraph')
|
||
assert.equal(content(markdownToAdf(':rule[a]\n')), 'unsupported-node-shape: rule takes the block form, ::rule')
|
||
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"
|
||
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')
|
||
assert.equal(content(markdownToAdf(':adf{}\n')), 'unsupported-node-shape: adf holds one json attribute alone')
|
||
assert.equal(content(markdownToAdf(':adf{json="{}" localId=x}\n')), 'unsupported-node-shape: adf holds one json attribute alone')
|
||
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(`\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(`\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'
|
||
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')
|
||
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')
|
||
const deep = `${'['.repeat(largestNesting + 2)}${']'.repeat(largestNesting + 2)}`
|
||
assert.equal(content(markdownToAdf(`:::tableCell {colwidth="${deep}"}\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')
|
||
assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot')
|
||
})
|
||
|
||
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')
|
||
assert.equal(content(markdownToAdf(':::rule\nPart.\n:::\n')), 'unsupported-node-shape: rule holds no content')
|
||
assert.equal(content(markdownToAdf('::bulletList\n')), 'unsupported-node-shape: bulletList spells its body in the container form, :::')
|
||
assert.equal(content(markdownToAdf(':::paragraph\n:::\n')), 'unsupported-node-shape: an empty paragraph takes the leaf form, ::')
|
||
assert.equal(content(markdownToAdf(':::paragraph\nOne.\n\nTwo.\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body')
|
||
assert.equal(content(markdownToAdf(':::paragraph\n---\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body')
|
||
assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\nx\n:::\n')), 'unsupported-node-shape: codeBlock takes one code block as its body')
|
||
assert.equal(content(markdownToAdf(':::paragraph\n\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')
|
||
})
|
||
|
||
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(code(markdownToAdf('Part :mention[@A\n')), 'malformed-directive')
|
||
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('<!-- note -->\n')), 'unmappable-html')
|
||
assert.equal(code(markdownToAdf('<div>\nx\n</div>\n')), 'unmappable-html')
|
||
assert.equal(code(markdownToAdf('<?php ?>\n')), 'unmappable-html')
|
||
assert.equal(code(markdownToAdf('<!DOCTYPE html>\n')), 'unmappable-html')
|
||
assert.equal(code(markdownToAdf('<![CDATA[x]]>\n')), 'unmappable-html')
|
||
assert.equal(code(markdownToAdf('<pre>\nx\n</pre>\n')), 'unmappable-html')
|
||
assert.equal(code(markdownToAdf('<span foo="bar">\n')), 'unmappable-html')
|
||
assert.equal(code(markdownToAdf('<div\n')), 'unmappable-html')
|
||
assert.equal(code(markdownToAdf('<?php\n')), 'unmappable-html')
|
||
assert.deepEqual(path(markdownToAdf('Part.\n\n<div>\n')), ['content', 1])
|
||
assert.equal(code(markdownToAdf('<div>\nx\n\n:::\n')), 'unmappable-html')
|
||
assert.equal(code(markdownToAdf('<div>\n- x\n</div>\n')), 'unmappable-html')
|
||
})
|
||
|
||
test('swallows an HTML block ahead of the claim a line inside it would make', () => {
|
||
assert.equal(code(markdownToAdf('<!--\n:::\n-->\n')), 'unmappable-html')
|
||
assert.equal(code(markdownToAdf('<div>\n| x |\n</div>\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('<div>\n')), { line: 1, offset: 0 })
|
||
assert.deepEqual(position(markdownToAdf('Part.\n\n<div>\n')), { line: 3, offset: 7 })
|
||
assert.deepEqual(position(markdownToAdf('> Part.\n>\n> a <span>b</span>\n')), { line: 3, offset: 10 })
|
||
assert.deepEqual(position(markdownToAdf('- Part.\n- a <span>b</span>\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 <span>b</span>\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 <span>c</span>\n')), { line: 1, offset: 0 })
|
||
assert.deepEqual(position(markdownToAdf('Part.\r\n\r\n<div>\r\n')), { line: 3, offset: 9 })
|
||
assert.deepEqual(position(markdownToAdf('a\u0000b\n\n<div>\n')), { line: 3, offset: 5 })
|
||
assert.deepEqual(position(markdownToAdf(':::caption\na <span>b</span>\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\n<span>b</span>\n')), { line: 2, offset: 10 })
|
||
assert.deepEqual(position(markdownToAdf('[a]: /a\n[b]: /b\n[c]: /c\n[d]: /d\n<span>x</span>\n')), { line: 5, offset: 32 })
|
||
assert.deepEqual(position(markdownToAdf('[a]:\n<the url>\n"Title"\n<span>b</span>\n')), { line: 4, offset: 23 })
|
||
assert.deepEqual(position(markdownToAdf('> [a]: /url\n> <span>b</span>\n')), { line: 2, offset: 12 })
|
||
assert.deepEqual(position(markdownToAdf(':::caption\n[a]: /url\n<span>b</span>\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<div>\n')), 'unmappable-html')
|
||
assert.deepEqual(path(markdownToAdf('> One\n<div>\n')), ['content', 1])
|
||
assert.deepEqual(path(markdownToAdf('> One\n<span>\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 <span> here.\n')), 'unmappable-html: no ADF node carries <span>')
|
||
assert.equal(content(markdownToAdf('Part </div> here.\n')), 'unmappable-html: no ADF node carries <div>')
|
||
assert.equal(content(markdownToAdf('Part <!-- note --> here.\n')), 'unmappable-html: no ADF node carries an HTML comment')
|
||
assert.equal(content(markdownToAdf('Part <?php ?> here.\n')), 'unmappable-html: no ADF node carries an HTML processing instruction')
|
||
assert.equal(content(markdownToAdf('Part <!DOCTYPE html> here.\n')), 'unmappable-html: no ADF node carries an HTML declaration')
|
||
assert.equal(content(markdownToAdf('Part <![CDATA[x]]> here.\n')), 'unmappable-html: no ADF node carries a CDATA section')
|
||
assert.equal(content(markdownToAdf('Part <!--> here.\n')), 'unmappable-html: no ADF node carries an HTML comment')
|
||
assert.equal(content(markdownToAdf('Part <!---> here.\n')), 'unmappable-html: no ADF node carries an HTML comment')
|
||
assert.equal(code(markdownToAdf('A <a href="/x" disabled\nid=y> b\n')), 'unmappable-html')
|
||
assert.equal(code(markdownToAdf('Part.\n<span>\n')), 'unmappable-html')
|
||
assert.deepEqual(path(markdownToAdf('Part.\n\nA <b>b</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 <b 6\n')), [paragraph('3 < 4 and 5 <b 6')])
|
||
assert.deepEqual(content(markdownToAdf('a <b"c> d\n')), [paragraph('a <b"c> d')])
|
||
assert.deepEqual(content(markdownToAdf('a <!-- b\n')), [paragraph('a <!-- b')])
|
||
assert.deepEqual(content(markdownToAdf('a </b c> d\n')), [paragraph('a </b c> d')])
|
||
assert.deepEqual(content(markdownToAdf('`<span>`\n')), [{ content: [codeSpan('<span>')], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('\\<span>\n')), [paragraph('<span>')])
|
||
})
|
||
|
||
test('reads the emphasis CommonMark matches, the marks nesting outermost first', () => {
|
||
assert.deepEqual(content(markdownToAdf('*a*\n')), [{ content: [marked('a', em)], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('_a_\n')), [{ content: [marked('a', em)], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('**a**\n')), [{ content: [marked('a', strong)], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('__a__\n')), [{ content: [marked('a', strong)], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('***a***\n')), [{ content: [marked('a', em, strong)], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('*a **b** c*\n')), [
|
||
{ content: [marked('a ', em), marked('b', em, strong), marked(' c', em)], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(markdownToAdf('a*b*c\n')), [{ content: [text('a'), marked('b', em), text('c')], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('# *a*\n')), [{ attrs: { level: 1 }, content: [marked('a', em)], type: 'heading' }])
|
||
})
|
||
|
||
test('leaves a delimiter run CommonMark pairs with nothing in the text it sits in', () => {
|
||
assert.deepEqual(content(markdownToAdf('a_b_c\n')), [paragraph('a_b_c')])
|
||
assert.deepEqual(content(markdownToAdf('*a\n')), [paragraph('*a')])
|
||
assert.deepEqual(content(markdownToAdf('a * b\n')), [paragraph('a * b')])
|
||
assert.deepEqual(content(markdownToAdf('**a*\n')), [{ content: [text('*'), marked('a', em)], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('*a**\n')), [{ content: [marked('a', em), text('*')], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('\\*a\\*\n')), [paragraph('*a*')])
|
||
assert.deepEqual(content(markdownToAdf('`*a*`\n')), [{ content: [codeSpan('*a*')], type: 'paragraph' }])
|
||
})
|
||
|
||
test('reads two tildes as strike, a single tilde and a longer run literal', () => {
|
||
assert.deepEqual(content(markdownToAdf('~~a~~\n')), [{ content: [marked('a', strike)], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('a ~b~ c\n')), [paragraph('a ~b~ c')])
|
||
assert.deepEqual(content(markdownToAdf('a ~~~b~~~ c\n')), [paragraph('a ~~~b~~~ c')])
|
||
assert.deepEqual(content(markdownToAdf('~~a **b**~~\n')), [{ content: [marked('a ', strike), marked('b', strike, strong)], type: 'paragraph' }])
|
||
})
|
||
|
||
test('reads an inline link, its destination and title', () => {
|
||
assert.deepEqual(content(markdownToAdf('[a](/url)\n')), [{ content: [marked('a', link('/url'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('[a](/url "t")\n')), [{ content: [marked('a', link('/url', 't'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('[a](\n/url\n"t" )\n')), [{ content: [marked('a', link('/url', 't'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('[a](<u v>)\n')), [{ content: [marked('a', link('u v'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('[a]()\n')), [{ content: [marked('a', link(''))], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('[a](/x(y))\n')), [{ content: [marked('a', link('/x(y)'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('[**a**](/u)\n')), [{ content: [marked('a', link('/u'), strong)], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('[a `b`](/u)\n')), [
|
||
{ content: [marked('a ', link('/u')), { marks: [link('/u'), { type: 'code' }], text: 'b', type: 'text' }], type: 'paragraph' },
|
||
])
|
||
})
|
||
|
||
test('decodes the escapes and the references a destination and a title hold', () => {
|
||
assert.deepEqual(content(markdownToAdf('[a](/x\\)y)\n')), [{ content: [marked('a', link('/x)y'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('[a](/u "He said \\"hi\\"")\n')), [{ content: [marked('a', link('/u', 'He said "hi"'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('[a](/x&y)\n')), [{ content: [marked('a', link('/x&y'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('[a][r]\n\n[r]: /x\\)y "He said \\"hi\\""\n')), [
|
||
{ content: [marked('a', link('/x)y', 'He said "hi"'))], type: 'paragraph' },
|
||
])
|
||
})
|
||
|
||
test('leaves the bracket pair no link parses as the text it holds', () => {
|
||
assert.deepEqual(content(markdownToAdf('[a\n')), [paragraph('[a')])
|
||
assert.deepEqual(content(markdownToAdf('[a] (/u)\n')), [paragraph('[a] (/u)')])
|
||
assert.deepEqual(content(markdownToAdf('a ] b\n')), [paragraph('a ] b')])
|
||
assert.deepEqual(content(markdownToAdf('[a](/u\n')), [paragraph('[a](/u')])
|
||
assert.deepEqual(content(markdownToAdf('[a](/u x)\n')), [paragraph('[a](/u x)')])
|
||
assert.deepEqual(content(markdownToAdf('[a](<u\n')), [paragraph('[a](<u')])
|
||
assert.deepEqual(content(markdownToAdf('![a\n')), [paragraph('![a')])
|
||
assert.deepEqual(content(markdownToAdf('[a [b](/u) c](/v)\n')), [
|
||
{ content: [text('[a '), marked('b', link('/u')), text(' c](/v)')], type: 'paragraph' },
|
||
])
|
||
})
|
||
|
||
test('reads the reference links a definition resolves, and leaves the rest literal', () => {
|
||
assert.deepEqual(content(markdownToAdf('[a][r]\n\n[r]: /url\n')), [{ content: [marked('a', link('/url'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('[a][]\n\n[a]: /url\n')), [{ content: [marked('a', link('/url'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('[a]\n\n[a]: /url\n')), [{ content: [marked('a', link('/url'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('[Foo\nBar][]\n\n[foo bar]: /url\n')), [{ content: [marked('Foo Bar', link('/url'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('[a][z]\n\n[a]: /url\n')), [paragraph('[a][z]')])
|
||
assert.deepEqual(content(markdownToAdf('[a]\n')), [paragraph('[a]')])
|
||
assert.deepEqual(content(markdownToAdf('[][]\n')), [paragraph('[][]')])
|
||
})
|
||
|
||
test('reads an autolink, the email form as the mailto link it means', () => {
|
||
assert.deepEqual(content(markdownToAdf('<https://example.com/>\n')), [
|
||
{ content: [marked('https://example.com/', link('https://example.com/'))], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(markdownToAdf('a <a@b.example.com> c\n')), [
|
||
{ content: [text('a '), marked('a@b.example.com', link('mailto:a@b.example.com')), text(' c')], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(markdownToAdf('a <a@b-c.example.com> d\n')), [
|
||
{ content: [text('a '), marked('a@b-c.example.com', link('mailto:a@b-c.example.com')), text(' d')], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(markdownToAdf('a <b@-c.example.com> d\n')), [paragraph('a <b@-c.example.com> d')])
|
||
assert.deepEqual(content(markdownToAdf('a <b[c@example.com> d\n')), [paragraph('a <b[c@example.com> d')])
|
||
assert.deepEqual(content(markdownToAdf('<https://example.com/?a=\\*>\n')), [
|
||
{ content: [marked('https://example.com/?a=\\*', link('https://example.com/?a=\\*'))], type: 'paragraph' },
|
||
])
|
||
assert.equal(code(markdownToAdf('<https://example.com/> <span>\n')), 'unmappable-html')
|
||
})
|
||
|
||
test('reads a lone image as the media the flavour spells for it', () => {
|
||
assert.deepEqual(content(markdownToAdf('\n')), [
|
||
image('https://example.com/moon.png', 'The moon'),
|
||
])
|
||
assert.deepEqual(content(markdownToAdf('\n')), [image('/u')])
|
||
assert.deepEqual(content(markdownToAdf('\n')), [image('/u', 'a')])
|
||
assert.deepEqual(content(markdownToAdf('- \n')), [bulletList(item(image('/u', 'a')))])
|
||
})
|
||
|
||
test('flattens the description of a lone image to the plain text alt holds', () => {
|
||
assert.deepEqual(content(markdownToAdf(' c](/v)\n')), [image('/v', 'a b c')])
|
||
assert.deepEqual(content(markdownToAdf(' d](/e)\n')), [image('/e', 'a b d')])
|
||
assert.deepEqual(content(markdownToAdf('\n')), [image('/u', 'a b')])
|
||
assert.deepEqual(content(markdownToAdf('\n')), [image('/u', 'a b')])
|
||
assert.deepEqual(content(markdownToAdf('\n')), [image('/u', 'a b')])
|
||
assert.deepEqual(content(markdownToAdf('![a :mention[@A]{id=b1c2} b](/u)\n')), [image('/u', 'a @A b')])
|
||
assert.deepEqual(content(markdownToAdf('![:mention[@A]{id=b1c2}](/u)\n')), [image('/u', '@A')])
|
||
})
|
||
|
||
test('leaves the brackets of an empty link text the text they are', () => {
|
||
assert.deepEqual(content(markdownToAdf('[](/u)\n')), [paragraph('[](/u)')])
|
||
assert.deepEqual(content(markdownToAdf('a [](/u) b\n')), [paragraph('a [](/u) b')])
|
||
// The pair gives the label back the way an unresolved one does, so the shortcut behind it still reads.
|
||
assert.deepEqual(content(markdownToAdf('[][r]\n\n[r]: /u\n')), [{ content: [text('[]'), marked('r', link('/u'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('\n')), [image('/u')])
|
||
})
|
||
|
||
test('refuses the image no ADF node carries where it sits', () => {
|
||
assert.equal(content(markdownToAdf('\n')), 'unmappable-image: no media node carries a link title')
|
||
assert.equal(content(markdownToAdf('See .\n')), 'unmappable-image: an image fits only as a paragraph of its own')
|
||
assert.equal(code(markdownToAdf('# \n')), 'unmappable-image')
|
||
assert.equal(code(markdownToAdf('**\n')), 'unmappable-image')
|
||
assert.equal(code(markdownToAdf('[](/v)\n')), 'unmappable-image')
|
||
assert.equal(code(markdownToAdf('\n')), 'unmappable-image')
|
||
assert.equal(code(markdownToAdf(' d\n')), 'unmappable-image')
|
||
assert.deepEqual(path(markdownToAdf('Part.\n\nSee .\n')), ['content', 1])
|
||
assert.deepEqual(content(markdownToAdf('![a]\n')), [paragraph('![a]')])
|
||
assert.deepEqual(content(markdownToAdf('a ! b\n')), [paragraph('a ! b')])
|
||
})
|
||
|
||
test('carries the mark a spelling nested inside its own kind names once', () => {
|
||
assert.deepEqual(content(markdownToAdf('*(*a*)*\n')), [{ content: [marked('(a)', em)], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf(`${'*'.repeat(600)}a${'*'.repeat(600)}\n`)), [{ content: [marked('a', strong)], type: 'paragraph' }])
|
||
assert.deepEqual(content(markdownToAdf('*a **b** c*\n')), [
|
||
{ content: [marked('a ', em), marked('b', em, strong), marked(' c', em)], type: 'paragraph' },
|
||
])
|
||
})
|
||
|
||
test('reads the content slot as the text attribute the node spells there', () => {
|
||
const status = (attrs: AdfAttributes): AdfNode[] => [{ content: [{ attrs, type: 'status' }], type: 'paragraph' }]
|
||
assert.deepEqual(content(markdownToAdf(':status[In review]{color=yellow}\n')), status({ color: 'yellow', text: 'In review' }))
|
||
assert.deepEqual(content(markdownToAdf(':status{color=neutral}\n')), status({ color: 'neutral' }))
|
||
assert.deepEqual(content(markdownToAdf(':status[]{color=neutral}\n')), status({ color: 'neutral', text: '' }))
|
||
assert.deepEqual(content(markdownToAdf(':status[ In review ]{color=yellow}\n')), status({ color: 'yellow', text: ' In review ' }))
|
||
assert.deepEqual(content(markdownToAdf(':status[In:text{text=" "}review]{color=yellow}\n')), status({ color: 'yellow', text: 'In review' }))
|
||
assert.deepEqual(content(markdownToAdf(':status[a\\]b]{color=yellow}\n')), status({ color: 'yellow', text: 'a]b' }))
|
||
assert.deepEqual(content(markdownToAdf('**:mention[@A]{id=b1c2}**\n')), [
|
||
{ content: [{ attrs: { id: 'b1c2', text: '@A' }, marks: [strong], type: 'mention' }], type: 'paragraph' },
|
||
])
|
||
})
|
||
|
||
test('names the content slot no one unmarked text node reads back from', () => {
|
||
assert.equal(content(markdownToAdf(':status[**A**]{color=yellow}\n')), 'unsupported-node-shape: the status content slot holds one unmarked text node')
|
||
assert.equal(code(markdownToAdf(':status[a`b`]{color=yellow}\n')), 'unsupported-node-shape')
|
||
assert.equal(code(markdownToAdf(':status[:date{timestamp=1}]{color=yellow}\n')), 'unsupported-node-shape')
|
||
assert.equal(content(markdownToAdf(':status[]{color=yellow}\n')), 'unmappable-image: an image fits only as a paragraph of its own')
|
||
assert.equal(code(markdownToAdf(':status[<div>]{color=yellow}\n')), 'unmappable-html')
|
||
assert.equal(code(markdownToAdf(':date[<div>]{timestamp=1}\n')), 'unmappable-html')
|
||
assert.equal(code(markdownToAdf(':widget[<div>]\n')), 'unmappable-html')
|
||
const spans = 'unspellable-whitespace: the status content slot holds a newline no inline directive spans'
|
||
assert.equal(content(markdownToAdf(':status[:text{text="\\n"}]{color=yellow}\n')), spans)
|
||
assert.equal(content(markdownToAdf(':status[a b]{color=yellow}\n')), spans)
|
||
assert.equal(content(markdownToAdf(':status[a b]{color=yellow}\n')), spans)
|
||
assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot')
|
||
})
|
||
|
||
test('reads the whitespace the reserved text directive carries', () => {
|
||
assert.deepEqual(content(markdownToAdf(':text{text=" "}a\n')), [paragraph(' a')])
|
||
assert.deepEqual(content(markdownToAdf('a:text{text="\\n"}b\n')), [paragraph('a\nb')])
|
||
assert.deepEqual(content(markdownToAdf('a:text{text="\\t"}\n')), [paragraph('a\t')])
|
||
assert.deepEqual(content(markdownToAdf('_:text{text=" "}a_\n')), [{ content: [marked(' a', em)], type: 'paragraph' }])
|
||
})
|
||
|
||
test('names the text directive spelling no whitespace run reads back from', () => {
|
||
const named = 'unsupported-node-shape: text spells one run of spaces and tabs, or one run of newlines'
|
||
assert.equal(content(markdownToAdf(':text{text=hi}\n')), named)
|
||
assert.equal(content(markdownToAdf(':text{text=" \\n"}\n')), named)
|
||
assert.equal(content(markdownToAdf(':text{text=""}\n')), named)
|
||
assert.equal(content(markdownToAdf(':text{}\n')), 'unsupported-node-shape: text holds one text attribute alone')
|
||
assert.equal(content(markdownToAdf(':text{localId=a text=" "}\n')), 'unsupported-node-shape: text holds one text attribute alone')
|
||
assert.equal(content(markdownToAdf(':text[a]{text=" "}\n')), 'unsupported-node-shape: text takes no content')
|
||
assert.equal(content(markdownToAdf(':text{text="\\u0020"}\n')), 'unsupported-node-shape: text spells its text attribute as text=" "')
|
||
})
|
||
|
||
test('reads the directive marks, the nesting outermost first', () => {
|
||
const wrapped = (...marks: AdfMark[]): AdfNode[] => [{ content: [marked('a', ...marks)], type: 'paragraph' }]
|
||
assert.deepEqual(content(markdownToAdf(':underline[a]\n')), wrapped(underline))
|
||
assert.deepEqual(content(markdownToAdf('_:underline[a]_\n')), wrapped(em, underline))
|
||
assert.deepEqual(content(markdownToAdf(':underline[_a_]\n')), wrapped(underline, em))
|
||
assert.deepEqual(content(markdownToAdf(':underline[:underline[a]]\n')), wrapped(underline))
|
||
assert.deepEqual(content(markdownToAdf(':textColor[a]{color="#ae2e24"}\n')), wrapped({ attrs: { color: '#ae2e24' }, type: 'textColor' }))
|
||
assert.deepEqual(content(markdownToAdf(':subsup[a]{type=sub}\n')), wrapped({ attrs: { type: 'sub' }, type: 'subsup' }))
|
||
assert.deepEqual(content(markdownToAdf(':border[a]{color="#091e42" size=2}\n')), wrapped({ attrs: { color: '#091e42', size: 2 }, type: 'border' }))
|
||
assert.deepEqual(content(markdownToAdf(':underline[a:date{timestamp=1}]\n')), [
|
||
{ content: [marked('a', underline), { attrs: { timestamp: '1' }, marks: [underline], type: 'date' }], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(markdownToAdf('_:underline[a:date{timestamp=1}]_\n')), [
|
||
{ content: [marked('a', em, underline), { attrs: { timestamp: '1' }, marks: [em, underline], type: 'date' }], type: 'paragraph' },
|
||
])
|
||
assert.equal(content(markdownToAdf(':border[a]{color="#091e42" size=x}\n')), 'unsupported-node-shape: the size attribute of border is no number')
|
||
})
|
||
|
||
test('names the mark markdown spells, never a directive', () => {
|
||
assert.equal(content(markdownToAdf(':em[a]\n')), 'unsupported-node-shape: em is spelled _x_, never as a directive')
|
||
assert.equal(content(markdownToAdf(':strong[a]\n')), 'unsupported-node-shape: strong is spelled **x**, never as a directive')
|
||
assert.equal(content(markdownToAdf(':strike[a]\n')), 'unsupported-node-shape: strike is spelled ~~x~~, never as a directive')
|
||
assert.equal(content(markdownToAdf(':code[a]\n')), 'unsupported-node-shape: code is spelled `x`, never as a directive')
|
||
assert.equal(content(markdownToAdf(':link[a]{href="/u"}\n')), 'unsupported-node-shape: link is spelled [x](url), never as a directive')
|
||
})
|
||
|
||
test('names the directive mark left without the content it wraps', () => {
|
||
const named = 'unsupported-node-shape: the underline mark wraps the [content] it marks'
|
||
assert.equal(content(markdownToAdf(':underline[]\n')), named)
|
||
assert.equal(content(markdownToAdf(':underline{}\n')), named)
|
||
})
|