diff --git a/browser-tests/convert-corpus.js b/browser-tests/convert-corpus.js index 84bba51..c9274c2 100644 --- a/browser-tests/convert-corpus.js +++ b/browser-tests/convert-corpus.js @@ -1,18 +1,18 @@ try { - const { adfToMarkdown, isAdfDocument, markdownToAdf } = await import('/dist/index.js') + const { adfToLosslessMarkdown, isAdfDocument, losslessMarkdownToAdf } = await import('/dist/index.js') // WebDriver's JSON reads -0 back as 0, so a document crosses as JSON text with -0 tagged; run.js revives it. const spelled = (result) => (result.ok ? { ok: true, value: JSON.stringify(result.value, (_, value) => (Object.is(value, -0) ? '\u0000-0' : value)) } : result) window.convertCorpus = (corpus) => ({ - errors: corpus.errors.map(({ markdown, name }) => ({ name, parsed: markdownToAdf(markdown) })), - normalization: corpus.normalization.map(({ markdown, name }) => ({ name, parsed: spelled(markdownToAdf(markdown)) })), + errors: corpus.errors.map(({ markdown, name }) => ({ name, parsed: losslessMarkdownToAdf(markdown) })), + normalization: corpus.normalization.map(({ markdown, name }) => ({ name, parsed: spelled(losslessMarkdownToAdf(markdown)) })), realPayloads: corpus.realPayloads.map(({ json, name }) => { const adf = JSON.parse(json) - const emitted = adfToMarkdown(adf) - return { emitted, isDocument: isAdfDocument(adf), name, parsed: emitted.ok ? spelled(markdownToAdf(emitted.value)) : undefined } + const emitted = adfToLosslessMarkdown(adf) + return { emitted, isDocument: isAdfDocument(adf), name, parsed: emitted.ok ? spelled(losslessMarkdownToAdf(emitted.value)) : undefined } }), roundTrip: corpus.roundTrip.map(({ json, markdown, name }) => { const adf = JSON.parse(json) - return { emitted: adfToMarkdown(adf), isDocument: isAdfDocument(adf), name, parsed: spelled(markdownToAdf(markdown)) } + return { emitted: adfToLosslessMarkdown(adf), isDocument: isAdfDocument(adf), name, parsed: spelled(losslessMarkdownToAdf(markdown)) } }), }) } catch (cause) { diff --git a/package-tests/consumer.ts b/package-tests/consumer.ts index d8d06bd..25a6f6e 100644 --- a/package-tests/consumer.ts +++ b/package-tests/consumer.ts @@ -1,13 +1,14 @@ -import { adfToMarkdown, adfToPlainMarkdown, isAdfDocument, markdownToAdf, plainMarkdownToAdf, type AdfDocument, type ConvertErrorCode, type ParseError, type Result } from '@larvit/adf-codec' +import { adfToLosslessMarkdown, adfToPortableMarkdown, commonMarkToAdf, isAdfDocument, losslessMarkdownToAdf, portableMarkdownToAdf, type AdfDocument, type ConvertErrorCode, type ParseError, type Result } from '@larvit/adf-codec' const document: AdfDocument = { content: [{ content: [{ text: 'x', type: 'text' }], type: 'paragraph' }], type: 'doc', version: 1 } -const emitted: Result = adfToMarkdown(document) -const parsed: Result = markdownToAdf('x\n') -const plainEmitted: Result = adfToPlainMarkdown(document) -const plainParsed: Result = plainMarkdownToAdf('x\n') +const emitted: Result = adfToLosslessMarkdown(document) +const parsed: Result = losslessMarkdownToAdf('x\n') +const portableEmitted: Result = adfToPortableMarkdown(document) +const portableParsed: Result = portableMarkdownToAdf('x\n') +const commonMarkParsed: Result = commonMarkToAdf('x\n') const guarded: boolean = isAdfDocument(document) const code: ConvertErrorCode | undefined = emitted.ok ? undefined : emitted.error.code const line: number | undefined = parsed.ok ? undefined : parsed.error.position.line -export const surface = { code, guarded, line, plainEmitted, plainParsed } +export const surface = { code, commonMarkParsed, guarded, line, portableEmitted, portableParsed } diff --git a/package-tests/node-floor.js b/package-tests/node-floor.js index c50919d..470dc53 100644 --- a/package-tests/node-floor.js +++ b/package-tests/node-floor.js @@ -1,9 +1,9 @@ import assert from 'node:assert/strict' -import { adfToMarkdown, markdownToAdf } from '@larvit/adf-codec' +import { adfToLosslessMarkdown, losslessMarkdownToAdf } from '@larvit/adf-codec' const document = { content: [{ content: [{ marks: [{ type: 'em' }], text: 'x', type: 'text' }], type: 'paragraph' }], type: 'doc', version: 1 } -const emitted = adfToMarkdown(document) +const emitted = adfToLosslessMarkdown(document) assert.ok(emitted.ok, emitted.ok ? '' : emitted.error.message) -assert.deepEqual(markdownToAdf(emitted.value), { ok: true, value: document }) +assert.deepEqual(losslessMarkdownToAdf(emitted.value), { ok: true, value: document }) diff --git a/src/conformance/adf-property.test.ts b/src/conformance/adf-property.test.ts index 0af5d37..8fc1786 100644 --- a/src/conformance/adf-property.test.ts +++ b/src/conformance/adf-property.test.ts @@ -4,23 +4,14 @@ import test from 'node:test' import type { AdfNode } from '../adf/document.ts' import { adfDocument, propertyRuns, propertyTimeout } from './property-harness.ts' -import { adfToMarkdown } from '../markdown/emit/adf-to-markdown.ts' -import { adfToPlainMarkdown, reduceToPlain } from '../markdown/plain/adf-to-plain-markdown.ts' -import { directivePrefix } from '../markdown/directive-syntax.ts' -import { markdownToAdf, plainMarkdownToAdf } from '../markdown/parse/markdown-to-adf.ts' -import { toEditorNormal } from '../markdown/plain/editor-normal.ts' +import { adfToLosslessMarkdown } from '../markdown/emit/adf-to-markdown.ts' +import { adfToPortableMarkdown, reduceToPortable } from '../markdown/portable/adf-to-portable-markdown.ts' +import { losslessMarkdownToAdf, portableMarkdownToAdf } from '../markdown/parse/markdown-to-adf.ts' +import { toEditorNormal } from '../markdown/portable/editor-normal.ts' const gateRuns = 1600 -const renamedPrefix = '!adg:' -// Renaming the prefix changes what markdown reads only where a directive was read. -function readsNoDirective(markdown: string): boolean { - const read = markdownToAdf(markdown) - const renamed = markdownToAdf(markdown.replaceAll(directivePrefix, renamedPrefix)) - return read.ok && renamed.ok && JSON.stringify(read.value).replaceAll(directivePrefix, renamedPrefix) === JSON.stringify(renamed.value) -} - -// Each block's text, an expand's title and an image's alt and url, in document order: what the plain pair keeps. +// Each block's text, an expand's title and an image's alt and url, in document order: what the portable pair keeps. function shownText(nodes: readonly AdfNode[]): string[] { const shown: string[] = [] for (const node of nodes) { @@ -37,9 +28,9 @@ function shownText(nodes: readonly AdfNode[]): string[] { test('a generated document emits markdown that reads back to it', { timeout: propertyTimeout }, () => { fc.assert( fc.property(adfDocument, (document) => { - const emitted = adfToMarkdown(document) + const emitted = adfToLosslessMarkdown(document) assert.ok(emitted.ok, emitted.ok ? '' : `${emitted.error.code}: ${emitted.error.message}`) - const read = markdownToAdf(emitted.value) + const read = losslessMarkdownToAdf(emitted.value) assert.ok(read.ok, read.ok ? '' : `${read.error.code}: ${read.error.message} — reading ${JSON.stringify(emitted.value)}`) assert.deepEqual(read.value, document, `reading ${JSON.stringify(emitted.value)}`) }), @@ -47,26 +38,25 @@ test('a generated document emits markdown that reads back to it', { timeout: pro ) }) -test('a generated document writes plain markdown refusing only what the guard refuses, and that markdown reads back to its text and to itself', { timeout: propertyTimeout }, () => { +test('a generated document writes portable markdown refusing only what the guard refuses, and that markdown reads back to its text and to itself', { timeout: propertyTimeout }, () => { fc.assert( fc.property(adfDocument, (document) => { - const written = adfToPlainMarkdown(document) + const written = adfToPortableMarkdown(document) assert.ok(written.ok, written.ok ? '' : `${written.error.code}: ${written.error.message}`) - assert.ok(readsNoDirective(written.value), `a directive in ${JSON.stringify(written.value)}`) - const read = plainMarkdownToAdf(written.value) + const read = portableMarkdownToAdf(written.value) assert.ok(read.ok, read.ok ? '' : `${read.error.code}: ${read.error.message} — reading ${JSON.stringify(written.value)}`) - const reduced = reduceToPlain(document) + const reduced = reduceToPortable(document) assert.deepEqual(shownText(read.value.content ?? []), reduced.ok ? shownText(reduced.value.content ?? []) : reduced, `reading ${JSON.stringify(written.value)}`) - assert.deepEqual(adfToPlainMarkdown(read.value), written, `reading ${JSON.stringify(written.value)}`) + assert.deepEqual(adfToPortableMarkdown(read.value), written, `reading ${JSON.stringify(written.value)}`) }), propertyRuns(gateRuns), ) }) -test('a generated document writes the plain markdown its editor-normal form writes', { timeout: propertyTimeout }, () => { +test('a generated document writes the portable markdown its editor-normal form writes', { timeout: propertyTimeout }, () => { fc.assert( fc.property(adfDocument, (document) => { - assert.deepEqual(adfToPlainMarkdown(document), adfToPlainMarkdown(toEditorNormal(document))) + assert.deepEqual(adfToPortableMarkdown(document), adfToPortableMarkdown(toEditorNormal(document))) }), propertyRuns(gateRuns), ) diff --git a/src/conformance/commonmark-spec.test.ts b/src/conformance/commonmark-spec.test.ts index 1059258..abb7905 100644 --- a/src/conformance/commonmark-spec.test.ts +++ b/src/conformance/commonmark-spec.test.ts @@ -6,8 +6,8 @@ import { readFileSync } from 'node:fs' import test from 'node:test' import type { AdfDocument, AdfNode } from '../adf/document.ts' -import { adfToMarkdown } from '../markdown/emit/adf-to-markdown.ts' -import { markdownToAdf } from '../markdown/parse/markdown-to-adf.ts' +import { adfToLosslessMarkdown } from '../markdown/emit/adf-to-markdown.ts' +import { commonMarkToAdf, losslessMarkdownToAdf } from '../markdown/parse/markdown-to-adf.ts' const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'corpus', 'commonmark-spec') const checks = ['count', 'fixpoint', 'text'] as const @@ -289,9 +289,9 @@ function concatenatedText(document: AdfNode): string { } function fixpointRefused(example: SpecExample, document: AdfDocument): string | undefined { - const emitted = adfToMarkdown(document) + const emitted = adfToLosslessMarkdown(document) if (!emitted.ok) return emitted.error.code - const again = markdownToAdf(emitted.value) + const again = losslessMarkdownToAdf(emitted.value) assert.ok(again.ok, `example ${example.example} emits markdown it cannot read back`) assert.deepEqual(again.value, document, `example ${example.example} does not hold its own round-trip`) return undefined @@ -312,7 +312,7 @@ function countMismatch(example: SpecExample, document: AdfDocument): string | un for (const example of spec) { test(`CommonMark example ${example.example} => ${example.section}`, () => { - const parse = markdownToAdf(example.markdown) + const parse = commonMarkToAdf(example.markdown) const refused = exampleToRefusal.get(example.example) if (refused !== undefined) { assert.ok(!parse.ok, `example ${example.example} was expected to refuse with ${refused} but parsed`) diff --git a/src/conformance/corpus.test.ts b/src/conformance/corpus.test.ts index 86fe0f6..0067e8e 100644 --- a/src/conformance/corpus.test.ts +++ b/src/conformance/corpus.test.ts @@ -4,10 +4,10 @@ import { fileURLToPath } from 'node:url' import { readFileSync, readdirSync } from 'node:fs' import test from 'node:test' -import { adfToMarkdown } from '../markdown/emit/adf-to-markdown.ts' +import { adfToLosslessMarkdown } from '../markdown/emit/adf-to-markdown.ts' import { isAdfDocument } from '../adf/document.ts' import { isJsonValue } from '../json-value.ts' -import { markdownToAdf } from '../markdown/parse/markdown-to-adf.ts' +import { losslessMarkdownToAdf } from '../markdown/parse/markdown-to-adf.ts' import { serializeCanonicalJson } from '../canonical-json.ts' const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'corpus') @@ -79,7 +79,7 @@ for (const directory of roundTripDirectories) { test(`${directory}/${name} emits its markdown byte for byte`, () => { const parsed: unknown = JSON.parse(readFileSync(join(roundTripRoot, directory, `${name}.json`), 'utf8')) assert.ok(isAdfDocument(parsed), `${name}.json is not an ADF document`) - const result = adfToMarkdown(parsed) + const result = adfToLosslessMarkdown(parsed) assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`) const expected = readFileSync(join(roundTripRoot, directory, `${name}.md`)) const emitted = Buffer.from(result.value, 'utf8') @@ -94,7 +94,7 @@ for (const directory of roundTripDirectories) { test(`${directory}/${name} reads its markdown back to the document beside it`, () => { const expected: unknown = JSON.parse(readFileSync(join(roundTripRoot, directory, `${name}.json`), 'utf8')) assert.ok(isAdfDocument(expected), `${name}.json is not an ADF document`) - const result = markdownToAdf(readFileSync(join(roundTripRoot, directory, `${name}.md`), 'utf8')) + const result = losslessMarkdownToAdf(readFileSync(join(roundTripRoot, directory, `${name}.md`), 'utf8')) assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`) assert.deepEqual(result.value, expected) }) @@ -122,12 +122,12 @@ for (const name of pairedNames(normalizationRoot, '.md', '.json')) { test(`normalization/${name} parses to the document beside it, which emits and reads back to itself`, () => { const expected: unknown = JSON.parse(readFileSync(join(normalizationRoot, `${name}.json`), 'utf8')) assert.ok(isAdfDocument(expected), `${name}.json is not an ADF document`) - const result = markdownToAdf(readFileSync(join(normalizationRoot, `${name}.md`), 'utf8')) + const result = losslessMarkdownToAdf(readFileSync(join(normalizationRoot, `${name}.md`), 'utf8')) assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`) assert.deepEqual(result.value, expected) - const emitted = adfToMarkdown(result.value) + const emitted = adfToLosslessMarkdown(result.value) assert.ok(emitted.ok, emitted.ok ? '' : `${emitted.error.code}: ${emitted.error.message}`) - const again = markdownToAdf(emitted.value) + const again = losslessMarkdownToAdf(emitted.value) assert.ok(again.ok, again.ok ? '' : `${again.error.code}: ${again.error.message}`) assert.deepEqual(again.value, expected) }) @@ -141,9 +141,9 @@ for (const name of names(realPayloadsRoot, '.json')) { test(`real-payloads/${name} emits markdown that reads back to it`, () => { const payload: unknown = JSON.parse(readFileSync(join(realPayloadsRoot, `${name}.json`), 'utf8')) assert.ok(isAdfDocument(payload), `${name}.json is not an ADF document`) - const emitted = adfToMarkdown(payload) + const emitted = adfToLosslessMarkdown(payload) assert.ok(emitted.ok, emitted.ok ? '' : `${emitted.error.code}: ${emitted.error.message}`) - const parsed = markdownToAdf(emitted.value) + const parsed = losslessMarkdownToAdf(emitted.value) assert.ok(parsed.ok, parsed.ok ? '' : `${parsed.error.code}: ${parsed.error.message}`) assert.deepEqual(parsed.value, payload) }) @@ -159,7 +159,7 @@ function lineStarting(markdown: string, offset: number): { line: number; offset: for (const name of pairedNames(errorsRoot, '.md', '.error')) { test(`errors/${name} is refused with the error it names, at a line of its own input`, () => { const markdown = readFileSync(join(errorsRoot, `${name}.md`), 'utf8') - const result = markdownToAdf(markdown) + const result = losslessMarkdownToAdf(markdown) assert.ok(!result.ok, result.ok ? `built ${JSON.stringify(result.value)}` : '') assert.equal(result.error.code, readFileSync(join(errorsRoot, `${name}.error`), 'utf8').trimEnd()) const { position } = result.error diff --git a/src/conformance/markdown-property.test.ts b/src/conformance/markdown-property.test.ts index d998377..85c52ad 100644 --- a/src/conformance/markdown-property.test.ts +++ b/src/conformance/markdown-property.test.ts @@ -8,10 +8,11 @@ import type { AttributeVocabulary } from '../adf/attribute-vocabulary.ts' import type { JsonValue } from '../json-value.ts' import type { Result } from '../result.ts' import { adfDocument, attributes, jsonKey, jsonValue, markdownPieces, propertyRuns, propertyTimeout, textOf } from './property-harness.ts' -import { adfToMarkdown } from '../markdown/emit/adf-to-markdown.ts' +import { adfToLosslessMarkdown } from '../markdown/emit/adf-to-markdown.ts' import { blockArgument, listBreakName, marksAttribute } from '../markdown/block-directive.ts' import { blockNodes } from '../adf/block-nodes.ts' import { carryName } from '../markdown/opaque-carry.ts' +import { commonMarkToAdf, losslessMarkdownToAdf } from '../markdown/parse/markdown-to-adf.ts' import { directivePrefix, spellAttributes, @@ -27,7 +28,6 @@ import { fencedCodeBlock } from '../markdown/commonmark/backtick-runs.ts' import { inlineNodes } from '../adf/inline-nodes.ts' import { markAttributes } from '../adf/mark-attributes.ts' import { markSpelling } from '../markdown/mark-spellings.ts' -import { markdownToAdf } from '../markdown/parse/markdown-to-adf.ts' import { nodeContent, nodeMarks } from '../adf/document.ts' import { serializeCanonicalJson } from '../canonical-json.ts' import { textDirectiveName } from '../markdown/text-directive.ts' @@ -382,7 +382,7 @@ const cleanMarkdown = markdownOf(false) const hostileMarkdown = markdownOf(true) const canonical = adfDocument - .map((document) => adfToMarkdown(document)) + .map((document) => adfToLosslessMarkdown(document)) .filter((emitted): emitted is Extract, { ok: true }> => emitted.ok) .map((emitted) => emitted.value) @@ -424,16 +424,16 @@ test('generated markdown refuses, or its spelling reads back and spells itself', let fixpoints = 0 fc.assert( fc.property(document, (input) => { - const parsed = markdownToAdf(input) + const parsed = losslessMarkdownToAdf(input) if (!parsed.ok) return - const emitted = adfToMarkdown(parsed.value) + const emitted = adfToLosslessMarkdown(parsed.value) assert.ok(emitted.ok, emitted.ok ? '' : `${emitted.error.code}: ${emitted.error.message} — spelling ${JSON.stringify(input)}`) fixpoints += 1 if (holdsDirectiveShape(parsed.value)) directiveShaped += 1 - const read = markdownToAdf(emitted.value) + const read = losslessMarkdownToAdf(emitted.value) assert.ok(read.ok, read.ok ? '' : `${read.error.code}: ${read.error.message} — reading ${JSON.stringify(emitted.value)}`) assert.deepEqual(read.value, parsed.value, `reading ${JSON.stringify(emitted.value)}`) - const respelled = adfToMarkdown(read.value) + const respelled = adfToLosslessMarkdown(read.value) assert.ok(respelled.ok, respelled.ok ? '' : `${respelled.error.code}: ${respelled.error.message} — spelling ${JSON.stringify(emitted.value)} again`) assert.equal(respelled.value, emitted.value) }), @@ -444,3 +444,18 @@ test('generated markdown refuses, or its spelling reads back and spells itself', assert.ok(directiveShaped >= directiveShapedFloor, `${directiveShaped} runs reaching the fixpoint held a node or mark outside CommonMark's own types, under the floor of ${directiveShapedFloor}`) }) + +test('generated markdown read as CommonMark refuses, or its document reads back through the lossless pair', { timeout: propertyTimeout }, () => { + fc.assert( + fc.property(document, (input) => { + const parsed = commonMarkToAdf(input) + if (!parsed.ok) return + const emitted = adfToLosslessMarkdown(parsed.value) + assert.ok(emitted.ok, emitted.ok ? '' : `${emitted.error.code}: ${emitted.error.message} — spelling ${JSON.stringify(input)}`) + const read = losslessMarkdownToAdf(emitted.value) + assert.ok(read.ok, read.ok ? '' : `${read.error.code}: ${read.error.message} — reading ${JSON.stringify(emitted.value)}`) + assert.deepEqual(read.value, parsed.value, `reading ${JSON.stringify(emitted.value)}`) + }), + propertyRuns(gateRuns), + ) +}) diff --git a/src/index.ts b/src/index.ts index e4de874..1804603 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ export type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from './adf/document.ts' export type { ConvertError, ConvertErrorCode, ConvertErrorPath, ParseError, Result, SourcePosition } from './result.ts' export type { JsonValue } from './json-value.ts' -export { adfToMarkdown } from './markdown/emit/adf-to-markdown.ts' -export { adfToPlainMarkdown } from './markdown/plain/adf-to-plain-markdown.ts' +export { adfToLosslessMarkdown } from './markdown/emit/adf-to-markdown.ts' +export { adfToPortableMarkdown } from './markdown/portable/adf-to-portable-markdown.ts' +export { commonMarkToAdf, losslessMarkdownToAdf, portableMarkdownToAdf } from './markdown/parse/markdown-to-adf.ts' export { isAdfDocument } from './adf/document.ts' -export { markdownToAdf, plainMarkdownToAdf } from './markdown/parse/markdown-to-adf.ts' diff --git a/src/markdown/code-language.ts b/src/markdown/code-language.ts index 846b64e..1bd48b8 100644 --- a/src/markdown/code-language.ts +++ b/src/markdown/code-language.ts @@ -1,3 +1,4 @@ +import type { Claims } from './portable/conventions.ts' import type { JsonValue } from '../json-value.ts' import { carryFenceType } from './opaque-carry.ts' import { infoStringCarries } from './commonmark/grammar.ts' @@ -5,8 +6,8 @@ import { infoStringCarries } from './commonmark/grammar.ts' export type LanguageSlot = { info: string; kind: 'fence' } | { kind: 'attribute' } | { kind: 'none' } // spec/flavour.md, The CommonMark blocks: the one slot a codeBlock's language rides. -export function languageSlot(language: JsonValue | undefined): LanguageSlot { +export function languageSlot(language: JsonValue | undefined, claims: Claims): LanguageSlot { if (language === undefined) return { kind: 'none' } - if (typeof language !== 'string' || carryFenceType(language) !== undefined || !infoStringCarries(language)) return { kind: 'attribute' } + if (typeof language !== 'string' || (claims.carryFence && carryFenceType(language) !== undefined) || !infoStringCarries(language)) return { kind: 'attribute' } return { info: language, kind: 'fence' } } diff --git a/src/markdown/emit/adf-to-markdown.test.ts b/src/markdown/emit/adf-to-markdown.test.ts index a876b39..984c4c9 100644 --- a/src/markdown/emit/adf-to-markdown.test.ts +++ b/src/markdown/emit/adf-to-markdown.test.ts @@ -4,7 +4,7 @@ import test from 'node:test' import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts' import type { JsonValue } from '../../json-value.ts' import type { Result } from '../../result.ts' -import { adfToMarkdown, markdownToAdf } from '../../index.ts' +import { adfToLosslessMarkdown, losslessMarkdownToAdf } from '../../index.ts' import { largestNesting } from '../../nesting.ts' function document(...content: AdfNode[]): AdfDocument { @@ -34,44 +34,44 @@ function position(result: Result): unknown { test('names the node a refusal came from, and no source the emitter never read', () => { const unspellable: AdfNode = { text: 'x', type: 'paragraph' } const list: AdfNode = { content: [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }, { content: [unspellable], type: 'listItem' }], type: 'bulletList' } - assert.deepEqual(path(adfToMarkdown(document(paragraph({ text: 'x', type: 'text' }), list))), ['content', 1, 'content', 1, 'content', 0]) - assert.deepEqual(path(adfToMarkdown(document(paragraph({ text: 'x', type: 'text' }, { text: 'x', type: 'status' })))), ['content', 0, 'content', 1]) - assert.deepEqual(path(adfToMarkdown({ type: 'doc', version: 2 })), []) - assert.equal(position(adfToMarkdown(document(paragraph({ text: 'x', type: 'text' }), list))), undefined) + assert.deepEqual(path(adfToLosslessMarkdown(document(paragraph({ text: 'x', type: 'text' }), list))), ['content', 1, 'content', 1, 'content', 0]) + assert.deepEqual(path(adfToLosslessMarkdown(document(paragraph({ text: 'x', type: 'text' }, { text: 'x', type: 'status' })))), ['content', 0, 'content', 1]) + assert.deepEqual(path(adfToLosslessMarkdown({ type: 'doc', version: 2 })), []) + assert.equal(position(adfToLosslessMarkdown(document(paragraph({ text: 'x', type: 'text' }), list))), undefined) }) test('refuses a value that is not an ADF document, naming the check it failed', () => { - assert.equal(markdown(adfToMarkdown({ type: 'doc', version: Number.NaN })), 'not-an-adf-document: an ADF document holds a version number: found NaN') + assert.equal(markdown(adfToLosslessMarkdown({ type: 'doc', version: Number.NaN })), 'not-an-adf-document: an ADF document holds a version number: found NaN') }) test('refuses a document version the markdown cannot carry', () => { - assert.equal(code(adfToMarkdown({ type: 'doc', version: 2 })), 'unsupported-document-version') + assert.equal(code(adfToLosslessMarkdown({ type: 'doc', version: 2 })), 'unsupported-document-version') }) test('carries a text node attribute no spelling holds', () => { assert.equal( - markdown(adfToMarkdown(document(paragraph({ attrs: { localId: 'a' }, text: 'x', type: 'text' })))), + markdown(adfToLosslessMarkdown(document(paragraph({ attrs: { localId: 'a' }, text: 'x', type: 'text' })))), '!adf:carry{json="{\\"attrs\\":{\\"localId\\":\\"a\\"},\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n', ) }) test('spells a CommonMark block as a directive where its own spelling holds neither attribute nor mark', () => { - assert.equal(markdown(adfToMarkdown(document({ attrs: { localId: 'a' }, type: 'paragraph' }))), '!adf:paragraph {localId=a}\n!adf:/paragraph\n') - assert.equal(markdown(adfToMarkdown(document({ attrs: { wrap: true }, type: 'codeBlock' }))), '!adf:codeBlock {wrap=true}\n```\n```\n!adf:/codeBlock\n') - assert.equal(markdown(adfToMarkdown(document(paragraph({ attrs: { localId: 'a' }, type: 'hardBreak' }, { text: 'x', type: 'text' })))), '!adf:hardBreak{localId=a}x\n') - assert.equal(markdown(adfToMarkdown(document({ marks: [{ type: 'border' }], type: 'blockquote' }))), '!adf:blockquote {marks="[{\\"type\\":\\"border\\"}]"}\n!adf:/blockquote\n') - assert.equal(markdown(adfToMarkdown(document({ type: 'listItem' }))), '!adf:listItem\n!adf:/listItem\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { localId: 'a' }, type: 'paragraph' }))), '!adf:paragraph {localId=a}\n!adf:/paragraph\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { wrap: true }, type: 'codeBlock' }))), '!adf:codeBlock {wrap=true}\n```\n```\n!adf:/codeBlock\n') + assert.equal(markdown(adfToLosslessMarkdown(document(paragraph({ attrs: { localId: 'a' }, type: 'hardBreak' }, { text: 'x', type: 'text' })))), '!adf:hardBreak{localId=a}x\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ marks: [{ type: 'border' }], type: 'blockquote' }))), '!adf:blockquote {marks="[{\\"type\\":\\"border\\"}]"}\n!adf:/blockquote\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ type: 'listItem' }))), '!adf:listItem\n!adf:/listItem\n') }) test('spells an ordered list from the order attribute its first marker is', () => { const items: AdfNode[] = [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }] - assert.equal(markdown(adfToMarkdown(document({ content: items, type: 'orderedList' }))), '!adf:orderedList\n!adf:listItem\nx\n!adf:/listItem\n!adf:/orderedList\n') - assert.equal(markdown(adfToMarkdown(document({ attrs: { order: 1 }, content: items, type: 'orderedList' }))), '1. x\n') - assert.equal(markdown(adfToMarkdown(document({ attrs: { order: 2 }, content: items, type: 'orderedList' }))), '2. x\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ content: items, type: 'orderedList' }))), '!adf:orderedList\n!adf:listItem\nx\n!adf:/listItem\n!adf:/orderedList\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { order: 1 }, content: items, type: 'orderedList' }))), '1. x\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { order: 2 }, content: items, type: 'orderedList' }))), '2. x\n') }) test('spells a code block language no info string holds as an attribute', () => { - const language = (value: string): string => markdown(adfToMarkdown(document({ attrs: { language: value }, type: 'codeBlock' }))) + const language = (value: string): string => markdown(adfToLosslessMarkdown(document({ attrs: { language: value }, type: 'codeBlock' }))) assert.equal(language(''), '!adf:codeBlock {language=""}\n```\n```\n!adf:/codeBlock\n') assert.equal(language('a`b'), '!adf:codeBlock {language="a\\u0060b"}\n```\n```\n!adf:/codeBlock\n') assert.equal(language(' sql'), '!adf:codeBlock {language=" sql"}\n```\n```\n!adf:/codeBlock\n') @@ -82,7 +82,7 @@ test('spells a code block language no info string holds as an attribute', () => }) test('spells a link destination CommonMark cannot as the directive link', () => { - const link = (href: string): string => markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs: { href }, type: 'link' }], text: 't', type: 'text' })))) + const link = (href: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs: { href }, type: 'link' }], text: 't', type: 'text' })))) assert.equal(link('https://example.com/a b>c'), '!adf:link[t]{href="https://example.com/a b>c"}\n') assert.equal(link(' assert.equal(link('https://example.com/a\nb'), '!adf:link[t]{href="https://example.com/a\\nb"}\n') const entity = 'https://example.com/?a=1&b=2' const autolinkShaped = document(paragraph({ marks: [{ attrs: { href: entity }, type: 'link' }], text: entity, type: 'text' })) - assert.equal(markdown(adfToMarkdown(autolinkShaped)), '!adf:link[https://example.com/?a=1\\&b=2]{href="https://example.com/?a=1\\u0026amp;b=2"}\n') + assert.equal(markdown(adfToLosslessMarkdown(autolinkShaped)), '!adf:link[https://example.com/?a=1\\&b=2]{href="https://example.com/?a=1\\u0026amp;b=2"}\n') }) test('spells a link carrying an attribute CommonMark cannot as the directive link, and refuses none', () => { const link = (attrs: Record): string => - markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'link' }], text: 't', type: 'text' })))) + markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs, type: 'link' }], text: 't', type: 'text' })))) assert.equal(link({ collection: 'c', href: 'https://example.com/' }), '!adf:link[t]{collection=c href="https://example.com/"}\n') assert.equal(link({ href: 'https://example.com/', id: 'i' }), '!adf:link[t]{href="https://example.com/" id=i}\n') assert.equal(link({ href: 'https://example.com/', occurrenceKey: 'k' }), '!adf:link[t]{href="https://example.com/" occurrenceKey=k}\n') @@ -103,7 +103,7 @@ test('spells a link carrying an attribute CommonMark cannot as the directive lin }) test('escapes the parenthesis a link destination leaves unbalanced, and no other', () => { - const link = (href: string): string => markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs: { href }, type: 'link' }], text: 't', type: 'text' })))) + const link = (href: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs: { href }, type: 'link' }], text: 't', type: 'text' })))) assert.equal(link('https://en.example.com/a_(b)'), '[t](https://en.example.com/a_(b))\n') assert.equal(link('https://example.com/a)b'), '[t](https://example.com/a\\)b)\n') assert.equal(link('https://example.com/a(b'), '[t](https://example.com/a\\(b)\n') @@ -113,7 +113,7 @@ test('escapes the parenthesis a link destination leaves unbalanced, and no other test('escapes the quote a link title holds, and spells the rest as the directive link', () => { const titled = (title: string): string => - markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs: { href: 'https://example.com/', title }, type: 'link' }], text: 't', type: 'text' })))) + markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ attrs: { href: 'https://example.com/', title }, type: 'link' }], text: 't', type: 'text' })))) assert.equal(titled('He said "hi"'), '[t](https://example.com/ "He said \\"hi\\"")\n') assert.equal(titled('a\nb'), '!adf:link[t]{href="https://example.com/" title="a\\nb"}\n') assert.equal(titled('a\\b'), '!adf:link[t]{href="https://example.com/" title="a\\\\b"}\n') @@ -121,7 +121,7 @@ test('escapes the quote a link title holds, and spells the rest as the directive }) test('carries a link mark the link spelling cannot write', () => { - const carried = (mark: AdfMark): string => markdown(adfToMarkdown(document(paragraph({ marks: [mark], text: 't', type: 'text' })))) + const carried = (mark: AdfMark): string => markdown(adfToLosslessMarkdown(document(paragraph({ marks: [mark], text: 't', type: 'text' })))) assert.equal( carried({ attrs: { href: 'x', rel: 'y' }, type: 'link' }), '!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":\\"x\\",\\"rel\\":\\"y\\"},\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n', @@ -131,7 +131,7 @@ test('carries a link mark the link spelling cannot write', () => { }) test('carries a mark the canonical spellings cannot nest', () => { - const carried = (...marks: AdfMark[]): string => markdown(adfToMarkdown(document(paragraph({ marks, text: 'x', type: 'text' })))) + const carried = (...marks: AdfMark[]): string => markdown(adfToLosslessMarkdown(document(paragraph({ marks, text: 'x', type: 'text' })))) assert.equal(carried({ type: 'annotation' }), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"annotation\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n') assert.equal( carried({ type: 'code' }, { type: 'strong' }), @@ -148,8 +148,8 @@ test('carries a mark the canonical spellings cannot nest', () => { }) function readsBack(doc: AdfDocument): string { - const spelled = markdown(adfToMarkdown(doc)) - assert.deepEqual(markdownToAdf(spelled), { ok: true, value: doc }, `reading ${JSON.stringify(spelled)}`) + const spelled = markdown(adfToLosslessMarkdown(doc)) + assert.deepEqual(losslessMarkdownToAdf(spelled), { ok: true, value: doc }, `reading ${JSON.stringify(spelled)}`) return spelled } @@ -160,7 +160,7 @@ test('spells a code span opening a line, whose backticks open no fence', () => { }) test('escapes the delimiter row a hard break leaves opening a pipe table with no leading pipe', () => { - const broken = (second: string): string => markdown(adfToMarkdown(document(paragraph({ text: 'a | b', type: 'text' }, { type: 'hardBreak' }, { text: second, type: 'text' })))) + const broken = (second: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ text: 'a | b', type: 'text' }, { type: 'hardBreak' }, { text: second, type: 'text' })))) assert.equal(broken('--- | ---'), 'a | b\\\n\\--- | ---\n') assert.equal(broken(':--- | ---:'), 'a | b\\\n\\:--- | ---:\n') assert.equal(broken('c | d'), 'a | b\\\nc | d\n') @@ -168,39 +168,39 @@ test('escapes the delimiter row a hard break leaves opening a pipe table with no test('parts two adjacent lists of the same kind, the marker spelling being what merges', () => { const list: AdfNode = { content: [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }], type: 'bulletList' } - assert.equal(markdown(adfToMarkdown(document(list, list))), '- x\n\n!adf:listBreak\n\n- x\n') + assert.equal(markdown(adfToLosslessMarkdown(document(list, list))), '- x\n\n!adf:listBreak\n\n- x\n') const ordered: AdfNode = { attrs: { order: 1 }, content: [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }], type: 'orderedList' } - assert.equal(markdown(adfToMarkdown(document(ordered, ordered))), '1. x\n\n!adf:listBreak\n\n1. x\n') - assert.equal(markdown(adfToMarkdown(document({ attrs: { panelType: 'info' }, content: [list, list], type: 'panel' }))), '!adf:panel info\n- x\n!adf:listBreak\n- x\n!adf:/panel\n') + assert.equal(markdown(adfToLosslessMarkdown(document(ordered, ordered))), '1. x\n\n!adf:listBreak\n\n1. x\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { panelType: 'info' }, content: [list, list], type: 'panel' }))), '!adf:panel info\n- x\n!adf:listBreak\n- x\n!adf:/panel\n') const nested: AdfNode = { content: [{ content: [list, list], type: 'listItem' }], type: 'bulletList' } - assert.equal(markdown(adfToMarkdown(document(nested))), '- - x\n\n !adf:listBreak\n\n - x\n') + assert.equal(markdown(adfToLosslessMarkdown(document(nested))), '- - x\n\n !adf:listBreak\n\n - x\n') const carried: AdfNode = { ...list, attrs: { unknown: 'x' } } - assert.ok(markdown(adfToMarkdown(document(carried, carried))).includes('```\n\n```adf:bulletList\n')) - assert.ok(markdown(adfToMarkdown(document(carried, list))).endsWith('```\n\n- x\n')) - assert.ok(markdown(adfToMarkdown(document(list, carried))).startsWith('- x\n\n```adf:bulletList\n')) + assert.ok(markdown(adfToLosslessMarkdown(document(carried, carried))).includes('```\n\n```adf:bulletList\n')) + assert.ok(markdown(adfToLosslessMarkdown(document(carried, list))).endsWith('```\n\n- x\n')) + assert.ok(markdown(adfToLosslessMarkdown(document(list, carried))).startsWith('- x\n\n```adf:bulletList\n')) }) test('carries a node type no section spells, the fence naming the type wherever an info string carries it', () => { - assert.equal(markdown(adfToMarkdown(document({ type: 'blockCard' }))), '```adf:blockCard\n{}\n```\n') - assert.equal(markdown(adfToMarkdown(document({ type: 'toString' }))), '```adf:toString\n{}\n```\n') - assert.equal(markdown(adfToMarkdown(document(paragraph({ type: 'blockCard' })))), '!adf:carry{json="{\\"type\\":\\"blockCard\\"}"}\n') - assert.equal(markdown(adfToMarkdown(document({ text: 'x', type: 'text' }))), '```adf:text\n{\n "text": "x"\n}\n```\n') - assert.equal(markdown(adfToMarkdown(document({ type: 'hardBreak' }))), '```adf:hardBreak\n{}\n```\n') - assert.equal(markdown(adfToMarkdown(document({ type: 'a&b' }))), '```adf:\n{\n "type": "a&b"\n}\n```\n') - assert.equal(markdown(adfToMarkdown(document({ type: 'a\\b' }))), '```adf:\n{\n "type": "a\\\\b"\n}\n```\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ type: 'blockCard' }))), '```adf:blockCard\n{}\n```\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ type: 'toString' }))), '```adf:toString\n{}\n```\n') + assert.equal(markdown(adfToLosslessMarkdown(document(paragraph({ type: 'blockCard' })))), '!adf:carry{json="{\\"type\\":\\"blockCard\\"}"}\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ text: 'x', type: 'text' }))), '```adf:text\n{\n "text": "x"\n}\n```\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ type: 'hardBreak' }))), '```adf:hardBreak\n{}\n```\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ type: 'a&b' }))), '```adf:\n{\n "type": "a&b"\n}\n```\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ type: 'a\\b' }))), '```adf:\n{\n "type": "a\\\\b"\n}\n```\n') }) test('spells the code block whose language opens with the reserved info string prefix', () => { - assert.equal(markdown(adfToMarkdown(document({ attrs: { language: 'adf:x' }, type: 'codeBlock' }))), '!adf:codeBlock {language="adf:x"}\n```\n```\n!adf:/codeBlock\n') - assert.equal(markdown(adfToMarkdown(document({ attrs: { language: 'carry' }, type: 'codeBlock' }))), '```carry\n```\n') - assert.equal(markdown(adfToMarkdown(document({ attrs: { language: 'adf' }, type: 'codeBlock' }))), '```adf\n```\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { language: 'adf:x' }, type: 'codeBlock' }))), '!adf:codeBlock {language="adf:x"}\n```\n```\n!adf:/codeBlock\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { language: 'carry' }, type: 'codeBlock' }))), '```carry\n```\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { language: 'adf' }, type: 'codeBlock' }))), '```adf\n```\n') }) test('breaks a mark run at the node it carries', () => { const strong: AdfMark = { type: 'strong' } const carried: AdfNode = { marks: [strong], type: 'placeholder' } assert.equal( - markdown(adfToMarkdown(document(paragraph({ marks: [strong], text: 'a', type: 'text' }, carried, { marks: [strong], text: 'b', type: 'text' })))), + markdown(adfToLosslessMarkdown(document(paragraph({ marks: [strong], text: 'a', type: 'text' }, carried, { marks: [strong], text: 'b', type: 'text' })))), '**a**!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"type\\":\\"placeholder\\"}"}**b**\n', ) }) @@ -208,52 +208,52 @@ test('breaks a mark run at the node it carries', () => { test('refuses a carried node nested deeper than the levels its position leaves', () => { let node: AdfNode = { type: 'blockCard' } for (let depth = 0; depth < 600; depth += 1) node = { content: [node], type: 'blockCard' } - assert.equal(code(adfToMarkdown(document(node))), 'unsupported-nesting-depth') - assert.equal(code(adfToMarkdown(document(paragraph(node)))), 'unsupported-nesting-depth') + assert.equal(code(adfToLosslessMarkdown(document(node))), 'unsupported-nesting-depth') + assert.equal(code(adfToLosslessMarkdown(document(paragraph(node)))), 'unsupported-nesting-depth') let shallow: AdfNode = { type: 'blockCard' } for (let depth = 0; depth < 200; depth += 1) shallow = { content: [shallow], type: 'blockCard' } - assert.ok(adfToMarkdown(document(shallow)).ok) + assert.ok(adfToLosslessMarkdown(document(shallow)).ok) let quoted: AdfNode = shallow for (let depth = 0; depth < 150; depth += 1) quoted = { content: [quoted], type: 'blockquote' } - assert.equal(code(adfToMarkdown(document(quoted))), 'unsupported-nesting-depth') + assert.equal(code(adfToLosslessMarkdown(document(quoted))), 'unsupported-nesting-depth') }) test('carries a code block holding a node no fence holds', () => { - assert.equal(markdown(adfToMarkdown(document({ content: [paragraph()], type: 'codeBlock' }))), '```adf:codeBlock\n{\n "content": [\n {\n "type": "paragraph"\n }\n ]\n}\n```\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ content: [paragraph()], type: 'codeBlock' }))), '```adf:codeBlock\n{\n "content": [\n {\n "type": "paragraph"\n }\n ]\n}\n```\n') for (const child of [{ attrs: {}, text: 'x', type: 'text' }, { content: [], text: 'x', type: 'text' }, { marks: [], text: 'x', type: 'text' }, { content: [{ text: 'lost', type: 'text' }], text: 'x', type: 'text' }]) { - assert.ok(markdown(adfToMarkdown(document({ content: [child], type: 'codeBlock' }))).startsWith('```adf:codeBlock\n'), JSON.stringify(child)) + assert.ok(markdown(adfToLosslessMarkdown(document({ content: [child], type: 'codeBlock' }))).startsWith('```adf:codeBlock\n'), JSON.stringify(child)) } }) test('spells a list its own content shape cannot hold as a directive', () => { - assert.equal(markdown(adfToMarkdown(document({ content: [paragraph()], type: 'bulletList' }))), '!adf:bulletList\n!adf:paragraph\n!adf:/paragraph\n!adf:/bulletList\n') - assert.equal(markdown(adfToMarkdown(document({ type: 'bulletList' }))), '!adf:bulletList\n!adf:/bulletList\n') - assert.equal(markdown(adfToMarkdown(document({ attrs: { order: 2 }, content: [], type: 'orderedList' }))), '!adf:orderedList {content=empty order=2}\n!adf:/orderedList\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ content: [paragraph()], type: 'bulletList' }))), '!adf:bulletList\n!adf:paragraph\n!adf:/paragraph\n!adf:/bulletList\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ type: 'bulletList' }))), '!adf:bulletList\n!adf:/bulletList\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { order: 2 }, content: [], type: 'orderedList' }))), '!adf:orderedList {content=empty order=2}\n!adf:/orderedList\n') }) test('spells an ordered list no marker fits as a directive', () => { const item: AdfNode = { content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' } const list = (order: number, items: number): AdfDocument => document({ attrs: { order }, content: Array.from({ length: items }, () => item), type: 'orderedList' }) - assert.equal(markdown(adfToMarkdown(list(1.5, 1))), '!adf:orderedList {order="1.5"}\n!adf:listItem\nx\n!adf:/listItem\n!adf:/orderedList\n') - assert.equal(markdown(adfToMarkdown(list(999999999, 1))), '999999999. x\n') - assert.equal(markdown(adfToMarkdown(list(999999999, 2))), '!adf:orderedList {order=999999999}\n!adf:listItem\nx\n!adf:/listItem\n!adf:listItem\nx\n!adf:/listItem\n!adf:/orderedList\n') + assert.equal(markdown(adfToLosslessMarkdown(list(1.5, 1))), '!adf:orderedList {order="1.5"}\n!adf:listItem\nx\n!adf:/listItem\n!adf:/orderedList\n') + assert.equal(markdown(adfToLosslessMarkdown(list(999999999, 1))), '999999999. x\n') + assert.equal(markdown(adfToLosslessMarkdown(list(999999999, 2))), '!adf:orderedList {order=999999999}\n!adf:listItem\nx\n!adf:/listItem\n!adf:listItem\nx\n!adf:/listItem\n!adf:/orderedList\n') }) test('carries a code mark over anything but text', () => { assert.equal( - markdown(adfToMarkdown(document(paragraph({ marks: [{ type: 'code' }], type: 'hardBreak' })))), + markdown(adfToLosslessMarkdown(document(paragraph({ marks: [{ type: 'code' }], type: 'hardBreak' })))), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"code\\"}],\\"type\\":\\"hardBreak\\"}"}\n', ) }) test('spells a heading level no ATX heading fits as a directive', () => { - assert.equal(markdown(adfToMarkdown(document({ attrs: { level: 7 }, content: [{ text: 'x', type: 'text' }], type: 'heading' }))), '!adf:heading {level=7}\nx\n!adf:/heading\n') - assert.equal(markdown(adfToMarkdown(document({ content: [{ text: 'x', type: 'text' }], type: 'heading' }))), '!adf:heading\nx\n!adf:/heading\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ attrs: { level: 7 }, content: [{ text: 'x', type: 'text' }], type: 'heading' }))), '!adf:heading {level=7}\nx\n!adf:/heading\n') + assert.equal(markdown(adfToLosslessMarkdown(document({ content: [{ text: 'x', type: 'text' }], type: 'heading' }))), '!adf:heading\nx\n!adf:/heading\n') }) test('escapes only text that would otherwise open a construct', () => { - const emitted = (text: string): string => markdown(adfToMarkdown(document(paragraph({ text, type: 'text' })))) + const emitted = (text: string): string => markdown(adfToLosslessMarkdown(document(paragraph({ text, type: 'text' })))) assert.equal(emitted('
'), '\\
\n') assert.equal(emitted(' { assert.equal(emitted('a c'), 'a \\ c\n') const later = paragraph({ text: 'a', type: 'text' }, { type: 'hardBreak' }, { text: ' \\\n')), 'unmappable-html') - assert.equal(code(markdownToAdf('
\nx\n
\n')), 'unmappable-html') - assert.equal(code(markdownToAdf('\n')), 'unmappable-html') - assert.equal(code(markdownToAdf('\n')), 'unmappable-html') - assert.equal(code(markdownToAdf('\n')), 'unmappable-html') - assert.equal(code(markdownToAdf('
\nx\n
\n')), 'unmappable-html') - assert.equal(code(markdownToAdf('\n')), 'unmappable-html') - assert.equal(code(markdownToAdf('\n')), ['content', 1]) - assert.equal(code(markdownToAdf('
\nx\n\n!adf:/panel\n')), 'unmappable-html') - assert.equal(code(markdownToAdf('
\n- x\n
\n')), 'unmappable-html') + assert.equal(code(losslessMarkdownToAdf('\n')), 'unmappable-html') + assert.equal(code(losslessMarkdownToAdf('
\nx\n
\n')), 'unmappable-html') + assert.equal(code(losslessMarkdownToAdf('\n')), 'unmappable-html') + assert.equal(code(losslessMarkdownToAdf('\n')), 'unmappable-html') + assert.equal(code(losslessMarkdownToAdf('\n')), 'unmappable-html') + assert.equal(code(losslessMarkdownToAdf('
\nx\n
\n')), 'unmappable-html') + assert.equal(code(losslessMarkdownToAdf('\n')), 'unmappable-html') + assert.equal(code(losslessMarkdownToAdf('\n')), ['content', 1]) + assert.equal(code(losslessMarkdownToAdf('
\nx\n\n!adf:/panel\n')), 'unmappable-html') + assert.equal(code(losslessMarkdownToAdf('
\n- x\n
\n')), 'unmappable-html') }) test('swallows an HTML block ahead of the claim a line inside it would make', () => { - assert.equal(code(markdownToAdf('\n')), 'unmappable-html') - assert.equal(code(markdownToAdf('
\n| x |\n
\n')), 'unmappable-html') + assert.equal(code(losslessMarkdownToAdf('\n')), 'unmappable-html') + assert.equal(code(losslessMarkdownToAdf('
\n| x |\n
\n')), 'unmappable-html') }) test('names the line and the offset in the input a refusal sits at, the innermost block winning', () => { - assert.deepEqual(position(markdownToAdf('
\n')), { line: 1, offset: 0 }) - assert.deepEqual(position(markdownToAdf('Part.\n\n
\n')), { line: 3, offset: 7 }) - assert.deepEqual(position(markdownToAdf('> Part.\n>\n> a b\n')), { line: 3, offset: 10 }) - assert.deepEqual(position(markdownToAdf('- Part.\n- a b\n')), { line: 2, offset: 8 }) - assert.deepEqual(position(markdownToAdf('Part.\n\n!adf:panel info\nMore.\n')), { line: 3, offset: 7 }) - assert.deepEqual(position(markdownToAdf('x\n\na b\n===\n')), { line: 3, offset: 3 }) - assert.deepEqual(position(markdownToAdf('x\n\n```adf:x\n{\n```\n')), { line: 3, offset: 3 }) - assert.deepEqual(position(markdownToAdf('x\n\n| a |\n')), { line: 3, offset: 3 }) - assert.deepEqual(position(markdownToAdf('a\nb c\n')), { line: 1, offset: 0 }) - assert.deepEqual(position(markdownToAdf('Part.\r\n\r\n
\r\n')), { line: 3, offset: 9 }) - assert.deepEqual(position(markdownToAdf('a\u0000b\n\n
\n')), { line: 3, offset: 5 }) - assert.deepEqual(position(markdownToAdf('!adf:caption\na b\n!adf:/caption\n')), { line: 2, offset: 13 }) - assert.deepEqual(position(markdownToAdf('x\n\n!adf:caption\n- a\n!adf:/caption\n')), { line: 3, offset: 3 }) + assert.deepEqual(position(losslessMarkdownToAdf('
\n')), { line: 1, offset: 0 }) + assert.deepEqual(position(losslessMarkdownToAdf('Part.\n\n
\n')), { line: 3, offset: 7 }) + assert.deepEqual(position(losslessMarkdownToAdf('> Part.\n>\n> a b\n')), { line: 3, offset: 10 }) + assert.deepEqual(position(losslessMarkdownToAdf('- Part.\n- a b\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 b\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 c\n')), { line: 1, offset: 0 }) + assert.deepEqual(position(losslessMarkdownToAdf('Part.\r\n\r\n
\r\n')), { line: 3, offset: 9 }) + assert.deepEqual(position(losslessMarkdownToAdf('a\u0000b\n\n
\n')), { line: 3, offset: 5 }) + assert.deepEqual(position(losslessMarkdownToAdf('!adf:caption\na b\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(markdownToAdf('[a]: /url\nb\n')), { line: 2, offset: 10 }) - assert.deepEqual(position(markdownToAdf('[a]: /a\n[b]: /b\n[c]: /c\n[d]: /d\nx\n')), { line: 5, offset: 32 }) - assert.deepEqual(position(markdownToAdf('[a]:\n\n"Title"\nb\n')), { line: 4, offset: 23 }) - assert.deepEqual(position(markdownToAdf('> [a]: /url\n> b\n')), { line: 2, offset: 12 }) - assert.deepEqual(position(markdownToAdf('!adf:caption\n[a]: /url\nb\n!adf:/caption\n')), { line: 3, offset: 23 }) + assert.deepEqual(position(losslessMarkdownToAdf('[a]: /url\nb\n')), { line: 2, offset: 10 }) + assert.deepEqual(position(losslessMarkdownToAdf('[a]: /a\n[b]: /b\n[c]: /c\n[d]: /d\nx\n')), { line: 5, offset: 32 }) + assert.deepEqual(position(losslessMarkdownToAdf('[a]:\n\n"Title"\nb\n')), { line: 4, offset: 23 }) + assert.deepEqual(position(losslessMarkdownToAdf('> [a]: /url\n> b\n')), { line: 2, offset: 12 }) + assert.deepEqual(position(losslessMarkdownToAdf('!adf:caption\n[a]: /url\nb\n!adf:/caption\n')), { line: 3, offset: 23 }) }) test('gives up the link reference definitions a paragraph opens with', () => { - assert.deepEqual(content(markdownToAdf('[a]: /url\n')), []) - assert.deepEqual(content(markdownToAdf('[a]: /url\n[b]: /other\nPart.\n')), [paragraph('Part.')]) - assert.deepEqual(content(markdownToAdf('[a]: /url\n"Title"\n\nPart.\n')), [paragraph('Part.')]) - assert.deepEqual(content(markdownToAdf('[a]: /url and more\n')), [paragraph('[a]: /url and more')]) - assert.deepEqual(content(markdownToAdf('Part.\n[a]: /url\n')), [paragraph('Part. [a]: /url')]) - assert.deepEqual(content(markdownToAdf('[a]: /url\n===\n')), [paragraph('===')]) + 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(markdownToAdf('\u00a0Part.\u00a0\n')), [paragraph('\u00a0Part.\u00a0')]) - assert.deepEqual(content(markdownToAdf(' \u3000Part.\t\n')), [paragraph('\u3000Part.')]) + 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(markdownToAdf('One\r\ntwo.\r\n')), [paragraph('One two.')]) - assert.deepEqual(content(markdownToAdf('One\rtwo.\r')), [paragraph('One two.')]) - assert.deepEqual(content(markdownToAdf('```\r\nx\r\n```\r\n')), [{ content: [text('x')], type: 'codeBlock' }]) - assert.deepEqual(content(markdownToAdf('a\u0000b\n')), [paragraph('a\ufffdb')]) + 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(markdownToAdf('> Ship it.\n>\n> Then tell them.\n')), [quote(paragraph('Ship it.'), paragraph('Then tell them.'))]) - assert.deepEqual(content(markdownToAdf('>Ship it.\n')), [quote(paragraph('Ship it.'))]) - assert.deepEqual(content(markdownToAdf(' > > Deep.\n')), [quote(quote(paragraph('Deep.')))]) - assert.deepEqual(content(markdownToAdf('>\n')), [quote()]) - assert.deepEqual(content(markdownToAdf('> One.\n\n> Two.\n')), [quote(paragraph('One.')), quote(paragraph('Two.'))]) - assert.deepEqual(content(markdownToAdf('Part.\n> Ship it.\n')), [paragraph('Part.'), quote(paragraph('Ship it.'))]) - assert.deepEqual(content(markdownToAdf(' > Code.\n')), [{ content: [text('> Code.')], type: 'codeBlock' }]) + 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(markdownToAdf('- Bolt M8\n- Nut M8\n')), [bulletList(item(paragraph('Bolt M8')), item(paragraph('Nut M8')))]) - assert.deepEqual(content(markdownToAdf('- Washer M8\n - Fibre\n')), [bulletList(item(paragraph('Washer M8'), bulletList(item(paragraph('Fibre')))))]) - assert.deepEqual(content(markdownToAdf('-\n')), [bulletList(item())]) - assert.deepEqual(content(markdownToAdf('- One\n\n Two.\n')), [bulletList(item(paragraph('One'), paragraph('Two.')))]) - assert.deepEqual(content(markdownToAdf('- Code.\n')), [bulletList(item({ content: [text('Code.')], type: 'codeBlock' }))]) - assert.deepEqual(content(markdownToAdf('- a\n* b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))]) - assert.deepEqual(content(markdownToAdf('- a\n\n+ b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))]) - assert.deepEqual(content(markdownToAdf('- a\n-\n\n- c\n')), [bulletList(item(paragraph('a')), item(), item(paragraph('c')))]) - assert.deepEqual(content(markdownToAdf('- a\n1. b\n')), [bulletList(item(paragraph('a'))), orderedList(1, item(paragraph('b')))]) - assert.deepEqual(content(markdownToAdf('- a\n\n[r]: /u\n\n- b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))]) - assert.deepEqual(content(markdownToAdf('-\n\n Part.\n')), [bulletList(item()), paragraph('Part.')]) + 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* b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))]) + assert.deepEqual(content(losslessMarkdownToAdf('- a\n\n+ b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))]) + 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('- a\n\n[r]: /u\n\n- b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))]) + assert.deepEqual(content(losslessMarkdownToAdf('-\n\n Part.\n')), [bulletList(item()), paragraph('Part.')]) }) test('reads an ordered list, its first marker the order attribute', () => { - assert.deepEqual(content(markdownToAdf('9. Bolt M8\n10. Nut M8\n')), [orderedList(9, item(paragraph('Bolt M8')), item(paragraph('Nut M8')))]) - assert.deepEqual(content(markdownToAdf('1) Loosen the clamp\n')), [orderedList(1, item(paragraph('Loosen the clamp')))]) - assert.deepEqual(content(markdownToAdf('1. a\n1) b\n')), [orderedList(1, item(paragraph('a')), item(paragraph('b')))]) - assert.deepEqual(content(markdownToAdf('0. Zero\n')), [orderedList(0, item(paragraph('Zero')))]) + 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('1. a\n1) b\n')), [orderedList(1, item(paragraph('a')), item(paragraph('b')))]) + 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(markdownToAdf('>\t\tfoo\n')), [quote({ content: [text(' foo')], type: 'codeBlock' })]) - assert.deepEqual(content(markdownToAdf('-\t\tfoo\n')), [bulletList(item({ content: [text(' foo')], type: 'codeBlock' }))]) - assert.deepEqual(content(markdownToAdf('- foo\n\n\t\tbar\n')), [bulletList(item(paragraph('foo'), { content: [text(' bar')], type: 'codeBlock' }))]) - assert.deepEqual(content(markdownToAdf('-\t foo\n')), [bulletList(item(paragraph('foo')))]) - assert.deepEqual(content(markdownToAdf(' - foo\n - bar\n\t - baz\n')), [ + 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(markdownToAdf('- a\n\n- b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))]) - assert.deepEqual(content(markdownToAdf('- a\n\n 2. b\n')), [bulletList(item(paragraph('a'), orderedList(2, item(paragraph('b')))))]) - assert.deepEqual(content(markdownToAdf('- a\n\n -\n')), [bulletList(item(paragraph('a'), bulletList(item())))]) + 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(markdownToAdf('Part.\n- a\n')), [paragraph('Part.'), bulletList(item(paragraph('a')))]) - assert.deepEqual(content(markdownToAdf('Part.\n1. a\n')), [paragraph('Part.'), orderedList(1, item(paragraph('a')))]) - assert.deepEqual(content(markdownToAdf('Part.\n2. a\n')), [paragraph('Part. 2. a')]) - assert.deepEqual(content(markdownToAdf('Part.\n*\n')), [paragraph('Part. *')]) - assert.deepEqual(content(markdownToAdf('Part.\n- - -\n')), [paragraph('Part.'), { type: 'rule' }]) - assert.deepEqual(content(markdownToAdf('Part.\n-\n')), [{ attrs: { level: 2 }, content: [text('Part.')], type: 'heading' }]) - assert.deepEqual(content(markdownToAdf('- a\n 2. b\n')), [bulletList(item(paragraph('a 2. b')))]) - assert.deepEqual(content(markdownToAdf('- a\n 1. b\n')), [bulletList(item(paragraph('a'), orderedList(1, item(paragraph('b')))))]) + 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(markdownToAdf('> One\ntwo.\n')), [quote(paragraph('One two.'))]) - assert.deepEqual(content(markdownToAdf('- One\ntwo.\n')), [bulletList(item(paragraph('One two.')))]) - assert.deepEqual(content(markdownToAdf('> One\n two.\n')), [quote(paragraph('One two.'))]) - assert.deepEqual(content(markdownToAdf('> One\n\ntwo.\n')), [quote(paragraph('One')), paragraph('two.')]) - assert.deepEqual(content(markdownToAdf('> One\n# Two\n')), [quote(paragraph('One')), { attrs: { level: 1 }, content: [text('Two')], type: 'heading' }]) - assert.deepEqual(content(markdownToAdf('> One\n---\n')), [quote(paragraph('One')), { type: 'rule' }]) - assert.deepEqual(content(markdownToAdf('> One\n```\n')), [quote(paragraph('One')), { type: 'codeBlock' }]) - assert.equal(code(markdownToAdf('> One\n
\n')), 'unmappable-html') - assert.deepEqual(path(markdownToAdf('> One\n
\n')), ['content', 1]) - assert.deepEqual(path(markdownToAdf('> One\n\n')), ['content', 0, 'content', 0]) + 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
\n')), 'unmappable-html') + assert.deepEqual(path(losslessMarkdownToAdf('> One\n
\n')), ['content', 1]) + assert.deepEqual(path(losslessMarkdownToAdf('> One\n\n')), ['content', 0, 'content', 0]) }) test('ends a lazy continuation at a claimed line', () => { - assert.equal(code(markdownToAdf('> Part.\n!adf:/panel\n')), 'malformed-directive') - assert.deepEqual(path(markdownToAdf('> Part.\n!adf:/panel\n')), ['content', 1]) - assert.equal(code(markdownToAdf('- Part.\n| x |\n')), 'malformed-pipe-table') + 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(markdownToAdf('> Part.\n>\n> !adf:expand\n')), ['content', 0, 'content', 1]) - assert.deepEqual(path(markdownToAdf('- Part.\n- | x |\n')), ['content', 0, 'content', 1, 'content', 0]) + 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(markdownToAdf('> '.repeat(501))), 'unsupported-nesting-depth') - assert.ok(markdownToAdf('> '.repeat(500)).ok) + 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(markdownToAdf(marks(largestNesting + 1))), 'unsupported-nesting-depth') - assert.deepEqual(content(markdownToAdf(marks(largestNesting))), [{ content: [marked('a', underline)], type: 'paragraph' }]) + 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(markdownToAdf(nest(repeated('panel'), '!adf:paragraph {localId=a-1}\nPart.\n!adf:/paragraph\n')).ok) - assert.deepEqual(position(markdownToAdf(nest(['expand', ...repeated('panel'), 'expand'], 'Part.\n'))), { line: 501, offset: 5501 }) - assert.equal(code(markdownToAdf(nest(['panel', ...repeated('expand'), 'panel'], 'Part.\n'))), 'unsupported-nesting-depth') + 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(markdownToAdf(listed(directiveLists)).ok) - assert.equal(code(markdownToAdf(listed(directiveLists + 1))), 'unsupported-nesting-depth') + 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. @@ -745,407 +767,407 @@ test('refuses input nested deeper than the parser carries', () => { 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(markdownToAdf(overflowed(493))), 'unsupported-node-shape') - assert.equal(code(markdownToAdf(overflowed(494))), 'unsupported-nesting-depth') + 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(markdownToAdf(rebasing(494)).ok) - assert.equal(code(markdownToAdf(rebasing(495))), 'unsupported-nesting-depth') - assert.ok(markdownToAdf(`${'- '.repeat(largestNesting)}a\n`).ok) - assert.equal(code(markdownToAdf(`${'- '.repeat(largestNesting + 1)}a\n`)), 'unsupported-nesting-depth') + 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(markdownToAdf('\\*not emphasis\\*\n')), [paragraph('*not emphasis*')]) - assert.deepEqual(content(markdownToAdf('\\\\\n')), [paragraph('\\')]) - assert.deepEqual(content(markdownToAdf('\\a \\\u00a0\n')), [paragraph('\\a \\\u00a0')]) - assert.deepEqual(content(markdownToAdf('Part\\\n')), [paragraph('Part\\')]) - assert.deepEqual(content(markdownToAdf('a\\`b`\n')), [paragraph('a`b`')]) + 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(markdownToAdf('& © ≧̸ ‌ Æ\n')), [paragraph('& \u00a9 \u2267\u0338 \u200c \u00c6')]) - assert.deepEqual(content(markdownToAdf('# " ♥\n')), [paragraph('# " \u2665')]) - assert.deepEqual(content(markdownToAdf('� � �\n')), [paragraph('\ufffd \ufffd \ufffd')]) - assert.deepEqual(content(markdownToAdf('&zzz; & &#; &\n')), [paragraph('&zzz; & &#; &')]) - assert.deepEqual(content(markdownToAdf('`not code`\n')), [paragraph('`not code`')]) - assert.deepEqual(content(markdownToAdf('a b c d𝔸e|f\n')), [paragraph('a\tb\nc d\u{1d538}e|f')]) + 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(markdownToAdf('Run `npm test` now.\n')), [ + assert.deepEqual(content(losslessMarkdownToAdf('Run `npm test` now.\n')), [ { content: [text('Run '), codeSpan('npm test'), text(' now.')], type: 'paragraph' }, ]) - assert.deepEqual(content(markdownToAdf('``a`b``\n')), [{ content: [codeSpan('a`b')], type: 'paragraph' }]) - assert.deepEqual(content(markdownToAdf('` `` `\n')), [{ content: [codeSpan('``')], type: 'paragraph' }]) - assert.deepEqual(content(markdownToAdf('` `\n')), [{ content: [codeSpan(' ')], type: 'paragraph' }]) - assert.deepEqual(content(markdownToAdf('`a\nb`\n')), [{ content: [codeSpan('a b')], type: 'paragraph' }]) - assert.deepEqual(content(markdownToAdf('`foo``bar`\n')), [{ content: [codeSpan('foo``bar')], type: 'paragraph' }]) - assert.deepEqual(content(markdownToAdf('`!adf:/panel` `~~x~~` `\\*` `&`\n')), [ + 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(markdownToAdf('`foo\n')), [paragraph('`foo')]) - assert.deepEqual(content(markdownToAdf('``foo`\n')), [paragraph('``foo`')]) + 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(markdownToAdf('One\\\ntwo.\n')), [{ content: [text('One'), hardBreak(), text('two.')], type: 'paragraph' }]) - assert.deepEqual(content(markdownToAdf('One \ntwo.\n')), [{ content: [text('One'), hardBreak(), text('two.')], type: 'paragraph' }]) - assert.deepEqual(content(markdownToAdf('One\\ \ntwo.\n')), [{ content: [text('One\\'), hardBreak(), text('two.')], type: 'paragraph' }]) - assert.deepEqual(content(markdownToAdf('One \\\ntwo.\n')), [{ content: [text('One '), hardBreak(), text('two.')], type: 'paragraph' }]) - assert.deepEqual(content(markdownToAdf('One \ntwo.\n')), [paragraph('One two.')]) - assert.deepEqual(content(markdownToAdf('One \t\ntwo.\n')), [paragraph('One two.')]) - assert.deepEqual(content(markdownToAdf('One \n')), [paragraph('One')]) - assert.deepEqual(content(markdownToAdf('> One\\\n> two.\n')), [quote({ content: [text('One'), hardBreak(), text('two.')], type: 'paragraph' })]) + 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('decodes the fenced info string the block walk leaves raw', () => { - assert.deepEqual(content(markdownToAdf('```java​script\nx\n```\n')), [ + assert.deepEqual(content(losslessMarkdownToAdf('```java​script\nx\n```\n')), [ { attrs: { language: 'java\u200bscript' }, content: [text('x')], type: 'codeBlock' }, ]) - assert.deepEqual(content(markdownToAdf('```\\#c\nx\n```\n')), [{ attrs: { language: '#c' }, content: [text('x')], type: 'codeBlock' }]) + 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', () => { - assert.equal(content(markdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: ') - assert.equal(content(markdownToAdf('Part
here.\n')), 'unmappable-html: no raw HTML converts at this version:
') - assert.equal(content(markdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML comment') - assert.equal(content(markdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML processing instruction') - assert.equal(content(markdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML declaration') - assert.equal(content(markdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: a CDATA section') - assert.equal(content(markdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML comment') - assert.equal(content(markdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML comment') - assert.equal(code(markdownToAdf('A b\n')), 'unmappable-html') - assert.equal(code(markdownToAdf('Part.\n\n')), 'unmappable-html') - assert.deepEqual(path(markdownToAdf('Part.\n\nA b.\n')), ['content', 1]) + assert.equal(content(losslessMarkdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: ') + assert.equal(content(losslessMarkdownToAdf('Part
here.\n')), 'unmappable-html: no raw HTML converts at this version:
') + assert.equal(content(losslessMarkdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML comment') + assert.equal(content(losslessMarkdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML processing instruction') + assert.equal(content(losslessMarkdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML declaration') + assert.equal(content(losslessMarkdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: a CDATA section') + assert.equal(content(losslessMarkdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML comment') + assert.equal(content(losslessMarkdownToAdf('Part here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML comment') + assert.equal(code(losslessMarkdownToAdf('A b\n')), 'unmappable-html') + assert.equal(code(losslessMarkdownToAdf('Part.\n\n')), 'unmappable-html') + assert.deepEqual(path(losslessMarkdownToAdf('Part.\n\nA b.\n')), ['content', 1]) }) test('leaves the angle bracket that opens no HTML construct to the text it sits in', () => { - assert.deepEqual(content(markdownToAdf('3 < 4 and 5 d\n')), [paragraph('a d')]) - assert.deepEqual(content(markdownToAdf('a