mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
1372 lines
107 KiB
TypeScript
1372 lines
107 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 { ConvertErrorCode, ParseError, Result, SourcePosition } from '../../result.ts'
|
||
import { commonMarkToAdf, losslessMarkdownToAdf } from './markdown-to-adf.ts'
|
||
import { largestNesting } from '../../nesting.ts'
|
||
import { portableMarkdownToAdf } from '../portable/portable-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
|
||
}
|
||
|
||
// A message is free text: a refusal pins its code and only the facts the message must name.
|
||
function refused(result: Result<AdfDocument>, errorCode: ConvertErrorCode, ...facts: string[]): void {
|
||
assert.equal(code(result), errorCode)
|
||
const message = result.ok ? '' : result.error.message
|
||
for (const fact of facts) assert.ok(message.includes(fact), `${JSON.stringify(message)} names ${fact}`)
|
||
}
|
||
|
||
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 firstContent(markdown: string): unknown {
|
||
const [block] = content(losslessMarkdownToAdf(markdown))
|
||
return typeof block === 'object' ? block.content : block
|
||
}
|
||
|
||
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('refuses a value that is not a string, in every reader, at the start of the input', () => {
|
||
const revoked = Proxy.revocable({}, {})
|
||
revoked.revoke()
|
||
const found: [unknown, string][] = [[null, 'null'], [undefined, 'undefined'], [42, 'a number'], [10n, 'a bigint'], [['x'], 'an object'], [revoked.proxy, 'an object']]
|
||
for (const read of [commonMarkToAdf, losslessMarkdownToAdf, portableMarkdownToAdf]) {
|
||
for (const [value, described] of found) {
|
||
const result = Reflect.apply(read, undefined, [value])
|
||
refused(result, 'not-a-string', 'a string', `found ${described}`)
|
||
assert.deepEqual([path(result), position(result)], [[], { line: 1, offset: 0 }])
|
||
}
|
||
assert.notStrictEqual(position(Reflect.apply(read, undefined, [null])), position(Reflect.apply(read, undefined, [null])))
|
||
}
|
||
})
|
||
|
||
const highlight: AdfMark = { attrs: { color: '#f8e6a0' }, type: 'backgroundColor' }
|
||
|
||
const flavourShapes: readonly [string, AdfNode[], AdfNode[], AdfNode[]][] = [
|
||
['!adf:panel info\nx\n!adf:/panel\n', [paragraph('!adf:panel info x !adf:/panel')], [{ attrs: { panelType: 'info' }, content: [paragraph('x')], type: 'panel' }], [paragraph('!adf:panel info x !adf:/panel')]],
|
||
['a!adf:textBreak{}a\n', [paragraph('a!adf:textBreak{}a')], [{ content: [text('a'), text('a')], type: 'paragraph' }], [paragraph('a!adf:textBreak{}a')]],
|
||
['!adf:doc {content=none}\n', [paragraph('!adf:doc {content=none}')], [], [paragraph('!adf:doc {content=none}')]],
|
||
['| a |\n| --- |\n| b |\n', [paragraph('| a | | --- | | b |')], [table(row(cell('tableHeader', text('a'))), row(cell('tableCell', text('b'))))], [table(row(cell('tableHeader', text('a'))), row(cell('tableCell', text('b'))))]],
|
||
['~~x~~\n', [paragraph('~~x~~')], [{ content: [marked('x', strike)], type: 'paragraph' }], [{ content: [marked('x', strike)], type: 'paragraph' }]],
|
||
['==x==\n', [paragraph('==x==')], [paragraph('==x==')], [{ content: [marked('x', highlight)], type: 'paragraph' }]],
|
||
['> [!NOTE]\n> x\n', [quote(paragraph('[!NOTE] x'))], [quote(paragraph('[!NOTE] x'))], [{ attrs: { panelType: 'info' }, content: [paragraph('x')], type: 'panel' }]],
|
||
['- [x] a\n', [bulletList(item(paragraph('[x] a')))], [bulletList(item(paragraph('[x] a')))], [{ content: [{ attrs: { state: 'DONE' }, content: [text('a')], type: 'taskItem' }], type: 'taskList' }]],
|
||
['```adf:futureBlock\n{}\n```\n', [{ attrs: { language: 'adf:futureBlock' }, content: [text('{}')], type: 'codeBlock' }], [{ type: 'futureBlock' }], [{ attrs: { language: 'adf:futureBlock' }, content: [text('{}')], type: 'codeBlock' }]],
|
||
]
|
||
|
||
for (const [markdown, commonMark, lossless, portable] of flavourShapes) {
|
||
test(`reads a shape a flavour claims as the flavour of the reader it reaches: ${JSON.stringify(markdown)}`, () => {
|
||
const withoutIds = (result: Result<AdfDocument>): AdfNode[] | string => JSON.parse(JSON.stringify(content(result)), (key, value: unknown) => (key === 'localId' || (key === 'attrs' && JSON.stringify(value) === '{}') ? undefined : value))
|
||
assert.deepEqual(content(commonMarkToAdf(markdown)), commonMark)
|
||
assert.deepEqual(content(losslessMarkdownToAdf(markdown)), lossless)
|
||
assert.deepEqual(withoutIds(portableMarkdownToAdf(markdown)), portable)
|
||
})
|
||
}
|
||
|
||
test('builds an empty document from input holding no block, and one holding no content key from the doc directive alone', () => {
|
||
assert.deepEqual(losslessMarkdownToAdf(''), { ok: true, value: { content: [], type: 'doc', version: 1 } })
|
||
assert.deepEqual(losslessMarkdownToAdf('\n \n\t\n'), { ok: true, value: { content: [], type: 'doc', version: 1 } })
|
||
assert.deepEqual(losslessMarkdownToAdf('!adf:doc {content=none}\n'), { ok: true, value: { type: 'doc', version: 1 } })
|
||
assert.deepEqual(losslessMarkdownToAdf('\n!adf:doc {content=none}\n\n'), { ok: true, value: { type: 'doc', version: 1 } })
|
||
const form = ['!adf:doc {content=none}']
|
||
refused(losslessMarkdownToAdf('!adf:doc\n'), 'unsupported-node-shape', ...form)
|
||
refused(losslessMarkdownToAdf('!adf:doc x {content=none}\n'), 'unsupported-node-shape', ...form)
|
||
refused(losslessMarkdownToAdf('!adf:doc {content=empty}\n'), 'unsupported-node-shape', 'empty markdown', '!adf:doc {content=none}', 'content=empty')
|
||
refused(losslessMarkdownToAdf('!adf:doc {content="none"}\n'), 'unsupported-node-shape', ...form)
|
||
const alone = ['delete the !adf:doc {content=none} line', 'whole document']
|
||
refused(losslessMarkdownToAdf('x\n\n!adf:doc {content=none}\n'), 'unsupported-node-shape', ...alone)
|
||
refused(losslessMarkdownToAdf('!adf:panel info\n!adf:doc {content=none}\n!adf:/panel\n'), 'unsupported-node-shape', ...alone)
|
||
refused(losslessMarkdownToAdf('!adf:doc\n!adf:/doc\n'), 'malformed-directive', '!adf:/doc', '\\!adf:')
|
||
refused(losslessMarkdownToAdf('a !adf:doc{content=none}\n'), 'unsupported-node-shape', '!adf:doc', 'inline form')
|
||
})
|
||
|
||
test('builds one paragraph from the lines a blank line does not part', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('One\ntwo.\n\nThree.\n')), [paragraph('One two.'), paragraph('Three.')])
|
||
})
|
||
|
||
test('reads an ATX heading and its level', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('# Assembly\n')), [{ attrs: { level: 1 }, content: [text('Assembly')], type: 'heading' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('###### M8\n')), [{ attrs: { level: 6 }, content: [text('M8')], type: 'heading' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' ## Parts ##\n')), [{ attrs: { level: 2 }, content: [text('Parts')], type: 'heading' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('#\n')), [{ attrs: { level: 1 }, type: 'heading' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('####### Seven\n')), [paragraph('####### Seven')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('#hashtag\n')), [paragraph('#hashtag')])
|
||
})
|
||
|
||
test('reads a setext underline as the heading level it spells', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('Assembly\n===\n')), [{ attrs: { level: 1 }, content: [text('Assembly')], type: 'heading' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('One\ntwo\n-\n')), [{ attrs: { level: 2 }, content: [text('One two')], type: 'heading' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('===\n')), [paragraph('===')])
|
||
})
|
||
|
||
test('reads a thematic break, the dashed one only where no paragraph is open', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('---\n')), [{ type: 'rule' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('Part.\n\n * * *\n')), [paragraph('Part.'), { type: 'rule' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('___\n')), [{ type: 'rule' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('Part.\n***\n')), [paragraph('Part.'), { type: 'rule' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('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(losslessMarkdownToAdf('```sql\nSELECT id\nFROM part\n```\n')), [
|
||
{ attrs: { language: 'sql' }, content: [text('SELECT id\nFROM part')], type: 'codeBlock' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('```\nx\n```\n')), [{ content: [text('x')], type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('``` rust \nx\n```\n')), [{ attrs: { language: 'rust' }, content: [text('x')], type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('```\n```\n')), [{ type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('```\nx\n')), [{ content: [text('x')], type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('````\n```\n````\n')), [{ content: [text('```')], type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('~~~ a`b\n```\n~~~\n')), [{ attrs: { language: 'a`b' }, content: [text('```')], type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('``` a`b\n')), [paragraph('``` a`b')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('```\n``` x\n```\n')), [{ content: [text('``` x')], type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('```\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 = '!adf:codeBlock {wrap=true}\n```rust\nfn main() {}\n```\n!adf:/codeBlock\n'
|
||
assert.deepEqual(content(losslessMarkdownToAdf(fenced)), [
|
||
{ attrs: { language: 'rust', wrap: true }, content: [text('fn main() {}')], type: 'codeBlock' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:codeBlock {wrap=true}\n```\n```\n!adf:/codeBlock\n')), [{ attrs: { wrap: true }, type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:codeBlock {language=""}\n```\nx\n```\n!adf:/codeBlock\n')), [
|
||
{ attrs: { language: '' }, content: [text('x')], type: 'codeBlock' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:codeBlock {wrap=true}\n fn()\n!adf:/codeBlock\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(losslessMarkdownToAdf('!adf:codeBlock {wrap=true}\n```\\#c\nx\n```\n!adf:/codeBlock\n')), [
|
||
{ attrs: { language: '#c', wrap: true }, content: [text('x')], type: 'codeBlock' },
|
||
])
|
||
})
|
||
|
||
test('names the slot a codeBlock spells its language outside of', () => {
|
||
const slot = ['codeBlock', 'info string']
|
||
refused(losslessMarkdownToAdf('!adf:codeBlock {language=rust wrap=true}\n```\nx\n```\n!adf:/codeBlock\n'), 'unsupported-node-shape', 'language=rust', 'info strings')
|
||
refused(losslessMarkdownToAdf('!adf:codeBlock {language="c sharp"}\n```\nx\n```\n```\ny\n```\n!adf:/codeBlock\n'), 'unsupported-node-shape', 'language="c sharp"', 'info strings')
|
||
refused(losslessMarkdownToAdf('!adf:codeBlock {language=rust}\n```sql\nx\n```\n!adf:/codeBlock\n'), 'unsupported-node-shape', ...slot)
|
||
refused(losslessMarkdownToAdf('!adf:codeBlock {wrap=true}\n```adf:x\nx\n```\n!adf:/codeBlock\n'), 'unsupported-node-shape', ...slot)
|
||
refused(losslessMarkdownToAdf('!adf:codeBlock {wrap=true}\n```a\\b\nx\n```\n!adf:/codeBlock\n'), 'unsupported-node-shape', ...slot)
|
||
})
|
||
|
||
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(losslessMarkdownToAdf(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(losslessMarkdownToAdf('| Part\n| -\n')), [table(row(cell('tableHeader', text('Part'))))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' | 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(losslessMarkdownToAdf('Part.\n| a |\n| --- |\n')), [paragraph('Part.'), header])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('| a |\n| --- |\nPart.\n')), [header, paragraph('Part.')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('> | a |\n> | --- |\n')), [quote(header)])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('- | a |\n | --- |\n')), [bulletList(item(header))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('| a |\n| --- |\n x\n')), [header, { content: [text('x')], type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('| a | b |\n| --- | --- |\nc | d\n')), [table(row(cell('tableHeader', text('a')), cell('tableHeader', text('b')))), paragraph('c | d')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\\| a |\n')), [paragraph('| a |')])
|
||
})
|
||
|
||
test('names the pipe table a claimed line does not spell', () => {
|
||
refused(losslessMarkdownToAdf('| a | b |\n'), 'malformed-pipe-table', '`-`', '\\|')
|
||
refused(losslessMarkdownToAdf('| a |\n| x |\n'), 'malformed-pipe-table', '`-`', '\\|')
|
||
refused(losslessMarkdownToAdf('| a | b |\n| :--- | ---: |\n'), 'malformed-pipe-table', 'alignment colon')
|
||
refused(losslessMarkdownToAdf('| a | b |\n| --- |\n'), 'malformed-pipe-table', '1 cell', '2 cells')
|
||
refused(losslessMarkdownToAdf('| a |\n| --- |\n| b | c |\n'), 'malformed-pipe-table', '2 cells', '1 cell')
|
||
assert.deepEqual(path(losslessMarkdownToAdf('Part.\n\n| a |\n')), ['content', 1])
|
||
})
|
||
|
||
test('names the pipe table whose rows open with no pipe', () => {
|
||
const bare = ['`|`', '\\|']
|
||
refused(losslessMarkdownToAdf('a | b\n--- | ---\n'), 'malformed-pipe-table', ...bare)
|
||
refused(losslessMarkdownToAdf('Intro.\na | b\n--- | ---\n'), 'malformed-pipe-table', ...bare)
|
||
refused(losslessMarkdownToAdf('a | b\n--- | ---\n===\n'), 'malformed-pipe-table', ...bare)
|
||
refused(losslessMarkdownToAdf('a | b\n:--- | ---:\n'), 'malformed-pipe-table', ...bare)
|
||
refused(losslessMarkdownToAdf('a |\n--- |\n'), 'malformed-pipe-table', ...bare)
|
||
assert.deepEqual(content(losslessMarkdownToAdf('| a | b |\n| --- | --- |\nc\n')), [table(row(cell('tableHeader', text('a')), cell('tableHeader', text('b')))), paragraph('c')])
|
||
refused(losslessMarkdownToAdf('| a | b |\n| --- | --- |\n| c |\n'), 'malformed-pipe-table', '1 cell', '2 cells')
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a | b\nc | d\n')), [paragraph('a | b c | d')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a | b\n--- | --- | ---\n')), [paragraph('a | b --- | --- | ---')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a \\| b\n--- | ---\n')), [paragraph('a | b --- | ---')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a\n---\n')), [{ attrs: { level: 2 }, content: [text('a')], type: 'heading' }])
|
||
assert.deepEqual(path(losslessMarkdownToAdf('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 = ['`|`', '\\|']
|
||
refused(losslessMarkdownToAdf('!adf:caption\na | b\n--- | ---\n!adf:/caption\n'), 'malformed-pipe-table', ...bare)
|
||
assert.deepEqual(position(losslessMarkdownToAdf('!adf:caption\na | b\n--- | ---\n!adf:/caption\n')), { line: 2, offset: 13 })
|
||
assert.equal(code(losslessMarkdownToAdf('!adf:caption\n| a |\n!adf:/caption\n')), 'malformed-pipe-table')
|
||
refused(losslessMarkdownToAdf('!adf:caption\n- a\n!adf:/caption\n'), 'unsupported-node-shape', 'caption', 'one paragraph')
|
||
})
|
||
|
||
test('reads a change of bullet character or ordered delimiter as a second list, in every reader', () => {
|
||
const parted = [bulletList(item(paragraph('a'))), bulletList(item(paragraph('b')))]
|
||
for (const read of [commonMarkToAdf, losslessMarkdownToAdf, portableMarkdownToAdf]) {
|
||
assert.deepEqual(content(read('- a\n* b\n')), parted)
|
||
assert.deepEqual(content(read('- a\n\n+ b\n')), parted)
|
||
assert.deepEqual(content(read('- a\n-\n\n* b\n')), [bulletList(item(paragraph('a')), item()), bulletList(item(paragraph('b')))])
|
||
assert.deepEqual(content(read('- a\n* b\n- c\n')), [...parted, bulletList(item(paragraph('c')))])
|
||
assert.deepEqual(content(read('1. a\n1) b\n')), [orderedList(1, item(paragraph('a'))), orderedList(1, item(paragraph('b')))])
|
||
assert.deepEqual(content(read('> - a\n> * b\n')), [quote(...parted)])
|
||
assert.deepEqual(content(read('- - a\n * b\n')), [bulletList(item(...parted))])
|
||
}
|
||
assert.deepEqual(path(losslessMarkdownToAdf('- a\n\n* b\n\n| x |\n')), ['content', 2])
|
||
})
|
||
|
||
test('reads the image a pipe cell holds as its linked alt text', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('| a |\n| --- |\n|  |\n')), [
|
||
{ content: [{ content: [cell('tableHeader', text('a'))], type: 'tableRow' }, { content: [cell('tableCell', marked('x', link('/u')))], type: 'tableRow' }], type: 'table' },
|
||
])
|
||
})
|
||
|
||
test('strips the opening fence indentation from the content lines it holds', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' ```\n x\n y\n ```\n')), [{ content: [text(' x\ny')], type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' ```\n\tx\n ```\n')), [{ content: [text(' x')], type: 'codeBlock' }])
|
||
})
|
||
|
||
test('reads an indented code block where no paragraph is open', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' SELECT id\n')), [{ content: [text('SELECT id')], type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\tSELECT id\n')), [{ content: [text('SELECT id')], type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' x\n')), [{ content: [text(' x')], type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' a\n\n b\n\nPart.\n')), [{ content: [text('a\n\nb')], type: 'codeBlock' }, paragraph('Part.')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('Part.\n more\n')), [paragraph('Part. more')])
|
||
})
|
||
|
||
test('claims a block line the prefix opens with no directive to parse it', () => {
|
||
assert.equal(code(losslessMarkdownToAdf('!adf:/panel\n')), 'malformed-directive')
|
||
assert.equal(code(losslessMarkdownToAdf('!adf:panel info\n')), 'malformed-directive')
|
||
assert.equal(code(losslessMarkdownToAdf('!adf:panel {a=1 a=2}\n!adf:/panel\n')), 'malformed-directive')
|
||
refused(losslessMarkdownToAdf('!adf:Panel\n'), 'malformed-directive', '\\!adf:')
|
||
assert.deepEqual(content(losslessMarkdownToAdf(':::panel info\nPart.\n:::\n')), [paragraph(':::panel info Part. :::')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('::rule\n')), [paragraph('::rule')])
|
||
})
|
||
|
||
test('reads the three directive forms into the nodes the tables name', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {localId=a-1}\n')), [{ attrs: { localId: 'a-1' }, type: 'rule' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:paragraph\n!adf:/paragraph\n')), [{ type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:bulletList\n!adf:/bulletList\n')), [{ type: 'bulletList' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' !adf:panel info\nPart.\n !adf:/panel\n')), [
|
||
{ attrs: { panelType: 'info' }, content: [paragraph('Part.')], type: 'panel' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:blockquote {localId=a-1}\n!adf:/blockquote\n')), [{ attrs: { localId: 'a-1' }, type: 'blockquote' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:heading {level=2 localId=a-1}\nPart.\n!adf:/heading\n')), [
|
||
{ attrs: { level: 2, localId: 'a-1' }, content: [text('Part.')], type: 'heading' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('Part!adf:hardBreak{}.\n')), [{ content: [text('Part'), hardBreak(), text('.')], type: 'paragraph' }])
|
||
})
|
||
|
||
test('names the CommonMark spelling to write for the directive form a node CommonMark spells', () => {
|
||
refused(losslessMarkdownToAdf('!adf:rule\n'), 'unsupported-node-shape', 'rule', '`---`')
|
||
refused(losslessMarkdownToAdf('!adf:blockquote\nPart.\n!adf:/blockquote\n'), 'unsupported-node-shape', 'blockquote', 'each line behind `> `')
|
||
refused(losslessMarkdownToAdf('!adf:heading {level=2}\nPart.\n!adf:/heading\n'), 'unsupported-node-shape', 'heading', '`## Title`')
|
||
refused(losslessMarkdownToAdf('!adf:paragraph\nPart.\n!adf:/paragraph\n'), 'unsupported-node-shape', 'paragraph', 'its text alone')
|
||
refused(losslessMarkdownToAdf('!adf:bulletList\n!adf:listItem\nPart.\n!adf:/listItem\n!adf:/bulletList\n'), 'unsupported-node-shape', 'bulletList', 'behind `- `', 'behind `* `')
|
||
refused(losslessMarkdownToAdf('!adf:orderedList {order=5}\n!adf:listItem\nPart.\n!adf:/listItem\n!adf:/orderedList\n'), 'unsupported-node-shape', 'orderedList', '`5. `', '`5) `')
|
||
refused(losslessMarkdownToAdf('!adf:codeBlock\n```\nx\n```\n!adf:/codeBlock\n'), 'unsupported-node-shape', 'codeBlock', 'fence')
|
||
refused(losslessMarkdownToAdf('!adf:mediaSingle {layout=center}\n!adf:media {alt=a type=external url=u}\n!adf:/mediaSingle\n'), 'unsupported-node-shape', 'mediaSingle', '``', '`[](href)`')
|
||
refused(losslessMarkdownToAdf('!adf:table\n!adf:tableRow\n!adf:tableHeader\nPart.\n!adf:/tableHeader\n!adf:/tableRow\n!adf:/table\n'), 'unsupported-node-shape', 'table', 'a pipe table')
|
||
// The item whose first line reads back as a thematic break keeps the directive form the emitter falls back to.
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:bulletList\n!adf:listItem\n---\n!adf:/listItem\n!adf:/bulletList\n')), [bulletList(item({ type: 'rule' }))])
|
||
})
|
||
|
||
test('refuses the directive form of a blockquote opening with a code span', () => {
|
||
refused(losslessMarkdownToAdf('!adf:blockquote\n` `` `\n!adf:/blockquote\n'), 'unsupported-node-shape', 'blockquote', 'each line behind `> `')
|
||
})
|
||
|
||
// The input is some 1000 characters short of V8's longest string; the `> ` the quote adds to each of its 600 lines passes it. Reading this much outlasts Bun's 5 s default timeout.
|
||
test('refuses a blockquote directive whose CommonMark spelling would pass V8\'s longest string', { timeout: 120_000 }, () => {
|
||
const fence = `\`\`\`\n${`${'a'.repeat(894_782)}\n`.repeat(300)}\`\`\`\n`
|
||
refused(losslessMarkdownToAdf(`!adf:blockquote\n${fence}\n${fence}!adf:/blockquote\n`), 'unsupported-node-shape', '536870888')
|
||
})
|
||
|
||
test('names the directive name no node reads back to', () => {
|
||
assert.equal(code(losslessMarkdownToAdf('!adf:widget info\nx\n!adf:/widget\n')), 'unknown-directive-name')
|
||
refused(losslessMarkdownToAdf('!adf:widget\n'), 'unknown-directive-name', 'widget', '\\!adf:')
|
||
refused(losslessMarkdownToAdf('!adf:widget[x]\n'), 'unknown-directive-name', 'widget', '\\!adf:')
|
||
refused(losslessMarkdownToAdf('ratio a!adf:b[c]{d}\n'), 'malformed-directive', 'key=value', '\\!adf:')
|
||
assert.deepEqual(path(losslessMarkdownToAdf('Part.\n\n!adf:widget\n')), ['content', 1])
|
||
})
|
||
|
||
test('names the container no closer closes inside the block holding it', () => {
|
||
const unclosed = (name: string): string[] => [`${name} container is unclosed`, `!adf:/${name}`, '\\!adf:']
|
||
refused(losslessMarkdownToAdf('Part.\n!adf:expand\n'), 'malformed-directive', ...unclosed('expand'))
|
||
assert.deepEqual(path(losslessMarkdownToAdf('Part.\n!adf:expand\n')), ['content', 1])
|
||
refused(losslessMarkdownToAdf('!adf:bulletList\n'), 'malformed-directive', ...unclosed('bulletList'))
|
||
refused(losslessMarkdownToAdf('- !adf:panel info\n Part.\n!adf:/panel\n'), 'malformed-directive', ...unclosed('panel'))
|
||
refused(losslessMarkdownToAdf('> !adf:panel info\n> - a\n> !adf:/panel\n'), 'malformed-directive', ...unclosed('panel'))
|
||
refused(losslessMarkdownToAdf('!adf:panel info\n!adf:expand\nPart.\n!adf:/panel\n'), 'malformed-directive', ...unclosed('expand'))
|
||
assert.deepEqual(path(losslessMarkdownToAdf('!adf:panel info\n!adf:expand\nPart.\n!adf:/panel\n')), ['content', 0, 'content', 0])
|
||
})
|
||
|
||
test('names the closer that finds no container open where it stands', () => {
|
||
const unopened = (name: string): string[] => [`!adf:/${name} closes no ${name}`, '\\!adf:']
|
||
refused(losslessMarkdownToAdf('Part.\n\n!adf:/panel\n'), 'malformed-directive', ...unopened('panel'))
|
||
refused(losslessMarkdownToAdf('!adf:panel info\n> !adf:/panel\n!adf:/panel\n'), 'malformed-directive', ...unopened('panel'))
|
||
refused(losslessMarkdownToAdf('!adf:panel info\n- !adf:/panel\n!adf:/panel\n'), 'malformed-directive', ...unopened('panel'))
|
||
refused(losslessMarkdownToAdf('!adf:panel info\nPart.\n!adf:/expand\n!adf:/panel\n'), 'malformed-directive', ...unopened('expand'))
|
||
})
|
||
|
||
test('names the leaf given a body at its opener, ahead of any refusal the leaf holds itself', () => {
|
||
refused(losslessMarkdownToAdf('!adf:rule\nPart.\n!adf:/rule\n'), 'malformed-directive', '!adf:/rule', '\\!adf:')
|
||
assert.deepEqual(position(losslessMarkdownToAdf('Part.\n\n!adf:rule\nPart.\n!adf:/rule\n')), { line: 3, offset: 7 })
|
||
assert.deepEqual(path(losslessMarkdownToAdf('!adf:rule {localId=a-1}\n!adf:rule {localId=a-2}\nPart.\n!adf:/rule\n')), ['content', 1])
|
||
assert.deepEqual(path(losslessMarkdownToAdf('!adf:rule {localId=a-1}\n!adf:/rule\n!adf:/rule\n')), ['content', 0])
|
||
assert.deepEqual(path(losslessMarkdownToAdf('Part.\n\n!adf:/rule\n')), ['content', 1])
|
||
assert.deepEqual(path(losslessMarkdownToAdf('!adf:panel info\n!adf:rule {localId=a-1}\n!adf:/panel\n!adf:/rule\n')), ['content', 1])
|
||
})
|
||
|
||
test('names the position a directive name the other one spells belongs to', () => {
|
||
refused(losslessMarkdownToAdf('!adf:em\na\n!adf:/em\n'), 'unsupported-node-shape', '_x_', 'block directive')
|
||
refused(losslessMarkdownToAdf('!adf:underline\n'), 'unsupported-node-shape', '!adf:underline[…]', 'block directive')
|
||
refused(losslessMarkdownToAdf('!adf:link\n'), 'unsupported-node-shape', '[x](url)', '!adf:link[…]', 'block directive')
|
||
refused(losslessMarkdownToAdf('!adf:text {text=" "}\n'), 'unsupported-node-shape', '!adf:text{…}', 'block form')
|
||
refused(losslessMarkdownToAdf('!adf:date {timestamp=1}\n'), 'unsupported-node-shape', '!adf:date{…}', 'block form')
|
||
refused(losslessMarkdownToAdf('!adf:paragraph[a]\n'), 'unsupported-node-shape', '!adf:paragraph', 'inline form')
|
||
refused(losslessMarkdownToAdf('!adf:rule[a]\n'), 'unsupported-node-shape', '!adf:rule', 'inline form')
|
||
assert.equal(code(losslessMarkdownToAdf('!adf:widget\na\n!adf:/widget\n')), 'unknown-directive-name')
|
||
assert.equal(code(losslessMarkdownToAdf('!adf:widget[a]\n')), 'unknown-directive-name')
|
||
})
|
||
|
||
test('names the reserved carry name a block directive spells', () => {
|
||
const reserved = ['carry', 'adf: fence']
|
||
refused(losslessMarkdownToAdf('!adf:carry\n'), 'malformed-directive', ...reserved)
|
||
refused(losslessMarkdownToAdf('!adf:carry\nx\n!adf:/carry\n'), 'malformed-directive', ...reserved)
|
||
assert.deepEqual(content(losslessMarkdownToAdf('```adf\nx\n```\n')), [{ attrs: { language: 'adf' }, content: [text('x')], type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('```carry\nx\n```\n')), [{ attrs: { language: 'carry' }, content: [text('x')], type: 'codeBlock' }])
|
||
})
|
||
|
||
const carried = '!adf:carry{json="{\\"type\\":\\"placeholder\\"}"}'
|
||
|
||
test('reads the carry fence back to the node its info string names and its JSON holds', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('```adf:blockCard\n{\n "attrs": {\n "url": "https://example.com/x"\n }\n}\n```\n')), [
|
||
{ attrs: { url: 'https://example.com/x' }, type: 'blockCard' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('```adf:\n{\n "type": "a`b"\n}\n```\n')), [{ type: 'a`b' }])
|
||
refused(losslessMarkdownToAdf('```adf:blockCard\n{\n "type": "blockCard"\n}\n```\n'), 'unsupported-node-shape', 'adf:blockCard', 'holds a type as well', '!adf:codeBlock {language="adf:blockCard"}')
|
||
refused(losslessMarkdownToAdf('```adf:\n{\n "type": "blockCard"\n}\n```\n'), 'unsupported-node-shape', 'adf:blockCard', 'drop type')
|
||
refused(losslessMarkdownToAdf('```adf:\n{}\n```\n'), 'unsupported-node-shape', 'no ADF node', '!adf:codeBlock {language="adf:"}')
|
||
const unnamed = ['spell it adf: with the type in the JSON', '!adf:codeBlock {language="adf:\\\\"}']
|
||
refused(losslessMarkdownToAdf('```adf:\\\\\n{}\n```\n'), 'unsupported-node-shape', ...unnamed)
|
||
})
|
||
|
||
test('reads the inline carry back to the node its json attribute holds', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf(`a ${carried} b\n`)), [
|
||
{ content: [text('a '), { type: 'placeholder' }, text(' b')], type: 'paragraph' },
|
||
])
|
||
})
|
||
|
||
test('reads an escaped JSON key as the key it spells after reading a key holding a backslash (https://issues.chromium.org/issues/521080746)', () => {
|
||
const extension = (parameters: string): string => `!adf:extension {extensionKey=toc extensionType=x parameters=${JSON.stringify(parameters)}}\n`
|
||
const read = content(losslessMarkdownToAdf([extension('{"\\b":1,"\\f":2}'), extension('{"\\b":3,"\\\\":4}'), extension('{"\\b":1,"\\f":2}')].join('\n')))
|
||
const node = (parameters: AdfAttributes): AdfNode => ({ attrs: { extensionKey: 'toc', extensionType: 'x', parameters }, type: 'extension' })
|
||
assert.deepEqual(read, [node({ '\b': 1, '\f': 2 }), node({ '\b': 3, '\\': 4 }), node({ '\b': 1, '\f': 2 })])
|
||
})
|
||
|
||
test('names the invalid JSON no opaque carry holds, and the spelling that keeps it literal', () => {
|
||
const fence = ['invalid JSON', '!adf:codeBlock {language="adf:x"}']
|
||
refused(losslessMarkdownToAdf('```adf:x\n{"attrs":\n```\n'), 'malformed-directive', ...fence)
|
||
refused(losslessMarkdownToAdf('```adf:x\n```\n'), 'malformed-directive', ...fence)
|
||
refused(losslessMarkdownToAdf('!adf:carry{json="{"}\n'), 'malformed-directive', 'invalid JSON', '\\!adf:')
|
||
refused(losslessMarkdownToAdf('!adf:carry{json=abc}\n'), 'malformed-directive', 'invalid JSON', '\\!adf:')
|
||
})
|
||
|
||
test('names the canonical spelling a carried JSON reads alone', () => {
|
||
const compact = ['compact, keys sorted', '\\!adf:']
|
||
refused(losslessMarkdownToAdf('```adf:blockCard\n{"attrs":{}}\n```\n'), 'unsupported-node-shape', 'two-space indent', '!adf:codeBlock {language="adf:blockCard"}')
|
||
refused(losslessMarkdownToAdf('!adf:carry{json="{\\"type\\": \\"blockCard\\"}"}\n'), 'unsupported-node-shape', ...compact)
|
||
refused(losslessMarkdownToAdf('!adf:carry{json="{\\"type\\":\\"blockCard\\",\\"attrs\\":{}}"}\n'), 'unsupported-node-shape', ...compact)
|
||
})
|
||
|
||
test('refuses compact JSON in the carry fence without building its two-space spelling', () => {
|
||
const json = `{"attrs":{"value":${'['.repeat(495)}${new Array(560_000).fill(0).join(',')}${']'.repeat(495)}}}`
|
||
refused(losslessMarkdownToAdf(`\`\`\`adf:unknownBlock\n${json}\n\`\`\`\n`), 'unsupported-node-shape', 'two-space indent', '!adf:codeBlock {language="adf:unknownBlock"}')
|
||
})
|
||
|
||
test('names the node JSON an opaque carry restores alone', () => {
|
||
refused(losslessMarkdownToAdf('```adf:x\n[]\n```\n'), 'unsupported-node-shape', 'no ADF node', '!adf:codeBlock {language="adf:x"}')
|
||
refused(losslessMarkdownToAdf('!adf:carry{json=null}\n'), 'unsupported-node-shape', 'no ADF node', '\\!adf:')
|
||
refused(losslessMarkdownToAdf('!adf:carry{json="{\\"kind\\":\\"x\\"}"}\n'), 'unsupported-node-shape', 'no ADF node', '\\!adf:')
|
||
})
|
||
|
||
test('refuses a carry holding an inline card a link marks, whether it stands alone, in a block or in a fence', () => {
|
||
const card = { attrs: { url: 'https://e.com/r' }, marks: [{ attrs: { href: 'https://e.com/' }, type: 'link' }], type: 'inlineCard' }
|
||
const json = JSON.stringify(card).replaceAll('"', '\\"')
|
||
for (const markdown of [`!adf:carry{json="${json}"}\n`, `\`\`\`adf:futureBlock\n${JSON.stringify({ content: [card] }, null, 2)}\n\`\`\`\n`]) {
|
||
refused(losslessMarkdownToAdf(markdown), 'unsupported-node-shape', 'every writer refuses', 'an inlineCard takes no link mark')
|
||
}
|
||
})
|
||
|
||
test('refuses a carry holding a shape every writer refuses, whether it stands alone, in a block or in a fence', () => {
|
||
const holdsText = ['every writer refuses', 'status', 'neither content nor text']
|
||
const status = { text: 'x', type: 'status' }
|
||
const json = JSON.stringify(status).replaceAll('"', '\\"')
|
||
refused(losslessMarkdownToAdf(`!adf:carry{json="${json}"}\n`), 'unsupported-node-shape', ...holdsText)
|
||
refused(losslessMarkdownToAdf(`\`\`\`adf:futureBlock\n${JSON.stringify({ content: [status] }, null, 2)}\n\`\`\`\n`), 'unsupported-node-shape', ...holdsText)
|
||
refused(losslessMarkdownToAdf(`\`\`\`adf:panel\n${JSON.stringify({ text: 'x' }, null, 2)}\n\`\`\`\n`), 'unsupported-node-shape', 'every writer refuses', 'panel', 'no text key')
|
||
})
|
||
|
||
test('names the shape the inline carry reads alone', () => {
|
||
refused(losslessMarkdownToAdf('!adf:carry[x]{json="{}"}\n'), 'unsupported-node-shape', 'carry takes no content')
|
||
refused(losslessMarkdownToAdf('!adf:carry{}\n'), 'unsupported-node-shape', 'one json attribute')
|
||
refused(losslessMarkdownToAdf('!adf:carry{json="{}" localId=x}\n'), 'unsupported-node-shape', 'one json attribute')
|
||
refused(losslessMarkdownToAdf('!adf:carry{json="null"}\n'), 'unsupported-node-shape', '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:x\n${prefix}${nested(levels)}\n${prefix}\`\`\`\n`
|
||
const deeper = (levels: number): string[] => [`deeper than the ${levels} levels`]
|
||
refused(losslessMarkdownToAdf(`!adf:carry{json="${nested(largestNesting + 2)}"}\n`), 'unsupported-nesting-depth', ...deeper(largestNesting))
|
||
refused(losslessMarkdownToAdf(fence('', largestNesting + 1)), 'unsupported-node-shape', 'two-space indent', '!adf:codeBlock {language="adf:x"}')
|
||
refused(losslessMarkdownToAdf(fence('> ', largestNesting + 1)), 'unsupported-nesting-depth', ...deeper(largestNesting - 1))
|
||
})
|
||
|
||
test('names the number no JSON spelling carries in an opaque carry', () => {
|
||
const named = ['a number JSON cannot spell']
|
||
refused(losslessMarkdownToAdf('!adf:carry{json="{\\"attrs\\":{\\"width\\":1e999},\\"type\\":\\"blockCard\\"}"}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('```adf:x\n1e999\n```\n'), 'unsupported-node-shape', ...named)
|
||
})
|
||
|
||
test('names the mark spelling no opaque carry sits inside', () => {
|
||
const named = ['marks=empty', 'out of the mark spelling']
|
||
refused(losslessMarkdownToAdf(`_a ${carried} b_\n`), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf(`**${carried}**\n`), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf(`~~a ${carried}~~\n`), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf(`[a ${carried} b](https://example.com/x)\n`), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf(`[*<http://x/>${carried}*](/w)\n`), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf(`!adf:underline[${carried}]\n`), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf(`!adf:textColor[a ${carried}]{color="#ae2e24"}\n`), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf(`\n`), 'unsupported-node-shape', ...named)
|
||
})
|
||
|
||
test('keeps the carry a mark spelling does not wrap', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf(`[a ${carried} b]\n`)), [
|
||
{ content: [text('[a '), { type: 'placeholder' }, text(' b]')], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(`**a**${carried}**b**\n`)), [
|
||
{ content: [marked('a', strong), { type: 'placeholder' }, marked('b', strong)], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(`\n`)), [image('https://example.com/i', 'a b')])
|
||
})
|
||
|
||
test('reads each attribute value as the type its section assigns', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf: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(losslessMarkdownToAdf('!adf:table {isNumberColumnEnabled=true}\n!adf:/table\n')), [{ attrs: { isNumberColumnEnabled: true }, type: 'table' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:tableCell {colwidth="340,420.5"}\n!adf:/tableCell\n')), [{ attrs: { colwidth: [340, 420.5] }, type: 'tableCell' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:tableCell {colwidth=340}\n!adf:/tableCell\n')), [{ attrs: { colwidth: [340] }, type: 'tableCell' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {localId=a-1}\n')), [{ attrs: { localId: 'a-1' }, type: 'rule' }])
|
||
})
|
||
|
||
test('reads the reserved marks key as the marks it names, in order', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {marks=em}\n')), [{ marks: [em], type: 'rule' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {localId=a-1 marks="breakout{mode=wide}"}\n')), [
|
||
{ attrs: { localId: 'a-1' }, marks: [{ attrs: { mode: 'wide' }, type: 'breakout' }], type: 'rule' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {marks="indentation{level=2} fragment{localId=f1 name=\\"Price list\\"}"}\n')), [
|
||
{ marks: [{ attrs: { level: 2 }, type: 'indentation' }, { attrs: { localId: 'f1', name: 'Price list' }, type: 'fragment' }], type: 'rule' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {marks="em{attrs=empty}"}\n')), [{ marks: [{ attrs: {}, type: 'em' }], type: 'rule' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {marks=empty}\n')), [{ marks: [], type: 'rule' }])
|
||
})
|
||
|
||
test('reads the reserved marks key as a JSON array where a mark is one the tables do not type', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:rule {marks="[{\\"type\\":\\"em\\"},{\\"attrs\\":{\\"color\\":\\"#fff\\"},\\"type\\":\\"backgroundColor\\"}]"}\n')), [
|
||
{ marks: [em, { attrs: { color: '#fff' }, type: 'backgroundColor' }], type: 'rule' },
|
||
])
|
||
})
|
||
|
||
test('names the marks key no marks read back from', () => {
|
||
const array = ['marks', 'rule', 'canonical JSON']
|
||
const named = ['marks', 'rule', 'one space apart']
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="[]"}\n'), 'unsupported-node-shape', ...array)
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="[1]"}\n'), 'unsupported-node-shape', ...array)
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="{}"}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="em strong"}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="em "}\n'), 'unsupported-node-shape', ...named)
|
||
const inBreakout = ['breakout', 'marks', 'rule']
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="breakout{width=1 mode=wide}"}\n'), 'unsupported-node-shape', ...inBreakout, 'mode before width')
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="breakout{mode}"}\n'), 'unsupported-node-shape', ...inBreakout, 'key=value')
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="breakout{mode=wide\\"x\\"}"}\n'), 'unsupported-node-shape', ...inBreakout, 'key=value')
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="breakout{mode=}"}\n'), 'unsupported-node-shape', ...inBreakout, 'key=value')
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="breakout{mode=wide"}\n'), 'unsupported-node-shape', ...inBreakout, 'closing brace')
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="em{attrs=empty}strong"}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="em{attrs=x}"}\n'), 'unsupported-node-shape', 'attrs', 'empty', 'attrs=x')
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks=x}\n'), 'unsupported-node-shape', 'names x', 'marks="[{\\"type\\":\\"x\\"}]"')
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="breakout{width=wide}"}\n'), 'unsupported-node-shape', 'width', 'breakout', 'no number')
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="breakout{bogus=1}"}\n'), 'unsupported-node-shape', 'breakout', 'bogus')
|
||
})
|
||
|
||
test('names the marks key spelled outside the canonical form', () => {
|
||
const spells = (spelling: string): string[] => ['rule', `marks=${spelling}`]
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="[{\\"type\\":\\"em\\"}]"}\n'), 'unsupported-node-shape', ...spells('em'))
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="[{\\"attrs\\":{},\\"type\\":\\"em\\"}]"}\n'), 'unsupported-node-shape', ...spells('"em{attrs=empty}"'))
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="[{\\"type\\":\\"em\\",\\"attrs\\":{}}]"}\n'), 'unsupported-node-shape', ...spells('"em{attrs=empty}"'))
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="em{}"}\n'), 'unsupported-node-shape', ...spells('em'))
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="em"}\n'), 'unsupported-node-shape', ...spells('em'))
|
||
refused(losslessMarkdownToAdf('!adf:rule {marks="breakout{mode=\\"wide\\"}"}\n'), 'unsupported-node-shape', 'breakout', 'mode=wide')
|
||
})
|
||
|
||
test('names the attribute a node holds no reading for', () => {
|
||
refused(losslessMarkdownToAdf('!adf:rule {bogus=1}\n'), 'unsupported-node-shape', 'rule', 'bogus')
|
||
refused(losslessMarkdownToAdf('!adf:media {width=wide}\n'), 'unsupported-node-shape', 'width', 'media', 'no number')
|
||
refused(losslessMarkdownToAdf('!adf:table {isNumberColumnEnabled=yes}\n!adf:/table\n'), 'unsupported-node-shape', 'isNumberColumnEnabled', 'table', 'no boolean')
|
||
refused(losslessMarkdownToAdf('!adf:media {width=true}\n'), 'unsupported-node-shape', 'width', 'media', 'no number')
|
||
refused(losslessMarkdownToAdf('!adf:tableCell {colwidth="340,"}\n!adf:/tableCell\n'), 'unsupported-node-shape', 'colwidth', 'tableCell', 'comma list of numbers')
|
||
refused(losslessMarkdownToAdf('!adf:tableCell {colwidth="[]"}\n!adf:/tableCell\n'), 'unsupported-node-shape', 'colwidth', 'tableCell', 'comma list of numbers')
|
||
refused(losslessMarkdownToAdf('!adf:tableCell {colwidth="[340,\\"420\\"]"}\n!adf:/tableCell\n'), 'unsupported-node-shape', 'colwidth', 'tableCell', 'comma list of numbers')
|
||
refused(losslessMarkdownToAdf('!adf:tableCell {colwidth=""}\n!adf:/tableCell\n'), 'unsupported-node-shape', 'colwidth', 'tableCell', 'comma list of numbers')
|
||
refused(losslessMarkdownToAdf('!adf:panel info {panelType=note}\nx\n!adf:/panel\n'), 'unsupported-node-shape', 'panelType', 'directive argument')
|
||
refused(losslessMarkdownToAdf('Part !adf:mention{id=b1c2 text=A}.\n'), 'unsupported-node-shape', 'mention', 'content slot')
|
||
})
|
||
|
||
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[] => [`the ${key} attribute of ${type}`, `${largestNesting} levels`]
|
||
refused(losslessMarkdownToAdf(`!adf:extension {parameters="${nested(largestNesting + 1)}"}\n`), 'unsupported-nesting-depth', ...deeper('parameters', 'extension'))
|
||
refused(losslessMarkdownToAdf(`!adf:rule {marks="${nested(largestNesting + 1)}"}\n`), 'unsupported-nesting-depth', ...deeper('marks', 'rule'))
|
||
refused(losslessMarkdownToAdf(`!adf:rule {marks="[{\\"attrs\\":{\\"deep\\":${nested(largestNesting - 2)}},\\"type\\":\\"em\\"}]"}\n`), 'unsupported-nesting-depth', ...deeper('marks', 'rule'))
|
||
refused(losslessMarkdownToAdf(`!adf:media {width="${nested(largestNesting + 1)}"}\n`), 'unsupported-node-shape', 'width', 'media', 'no number')
|
||
})
|
||
|
||
test('names the attribute value spelled outside the canonical form', () => {
|
||
refused(losslessMarkdownToAdf('!adf:rule {localId="a-1"}\n'), 'unsupported-node-shape', 'localId=a-1')
|
||
refused(losslessMarkdownToAdf('!adf:media {width="20.0"}\n'), 'unsupported-node-shape', 'width=20')
|
||
refused(losslessMarkdownToAdf('!adf:tableCell {colwidth="340, 420"}\n!adf:/tableCell\n'), 'unsupported-node-shape', 'colwidth="340,420"')
|
||
refused(losslessMarkdownToAdf('!adf:tableCell {colwidth="340"}\n!adf:/tableCell\n'), 'unsupported-node-shape', 'colwidth=340')
|
||
refused(losslessMarkdownToAdf('!adf:tableCell {colwidth="[340,420]"}\n!adf:/tableCell\n'), 'unsupported-node-shape', 'colwidth="340,420"')
|
||
})
|
||
|
||
test('names the argument and the body a node takes no reading for', () => {
|
||
refused(losslessMarkdownToAdf('!adf:rule x\n'), 'unsupported-node-shape', 'rule', 'no argument')
|
||
refused(losslessMarkdownToAdf('!adf:paragraph\nOne.\n\nTwo.\n!adf:/paragraph\n'), 'unsupported-node-shape', 'paragraph', 'one paragraph')
|
||
refused(losslessMarkdownToAdf('!adf:paragraph\n---\n!adf:/paragraph\n'), 'unsupported-node-shape', 'paragraph', 'one paragraph')
|
||
refused(losslessMarkdownToAdf('!adf:codeBlock {wrap=true}\nx\n!adf:/codeBlock\n'), 'unsupported-node-shape', 'codeBlock', 'code blocks', 'another block')
|
||
refused(losslessMarkdownToAdf('!adf:codeBlock {wrap=true}\n!adf:/codeBlock\n'), 'unsupported-node-shape', 'codeBlock', 'code blocks', 'holds none')
|
||
refused(losslessMarkdownToAdf('Part !adf:date[now]{timestamp=1}.\n'), 'unsupported-node-shape', 'date', 'no content')
|
||
})
|
||
|
||
test('leaves the text that opens no directive the text it is', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('At 10:30 :smile: today.\n')), [paragraph('At 10:30 :smile: today.')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(':mention[@A] and !adfx and a!\n')), [paragraph(':mention[@A] and !adfx and a!')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\\!adf:mention[@A]\n')), [paragraph('!adf:mention[@A]')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('`!adf:mention[@A]`\n')), [{ content: [codeSpan('!adf:mention[@A]')], type: 'paragraph' }])
|
||
})
|
||
|
||
test('names the unescaped prefix that completes no directive', () => {
|
||
const named = ['no directive', '\\!adf:']
|
||
refused(losslessMarkdownToAdf('Part !adf: here.\n'), 'malformed-directive', ...named)
|
||
refused(losslessMarkdownToAdf('Part !adf:Mention[@A] here.\n'), 'malformed-directive', ...named)
|
||
refused(losslessMarkdownToAdf('Part !adf:mention @A here.\n'), 'malformed-directive', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:underline[a !adf: b]\n'), 'malformed-directive', ...named)
|
||
})
|
||
|
||
test('names the inline directive left unclosed at the end of its line', () => {
|
||
refused(losslessMarkdownToAdf('Part !adf:mention[@A\n'), 'malformed-directive', '[content] is unclosed', '\\!adf:')
|
||
assert.equal(code(losslessMarkdownToAdf('Part !adf:mention[@A]{id=\n')), 'malformed-directive')
|
||
assert.deepEqual(path(losslessMarkdownToAdf('> Part !adf:mention[@A\n')), ['content', 0, 'content', 0])
|
||
})
|
||
|
||
test('claims a block-level pipe with no table to parse it', () => {
|
||
assert.equal(code(losslessMarkdownToAdf('| Part | Qty |\n')), 'malformed-pipe-table')
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\\| Part\n')), [paragraph('| Part')])
|
||
})
|
||
|
||
test('refuses the raw HTML no element mapping carries', () => {
|
||
assert.equal(code(losslessMarkdownToAdf('<!-- note -->\n')), 'unmappable-html')
|
||
assert.equal(code(losslessMarkdownToAdf('<div>\nx\n</div>\n')), 'unmappable-html')
|
||
assert.equal(code(losslessMarkdownToAdf('<?php ?>\n')), 'unmappable-html')
|
||
assert.equal(code(losslessMarkdownToAdf('<!DOCTYPE html>\n')), 'unmappable-html')
|
||
assert.equal(code(losslessMarkdownToAdf('<![CDATA[x]]>\n')), 'unmappable-html')
|
||
assert.equal(code(losslessMarkdownToAdf('<pre>\nx\n</pre>\n')), 'unmappable-html')
|
||
assert.equal(code(losslessMarkdownToAdf('<span foo="bar">\n')), 'unmappable-html')
|
||
assert.equal(code(losslessMarkdownToAdf('<div\n')), 'unmappable-html')
|
||
assert.equal(code(losslessMarkdownToAdf('<?php\n')), 'unmappable-html')
|
||
assert.deepEqual(path(losslessMarkdownToAdf('Part.\n\n<div>\n')), ['content', 1])
|
||
assert.equal(code(losslessMarkdownToAdf('<div>\nx\n\n!adf:/panel\n')), 'unmappable-html')
|
||
assert.equal(code(losslessMarkdownToAdf('<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(losslessMarkdownToAdf('<!--\n!adf:/panel\n-->\n')), 'unmappable-html')
|
||
assert.equal(code(losslessMarkdownToAdf('<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(losslessMarkdownToAdf('<div>\n')), { line: 1, offset: 0 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('Part.\n\n<div>\n')), { line: 3, offset: 7 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('> Part.\n>\n> a <span>b</span>\n')), { line: 3, offset: 10 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('- Part.\n- a <span>b</span>\n')), { line: 2, offset: 8 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('Part.\n\n!adf:panel info\nMore.\n')), { line: 3, offset: 7 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('x\n\na <span>b</span>\n===\n')), { line: 3, offset: 3 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('x\n\n```adf:x\n{\n```\n')), { line: 3, offset: 3 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('x\n\n| a |\n')), { line: 3, offset: 3 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('a\nb <span>c</span>\n')), { line: 1, offset: 0 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('Part.\r\n\r\n<div>\r\n')), { line: 3, offset: 9 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('a\u0000b\n\n<div>\n')), { line: 3, offset: 5 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('!adf:caption\na <span>b</span>\n!adf:/caption\n')), { line: 2, offset: 13 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('x\n\n!adf:caption\n- a\n!adf:/caption\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(losslessMarkdownToAdf('[a]: /url\n<span>b</span>\n')), { line: 2, offset: 10 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('[a]: /a\n[b]: /b\n[c]: /c\n[d]: /d\n<span>x</span>\n')), { line: 5, offset: 32 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('[a]:\n<the url>\n"Title"\n<span>b</span>\n')), { line: 4, offset: 23 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('> [a]: /url\n> <span>b</span>\n')), { line: 2, offset: 12 })
|
||
assert.deepEqual(position(losslessMarkdownToAdf('!adf:caption\n[a]: /url\n<span>b</span>\n!adf:/caption\n')), { line: 3, offset: 23 })
|
||
})
|
||
|
||
test('gives up the link reference definitions a paragraph opens with', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a]: /url\n')), [])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a]: /url\n[b]: /other\nPart.\n')), [paragraph('Part.')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a]: /url\n"Title"\n\nPart.\n')), [paragraph('Part.')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a]: /url and more\n')), [paragraph('[a]: /url and more')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('Part.\n[a]: /url\n')), [paragraph('Part. [a]: /url')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a]: /url\n===\n')), [paragraph('===')])
|
||
})
|
||
|
||
test('keeps the whitespace CommonMark strips no more of than a space or a tab', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\u00a0Part.\u00a0\n')), [paragraph('\u00a0Part.\u00a0')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' \u3000Part.\t\n')), [paragraph('\u3000Part.')])
|
||
})
|
||
|
||
test('normalizes the line endings and the null character CommonMark replaces', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('One\r\ntwo.\r\n')), [paragraph('One two.')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('One\rtwo.\r')), [paragraph('One two.')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('```\r\nx\r\n```\r\n')), [{ content: [text('x')], type: 'codeBlock' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a\u0000b\n')), [paragraph('a\ufffdb')])
|
||
})
|
||
|
||
test('reads a blockquote and the blocks its prefix carries', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('> Ship it.\n>\n> Then tell them.\n')), [quote(paragraph('Ship it.'), paragraph('Then tell them.'))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('>Ship it.\n')), [quote(paragraph('Ship it.'))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' > > Deep.\n')), [quote(quote(paragraph('Deep.')))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('>\n')), [quote()])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('> One.\n\n> Two.\n')), [quote(paragraph('One.')), quote(paragraph('Two.'))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('Part.\n> Ship it.\n')), [paragraph('Part.'), quote(paragraph('Ship it.'))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' > Code.\n')), [{ content: [text('> Code.')], type: 'codeBlock' }])
|
||
})
|
||
|
||
test('reads a bullet list, the marker width setting the continuation', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('- Bolt M8\n- Nut M8\n')), [bulletList(item(paragraph('Bolt M8')), item(paragraph('Nut M8')))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('- Washer M8\n - Fibre\n')), [bulletList(item(paragraph('Washer M8'), bulletList(item(paragraph('Fibre')))))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('-\n')), [bulletList(item())])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('- One\n\n Two.\n')), [bulletList(item(paragraph('One'), paragraph('Two.')))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('- Code.\n')), [bulletList(item({ content: [text('Code.')], type: 'codeBlock' }))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('- a\n-\n\n- c\n')), [bulletList(item(paragraph('a')), item(), item(paragraph('c')))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('- a\n1. b\n')), [bulletList(item(paragraph('a'))), orderedList(1, item(paragraph('b')))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('-\n\n Part.\n')), [bulletList(item()), paragraph('Part.')])
|
||
})
|
||
|
||
test('ends a list at a link reference definition, in every reader', () => {
|
||
for (const read of [commonMarkToAdf, losslessMarkdownToAdf, portableMarkdownToAdf]) {
|
||
assert.deepEqual(content(read('- a\n\n[r]: /u\n\n- b\n')), [bulletList(item(paragraph('a'))), bulletList(item(paragraph('b')))])
|
||
assert.deepEqual(content(read('- a\n\n[r]: /u\n- b\n')), [bulletList(item(paragraph('a'))), bulletList(item(paragraph('b')))])
|
||
assert.deepEqual(content(read('-\n\n[r]: /u\n\n- b\n')), [bulletList(item()), bulletList(item(paragraph('b')))])
|
||
assert.deepEqual(content(read('- a\n\n [r]: /u\n- b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))])
|
||
}
|
||
})
|
||
|
||
test('reads an ordered list, its first marker the order attribute', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('9. Bolt M8\n10. Nut M8\n')), [orderedList(9, item(paragraph('Bolt M8')), item(paragraph('Nut M8')))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('1) Loosen the clamp\n')), [orderedList(1, item(paragraph('Loosen the clamp')))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('0. Zero\n')), [orderedList(0, item(paragraph('Zero')))])
|
||
})
|
||
|
||
test('measures a tab from the column the containers cut it to', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('>\t\tfoo\n')), [quote({ content: [text(' foo')], type: 'codeBlock' })])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('-\t\tfoo\n')), [bulletList(item({ content: [text(' foo')], type: 'codeBlock' }))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('- foo\n\n\t\tbar\n')), [bulletList(item(paragraph('foo'), { content: [text(' bar')], type: 'codeBlock' }))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('-\t foo\n')), [bulletList(item(paragraph('foo')))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' - 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(losslessMarkdownToAdf('- a\n\n- b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('- a\n\n 2. b\n')), [bulletList(item(paragraph('a'), orderedList(2, item(paragraph('b')))))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('- 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(losslessMarkdownToAdf('Part.\n- a\n')), [paragraph('Part.'), bulletList(item(paragraph('a')))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('Part.\n1. a\n')), [paragraph('Part.'), orderedList(1, item(paragraph('a')))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('Part.\n2. a\n')), [paragraph('Part. 2. a')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('Part.\n*\n')), [paragraph('Part. *')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('Part.\n- - -\n')), [paragraph('Part.'), { type: 'rule' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('Part.\n-\n')), [{ attrs: { level: 2 }, content: [text('Part.')], type: 'heading' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('- a\n 2. b\n')), [bulletList(item(paragraph('a 2. b')))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('- 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(losslessMarkdownToAdf('> One\ntwo.\n')), [quote(paragraph('One two.'))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('- One\ntwo.\n')), [bulletList(item(paragraph('One two.')))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('> One\n two.\n')), [quote(paragraph('One two.'))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('> One\n\ntwo.\n')), [quote(paragraph('One')), paragraph('two.')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('> One\n# Two\n')), [quote(paragraph('One')), { attrs: { level: 1 }, content: [text('Two')], type: 'heading' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('> One\n---\n')), [quote(paragraph('One')), { type: 'rule' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('> One\n```\n')), [quote(paragraph('One')), { type: 'codeBlock' }])
|
||
assert.equal(code(losslessMarkdownToAdf('> One\n<div>\n')), 'unmappable-html')
|
||
assert.deepEqual(path(losslessMarkdownToAdf('> One\n<div>\n')), ['content', 1])
|
||
assert.deepEqual(path(losslessMarkdownToAdf('> One\n<span>\n')), ['content', 0, 'content', 0])
|
||
})
|
||
|
||
test('ends a lazy continuation at a claimed line', () => {
|
||
assert.equal(code(losslessMarkdownToAdf('> Part.\n!adf:/panel\n')), 'malformed-directive')
|
||
assert.deepEqual(path(losslessMarkdownToAdf('> Part.\n!adf:/panel\n')), ['content', 1])
|
||
assert.equal(code(losslessMarkdownToAdf('- Part.\n| x |\n')), 'malformed-pipe-table')
|
||
})
|
||
|
||
test('names the block the claim inside a container opens', () => {
|
||
assert.deepEqual(path(losslessMarkdownToAdf('> Part.\n>\n> !adf:expand\n')), ['content', 0, 'content', 1])
|
||
assert.deepEqual(path(losslessMarkdownToAdf('- Part.\n- | x |\n')), ['content', 0, 'content', 1, 'content', 0])
|
||
})
|
||
|
||
test('refuses input nested deeper than the parser carries', () => {
|
||
assert.equal(code(losslessMarkdownToAdf('> '.repeat(501))), 'unsupported-nesting-depth')
|
||
assert.ok(losslessMarkdownToAdf('> '.repeat(500)).ok)
|
||
const marks = (levels: number): string => `${'!adf:underline['.repeat(levels)}a${']'.repeat(levels)}\n`
|
||
assert.equal(code(losslessMarkdownToAdf(marks(largestNesting + 1))), 'unsupported-nesting-depth')
|
||
assert.deepEqual(content(losslessMarkdownToAdf(marks(largestNesting))), [{ content: [marked('a', underline)], type: 'paragraph' }])
|
||
const nest = (names: readonly string[], body: string): string => [...names.map((name) => `!adf:${name}\n`), body, ...names.map((name) => `!adf:/${name}\n`).reverse()].join('')
|
||
const repeated = (name: string, levels: number = largestNesting): string[] => Array.from({ length: levels }, () => name)
|
||
assert.ok(losslessMarkdownToAdf(nest(repeated('panel'), '!adf:paragraph {localId=a-1}\nPart.\n!adf:/paragraph\n')).ok)
|
||
assert.deepEqual(position(losslessMarkdownToAdf(nest(['expand', ...repeated('panel'), 'expand'], 'Part.\n'))), { line: 501, offset: 5501 })
|
||
assert.equal(code(losslessMarkdownToAdf(nest(['panel', ...repeated('expand'), 'panel'], 'Part.\n'))), 'unsupported-nesting-depth')
|
||
const listed = (levels: number): string => `${'!adf:bulletList\n!adf:listItem\n---\n'.repeat(levels)}${'!adf:/listItem\n!adf:/bulletList\n'.repeat(levels)}`
|
||
const directiveLists = largestNesting / 2
|
||
assert.ok(losslessMarkdownToAdf(listed(directiveLists)).ok)
|
||
assert.equal(code(losslessMarkdownToAdf(listed(directiveLists + 1))), 'unsupported-nesting-depth')
|
||
const asking = (body: string): string => `!adf:bulletList\n!adf:listItem\n${body}!adf:/listItem\n!adf:/bulletList\n`
|
||
// Two items past the largest list marker leave the list no readable spelling, so the emitter
|
||
// walks it as list plus item where the parser counted one level.
|
||
const overflowing = (body: string): string => {
|
||
const marker = '999999999. '
|
||
return `${marker}${body.replace(/^(?!$)/gm, ' '.repeat(marker.length)).slice(marker.length)}\n${marker}z\n`
|
||
}
|
||
const overflowed = (panels: number): string => asking(overflowing(overflowing(asking(`---\n${nest(repeated('panel', panels), 'Part.\n')}`))))
|
||
assert.equal(code(losslessMarkdownToAdf(overflowed(493))), 'unsupported-node-shape')
|
||
assert.equal(code(losslessMarkdownToAdf(overflowed(494))), 'unsupported-nesting-depth')
|
||
const rebasing = (panels: number): string => asking(`---\n${overflowing(asking(`---\n${nest(repeated('panel', panels), 'Part.\n')}`))}`)
|
||
assert.ok(losslessMarkdownToAdf(rebasing(494)).ok)
|
||
assert.equal(code(losslessMarkdownToAdf(rebasing(495))), 'unsupported-nesting-depth')
|
||
assert.ok(losslessMarkdownToAdf(`${'- '.repeat(largestNesting)}a\n`).ok)
|
||
assert.equal(code(losslessMarkdownToAdf(`${'- '.repeat(largestNesting + 1)}a\n`)), 'unsupported-nesting-depth')
|
||
})
|
||
|
||
test('decodes the backslash escapes CommonMark spells, and keeps the rest literal', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\\*not emphasis\\*\n')), [paragraph('*not emphasis*')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\\\\\n')), [paragraph('\\')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\\a \\\u00a0\n')), [paragraph('\\a \\\u00a0')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('Part\\\n')), [paragraph('Part\\')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a\\`b`\n')), [paragraph('a`b`')])
|
||
})
|
||
|
||
test('decodes the entity references HTML5 names, and the numeric ones', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('& © ≧̸ ‌ Æ\n')), [paragraph('& \u00a9 \u2267\u0338 \u200c \u00c6')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('# " ♥\n')), [paragraph('# " \u2665')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('� � �\n')), [paragraph('\ufffd \ufffd \ufffd')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('&zzz; & &#; &\n')), [paragraph('&zzz; & &#; &')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('`not code`\n')), [paragraph('`not code`')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('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(losslessMarkdownToAdf('Run `npm test` now.\n')), [
|
||
{ content: [text('Run '), codeSpan('npm test'), text(' now.')], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('``a`b``\n')), [{ content: [codeSpan('a`b')], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('` `` `\n')), [{ content: [codeSpan('``')], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('` `\n')), [{ content: [codeSpan(' ')], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('`a\nb`\n')), [{ content: [codeSpan('a b')], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('`foo``bar`\n')), [{ content: [codeSpan('foo``bar')], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('`!adf:/panel` `~~x~~` `\\*` `&`\n')), [
|
||
{
|
||
content: [codeSpan('!adf:/panel'), text(' '), codeSpan('~~x~~'), text(' '), codeSpan('\\*'), text(' '), codeSpan('&')],
|
||
type: 'paragraph',
|
||
},
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('`foo\n')), [paragraph('`foo')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('``foo`\n')), [paragraph('``foo`')])
|
||
})
|
||
|
||
test('reads a hard break from a trailing backslash and from two trailing spaces alike', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('One\\\ntwo.\n')), [{ content: [text('One'), hardBreak(), text('two.')], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('One \ntwo.\n')), [{ content: [text('One'), hardBreak(), text('two.')], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('One\\ \ntwo.\n')), [{ content: [text('One\\'), hardBreak(), text('two.')], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('One \\\ntwo.\n')), [{ content: [text('One '), hardBreak(), text('two.')], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('One \ntwo.\n')), [paragraph('One two.')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('One \t\ntwo.\n')), [paragraph('One two.')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('One \n')), [paragraph('One')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('> One\\\n> two.\n')), [quote({ content: [text('One'), hardBreak(), text('two.')], type: 'paragraph' })])
|
||
})
|
||
|
||
test('gives the hard break inside a mark spelling no marks, and reads a link of hard breaks as its destination, in every reader', () => {
|
||
const broken = (...marks: AdfMark[]): AdfNode[] => [{ content: [marked('a', ...marks), hardBreak(), marked('b', ...marks)], type: 'paragraph' }]
|
||
for (const read of [commonMarkToAdf, losslessMarkdownToAdf, portableMarkdownToAdf]) {
|
||
assert.deepEqual(content(read('*a \nb*\n')), broken(em))
|
||
assert.deepEqual(content(read('**a\\\nb**\n')), broken(strong))
|
||
assert.deepEqual(content(read('[a\\\nb](/u)\n')), broken(link('/u')))
|
||
}
|
||
assert.deepEqual(content(losslessMarkdownToAdf('~~a\\\nb~~\n')), broken(strike))
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:underline[a!adf:hardBreak{}b]\n')), broken(underline))
|
||
const localId = '01a0431d-201e-7400-a175-9c04d3628f95'
|
||
assert.deepEqual(content(losslessMarkdownToAdf(`_!adf:hardBreak{localId=${localId}}_\n`)), [{ content: [{ attrs: { localId }, type: 'hardBreak' }], type: 'paragraph' }])
|
||
for (const read of [commonMarkToAdf, losslessMarkdownToAdf, portableMarkdownToAdf]) {
|
||
assert.deepEqual(content(read('[\\\n](/u)\n')), [{ content: [marked('/u', link('/u')), hardBreak()], type: 'paragraph' }])
|
||
assert.deepEqual(content(read('a[\\\n]()b\n')), [{ content: [text('a'), hardBreak(), text('b')], type: 'paragraph' }])
|
||
}
|
||
})
|
||
|
||
test('gives an inline node no mark Atlassian\'s schema withholds from it', () => {
|
||
const mention: AdfNode = { attrs: { id: 'b1c2', text: '@A' }, type: 'mention' }
|
||
const media: AdfNode = { attrs: { id: 'm1' }, type: 'mediaInline' }
|
||
const border: AdfMark = { attrs: { color: '#091e42', size: 2 }, type: 'border' }
|
||
const read = (markdown: string): AdfNode[] | string => content(losslessMarkdownToAdf(markdown))
|
||
assert.deepEqual(read('**!adf:mention[@A]{id=b1c2}**\n'), [{ content: [mention], type: 'paragraph' }])
|
||
assert.deepEqual(read('*a !adf:date{timestamp=1}*\n'), [{ content: [marked('a ', em), { attrs: { timestamp: '1' }, type: 'date' }], type: 'paragraph' }])
|
||
assert.deepEqual(read('!adf:underline[a !adf:emoji[🎉]{shortName=":tada:"}]\n'), [
|
||
{ content: [marked('a ', underline), { attrs: { shortName: ':tada:', text: '🎉' }, type: 'emoji' }], type: 'paragraph' },
|
||
])
|
||
const unlinkable = ['take no link', 'write the link around text beside them']
|
||
refused(losslessMarkdownToAdf('[!adf:status[Done]{color=green}](/u)\n'), 'unsupported-node-shape', ...unlinkable)
|
||
refused(losslessMarkdownToAdf('[!adf:status[Done]{color=green}\\\n](/u)\n'), 'unsupported-node-shape', ...unlinkable)
|
||
assert.deepEqual(read('[a !adf:inlineCard{url="https://x/"}](/u)\n'), [{ content: [marked('a ', link('/u')), { attrs: { url: 'https://x/' }, type: 'inlineCard' }], type: 'paragraph' }])
|
||
assert.deepEqual(read('_!adf:mediaInline{id=m1}_\n'), [{ content: [media], type: 'paragraph' }])
|
||
assert.deepEqual(read('!adf:border[!adf:mediaInline{id=m1}]{color="#091e42" size=2}\n'), [{ content: [{ ...media, marks: [border] }], type: 'paragraph' }])
|
||
assert.deepEqual(read('[!adf:mediaInline{id=m1}](/u)\n'), [{ content: [{ ...media, marks: [link('/u')] }], type: 'paragraph' }])
|
||
assert.deepEqual(read('!adf:border[a !adf:mediaInline{id=m1}]{color="#091e42" size=2}\n'), [{ content: [{ text: 'a ', type: 'text' }, { ...media, marks: [border] }], type: 'paragraph' }])
|
||
const unborderable = ['take no border mark', 'only a mediaInline takes border', 'delete the border spelling']
|
||
const wrapsOnly = (name: string): string[] => [`take no ${name} mark`, `write the ${name} mark around text beside them`]
|
||
refused(losslessMarkdownToAdf('!adf:border[a]{color="#091e42" size=2}\n'), 'unsupported-node-shape', ...unborderable)
|
||
refused(losslessMarkdownToAdf('_!adf:border[a]{color="#091e42" size=2}_\n'), 'unsupported-node-shape', ...unborderable)
|
||
refused(losslessMarkdownToAdf('!adf:underline[!adf:border[a]{color="#091e42" size=2}]\n'), 'unsupported-node-shape', ...unborderable)
|
||
refused(losslessMarkdownToAdf('!adf:border[a!adf:textBreak{}b]{color="#091e42" size=2}\n'), 'unsupported-node-shape', ...unborderable)
|
||
refused(losslessMarkdownToAdf('!adf:underline[!adf:mention[@A]{id=b1c2}]\n'), 'unsupported-node-shape', ...wrapsOnly('underline'))
|
||
refused(losslessMarkdownToAdf('!adf:underline[!adf:mediaInline{id=m1}]\n'), 'unsupported-node-shape', ...wrapsOnly('underline'))
|
||
refused(losslessMarkdownToAdf('!adf:link[!adf:status[Done]{color=green}]{collection=c href="/u"}\n'), 'unsupported-node-shape', ...wrapsOnly('link'))
|
||
})
|
||
|
||
test('gives code text no mark but code, link and annotation', () => {
|
||
const code: AdfMark = { type: 'code' }
|
||
for (const read of [commonMarkToAdf, losslessMarkdownToAdf, portableMarkdownToAdf]) {
|
||
assert.deepEqual(content(read('**`npm ci`**\n')), [{ content: [marked('npm ci', code)], type: 'paragraph' }])
|
||
assert.deepEqual(content(read('*run `npm ci`*\n')), [{ content: [marked('run ', em), marked('npm ci', code)], type: 'paragraph' }])
|
||
assert.deepEqual(content(read('**[`npm ci`](/u)**\n')), [{ content: [marked('npm ci', link('/u'), code)], type: 'paragraph' }])
|
||
}
|
||
assert.deepEqual(content(portableMarkdownToAdf('~~`npm ci`~~\n')), [{ content: [marked('npm ci', code)], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:underline[run `npm ci`]\n')), [{ content: [marked('run ', underline), marked('npm ci', code)], type: 'paragraph' }])
|
||
refused(losslessMarkdownToAdf('!adf:underline[`npm ci`]\n'), 'unsupported-node-shape', 'underline', 'write the underline mark around text beside them')
|
||
})
|
||
|
||
test('decodes the fenced info string the block walk leaves raw', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('```java​script\nx\n```\n')), [
|
||
{ attrs: { language: 'java\u200bscript' }, content: [text('x')], type: 'codeBlock' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('```\\#c\nx\n```\n')), [{ attrs: { language: '#c' }, content: [text('x')], type: 'codeBlock' }])
|
||
})
|
||
|
||
test('refuses the raw inline HTML no element mapping carries, naming it', () => {
|
||
refused(losslessMarkdownToAdf('Part <span> here.\n'), 'unmappable-html', '<span>')
|
||
refused(losslessMarkdownToAdf('Part </div> here.\n'), 'unmappable-html', '<div>')
|
||
refused(losslessMarkdownToAdf('Part <!-- note --> here.\n'), 'unmappable-html', 'an HTML comment')
|
||
refused(losslessMarkdownToAdf('Part <?php ?> here.\n'), 'unmappable-html', 'an HTML processing instruction')
|
||
refused(losslessMarkdownToAdf('Part <!DOCTYPE html> here.\n'), 'unmappable-html', 'an HTML declaration')
|
||
refused(losslessMarkdownToAdf('Part <![CDATA[x]]> here.\n'), 'unmappable-html', 'a CDATA section')
|
||
refused(losslessMarkdownToAdf('Part <!--> here.\n'), 'unmappable-html', 'an HTML comment')
|
||
refused(losslessMarkdownToAdf('Part <!---> here.\n'), 'unmappable-html', 'an HTML comment')
|
||
assert.equal(code(losslessMarkdownToAdf('A <a href="/x" disabled\nid=y> b\n')), 'unmappable-html')
|
||
assert.equal(code(losslessMarkdownToAdf('Part.\n<span>\n')), 'unmappable-html')
|
||
assert.deepEqual(path(losslessMarkdownToAdf('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(losslessMarkdownToAdf('3 < 4 and 5 <b 6\n')), [paragraph('3 < 4 and 5 <b 6')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a <b"c> d\n')), [paragraph('a <b"c> d')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a <!-- b\n')), [paragraph('a <!-- b')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a </b c> d\n')), [paragraph('a </b c> d')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('`<span>`\n')), [{ content: [codeSpan('<span>')], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\\<span>\n')), [paragraph('<span>')])
|
||
})
|
||
|
||
test('reads the emphasis CommonMark matches, the marks nesting outermost first', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('*a*\n')), [{ content: [marked('a', em)], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('_a_\n')), [{ content: [marked('a', em)], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('**a**\n')), [{ content: [marked('a', strong)], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('__a__\n')), [{ content: [marked('a', strong)], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('***a***\n')), [{ content: [marked('a', em, strong)], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('*a **b** c*\n')), [
|
||
{ content: [marked('a ', em), marked('b', em, strong), marked(' c', em)], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a*b*c\n')), [{ content: [text('a'), marked('b', em), text('c')], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('# *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(losslessMarkdownToAdf('a_b_c\n')), [paragraph('a_b_c')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('*a\n')), [paragraph('*a')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a * b\n')), [paragraph('a * b')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('**a*\n')), [{ content: [text('*'), marked('a', em)], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('*a**\n')), [{ content: [marked('a', em), text('*')], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\\*a\\*\n')), [paragraph('*a*')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('`*a*`\n')), [{ content: [codeSpan('*a*')], type: 'paragraph' }])
|
||
})
|
||
|
||
test('reads two tildes as strike, a single tilde and a longer run literal', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('~~a~~\n')), [{ content: [marked('a', strike)], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a ~b~ c\n')), [paragraph('a ~b~ c')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a ~~~b~~~ c\n')), [paragraph('a ~~~b~~~ c')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('~~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(losslessMarkdownToAdf('[a](/url)\n')), [{ content: [marked('a', link('/url'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a](/url "t")\n')), [{ content: [marked('a', link('/url', 't'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a](\n/url\n"t" )\n')), [{ content: [marked('a', link('/url', 't'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a](<u v>)\n')), [{ content: [marked('a', link('u v'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a]()\n')), [{ content: [marked('a', link(''))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a](/x(y))\n')), [{ content: [marked('a', link('/x(y)'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[**a**](/u)\n')), [{ content: [marked('a', link('/u'), strong)], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[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(losslessMarkdownToAdf('[a](/x\\)y)\n')), [{ content: [marked('a', link('/x)y'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a](/u "He said \\"hi\\"")\n')), [{ content: [marked('a', link('/u', 'He said "hi"'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a](/x&y)\n')), [{ content: [marked('a', link('/x&y'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[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(losslessMarkdownToAdf('[a\n')), [paragraph('[a')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a] (/u)\n')), [paragraph('[a] (/u)')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a ] b\n')), [paragraph('a ] b')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a](/u\n')), [paragraph('[a](/u')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a](/u x)\n')), [paragraph('[a](/u x)')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a](b(c "t")\n')), [paragraph('[a](b(c "t")')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a][r]\n\n[r]: b(c "t"\n')), [paragraph('[a][r]'), paragraph('[r]: b(c "t"')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a](<u\n')), [paragraph('[a](<u')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('![a\n')), [paragraph('![a')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a [b](/u) c](/v)\n')), [
|
||
{ content: [text('[a '), marked('b', link('/u')), text(' c](/v)')], type: 'paragraph' },
|
||
])
|
||
})
|
||
|
||
test('leaves a link literal whose target runs past the directive content holding it', () => {
|
||
assert.deepEqual(firstContent('!adf:underline[[a](b]c)\n'), [marked('[a](b', underline), text('c)')])
|
||
assert.deepEqual(firstContent('!adf:underline[[a](<b]>)\n'), [marked('[a](<b', underline), text('>)')])
|
||
assert.deepEqual(firstContent('!adf:underline[[a](b "t]")\n'), [marked('[a](b "t', underline), text('")')])
|
||
assert.deepEqual(firstContent('!adf:underline[[a](b]) [c](d)\n'), [marked('[a](b', underline), text(') '), marked('c', link('d'))])
|
||
})
|
||
|
||
test('leaves the brackets of a link whose text already holds one the text they are', () => {
|
||
const held: AdfMark = { attrs: { collection: 'c', href: '/u' }, type: 'link' }
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[<http://x/>](/v)\n')), [
|
||
{ content: [text('['), marked('http://x/', link('http://x/')), text('](/v)')], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a<http://x/>b](/v)\n')), [
|
||
{ content: [text('[a'), marked('http://x/', link('http://x/')), text('b](/v)')], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[!adf:link[a]{collection=c href="/u"}](/v)\n')), [
|
||
{ content: [text('['), marked('a', held), text('](/v)')], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[<http://x/>][r]\n\n[r]: /v\n')), [
|
||
{ content: [text('['), marked('http://x/', link('http://x/')), text(']'), marked('r', link('/v'))], type: 'paragraph' },
|
||
])
|
||
})
|
||
|
||
test('keeps the carry and the image the brackets a nested link leaves literal hold', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf(`[<http://x/>${carried}](/w)\n`)), [
|
||
{ content: [text('['), marked('http://x/', link('http://x/')), { type: 'placeholder' }, text('](/w)')], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(`[[<http://x/>](/c)${carried}](/w)\n`)), [
|
||
{ content: [text('[['), marked('http://x/', link('http://x/')), text('](/c)'), { type: 'placeholder' }, text('](/w)')], type: 'paragraph' },
|
||
])
|
||
// The failing bracket sits inside the image, not beside it: beside it the link-marked piece survives, and the deactivation stops being what the assertion pins.
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' ](/i)[](/y)](/w)](/v)\n')), [image('/v', 'a b [[http://x/](/c)](/w)')])
|
||
})
|
||
|
||
test('reads the reference links a definition resolves, and leaves the rest literal', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a][r]\n\n[r]: /url\n')), [{ content: [marked('a', link('/url'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a][]\n\n[a]: /url\n')), [{ content: [marked('a', link('/url'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a]\n\n[a]: /url\n')), [{ content: [marked('a', link('/url'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[Foo\nBar][]\n\n[foo bar]: /url\n')), [{ content: [marked('Foo Bar', link('/url'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a][z]\n\n[a]: /url\n')), [paragraph('[a][z]')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[a]\n')), [paragraph('[a]')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[][]\n')), [paragraph('[][]')])
|
||
})
|
||
|
||
test('leaves a link label past 999 characters literal, an escape counting as two, in every reader', () => {
|
||
const longest = `foo${' '.repeat(993)}bar`
|
||
const tooLong = `foo${' '.repeat(994)}bar`
|
||
const escaped = '\\*'.repeat(499)
|
||
const escapedTooLong = '\\*'.repeat(500)
|
||
for (const read of [commonMarkToAdf, losslessMarkdownToAdf, portableMarkdownToAdf]) {
|
||
assert.deepEqual(content(read(`[a][${escaped}]\n\n[${escaped}]: /u\n`)), [{ content: [marked('a', link('/u'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(read(`[a][${escapedTooLong}]\n\n[${escapedTooLong}]: /u\n`)), [paragraph(`[a][${'*'.repeat(500)}]`), paragraph(`[${'*'.repeat(500)}]: /u`)])
|
||
assert.deepEqual(content(read(`[${longest}]\n\n[foo bar]: /u\n`)), [{ content: [marked(longest, link('/u'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(read(`[${longest}][]\n\n[foo bar]: /u\n`)), [{ content: [marked(longest, link('/u'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(read(`[${tooLong}]\n\n[foo bar]: /u\n`)), [paragraph(`[${tooLong}]`)])
|
||
assert.deepEqual(content(read(`[${tooLong}][]\n\n[foo bar]: /u\n`)), [paragraph(`[${tooLong}][]`)])
|
||
}
|
||
})
|
||
|
||
test('reads an autolink, the email form as the mailto link it means', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('<https://example.com/>\n')), [
|
||
{ content: [marked('https://example.com/', link('https://example.com/'))], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('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(losslessMarkdownToAdf('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(losslessMarkdownToAdf('a <b@-c.example.com> d\n')), [paragraph('a <b@-c.example.com> d')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a <b[c@example.com> d\n')), [paragraph('a <b[c@example.com> d')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('<https://example.com/?a=\\*>\n')), [
|
||
{ content: [marked('https://example.com/?a=\\*', link('https://example.com/?a=\\*'))], type: 'paragraph' },
|
||
])
|
||
assert.equal(code(losslessMarkdownToAdf('<https://example.com/> <span>\n')), 'unmappable-html')
|
||
})
|
||
|
||
test('reads a lone image as the media the flavour spells for it', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\n')), [
|
||
image('https://example.com/moon.png', 'The moon'),
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\n')), [image('/u')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\n')), [image('/u', 'a')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('- \n')), [bulletList(item(image('/u', 'a')))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('> \n')), [quote(image('/u', 'a'))])
|
||
})
|
||
|
||
test('flattens the description of a lone image to the plain text alt holds', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' c](/v)\n')), [image('/v', 'a b c')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(' d](/e)\n')), [image('/e', 'a b d')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('](/h)](/v)\n')), [image('/v', 'x y a')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(']{href="a\\nb"}](/v)\n')), [image('/v', 'x y a')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('](/h)*](/v)\n')), [image('/v', 'x y a')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('](/h)]](/v)\n')), [image('/v', 'x y a')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('](/h)* z]](/v)\n')), [image('/v', 'x y a z')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('](/h)]{color="#ae2e24"}]](/v)\n')), [image('/v', 'x y a')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(']{href="a\\nb"}]](/v)\n')), [image('/v', 'x y a')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(']]{href="a\\nb"}](/v)\n')), [image('/v', 'x y a')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\n')), [image('/u', 'a b')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\n')), [image('/u', 'a b')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\n')), [image('/u', 'a b')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('![a !adf:mention[@A]{id=b1c2} b](/u)\n')), [image('/u', 'a @A b')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('![!adf:mention[@A]{id=b1c2}](/u)\n')), [image('/u', '@A')])
|
||
})
|
||
|
||
test('reads an empty link text as its destination, and an empty destination as nothing', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[](/u)\n')), [{ content: [marked('/u', link('/u'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a [](/u "t") b\n')), [{ content: [text('a '), marked('/u', link('/u', 't')), text(' b')], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[][r]\n\n[r]: /u\n')), [{ content: [marked('/u', link('/u'))], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a []() b\n')), [paragraph('a b')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[]()\n')), [{ type: 'paragraph' }])
|
||
})
|
||
|
||
test('reads a titled image alone in its paragraph as the image captioned with its title', () => {
|
||
const caption: AdfNode = { content: [text('The moon & stars')], type: 'caption' }
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\n')), [{ ...image('/u', 'a'), content: [...(image('/u', 'a').content ?? []), caption] }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('\n')), [image('/u', 'a')])
|
||
})
|
||
|
||
test('reads an image alone in a link as the image the link marks, and the link a title gives as its own', () => {
|
||
const linked = (node: AdfNode, mark: AdfMark): AdfNode => ({ ...node, content: (node.content ?? []).map((media) => ({ ...media, marks: [mark] })) })
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[](/v "t")\n')), [linked(image('/u', 'a'), link('/v', 't'))])
|
||
const captioned = linked(image('/u', 'a'), link('/v'))
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[](/v)\n')), [{ ...captioned, content: [...(captioned.content ?? []), { content: [text('c')], type: 'caption' }] }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[![a][i]][v]\n\n[i]: /u\n[v]: /v\n')), [linked(image('/u', 'a'), link('/v'))])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('[[](/v)](/w)\n')), [{ content: [text('['), marked('a', link('/v')), text(' ('), marked('image', link('/u')), text(')](/w)')], type: 'paragraph' }])
|
||
})
|
||
|
||
test('reads an image no paragraph holds alone as its alt text, linked to its destination', () => {
|
||
assert.deepEqual(firstContent('See .\n'), [text('See '), marked('a', link('/u', 't')), text('.')])
|
||
assert.deepEqual(firstContent('See .\n'), [text('See '), marked('/u', link('/u')), text('.')])
|
||
assert.deepEqual(firstContent('See ![]().\n'), [text('See .')])
|
||
assert.deepEqual(firstContent('**\n'), [marked('a', { type: 'em' }, link('/u'))])
|
||
assert.deepEqual(firstContent('\n'), [marked('a', link('/u')), marked('b', link('/v'))])
|
||
assert.deepEqual(firstContent('# \n'), [marked('a', link('/u'))])
|
||
assert.deepEqual(firstContent('!adf:paragraph {localId=p}\n\n!adf:/paragraph\n'), [marked('a', link('/u'))])
|
||
assert.deepEqual(firstContent(' d\n'), [text('![a '), marked('b', link('/c')), text(' d')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('![a]\n')), [paragraph('![a]')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a ! b\n')), [paragraph('a ! b')])
|
||
})
|
||
|
||
test('reads an image inside a link not alone in its paragraph as plain alt text the link marks, then (image) linked to its destination', () => {
|
||
const image = (destination: string, title?: string): AdfNode[] => [text(' ('), marked('image', link(destination, title)), text(')')]
|
||
assert.deepEqual(firstContent('See [](/v).\n'), [text('See '), marked('a', link('/v')), text(' ('), marked('image', link('/u', 't')), text(').')])
|
||
assert.deepEqual(firstContent('See [](/v).\n'), [text('See '), marked('/v', link('/v')), text(' ('), marked('image', link('/u')), text(').')])
|
||
assert.deepEqual(firstContent('[b  c](/v)\n'), [marked('b a c', link('/v')), ...image('/u', 't')])
|
||
assert.deepEqual(firstContent('[ and ](/v)\n'), [marked('a and b', link('/v')), text(' ('), marked('image', link('/u')), text(') ('), marked('image', link('/w')), text(')')])
|
||
assert.deepEqual(firstContent('*[b ](/v)*\n'), [marked('b a', em, link('/v')), marked(' (', em), marked('image', em, link('/u')), marked(')', em)])
|
||
assert.deepEqual(firstContent('!adf:underline[[b ](/v)]\n'), [marked('b a', underline, link('/v')), marked(' (', underline), marked('image', underline, link('/u')), marked(')', underline)])
|
||
assert.deepEqual(firstContent('!adf:underline[[b ](/v) c]\n'), [marked('b a', underline, link('/v')), marked(' (', underline), marked('image', underline, link('/u')), marked(') c', underline)])
|
||
assert.deepEqual(firstContent('[x !adf:underline[[]()]](/w)\n'), [text('[x '), marked(' (', underline), marked('image', underline, link('/u')), marked(')', underline), text('](/w)')])
|
||
const unspellable = { attrs: { href: 'a\nb' }, type: 'link' }
|
||
assert.deepEqual(firstContent('!adf:link[b ]{href="a\\nb"}\n'), [marked('b a', unspellable), ...image('/u')])
|
||
assert.deepEqual(firstContent('!adf:link[!adf:underline[]]{href="a\\nb"}\n'), [marked('a', unspellable, { type: 'underline' }), ...image('/u')])
|
||
assert.deepEqual(firstContent('!adf:link[ !adf:underline[]]{href="a\\nb"}\n'), [
|
||
marked('a ', unspellable),
|
||
marked('b', unspellable, { type: 'underline' }),
|
||
text(' ('),
|
||
marked('image', link('/u')),
|
||
text(') ('),
|
||
marked('image', link('/w')),
|
||
text(')'),
|
||
])
|
||
refused(losslessMarkdownToAdf('!adf:link[x [](/v)]{href="a\\nb"}\n'), 'unsupported-node-shape', 'no link wraps a link')
|
||
})
|
||
|
||
test('writes no (image) after a link to the image itself, unless the image holds a title the link does not', () => {
|
||
assert.deepEqual(firstContent('See [](/u).\n'), [text('See '), marked('a', link('/u')), text('.')])
|
||
assert.deepEqual(firstContent('See [](/u "t").\n'), [text('See '), marked('a', link('/u', 't')), text('.')])
|
||
assert.deepEqual(firstContent('See [](/u "t").\n'), [text('See '), marked('a', link('/u', 't')), text('.')])
|
||
assert.deepEqual(firstContent('See [](/u).\n'), [text('See '), marked('a', link('/u')), text(' ('), marked('image', link('/u', 't')), text(').')])
|
||
assert.deepEqual(firstContent('[b  ](/u)\n'), [marked('b a c', link('/u')), text(' ('), marked('image', link('/w')), text(')')])
|
||
assert.deepEqual(firstContent('See [](/u).\n'), [text('See '), marked('a', link('/u')), text('.')])
|
||
const titled = { attrs: { href: 'a\nb', title: 't' }, type: 'link' }
|
||
assert.deepEqual(firstContent('!adf:link[b ]{href="a\\nb"}\n'), [marked('b a', { attrs: { href: 'a\nb' }, type: 'link' })])
|
||
assert.deepEqual(firstContent('!adf:link[b ]{href="a\\nb" title=t}\n'), [marked('b a', titled)])
|
||
assert.deepEqual(firstContent('!adf:link[!adf:underline[![a]()]]{href="a\\nb"}\n'), [marked('a', { attrs: { href: 'a\nb' }, type: 'link' }, { type: 'underline' }), text(' ('), marked('image', link('')), text(')')])
|
||
assert.deepEqual(firstContent('!adf:link[b ]{href="a\\nb" title=t}\n'), [marked('b a', titled), text(' ('), marked('image', link('a\nb', 'u')), text(')')])
|
||
})
|
||
|
||
test('carries the mark a spelling nested inside its own kind names once', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('*(*a*)*\n')), [{ content: [marked('(a)', em)], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf(`${'*'.repeat(600)}a${'*'.repeat(600)}\n`)), [{ content: [marked('a', strong)], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('*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(losslessMarkdownToAdf('!adf:status[In review]{color=yellow}\n')), status({ color: 'yellow', text: 'In review' }))
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:status{color=neutral}\n')), status({ color: 'neutral' }))
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:status[]{color=neutral}\n')), status({ color: 'neutral', text: '' }))
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:status[ In review ]{color=yellow}\n')), status({ color: 'yellow', text: ' In review ' }))
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:status[In!adf:text{text=" "}review]{color=yellow}\n')), status({ color: 'yellow', text: 'In review' }))
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:status[a\\]b]{color=yellow}\n')), status({ color: 'yellow', text: 'a]b' }))
|
||
})
|
||
|
||
test('names the content slot no lone plain text node reads back from', () => {
|
||
const named = ['status content slot', 'one text node']
|
||
refused(losslessMarkdownToAdf('!adf:status[**A**]{color=yellow}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:status[!adf:carry{json="{\\"attrs\\":{\\"localId\\":\\"a\\"},\\"text\\":\\"A\\",\\"type\\":\\"text\\"}"}]{color=yellow}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:status[!adf:carry{json="{\\"content\\":[{\\"text\\":\\"B\\",\\"type\\":\\"text\\"}],\\"text\\":\\"A\\",\\"type\\":\\"text\\"}"}]{color=yellow}\n'), 'unsupported-node-shape', ...named)
|
||
assert.equal(code(losslessMarkdownToAdf('!adf:status[a`b`]{color=yellow}\n')), 'unsupported-node-shape')
|
||
assert.equal(code(losslessMarkdownToAdf('!adf:status[!adf:date{timestamp=1}]{color=yellow}\n')), 'unsupported-node-shape')
|
||
refused(losslessMarkdownToAdf('!adf:status[]{color=yellow}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:link[x !adf:status[]{color=yellow}]{href="a\\nb"}\n'), 'unsupported-node-shape', ...named)
|
||
assert.equal(code(losslessMarkdownToAdf('!adf:status[<div>]{color=yellow}\n')), 'unmappable-html')
|
||
assert.equal(code(losslessMarkdownToAdf('!adf:date[<div>]{timestamp=1}\n')), 'unmappable-html')
|
||
assert.equal(code(losslessMarkdownToAdf('!adf:widget[<div>]\n')), 'unmappable-html')
|
||
const spans = ['status content slot', 'line ending', 'replace it with a space', '!adf:carry']
|
||
refused(losslessMarkdownToAdf('!adf:status[!adf:text{text="\\n"}]{color=yellow}\n'), 'unsupported-node-shape', ...spans)
|
||
refused(losslessMarkdownToAdf('!adf:status[a b]{color=yellow}\n'), 'unsupported-node-shape', ...spans)
|
||
refused(losslessMarkdownToAdf('!adf:status[a b]{color=yellow}\n'), 'unsupported-node-shape', ...spans)
|
||
refused(losslessMarkdownToAdf('!adf:status[!adf:text{text="\\u0000"}]{color=yellow}\n'), 'unsupported-node-shape', 'null character', '!adf:carry')
|
||
refused(losslessMarkdownToAdf('Part !adf:mention{id=b1c2 text=A}.\n'), 'unsupported-node-shape', 'mention', 'content slot')
|
||
})
|
||
|
||
test('reads the characters CommonMark rewrites from their spellings', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a b!adf:text{text="\\u0000\\u0000"}\n')), [paragraph('a\rb\u0000\u0000')])
|
||
refused(losslessMarkdownToAdf('!adf:text{text="\\u0000a"}\n'), 'unsupported-node-shape', 'one run of null characters')
|
||
})
|
||
|
||
test('reads the whitespace the reserved text directive carries', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:text{text=" "}a\n')), [paragraph(' a')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a!adf:text{text="\\n"}b\n')), [paragraph('a\nb')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a!adf:text{text="\\t"}\n')), [paragraph('a\t')])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('_!adf:text{text=" "}a_\n')), [{ content: [marked(' a', em)], type: 'paragraph' }])
|
||
})
|
||
|
||
test('names the text directive spelling no whitespace run reads back from', () => {
|
||
const named = ['one run of spaces and tabs, one run of newlines, or one run of null characters']
|
||
refused(losslessMarkdownToAdf('!adf:text{text=hi}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:text{text=" \\n"}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:text{text=""}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:text{}\n'), 'unsupported-node-shape', 'one text attribute')
|
||
refused(losslessMarkdownToAdf('!adf:text{localId=a text=" "}\n'), 'unsupported-node-shape', 'one text attribute')
|
||
refused(losslessMarkdownToAdf('!adf:text[a]{text=" "}\n'), 'unsupported-node-shape', 'text takes no content')
|
||
refused(losslessMarkdownToAdf('!adf:text{text="\\u0020"}\n'), 'unsupported-node-shape', 'text=" "')
|
||
})
|
||
|
||
test('reads the directive marks, the nesting outermost first', () => {
|
||
const wrapped = (...marks: AdfMark[]): AdfNode[] => [{ content: [marked('a', ...marks)], type: 'paragraph' }]
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:underline[a]\n')), wrapped(underline))
|
||
assert.deepEqual(content(losslessMarkdownToAdf('_!adf:underline[a]_\n')), wrapped(em, underline))
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:underline[_a_]\n')), wrapped(underline, em))
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:underline[!adf:underline[a]]\n')), wrapped(underline))
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:textColor[a]{color="#ae2e24"}\n')), wrapped({ attrs: { color: '#ae2e24' }, type: 'textColor' }))
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:subsup[a]{type=sub}\n')), wrapped({ attrs: { type: 'sub' }, type: 'subsup' }))
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:underline[a!adf:date{timestamp=1}]\n')), [
|
||
{ content: [marked('a', underline), { attrs: { timestamp: '1' }, type: 'date' }], type: 'paragraph' },
|
||
])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('_!adf:underline[a!adf:date{timestamp=1}]_\n')), [
|
||
{ content: [marked('a', em, underline), { attrs: { timestamp: '1' }, type: 'date' }], type: 'paragraph' },
|
||
])
|
||
refused(losslessMarkdownToAdf('!adf:border[a]{color="#091e42" size=x}\n'), 'unsupported-node-shape', 'size', 'border', 'no number')
|
||
})
|
||
|
||
test('names the mark markdown spells, never a directive', () => {
|
||
refused(losslessMarkdownToAdf('!adf:em[a]\n'), 'unsupported-node-shape', '_x_')
|
||
refused(losslessMarkdownToAdf('!adf:strong[a]\n'), 'unsupported-node-shape', '**x**')
|
||
refused(losslessMarkdownToAdf('!adf:strike[a]\n'), 'unsupported-node-shape', '~~x~~')
|
||
refused(losslessMarkdownToAdf('!adf:code[a]\n'), 'unsupported-node-shape', '`x`')
|
||
})
|
||
|
||
test('refuses the directive link CommonMark could spell, and reads the one it could not', () => {
|
||
const spellable = ['[text](url "title")', '<url>']
|
||
refused(losslessMarkdownToAdf('!adf:link[a]{href="/u"}\n'), 'unsupported-node-shape', ...spellable)
|
||
refused(losslessMarkdownToAdf('See !adf:link[a]{href="/u"}.\n'), 'unsupported-node-shape', ...spellable)
|
||
refused(losslessMarkdownToAdf('!adf:link[https://example.com/]{href="https://example.com/"}\n'), 'unsupported-node-shape', ...spellable)
|
||
refused(losslessMarkdownToAdf('# !adf:link[`]: a`]{href="/u"}\n'), 'unsupported-node-shape', ...spellable)
|
||
refused(losslessMarkdownToAdf('| !adf:link[`]: a`]{href="/u"} |\n| --- |\n'), 'unsupported-node-shape', ...spellable)
|
||
refused(losslessMarkdownToAdf('!adf:underline[!adf:link[a]{href="/u"}]\n'), 'unsupported-node-shape', ...spellable)
|
||
assert.deepEqual(path(losslessMarkdownToAdf('Part.\n\nSee !adf:link[a]{href="/u"}.\n')), ['content', 1])
|
||
const opening: AdfNode = { marks: [{ attrs: { href: '/u' }, type: 'link' }, { type: 'code' }], text: ']: a', type: 'text' }
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:link[`]: a`]{href="/u"}\n')), [{ content: [opening], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:heading {level=1 localId=h}\n!adf:link[`]: a`]{href="/u"}\n!adf:/heading\n')), [
|
||
{ attrs: { level: 1, localId: 'h' }, content: [opening], type: 'heading' },
|
||
])
|
||
})
|
||
|
||
test('names the href the directive link spells no value for', () => {
|
||
const named = ['link mark spells its href']
|
||
refused(losslessMarkdownToAdf('!adf:link[a]\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:link[a]{title=t}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('See !adf:link[a]{id=01a032c3-7a90-70c9-88f6-c60f710eda07}.\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:link[<http://x/>]{collection=c}\n'), 'unsupported-node-shape', ...named)
|
||
})
|
||
|
||
test('names the link a directive link wraps, no link holding another', () => {
|
||
const named = ['no link wraps a link']
|
||
refused(losslessMarkdownToAdf('!adf:link[<http://x/>]{collection=c href="/u"}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:link[[a](/v)]{collection=c href="/u"}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:link[a <http://x/> b]{collection=c href="/u"}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:link[<http://x/>]{href="/u"}\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:link[x !adf:underline[[]()]]{href="a\\nb"}\n'), 'unsupported-node-shape', ...named)
|
||
})
|
||
|
||
test('names the directive mark left without the content it wraps', () => {
|
||
const named = ['underline', 'wraps none']
|
||
refused(losslessMarkdownToAdf('!adf:underline[]\n'), 'unsupported-node-shape', ...named)
|
||
refused(losslessMarkdownToAdf('!adf:underline{}\n'), 'unsupported-node-shape', ...named)
|
||
const wrapsOnly = (name: string): string[] => [`take no ${name} mark`, `write the ${name} mark around text beside them`]
|
||
refused(losslessMarkdownToAdf('!adf:underline[!adf:hardBreak{}]\n'), 'unsupported-node-shape', ...wrapsOnly('underline'))
|
||
refused(losslessMarkdownToAdf('!adf:link[!adf:hardBreak{}]{href="/u" title=t}\n'), 'unsupported-node-shape', ...wrapsOnly('link'))
|
||
})
|
||
|
||
test('reads the text break only between two text nodes CommonMark joins, building no node', () => {
|
||
assert.deepEqual(content(losslessMarkdownToAdf('a!adf:textBreak{}b\n')), [{ content: [text('a'), text('b')], type: 'paragraph' }])
|
||
assert.deepEqual(content(losslessMarkdownToAdf('==a!adf:textBreak{}b==\n')), [{ content: [text('==a'), text('b==')], type: 'paragraph' }])
|
||
const parts = ['delete !adf:textBreak{} here', 'two runs of text with the same formatting']
|
||
const carried = '!adf:carry{json="{\\"text\\":\\"b\\",\\"type\\":\\"text\\"}"}'
|
||
for (const markdown of ['!adf:textBreak{}a\n', 'a!adf:textBreak{}\n', 'a!adf:textBreak{}!adf:textBreak{}b\n', '**a**!adf:textBreak{}b\n', '[!adf:textBreak{}](/u)\n', `a!adf:textBreak{}${carried}\n`]) {
|
||
refused(losslessMarkdownToAdf(markdown), 'unsupported-node-shape', ...parts)
|
||
}
|
||
refused(losslessMarkdownToAdf('!adf:status[a!adf:textBreak{}b]\n'), 'unsupported-node-shape', 'delete !adf:textBreak{}', 'content slot')
|
||
refused(losslessMarkdownToAdf('\n'), 'unsupported-node-shape', 'delete !adf:textBreak{}', 'image description')
|
||
refused(losslessMarkdownToAdf('a!adf:textBreak{x=y}b\n'), 'unsupported-node-shape', '!adf:textBreak{}', 'spells more')
|
||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:carry{json="{\\"type\\":\\"textBreak\\"}"}\n')), [{ content: [{ type: 'textBreak' }], type: 'paragraph' }])
|
||
})
|
||
|
||
test('leads a refusal ordinary editing meets with the edit that fixes it', () => {
|
||
const leads = (markdown: string, edit: string): void => {
|
||
const result = losslessMarkdownToAdf(markdown)
|
||
assert.equal(code(result), 'unsupported-node-shape')
|
||
assert.ok(!result.ok && result.error.message.startsWith(edit), edit)
|
||
}
|
||
leads('!adf:paragraph {content=empty}\nText.\n!adf:/paragraph\n', 'remove content=empty')
|
||
leads('!adf:codeBlock\n```\na\n```\n```\n```\n!adf:/codeBlock\n', 'delete the empty fence')
|
||
leads('!adf:codeBlock {attrs=empty}\n```js\na\n```\n```js\nb\n```\n!adf:/codeBlock\n', 'remove attrs=empty')
|
||
refused(losslessMarkdownToAdf('!adf:panel info {attrs=empty}\nText.\n!adf:/panel\n'), 'unsupported-node-shape', 'drop attrs=empty')
|
||
})
|