5b2: name the violation in every error message, and the escape a claimed line takes
This commit is contained in:
@@ -38,8 +38,8 @@ test('names the node a refusal came from, and no source the emitter never read',
|
||||
assert.equal(position(adfToMarkdown(document(paragraph({ text: 'x', type: 'text' }), list))), undefined)
|
||||
})
|
||||
|
||||
test('refuses a value that is not an ADF document', () => {
|
||||
assert.equal(code(adfToMarkdown({ type: 'doc', version: Number.NaN })), 'not-an-adf-document')
|
||||
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')
|
||||
})
|
||||
|
||||
test('refuses a document version the markdown cannot carry', () => {
|
||||
@@ -186,8 +186,14 @@ test('refuses a carried node nested deeper than the levels its position leaves',
|
||||
})
|
||||
|
||||
test('refuses a node whose content model the canonical form cannot emit', () => {
|
||||
assert.equal(code(adfToMarkdown(document({ content: [paragraph()], type: 'codeBlock' }))), 'unsupported-node-shape')
|
||||
assert.equal(code(adfToMarkdown(document({ content: [{ content: [{ text: 'lost', type: 'text' }], text: 'x', type: 'text' }], type: 'codeBlock' }))), 'unsupported-node-shape')
|
||||
assert.equal(
|
||||
markdown(adfToMarkdown(document({ content: [paragraph()], type: 'codeBlock' }))),
|
||||
'unsupported-node-shape: a codeBlock holds plain text nodes only: this paragraph node is not one',
|
||||
)
|
||||
assert.equal(
|
||||
markdown(adfToMarkdown(document({ content: [{ content: [{ text: 'lost', type: 'text' }], text: 'x', type: 'text' }], type: 'codeBlock' }))),
|
||||
'unsupported-node-shape: a codeBlock holds plain text nodes only: this text node is not one',
|
||||
)
|
||||
})
|
||||
|
||||
test('spells a list its own content shape cannot hold as a directive', () => {
|
||||
@@ -320,7 +326,10 @@ test('refuses marks and attributes nested deeper than the emitter carries', () =
|
||||
assert.equal(code(adfToMarkdown(document(paragraph({ marks, text: 'x', type: 'text' })))), 'unsupported-nesting-depth')
|
||||
let attrs: AdfMark['attrs'] = { depth: 'x' }
|
||||
for (let depth = 0; depth < 600; depth += 1) attrs = { depth: attrs }
|
||||
assert.equal(code(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))), 'not-an-adf-document')
|
||||
assert.equal(
|
||||
markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))),
|
||||
"not-an-adf-document: an ADF document's content holds ADF nodes: one of them is not",
|
||||
)
|
||||
})
|
||||
|
||||
test('escapes a literal delimiter that would merge with an emitted one', () => {
|
||||
@@ -383,18 +392,26 @@ test('spells a list item whose marker completes a thematic break as a directive'
|
||||
})
|
||||
|
||||
test('refuses the characters CommonMark rewrites', () => {
|
||||
assert.equal(code(adfToMarkdown(document({ content: [{ text: 'a\rb', type: 'text' }], type: 'codeBlock' }))), 'unspellable-whitespace')
|
||||
assert.equal(
|
||||
markdown(adfToMarkdown(document({ content: [{ text: 'a\rb', type: 'text' }], type: 'codeBlock' }))),
|
||||
'unspellable-whitespace: a codeBlock holds no carriage return CommonMark keeps: this text holds one',
|
||||
)
|
||||
assert.equal(code(adfToMarkdown(document(paragraph({ text: 'a\u0000b', type: 'text' })))), 'unspellable-character')
|
||||
assert.equal(code(adfToMarkdown(document({ content: [{ text: 'a\u0000b', type: 'text' }], type: 'codeBlock' }))), 'unspellable-character')
|
||||
assert.equal(code(adfToMarkdown(document({ content: [{ text: '', type: 'text' }], type: 'codeBlock' }))), 'unsupported-node-shape')
|
||||
assert.equal(
|
||||
markdown(adfToMarkdown(document({ content: [{ text: '', type: 'text' }], type: 'codeBlock' }))),
|
||||
'unsupported-node-shape: a codeBlock holds plain text nodes only: this text node is not one',
|
||||
)
|
||||
})
|
||||
|
||||
test('refuses a text node the spelling would empty out', () => {
|
||||
const nested: AdfNode[] = [{ text: 'lost', type: 'text' }]
|
||||
assert.equal(code(adfToMarkdown(document(paragraph({ text: '', type: 'text' })))), 'unsupported-node-shape')
|
||||
assert.equal(code(adfToMarkdown(document(paragraph({ marks: [{ type: 'code' }], text: '', type: 'text' })))), 'unsupported-node-shape')
|
||||
assert.equal(code(adfToMarkdown(document(paragraph({ content: nested, text: 'x', type: 'text' })))), 'unsupported-node-shape')
|
||||
assert.equal(code(adfToMarkdown(document(paragraph({ content: nested, marks: [{ type: 'code' }], text: 'x', type: 'text' })))), 'unsupported-node-shape')
|
||||
const empty = 'unsupported-node-shape: a text node holds text: this one has none'
|
||||
const holding = 'unsupported-node-shape: a text node holds no content: this one holds some'
|
||||
assert.equal(markdown(adfToMarkdown(document(paragraph({ text: '', type: 'text' })))), empty)
|
||||
assert.equal(markdown(adfToMarkdown(document(paragraph({ marks: [{ type: 'code' }], text: '', type: 'text' })))), empty)
|
||||
assert.equal(markdown(adfToMarkdown(document(paragraph({ content: nested, text: 'x', type: 'text' })))), holding)
|
||||
assert.equal(markdown(adfToMarkdown(document(paragraph({ content: nested, marks: [{ type: 'code' }], text: 'x', type: 'text' })))), holding)
|
||||
})
|
||||
|
||||
test('carries a mark run whose edge holds whitespace CommonMark flanking counts', () => {
|
||||
@@ -478,8 +495,8 @@ test('carries a block node mark in the reserved attribute', () => {
|
||||
})
|
||||
|
||||
test('refuses the content a directive body has no room for', () => {
|
||||
assert.equal(code(adfToMarkdown(document({ content: [paragraph()], type: 'media' }))), 'unsupported-node-shape')
|
||||
assert.equal(code(adfToMarkdown(document({ text: 'x', type: 'panel' }))), 'unsupported-node-shape')
|
||||
assert.equal(markdown(adfToMarkdown(document({ content: [paragraph()], type: 'media' }))), 'unsupported-node-shape: a media holds no content: this one holds some')
|
||||
assert.equal(markdown(adfToMarkdown(document({ text: 'x', type: 'panel' }))), 'unsupported-node-shape: a panel carries no text: this one holds text')
|
||||
})
|
||||
|
||||
test('separates blocks in a container body by a blank line only where the fence is not separation already', () => {
|
||||
@@ -578,13 +595,14 @@ test('carries an inline node attribute no section spells', () => {
|
||||
})
|
||||
|
||||
test('refuses the content and slot an inline directive has no room for', () => {
|
||||
const refused = (node: AdfNode): string => code(adfToMarkdown(document(paragraph(node, { text: 'y', type: 'text' }))))
|
||||
assert.equal(refused({ content: [{ text: 'x', type: 'text' }], type: 'status' }), 'unsupported-node-shape')
|
||||
assert.equal(refused({ text: 'x', type: 'status' }), 'unsupported-node-shape')
|
||||
assert.equal(refused({ content: [{ text: 'x', type: 'text' }], type: 'hardBreak' }), 'unsupported-node-shape')
|
||||
assert.equal(refused({ text: 'x', type: 'hardBreak' }), 'unsupported-node-shape')
|
||||
assert.equal(refused({ attrs: { text: 'a\nb' }, type: 'status' }), 'unspellable-whitespace')
|
||||
assert.equal(refused({ attrs: { text: 'a\u0000b' }, type: 'status' }), 'unspellable-character')
|
||||
const refused = (node: AdfNode): string => markdown(adfToMarkdown(document(paragraph(node, { text: 'y', type: 'text' }))))
|
||||
const neither = (type: string, held: string): string => `unsupported-node-shape: a ${type} node holds neither content nor text: this one holds ${held}`
|
||||
assert.equal(refused({ content: [{ text: 'x', type: 'text' }], type: 'status' }), neither('status', 'content'))
|
||||
assert.equal(refused({ text: 'x', type: 'status' }), neither('status', 'text'))
|
||||
assert.equal(refused({ content: [{ text: 'x', type: 'text' }], type: 'hardBreak' }), neither('hardBreak', 'content'))
|
||||
assert.equal(refused({ text: 'x', type: 'hardBreak' }), neither('hardBreak', 'text'))
|
||||
assert.equal(refused({ attrs: { text: 'a\nb' }, type: 'status' }), 'unspellable-whitespace: the status content slot holds a newline no inline directive spans')
|
||||
assert.equal(refused({ attrs: { text: 'a\u0000b' }, type: 'status' }), 'unspellable-character: a status content slot holds a null character CommonMark replaces')
|
||||
})
|
||||
|
||||
test('spells the directive marks around the longest run they cover', () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||
import type { BlockDirective } from '../../adf/block-directives.ts'
|
||||
import { adfDocumentFault, carriesOnly } from '../../adf/document.ts'
|
||||
import { blockDirective } from '../../adf/block-directives.ts'
|
||||
import { carriedBlock } from '../opaque-carry.ts'
|
||||
import { carriesOnly, isAdfDocument } from '../../adf/document.ts'
|
||||
import { emitInlineLine } from './inline-line.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { fencedCodeBlock } from '../backtick-runs.ts'
|
||||
@@ -22,7 +22,8 @@ type PlacedBlock = EmittedBlock & { node: AdfNode; path: ConvertErrorPath }
|
||||
const largestListMarker = 999999999
|
||||
|
||||
export function adfToMarkdown(document: AdfDocument): Result<string> {
|
||||
if (!isAdfDocument(document)) return failure('not-an-adf-document', 'the value is not an ADF document', [])
|
||||
const fault = adfDocumentFault(document)
|
||||
if (fault !== undefined) return failure('not-an-adf-document', fault, [])
|
||||
if (document.version !== 1) return failure('unsupported-document-version', `no markdown spelling carries ADF version ${document.version}`, [])
|
||||
const blocks = emitBlocks(document.content ?? [], 'document', [], 0)
|
||||
if (!blocks.ok) return blocks
|
||||
@@ -111,9 +112,9 @@ function commonMarkText(text: string): EmittedBlock {
|
||||
}
|
||||
|
||||
function emitDirectiveBlock(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
|
||||
if (node.text !== undefined) return failure('unsupported-node-shape', `a ${node.type} carries no text`, path)
|
||||
if (node.text !== undefined) return failure('unsupported-node-shape', `a ${node.type} carries no text: this one holds text`, path)
|
||||
const content = node.content ?? []
|
||||
if (directive.contentModel === 'none' && content.length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content`, path)
|
||||
if (directive.contentModel === 'none' && content.length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content: this one holds some`, path)
|
||||
if (directive.contentModel === 'code') return emitCodeDirective(node, directive, path, depth)
|
||||
const header = spellDirectiveHeader(node, directive)
|
||||
if (header === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
||||
@@ -176,9 +177,9 @@ function codeBlockText(node: AdfNode, path: ConvertErrorPath): Result<string> {
|
||||
(child.marks ?? []).length > 0 ||
|
||||
Object.keys(child.attrs ?? {}).length > 0
|
||||
) {
|
||||
return failure('unsupported-node-shape', 'a codeBlock holds plain text nodes only', childPath)
|
||||
return failure('unsupported-node-shape', `a codeBlock holds plain text nodes only: this ${child.type} node is not one`, childPath)
|
||||
}
|
||||
if (/\r/.test(child.text)) return failure('unspellable-whitespace', 'a codeBlock holds no carriage return CommonMark keeps', childPath)
|
||||
if (/\r/.test(child.text)) return failure('unspellable-whitespace', 'a codeBlock holds no carriage return CommonMark keeps: this text holds one', childPath)
|
||||
if (holdsNullCharacter(child.text)) return failure('unspellable-character', 'a codeBlock holds a null character CommonMark replaces', childPath)
|
||||
text += child.text
|
||||
}
|
||||
|
||||
@@ -127,8 +127,10 @@ function syntax(text: string): InlineSegment {
|
||||
}
|
||||
|
||||
function refuseContentAndText(node: AdfNode, path: ConvertErrorPath): Result<null> {
|
||||
if ((node.content ?? []).length > 0 || node.text !== undefined) {
|
||||
return failure('unsupported-node-shape', `a ${node.type} node holds neither content nor text`, path)
|
||||
const holdsContent = (node.content ?? []).length > 0
|
||||
if (holdsContent || node.text !== undefined) {
|
||||
const held = holdsContent ? 'content' : 'text'
|
||||
return failure('unsupported-node-shape', `a ${node.type} node holds neither content nor text: this one holds ${held}`, path)
|
||||
}
|
||||
return success(null)
|
||||
}
|
||||
@@ -216,8 +218,8 @@ function emitInlineDirective(node: AdfNode, directive: InlineDirective, index: n
|
||||
|
||||
function emitText(node: AdfNode, context: InlineContext, index: number, path: ConvertErrorPath): Result<Emission> {
|
||||
if (Object.keys(node.attrs ?? {}).length > 0) return success({ carry: { first: index, last: index } })
|
||||
if (typeof node.text !== 'string' || node.text === '') return failure('unsupported-node-shape', 'a text node holds text', path)
|
||||
if ((node.content ?? []).length > 0) return failure('unsupported-node-shape', 'a text node holds no content', path)
|
||||
if (typeof node.text !== 'string' || node.text === '') return failure('unsupported-node-shape', 'a text node holds text: this one has none', path)
|
||||
if ((node.content ?? []).length > 0) return failure('unsupported-node-shape', 'a text node holds no content: this one holds some', path)
|
||||
if (/\r/.test(node.text)) return failure('unspellable-whitespace', 'a text node holds a carriage return CommonMark rewrites', path)
|
||||
if (holdsNullCharacter(node.text)) return failure('unspellable-character', 'a text node holds a null character CommonMark replaces', path)
|
||||
const escaping: InlineEscaping = context.bracketed ? 'bracketed' : 'backslash'
|
||||
@@ -259,8 +261,8 @@ function emitCodeSpan(nodes: readonly AdfNode[], depth: number, range: NodeRange
|
||||
let text = ''
|
||||
for (const node of nodes) {
|
||||
if (node.type !== 'text' || (node.marks ?? []).length !== depth + 1) return success({ carry: range })
|
||||
if (typeof node.text !== 'string' || node.text === '') return failure('unsupported-node-shape', 'a text node holds text', path)
|
||||
if ((node.content ?? []).length > 0) return failure('unsupported-node-shape', 'a text node holds no content', path)
|
||||
if (typeof node.text !== 'string' || node.text === '') return failure('unsupported-node-shape', 'a text node holds text: this one has none', path)
|
||||
if ((node.content ?? []).length > 0) return failure('unsupported-node-shape', 'a text node holds no content: this one holds some', path)
|
||||
text += node.text
|
||||
}
|
||||
if (/[\n\r]/.test(text)) return success({ carry: range })
|
||||
|
||||
Reference in New Issue
Block a user