import assert from 'node:assert/strict' import test from 'node:test' import type { AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts' import type { ParseError, Result } from '../../result.ts' import { adfToHtml, htmlToAdf } from '../../index.ts' const ids = { item: '019ad5af-efd9-7f4e-a7c9-6aef16d4945f', list: '019d8e77-7df8-7a83-91ea-43a7ca2aae3b', mention: '0199f4c2-8b1e-7d03-9a6f-2c5e8d71b4a0', paragraph: '01957ad9-e3f7-70aa-a5f3-4c013274f488', quote: '019d2fe4-3e67-7b76-b40a-a776a8f76aea', } function document(...content: AdfNode[]): AdfDocument { return { content, type: 'doc', version: 1 } } function paragraph(...content: AdfNode[]): AdfNode { return content.length === 0 ? { type: 'paragraph' } : { content, type: 'paragraph' } } function heading(level: number, ...content: AdfNode[]): AdfNode { return content.length === 0 ? { attrs: { level }, type: 'heading' } : { attrs: { level }, content, type: 'heading' } } function text(value: string, ...marks: AdfMark[]): AdfNode { return marks.length === 0 ? { text: value, type: 'text' } : { marks, text: value, type: 'text' } } function item(...content: AdfNode[]): AdfNode { return { content, type: 'listItem' } } function read(result: Result): AdfDocument | string { return result.ok ? result.value : `${result.error.code}: ${result.error.message}` } function refusal(result: Result): string { return result.ok ? 'read' : `${result.error.code} at /${result.error.path.join('/')} line ${result.error.position.line} offset ${result.error.position.offset}` } function message(result: Result): string { return result.ok ? 'read' : result.error.message } const documentCarry = carry('div', { type: 'doc', version: 1 }, '') const mention: AdfNode = { attrs: { id: ids.mention, text: '@Mikael' }, type: 'mention' } const panel: AdfNode = { attrs: { panelType: 'info' }, content: [paragraph(text('Check the collation.'))], type: 'panel' } const rule = JSON.stringify({ type: 'unknownBlock' }).replaceAll('"', '"') // The carry element of `node`, showing `shown`. function carry(tag: 'div' | 'span', node: unknown, shown: string): string { return `<${tag} class="adf-carry" data-adf="${JSON.stringify(node).replaceAll('"', '"')}">${shown}` } function nested(value: unknown, levels: number, wrap: (inner: unknown) => unknown): unknown { let wrapped = value for (let level = 0; level < levels; level += 1) wrapped = wrap(wrapped) return wrapped } test('reads an empty string as a document of no blocks, and the document\'s carry as one holding no content key', () => { assert.deepEqual(read(htmlToAdf('')), document()) assert.deepEqual(read(htmlToAdf(`${documentCarry}\n`)), { type: 'doc', version: 1 }) }) test('reads paragraphs, headings and rules, a line feed after each', () => { assert.deepEqual(read(htmlToAdf('

Level 1

\n

Read the manual.

\n
\n
Level 6
\n')), document(heading(1, text('Level 1')), paragraph(text('Read the manual.')), { type: 'rule' }, heading(6, text('Level 6')))) assert.deepEqual(read(htmlToAdf('

\n

\n')), document(paragraph(), heading(2))) }) test('reads text\'s marks from its elements, the outermost first, and each run of text as one text node', () => { const link: AdfMark = { attrs: { href: 'https://example.com/a?b=1&c=2', title: 'The "A" page' }, type: 'link' } const html = '

Read the manual: npm ci or this
Line two

\n' const line = paragraph(text('Read '), text('the', { type: 'em' }), text(' '), text('manual', { type: 'strong' }, { type: 'em' }), text(': '), text('npm ci', { type: 'code' }, link), text(' or '), text('this', { attrs: { href: 'https://example.com/' }, type: 'link' }), { type: 'hardBreak' }, text('Line two')) assert.deepEqual(read(htmlToAdf(html)), document(line)) assert.deepEqual(read(htmlToAdf('

y

\n')), document(paragraph(text('y', { attrs: { href: 'x', title: '' }, type: 'link' })))) }) test('reads escaped text, and keeps tabs, line feeds and form feeds', () => { assert.deepEqual(read(htmlToAdf('

a < b && c > "d" \'e\'\tf\ng\fh &lt;

\n')), document(paragraph(text('a < b && c > "d" \'e\'\tf\ng\fh <')))) }) test('reads lists, an item\'s first paragraph on its line and its other blocks a line each', () => { assert.deepEqual(read(htmlToAdf('
    \n
  • Bolt
  • \n
  • Nut
  • \n
\n')), document({ content: [item(paragraph(text('Bolt'))), item(paragraph(text('Nut')))], type: 'bulletList' })) const listed: AdfNode = { attrs: { order: 3 }, content: [item(paragraph(text('Bolt')), { content: [item(paragraph(text('M8')))], type: 'bulletList' }), item(paragraph(text('a')), paragraph(text('b')))], type: 'orderedList' } assert.deepEqual(read(htmlToAdf('
    \n
  1. Bolt\n
      \n
    • M8
    • \n
    \n
  2. \n
  3. a\n

    b

    \n
  4. \n
\n')), document(listed)) assert.deepEqual(read(htmlToAdf('
    \n
  1. \n
    \n
  2. \n
  3. \n
\n')), document({ content: [item({ type: 'rule' }), { type: 'listItem' }], type: 'orderedList' })) assert.deepEqual(read(htmlToAdf('
    \n
  • \n

    \n
  • \n
\n')), document({ content: [item(paragraph())], type: 'bulletList' })) assert.deepEqual(read(htmlToAdf('
    \n
  1. x
  2. \n
\n')), document({ attrs: { order: -2 }, content: [item(paragraph(text('x')))], type: 'orderedList' })) }) test('reads the text ending an item\'s line up to the line feed before the item\'s next block', () => { const listed = (...blocks: AdfNode[]): AdfDocument => document({ content: [item(...blocks)], type: 'bulletList' }) assert.deepEqual(read(htmlToAdf('
    \n
  • a\n\n

    b

    \n
  • \n
\n')), listed(paragraph(text('a\n')), paragraph(text('b')))) assert.deepEqual(read(htmlToAdf('
    \n

  • \n
    \n
  • \n
\n')), listed(paragraph({ type: 'hardBreak' }), { type: 'rule' })) assert.deepEqual(read(htmlToAdf('
    \n
  • \n
  • \n
\n')), listed(paragraph(text('\n')))) assert.deepEqual(read(htmlToAdf('
    \n
  • \na
  • \n
\n')), listed(paragraph(text('\n'), text('a', { type: 'em' })))) }) test('reads an item\'s first paragraph holding an attribute as its element on the item\'s line', () => { const listed = (...blocks: AdfNode[]): AdfDocument => document({ content: [item(...blocks)], type: 'bulletList' }) const held: AdfNode = { attrs: { localId: ids.paragraph }, content: [text('Bolt')], type: 'paragraph' } assert.deepEqual(read(htmlToAdf(`
    \n
  • Bolt

  • \n
\n`)), listed(held)) assert.deepEqual(read(htmlToAdf(`
    \n
  • Bolt

    \n
      \n
    • M8
    • \n
    \n
  • \n
\n`)), listed(held, { content: [item(paragraph(text('M8')))], type: 'bulletList' })) assert.deepEqual(read(htmlToAdf(`
    \n
  • \n
\n`)), listed({ attrs: { localId: ids.paragraph }, type: 'paragraph' })) }) test('reads data-* attributes as the attributes they name, a value that is no string from its JSON', () => { const localId = { localId: ids.paragraph } assert.deepEqual(read(htmlToAdf(`

x

\n`)), document({ attrs: { level: 2, ...localId }, content: [text('x')], type: 'heading' })) assert.deepEqual(read(htmlToAdf(`
\n

x

\n
\n`)), document({ attrs: { localId: ids.quote }, content: [paragraph(text('x'))], type: 'blockquote' })) const listed: AdfNode = { attrs: { localId: ids.list, order: 3 }, content: [{ attrs: { localId: ids.item }, content: [{ attrs: localId, content: [text('Bolt')], type: 'paragraph' }], type: 'listItem' }], type: 'orderedList' } assert.deepEqual(read(htmlToAdf(`
    \n
  1. Bolt

  2. \n
\n`)), document(listed)) const code: AdfNode = { attrs: { hideLineNumbers: false, language: 'js', wrap: true, ...localId }, content: [text('x')], type: 'codeBlock' } assert.deepEqual(read(htmlToAdf(`
x
\n`)), document(code)) assert.deepEqual(read(htmlToAdf('
\n')), document({ attrs: { color: '#ccc', style: 'a"b', weight: -0 }, type: 'rule' })) }) test('reads quotes, code blocks and containers holding no content key', () => { assert.deepEqual(read(htmlToAdf('
\n

Quoted

\n
\n

Twice

\n
\n
\n')), document({ content: [paragraph(text('Quoted')), { content: [paragraph(text('Twice'))], type: 'blockquote' }], type: 'blockquote' })) assert.deepEqual(read(htmlToAdf('
if (a < b)\n  run()
\n')), document({ attrs: { language: 'javascript' }, content: [text('if (a < b)\n run()')], type: 'codeBlock' })) assert.deepEqual(read(htmlToAdf('
plain
\n
\n')), document({ content: [text('plain')], type: 'codeBlock' }, { type: 'codeBlock' })) assert.deepEqual(read(htmlToAdf('
\n
    \n
      \n')), document({ type: 'blockquote' }, { type: 'bulletList' }, { type: 'orderedList' })) }) test('reads an image standing alone in a paragraph, a link around it where one marks it', () => { const image = (media: AdfNode): AdfNode => ({ attrs: { layout: 'center' }, content: [media], type: 'mediaSingle' }) assert.deepEqual(read(htmlToAdf('

      The moon

      \n')), document(image({ attrs: { alt: 'The moon', type: 'external', url: 'https://example.com/moon.jpg' }, type: 'media' }))) const linked = image({ attrs: { type: 'external', url: 'https://example.com/moon.jpg' }, marks: [{ attrs: { href: 'https://example.com/', title: 'Home' }, type: 'link' }], type: 'media' }) assert.deepEqual(read(htmlToAdf('

      \n')), document(linked)) }) test('reads a carry element from its data-adf, what it shows read back as adfToHtml writes it', () => { assert.deepEqual(read(htmlToAdf(`${carry('div', panel, '

      Check the collation.

      ')}\n

      Ask ${carry('span', mention, '@Mikael')}

      \n`)), document(panel, paragraph(text('Ask '), mention))) assert.deepEqual(read(htmlToAdf(`

      ${carry('span', text('a\u0085b', { type: 'underline' }), 'ab').replace('\u0085', '\\u0085')}

      \n`)), document(paragraph(text('a\u0085b', { type: 'underline' })))) }) test('reads a carry element holding a node HTML writes as its own element as that node', () => { assert.deepEqual(read(htmlToAdf(`${carry('div', paragraph(text('x')), '

      x

      ')}\n`)), document(paragraph(text('x')))) assert.deepEqual(read(htmlToAdf(`

      a${carry('span', text('b'), 'b')}

      \n`)), document(paragraph(text('a'), text('b')))) assert.deepEqual(read(htmlToAdf(`
        \n
      • \n${carry('div', paragraph(text('x')), '

        x

        ')}\n
      • \n
      \n`)), document({ content: [item(paragraph(text('x')))], type: 'bulletList' })) }) // Each group's cases, the HTML and its refusal. const refusals: Readonly> = { 'refuses block shapes adfToHtml would not write': [ ['
        \n
      • Bolt

      • \n
      \n', 'unmappable-html at /content/0/content/0/content/0 line 2 offset 5'], ['
        \n
      • \n

        Bolt

        \n
      • \n
      \n', 'unmappable-html at /content/0/content/0/content/0 line 3 offset 10'], [`
        \n
      • \n

        \n
      • \n
      \n`, 'unmappable-html at /content/0/content/0/content/0 line 3 offset 10'], ['
        \n

      • \n
      \n', 'unmappable-html at /content/0/content/0 line 2 offset 5'], ['
        \n
      • a
      \n', 'unmappable-html at /content/0/content/0 line 2 offset 5'], [`
        \n
      • a

      \n`, 'unmappable-html at /content/0/content/0 line 2 offset 5'], ['
        \n

        a

        \n
      \n', 'unmappable-html at /content/0/content/0 line 2 offset 5'], ['
        \n
      \n', 'unmappable-html at /content/0/content/0 line 2 offset 5'], ['
      • a
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['
      \n

      a

      \n', 'unmappable-html at /content/0 line 2 offset 13'], ['

      a

      b

      \n', 'unmappable-html at / line 1 offset 0'], ['
        \n
      1. a
      2. \n
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['
        \n
      1. a
      2. \n
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['
        \n
      1. a
      2. \n
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['
        \n
      1. a
      2. \n
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['
        \n
      1. a
      2. \n
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['
        \n
      1. a
      2. \n
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['
      x
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['
      x
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['
      x
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['
      x
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['
      a &bogus; b
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['
      x\n`, 'unmappable-html at /content/0 line 1 offset 0'],
          [`
      \n`, 'unmappable-html at /content/0 line 1 offset 0'], [`
      \n`, 'unmappable-html at /content/0 line 1 offset 0'], [`
      \n`, 'unmappable-html at /content/0 line 1 offset 0'], ['
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['
      \n', 'unsupported-node-shape at /content/0 line 1 offset 0'], ['
      \n', 'unsupported-node-shape at /content/0 line 1 offset 0'], ['

      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['

      \n', 'unsupported-node-shape at /content/0/content/0 line 1 offset 0'], ['
      \n', 'unsupported-node-shape at /content/0 line 1 offset 0'], ['

      \n', 'unsupported-node-shape at /content/0/content/0 line 1 offset 0'], ['

      a

      \n', 'unmappable-html at /content/0/content/0 line 1 offset 0'], ], 'refuses an edit to what a carry element shows': [ [`

      a

      \n${carry('div', panel, '

      Check the collation.

      ')}\n`, 'unmappable-html at /content/1 line 2 offset 9'], [`

      Ask ${carry('span', mention, '')}

      \n`, 'unmappable-html at /content/0/content/1 line 1 offset 0'], ], 'refuses the document\'s carry anywhere but as the whole document, a line feed ending it': [ [documentCarry, 'unmappable-html at / line 1 offset 0'], [`${documentCarry}\n\n`, `unmappable-html at / line 2 offset ${documentCarry.length + 1}`], [`${documentCarry}X`, 'unmappable-html at / line 1 offset 0'], [`${documentCarry}X
      \n`, 'unmappable-html at / line 1 offset 0'], [`${documentCarry.replace('>
      ', '>x')}\n`, 'unmappable-html at / line 1 offset 0'], [`${documentCarry}\n
      \n`, 'unsupported-node-shape at /content/0 line 1 offset 0'], [`
      \n${documentCarry}\n`, 'unsupported-node-shape at /content/1 line 2 offset 5'], [`
      \n${documentCarry}\n
      \n`, 'unsupported-node-shape at /content/0/content/0 line 2 offset 13'], [`${carry('div', { type: 'doc', version: 2 }, '')}\n`, 'unsupported-node-shape at /content/0 line 1 offset 0'], ], 'refuses a data-* attribute out of the order adfToHtml writes as unmappable-html, and one no attribute of its node spells as unsupported-node-shape': [ ['
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['
      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['

      a

      \n', 'unsupported-node-shape at /content/0 line 1 offset 0'], ['

      a

      \n', 'unsupported-node-shape at /content/0 line 1 offset 0'], ['
        \n
      1. a
      2. \n
      \n', 'unsupported-node-shape at /content/0 line 1 offset 0'], ['
      x
      \n', 'unsupported-node-shape at /content/0 line 1 offset 0'], ['

      a

      \n', 'unsupported-node-shape at /content/0 line 1 offset 0'], ['
      x
      \n', 'unsupported-node-shape at /content/0 line 1 offset 0'], ['
      x
      \n', 'unsupported-node-shape at /content/0 line 1 offset 0'], ['
      x
      \n', 'unsupported-node-shape at /content/0 line 1 offset 0'], ['
      x
      \n', 'unsupported-node-shape at /content/0 line 1 offset 0'], ['
      \n', 'unsupported-node-shape at /content/0 line 1 offset 0'], ['
      \n', 'unsupported-node-shape at /content/0 line 1 offset 0'], ['
        \n
      • a
      • \n
      \n', 'unsupported-node-shape at /content/0/content/0 line 2 offset 5'], [`
      \n`, 'unsupported-node-shape at /content/0 line 1 offset 0'], ], 'refuses HTML adfToHtml does not write as unmappable-html, at the start of the line it stands on': [ ['

      x

      ', 'unmappable-html at / line 1 offset 0'], ['

      x

      X', 'unmappable-html at / line 1 offset 0'], ['

      x

      \n\n', 'unmappable-html at /content/1 line 2 offset 9'], ['\n', 'unmappable-html at /content/0 line 1 offset 0'], ['

      a

      \n
      b
      \n', 'unmappable-html at /content/1 line 2 offset 9'], ['

      a

      \n

      b

      \n', 'unmappable-html at /content/1 line 2 offset 9'], ['x\n', 'unmappable-html at /content/0 line 1 offset 0'], ['

      a

      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['

      ab

      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['

      a
      b

      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['

      it's

      \n', 'unmappable-html at /content/0/content/0 line 1 offset 0'], ['

      a > b

      \n', 'unmappable-html at /content/0/content/0 line 1 offset 0'], ['

      a\rb

      \n', 'unmappable-html at /content/0/content/0 line 1 offset 0'], ['

      a

      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['

      a

      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['

      a

      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['

      a

      \n
      b\nc
      \nd\n', 'unmappable-html at /content/2 line 4 offset 37'], ], 'refuses an image adfToHtml would not write': [ ['

      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['

      b

      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['

      b

      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['

      b

      \n', 'unmappable-html at /content/0 line 1 offset 0'], ['

      \n', 'unmappable-html at /content/0 line 1 offset 0'], [`

      \n`, 'unmappable-html at /content/0 line 1 offset 0'], ], 'refuses marks adfToHtml would not write around text': [ ['

      a

      \n', 'unmappable-html at /content/0/content/0 line 1 offset 0'], ['

      \n', 'unmappable-html at /content/0/content/0 line 1 offset 0'], ['

      ab

      \n', 'unmappable-html at /content/0/content/0 line 1 offset 0'], ['

      a

      \n', 'unmappable-html at /content/0/content/0 line 1 offset 0'], ['

      a &bogus; b

      \n', 'unmappable-html at /content/0/content/0 line 1 offset 0'], ['

      a

      \n', 'unmappable-html at /content/0/content/1 line 1 offset 0'], ['

      a', 'unmappable-html at /content/0/content/0 line 1 offset 0'], ['

      a', 'unmappable-html at /content/0 line 1 offset 0'], ], } for (const [group, cases] of Object.entries(refusals)) { for (const [html, expected] of cases) test(`${group}: ${JSON.stringify(html)}`, () => assert.equal(refusal(htmlToAdf(html)), expected)) } const notStrings: Readonly> = { null: null, number: 5, object: {}, undefined: undefined } for (const [kind, value] of Object.entries(notStrings)) { test(`refuses a value that is not a string as not-a-string: ${kind}`, () => { // @ts-expect-error -- a caller without types can pass anything. assert.equal(refusal(htmlToAdf(value)), 'not-a-string at / line 1 offset 0') }) } test('refuses a tag of three million attributes, past what one pattern over them all holds, as a value', () => { assert.equal(refusal(htmlToAdf(`x

      \n`)), 'unmappable-html at /content/0 line 1 offset 0') }) const messages: readonly (readonly [string, string])[] = [ ['

      ab

      \n', ' as written here is no inline element of the dialect: found "b

      \\n"'], ['

      a > b

      \n', 'text escapes &, <, > and " as &, <, > and ", and holds no character HTML cannot hold: found "> b

      \\n"'], ['

      a

      \n', ' holds its attributes as name="value", each value escaped as text is, then >: found "b\\">a

      \\n"'], ['

      a \u00a0

      \n', 'text escapes &, <, > and " as &, <, > and ", and holds no character HTML cannot hold: found " \\u00a0

      \\n"'], ['
      a
      : found "x
      \n', ' as written here is no code element of the dialect, which writes or : found "\n
    1. a\n', 'an item\'s line ends at
    2. , or at a line feed before the item\'s next block, and its text spells < as <: found "\\n"'], ['
        \n
      • ab
      • \n
      \n', ' as written here is no inline element of the dialect: found "b\\n\\n"'], ['
        \n
      • if a < b
      • \n
      \n', 'an item\'s line ends at , or at a line feed before the item\'s next block, and its text spells < as <: found "< b\\n\\n"'], ['
        \n
      • see x
      • \n
      \n', ' holds its attributes as name="value", each value escaped as text is, then >: found "&b=2\\">x\\n\\n"'], ['

      x

      \n', ' as written here is no mark element of the dialect: found "x

      \\n"'], ['

      x

      \n', ' holds its attributes as name="value", each value escaped as text is, then >: found "&b=2\\">x

      "…'], [`

      a

      \n${carry('div', panel, '

      Check the collation.

      ')}\n`, 'a carry element shows its node\'s text as adfToHtml writes it, then ends at ; an edit to it is no edit to the node: found "collation.

      assert.equal(message(htmlToAdf(html)), expected)) test('reads blocks nested 500 levels, and refuses the next level, a list\'s items one level below it', () => { const quotes = (levels: number): string => `${'
      \n'.repeat(levels)}

      x

      ${'\n
      '.repeat(levels)}\n` assert.equal(refusal(htmlToAdf(quotes(500))), 'read') assert.equal(refusal(htmlToAdf(quotes(501))).startsWith(`unsupported-nesting-depth at /${'content/0/'.repeat(501)}content/0 line ${502}`), true) const lists = (levels: number): string => `${'
        \n
      • x\n'.repeat(levels)}
        ${'\n
      • \n
      '.repeat(levels)}\n` assert.equal(refusal(htmlToAdf(lists(500))), 'read') assert.equal(refusal(htmlToAdf(lists(501))).startsWith('unsupported-nesting-depth'), true) assert.equal(refusal(htmlToAdf('
      \n'.repeat(1_000_000))).startsWith('unsupported-nesting-depth'), true) }) test('refuses a carried node whose JSON nests past the levels its depth leaves, and an inline carry past 500', () => { const carried = carry('div', { attrs: { a: nested(1, 498, (inner) => [inner]) }, type: 'unknownBlock' }, '') assert.equal(refusal(htmlToAdf(`${carried}\n`)), 'read') assert.equal(refusal(htmlToAdf(`
      \n${carried}\n
      \n`)), 'unsupported-nesting-depth at /content/0/content/0 line 2 offset 13') const inline = nested({ text: 'x', type: 'text' }, 500, (inner) => ({ content: [inner], type: 'unknownInline' })) assert.equal(refusal(htmlToAdf(`

      ${carry('span', inline, '')}

      \n`)), 'unsupported-nesting-depth at /content/0/content/0 line 1 offset 0') }) // The documents adfToHtml's own tests write, by what each holds. const written: Readonly> = { 'a panel holding a code block': document({ attrs: { panelType: 'info' }, content: [paragraph(text('Run this:')), { attrs: { language: 'python' }, content: [text('if ready:\n deploy()')], type: 'codeBlock' }], type: 'panel' }), 'adjacent unmarked text': document(paragraph(text('a'), text('b'), text('c', { type: 'em' }), text('d'), text('e'))), 'no content key': { type: 'doc', version: 1 }, } for (const [holding, adf] of Object.entries(written)) { test(`reads back what adfToHtml writes for a document holding ${holding}`, () => { const html = adfToHtml(adf) assert.ok(html.ok) assert.deepEqual(read(htmlToAdf(html.value)), adf) }) }