Merge pull request '5b2: the error messages' (#45) from error-messages into main
CI / gate (push) Successful in 9s
CI / gate (push) Successful in 9s
This commit was merged in pull request #45.
This commit is contained in:
@@ -91,6 +91,10 @@ Pre-1.0, normal 0.x rules.
|
||||
The error surface is a contract too. `ConvertError` is `{ code, message, path, position? }` — the
|
||||
code from a closed list a consumer may switch exhaustively, the message free text, the path the
|
||||
node's place from the document root, the position where a parse read the refusal in its input.
|
||||
A message names the violation, not the rule alone — a rule by itself states a truth the reader
|
||||
must invert before it reads as a failure — and where the flavour's claim refuses ordinary prose it
|
||||
names the escape that unclaims the form claimed: `\:::` for a directive line, `\|` for every pipe
|
||||
row, `\:` for an inline directive.
|
||||
Adding, removing or renaming a code is breaking, so a milestone meeting a new failure cause
|
||||
reuses a code where one fits; the list is complete at `0.1.0`. A code names the
|
||||
cause; where one cause recurs across node types or across directions, one code covers them all and
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --test --experimental-test-coverage --test-coverage-exclude=\"src/**/*.test.ts\" --test-coverage-branches=97.95 --test-coverage-functions=100 --test-coverage-lines=100 \"src/**/*.test.ts\"",
|
||||
"test": "node --test --experimental-test-coverage --test-coverage-exclude=\"src/**/*.test.ts\" --test-coverage-branches=98 --test-coverage-functions=100 --test-coverage-lines=100 \"src/**/*.test.ts\"",
|
||||
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.build.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,24 +1,40 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { isAdfDocument } from './document.ts'
|
||||
import { adfDocumentFault, isAdfDocument } from './document.ts'
|
||||
|
||||
function fault(value: unknown): string {
|
||||
return adfDocumentFault(value) ?? 'accepted'
|
||||
}
|
||||
|
||||
test('accepts an editor-normal document', () => {
|
||||
assert.equal(isAdfDocument({ content: [{ content: [{ text: 'x', type: 'text' }], type: 'paragraph' }], type: 'doc', version: 1 }), true)
|
||||
assert.equal(isAdfDocument({ type: 'doc', version: 1 }), true)
|
||||
assert.equal(fault({ content: [{ content: [{ text: 'x', type: 'text' }], type: 'paragraph' }], type: 'doc', version: 1 }), 'accepted')
|
||||
})
|
||||
|
||||
test('rejects anything that is not a doc node', () => {
|
||||
test('names the check anything that is not a doc node failed', () => {
|
||||
assert.equal(isAdfDocument(null), false)
|
||||
assert.equal(isAdfDocument([]), false)
|
||||
assert.equal(isAdfDocument('doc'), false)
|
||||
assert.equal(isAdfDocument({ type: 'paragraph', version: 1 }), false)
|
||||
assert.equal(isAdfDocument({ type: 'doc' }), false)
|
||||
assert.equal(isAdfDocument({ type: 'doc', version: Number.NaN }), false)
|
||||
assert.equal(isAdfDocument({ extra: 1, type: 'doc', version: 1 }), false)
|
||||
assert.equal(fault(null), 'an ADF document is an object: found null')
|
||||
assert.equal(fault(undefined), 'an ADF document is an object: found undefined')
|
||||
assert.equal(fault([]), 'an ADF document is an object: found an array')
|
||||
assert.equal(fault('doc'), 'an ADF document is an object: found "doc"')
|
||||
assert.equal(fault('x'.repeat(200000)), `an ADF document is an object: found "${'x'.repeat(40)}…"`)
|
||||
assert.equal(fault(function named(first: number, second: number) { return first + second }), 'an ADF document is an object: found a function')
|
||||
assert.equal(fault({ fields: { description: { type: 'doc', version: 1 } } }), 'an ADF document holds content, type and version alone: found the key fields')
|
||||
assert.equal(fault({ extra: 1, type: 'doc', version: 1 }), 'an ADF document holds content, type and version alone: found the key extra')
|
||||
assert.equal(fault({ version: 1 }), 'an ADF document holds type "doc": found no type field')
|
||||
assert.equal(fault({ type: 'document', version: 1 }), 'an ADF document holds type "doc": found "document"')
|
||||
assert.equal(fault({ type: 'paragraph', version: 1 }), 'an ADF document holds type "doc": found "paragraph"')
|
||||
assert.equal(fault({ type: 1, version: 1 }), 'an ADF document holds type "doc": found 1')
|
||||
assert.equal(fault({ type: 'doc' }), 'an ADF document holds a version number: found no version field')
|
||||
assert.equal(fault({ type: 'doc', version: '1' }), 'an ADF document holds a version number: found "1"')
|
||||
assert.equal(fault({ type: 'doc', version: Number.NaN }), 'an ADF document holds a version number: found NaN')
|
||||
})
|
||||
|
||||
test('rejects a node whose shape ProseMirror JSON cannot hold', () => {
|
||||
assert.equal(fault({ content: {}, type: 'doc', version: 1 }), "an ADF document's content is an array: found an object")
|
||||
assert.equal(fault({ content: [{ type: 1 }], type: 'doc', version: 1 }), "an ADF document's content holds ADF nodes: one of them is not")
|
||||
assert.equal(isAdfDocument({ content: [{ type: 1 }], type: 'doc', version: 1 }), false)
|
||||
assert.equal(isAdfDocument({ content: [{ text: 1, type: 'text' }], type: 'doc', version: 1 }), false)
|
||||
assert.equal(isAdfDocument({ content: [{ node: 'x', type: 'paragraph' }], type: 'doc', version: 1 }), false)
|
||||
|
||||
+30
-8
@@ -25,16 +25,28 @@ const documentKeys = ['content', 'type', 'version']
|
||||
const markKeys = ['attrs', 'type']
|
||||
const nodeKeys = ['attrs', 'content', 'marks', 'text', 'type']
|
||||
|
||||
export function adfDocumentFault(value: unknown): string | undefined {
|
||||
if (!isRecord(value)) return `an ADF document is an object: found ${describe(value)}`
|
||||
const extra = extraKey(value, documentKeys)
|
||||
if (extra !== undefined) return `an ADF document holds content, type and version alone: found the key ${extra}`
|
||||
if (!('type' in value)) return 'an ADF document holds type "doc": found no type field'
|
||||
if (value['type'] !== 'doc') return `an ADF document holds type "doc": found ${describe(value['type'])}`
|
||||
if (!('version' in value)) return 'an ADF document holds a version number: found no version field'
|
||||
const version = value['version']
|
||||
if (typeof version !== 'number' || !Number.isFinite(version)) return `an ADF document holds a version number: found ${describe(version)}`
|
||||
if (!('content' in value)) return undefined
|
||||
const content = value['content']
|
||||
if (!Array.isArray(content)) return `an ADF document's content is an array: found ${describe(content)}`
|
||||
return isNodeArray(content) ? undefined : "an ADF document's content holds ADF nodes: one of them is not"
|
||||
}
|
||||
|
||||
export function carriesOnly(node: AdfNode, attributes: readonly string[]): boolean {
|
||||
if ((node.marks ?? []).length > 0 || node.text !== undefined) return false
|
||||
return holdsOnly(node.attrs ?? {}, attributes)
|
||||
}
|
||||
|
||||
export function isAdfDocument(value: unknown): value is AdfDocument {
|
||||
if (!isRecord(value) || !holdsOnly(value, documentKeys)) return false
|
||||
if (value['type'] !== 'doc') return false
|
||||
if (typeof value['version'] !== 'number' || !Number.isFinite(value['version'])) return false
|
||||
return !('content' in value) || isNodeArray(value['content'])
|
||||
return adfDocumentFault(value) === undefined
|
||||
}
|
||||
|
||||
export function isAdfNode(value: unknown): value is AdfNode {
|
||||
@@ -47,8 +59,7 @@ export function isAdfMark(value: unknown): value is AdfMark {
|
||||
return !('attrs' in value) || isAttributes(value['attrs'])
|
||||
}
|
||||
|
||||
function isNodeArray(value: unknown): value is AdfNode[] {
|
||||
if (!Array.isArray(value)) return false
|
||||
function isNodeArray(value: readonly unknown[]): boolean {
|
||||
const pending: unknown[] = [...value]
|
||||
while (pending.length > 0) {
|
||||
const node = pending.pop()
|
||||
@@ -78,6 +89,17 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function holdsOnly(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
||||
return Object.keys(value).every((key) => keys.includes(key))
|
||||
function describe(value: unknown): string {
|
||||
if (typeof value === 'string') return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}…` : value)
|
||||
if (typeof value === 'function') return 'a function'
|
||||
if (typeof value === 'object' && value !== null) return Array.isArray(value) ? 'an array' : 'an object'
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function extraKey(value: Record<string, unknown>, keys: readonly string[]): string | undefined {
|
||||
return Object.keys(value).find((key) => !keys.includes(key))
|
||||
}
|
||||
|
||||
function holdsOnly(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
||||
return extraKey(value, keys) === undefined
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ test('names the {attrs} keys read out of the alphabetical order canonical form s
|
||||
})
|
||||
|
||||
test('spells an empty {attrs} only where the brace itself claims the directive', () => {
|
||||
const omitted = 'an empty {attrs} is omitted unless the { itself claims the directive'
|
||||
const omitted = 'an empty {attrs} is omitted unless the { itself claims the directive: this one spells {}'
|
||||
assert.equal(fault('::rule {}'), omitted)
|
||||
assert.equal(fault(':::panel info {}'), omitted)
|
||||
assert.equal(inline(':underline[a]{}'), omitted)
|
||||
@@ -78,26 +78,27 @@ test('spells an empty {attrs} only where the brace itself claims the directive',
|
||||
})
|
||||
|
||||
test('names the directive line no spelling reads', () => {
|
||||
assert.equal(fault('::Panel'), 'a directive name reads [a-z][A-Za-z0-9]*')
|
||||
assert.equal(fault('::1panel'), 'a directive name reads [a-z][A-Za-z0-9]*')
|
||||
assert.equal(fault('::panel info'), 'a directive line reads a name, one bare argument and {attrs}, one space apart')
|
||||
assert.equal(fault('::panel info extra'), 'a directive line reads a name, one bare argument and {attrs}, one space apart')
|
||||
assert.equal(fault('::panel{}'), 'a directive line reads a name, one bare argument and {attrs}, one space apart')
|
||||
assert.equal(fault('::panel info{}'), 'a directive line reads a name, one bare argument and {attrs}, one space apart')
|
||||
assert.equal(fault('::panel {a=1} x'), 'a directive line reads a name, one bare argument and {attrs}, one space apart')
|
||||
assert.equal(fault('::Panel'), 'a directive name reads [a-z][A-Za-z0-9]*: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::1panel'), 'a directive name reads [a-z][A-Za-z0-9]*: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel info'), 'a directive line reads a name, one bare argument and {attrs}, one space apart: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel info extra'), 'a directive line reads a name, one bare argument and {attrs}, one space apart: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel{}'), 'a directive line reads a name, one bare argument and {attrs}, one space apart: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel info{}'), 'a directive line reads a name, one bare argument and {attrs}, one space apart: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel {a=1} x'), 'a directive line reads a name, one bare argument and {attrs}, one space apart: this one does not; \\::: keeps the line literal text')
|
||||
})
|
||||
|
||||
test('names the attributes no spelling reads', () => {
|
||||
assert.equal(fault('::panel {a=1'), 'the {attrs} closing brace is missing')
|
||||
assert.equal(fault('::panel {a="x}'), 'the {attrs} quoted value is unclosed')
|
||||
assert.equal(fault('::panel {a="\\uzzzz"}'), 'the {attrs} quoted value is not a JSON string')
|
||||
assert.equal(fault('::panel {a}'), 'an attribute reads key=value, the value bare or double-quoted')
|
||||
assert.equal(fault('::panel {a=}'), 'an attribute reads key=value, the value bare or double-quoted')
|
||||
assert.equal(fault('::panel {=1}'), 'an attribute reads key=value, the value bare or double-quoted')
|
||||
assert.equal(fault('::panel {a=1 b=2}'), 'an attribute reads key=value, the value bare or double-quoted')
|
||||
assert.equal(fault('::panel { a=1}'), 'an attribute reads key=value, the value bare or double-quoted')
|
||||
assert.equal(fault('::panel {a=1 }'), 'an attribute reads key=value, the value bare or double-quoted')
|
||||
assert.equal(fault('::panel {a}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel {a=}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel {=1}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel {a=1 b=2}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel { a=1}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel {a=1 }'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\::: keeps the line literal text')
|
||||
assert.equal(fault('::panel {a=1 a=2}'), 'the attribute key a is spelled twice')
|
||||
assert.equal(inline(':mention[@A]{id}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\: keeps the colon literal')
|
||||
})
|
||||
|
||||
test('breaks the directive on the raw characters a quoted value spells as escapes', () => {
|
||||
@@ -131,10 +132,10 @@ test('binds an inline directive as a unit, its content balancing brackets like l
|
||||
})
|
||||
|
||||
test('names the inline directive left unclosed at the end of its line', () => {
|
||||
assert.equal(inline(':mention[@A'), 'an inline directive [content] is unclosed')
|
||||
assert.equal(inline(':mention[@A\nB]'), 'an inline directive [content] is unclosed')
|
||||
assert.equal(inline(':mention[a `b\nc` d]'), 'an inline directive [content] is unclosed')
|
||||
assert.equal(inline(':underline[:status[x'), 'an inline directive [content] is unclosed')
|
||||
assert.equal(inline(':mention[@A'), 'an inline directive [content] is unclosed; \\: keeps the colon literal')
|
||||
assert.equal(inline(':mention[@A\nB]'), 'an inline directive [content] is unclosed; \\: keeps the colon literal')
|
||||
assert.equal(inline(':mention[a `b\nc` d]'), 'an inline directive [content] is unclosed; \\: keeps the colon literal')
|
||||
assert.equal(inline(':underline[:status[x'), 'an inline directive [content] is unclosed; \\: keeps the colon literal')
|
||||
assert.equal(inline(':mention[@A]{id=1'), 'the {attrs} closing brace is missing')
|
||||
assert.equal(inline(':mention{id=1'), 'the {attrs} closing brace is missing')
|
||||
assert.equal(inline(':text{text="a\nb"}'), 'the {attrs} quoted value is not a JSON string')
|
||||
|
||||
@@ -36,11 +36,14 @@ const quotedEscapes = new RegExp(reservedSource, 'g')
|
||||
const rawReserved = new RegExp(reservedSource)
|
||||
const noAttributes: DirectiveAttributes = new Map()
|
||||
|
||||
const emptyFault = 'an empty {attrs} is omitted unless the { itself claims the directive'
|
||||
const nameFault = 'a directive name reads [a-z][A-Za-z0-9]*'
|
||||
export const directiveLineEscape = '\\::: keeps the line literal text'
|
||||
export const inlineDirectiveEscape = '\\: keeps the colon literal'
|
||||
|
||||
const emptyFault = 'an empty {attrs} is omitted unless the { itself claims the directive: this one spells {}'
|
||||
const nameFault = `a directive name reads [a-z][A-Za-z0-9]*: this one does not; ${directiveLineEscape}`
|
||||
const orderFault = 'the {attrs} keys read in alphabetical order'
|
||||
const pairFault = 'an attribute reads key=value, the value bare or double-quoted'
|
||||
const shapeFault = 'a directive line reads a name, one bare argument and {attrs}, one space apart'
|
||||
const pairFault = 'an attribute reads key=value, the value bare or double-quoted: this one does not'
|
||||
const shapeFault = `a directive line reads a name, one bare argument and {attrs}, one space apart: this one does not; ${directiveLineEscape}`
|
||||
|
||||
export function attributeValue(text: string, kind: AttributeKind): VocabularyValue | undefined {
|
||||
if (kind === 'string') return { kind, value: text }
|
||||
@@ -78,9 +81,9 @@ export function readInlineDirective(text: string, index: number): Read<Directive
|
||||
}
|
||||
|
||||
export function readSoleStringAttribute(span: DirectiveSpan, key: string): Read<string> {
|
||||
if (span.content !== undefined) return { fault: unsupportedNodeShape(`${span.name} takes no content`) }
|
||||
if (span.content !== undefined) return { fault: unsupportedNodeShape(`${span.name} takes no content: this one holds some`) }
|
||||
const spelled = span.attributes.get(key)
|
||||
if (spelled === undefined || span.attributes.size !== 1) return { fault: unsupportedNodeShape(`${span.name} holds one ${key} attribute alone`) }
|
||||
if (spelled === undefined || span.attributes.size !== 1) return { fault: unsupportedNodeShape(`${span.name} holds one ${key} attribute alone: this one does not`) }
|
||||
const spelling = spellStringAttribute(spelled.decoded)
|
||||
if (spelling !== spelled.spelling) return { fault: unsupportedNodeShape(`${span.name} spells its ${key} attribute as ${key}=${spelling}`) }
|
||||
return { value: spelled.decoded }
|
||||
@@ -121,14 +124,18 @@ export function spellVocabulary(pairs: readonly VocabularyPair[]): [string, stri
|
||||
return pairs.map((pair): [string, string] => [pair.key, spellAttributeValue(pair)])
|
||||
}
|
||||
|
||||
export function unknownDirectiveFault(name: string): ConvertFault {
|
||||
return { code: 'unknown-directive-name', message: `the directive name ${name} reads back to no node` }
|
||||
export function unknownDirectiveFault(name: string, escape: string): ConvertFault {
|
||||
return { code: 'unknown-directive-name', message: `the directive name ${name} reads back to no node; ${escape}` }
|
||||
}
|
||||
|
||||
export function unsupportedNodeShape(message: string): ConvertFault {
|
||||
return { code: 'unsupported-node-shape', message }
|
||||
}
|
||||
|
||||
function attributePairFault(escape: string): ConvertFault {
|
||||
return malformedDirective(`${pairFault}; ${escape}`)
|
||||
}
|
||||
|
||||
function keyOrder(left: string, right: string): number {
|
||||
if (left < right) return -1
|
||||
return left > right ? 1 : 0
|
||||
@@ -162,7 +169,7 @@ function readDirectiveHeader(rest: string): Read<{ argument: string | undefined;
|
||||
cursor += 1 + argument.length
|
||||
}
|
||||
if (rest.charAt(cursor) === ' ' && rest.charAt(cursor + 1) === '{') {
|
||||
const read = readAttributes(rest, cursor + 1)
|
||||
const read = readAttributes(rest, cursor + 1, directiveLineEscape)
|
||||
if (read.fault !== undefined) return { fault: read.fault }
|
||||
if (read.value.attributes.size === 0) return { fault: malformedDirective(emptyFault) }
|
||||
attributes = read.value.attributes
|
||||
@@ -188,7 +195,7 @@ function readNestedDirective(text: string, index: number, depth: number): Read<D
|
||||
}
|
||||
let attributes = noAttributes
|
||||
if (text.charAt(cursor) === '{') {
|
||||
const read = readAttributes(text, cursor)
|
||||
const read = readAttributes(text, cursor, inlineDirectiveEscape)
|
||||
if (read.fault !== undefined) return { fault: read.fault }
|
||||
if (read.value.attributes.size === 0 && content !== undefined) return { fault: malformedDirective(emptyFault) }
|
||||
attributes = read.value.attributes
|
||||
@@ -224,7 +231,7 @@ function readDirectiveContent(text: string, start: number, depth: number): Read<
|
||||
if (character === ']') brackets -= 1
|
||||
cursor += 1
|
||||
}
|
||||
return { fault: malformedDirective('an inline directive [content] is unclosed') }
|
||||
return { fault: malformedDirective(`an inline directive [content] is unclosed; ${inlineDirectiveEscape}`) }
|
||||
}
|
||||
|
||||
// `undefined` where the span crosses the line ending an inline directive may not cross.
|
||||
@@ -235,16 +242,16 @@ function readCodeSpanEnd(text: string, index: number): number | undefined {
|
||||
return text.slice(index, closer + opener).includes('\n') ? undefined : closer + opener
|
||||
}
|
||||
|
||||
function readAttributes(text: string, index: number): Read<Attributes> {
|
||||
function readAttributes(text: string, index: number, escape: string): Read<Attributes> {
|
||||
const attributes = new Map<string, DirectiveValue>()
|
||||
let cursor = index + 1
|
||||
let previous = ''
|
||||
while (cursor < text.length && text.charAt(cursor) !== '}') {
|
||||
if (attributes.size > 0) {
|
||||
if (text.charAt(cursor) !== ' ') return { fault: malformedDirective(pairFault) }
|
||||
if (text.charAt(cursor) !== ' ') return { fault: attributePairFault(escape) }
|
||||
cursor += 1
|
||||
}
|
||||
const pair = readAttributePair(text, cursor)
|
||||
const pair = readAttributePair(text, cursor, escape)
|
||||
if (pair.fault !== undefined) return { fault: pair.fault }
|
||||
const key = pair.value.key
|
||||
if (attributes.has(key)) return { fault: malformedDirective(`the attribute key ${key} is spelled twice`) }
|
||||
@@ -257,10 +264,10 @@ function readAttributes(text: string, index: number): Read<Attributes> {
|
||||
return { value: { attributes, length: cursor + 1 - index } }
|
||||
}
|
||||
|
||||
function readAttributePair(text: string, index: number): Read<AttributePair> {
|
||||
function readAttributePair(text: string, index: number, escape: string): Read<AttributePair> {
|
||||
bareRun.lastIndex = index
|
||||
const key = bareRun.exec(text)?.[0]
|
||||
if (key === undefined || text.charAt(index + key.length) !== '=') return { fault: malformedDirective(pairFault) }
|
||||
if (key === undefined || text.charAt(index + key.length) !== '=') return { fault: attributePairFault(escape) }
|
||||
const start = index + key.length + 1
|
||||
if (text.charAt(start) === '"') {
|
||||
const quoted = readQuotedValue(text, start)
|
||||
@@ -269,7 +276,7 @@ function readAttributePair(text: string, index: number): Read<AttributePair> {
|
||||
}
|
||||
bareRun.lastIndex = start
|
||||
const bare = bareRun.exec(text)?.[0]
|
||||
if (bare === undefined) return { fault: malformedDirective(pairFault) }
|
||||
if (bare === undefined) return { fault: attributePairFault(escape) }
|
||||
return { value: { end: start + bare.length, key, value: { decoded: bare, spelling: bare } } }
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -56,7 +56,7 @@ function readCarriedJson(raw: string, spelling: JsonSpelling, levels: number): R
|
||||
const shape = spelling === 'compact' ? 'compact, keys sorted' : 'two-space indent, keys sorted'
|
||||
return { fault: unsupportedNodeShape(`the opaque carry spells its node's JSON canonically: ${shape}`) }
|
||||
}
|
||||
if (!isAdfNode(value)) return { fault: unsupportedNodeShape("the opaque carry holds one ADF node's JSON") }
|
||||
if (!isAdfNode(value)) return { fault: unsupportedNodeShape("the opaque carry holds one ADF node's JSON: this JSON is no ADF node") }
|
||||
return { value }
|
||||
}
|
||||
|
||||
|
||||
@@ -108,5 +108,5 @@ test('names the directive fence a container does not sit longer than', () => {
|
||||
assert.deepEqual(faults('::::panel info\n:::expand\nPart.\n:::::\n::::\n'), [])
|
||||
assert.deepEqual(faults(':::panel info\nPart.\n'), ['a container fenced with 3 colons is unclosed'])
|
||||
assert.deepEqual(faults('- :::panel info\n\nPart.\n'), ['a container fenced with 3 colons is unclosed'])
|
||||
assert.deepEqual(faults('Part.\n\n:::\n'), ['a closing fence closes no open container'])
|
||||
assert.deepEqual(faults('Part.\n\n:::\n'), ['a closing fence closes no open container; \\::: keeps the line literal text'])
|
||||
})
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
replaceNullCharacters,
|
||||
setextHeadingLevel,
|
||||
} from '../commonmark-grammar.ts'
|
||||
import { directiveLineEscape, malformedDirective, readDirectiveLine } from '../directive-syntax.ts'
|
||||
import { isPipeAlignment, isPipeDelimiter, malformedPipeTable, pipeCells } from '../pipe-table-syntax.ts'
|
||||
import { malformedDirective, readDirectiveLine } from '../directive-syntax.ts'
|
||||
import { readLinkDefinitions } from './link-reference-definitions.ts'
|
||||
|
||||
export type Block = { position: SourcePosition } & (
|
||||
@@ -244,7 +244,7 @@ function applyDirectiveLine(walk: Walk, directive: DirectiveLine): void {
|
||||
|
||||
function closeDirective(walk: Walk, colons: number, enclosing: { container: OpenDirective; depth: number } | undefined): void {
|
||||
if (enclosing === undefined) {
|
||||
pushFault(walk, malformedDirective('a closing fence closes no open container'))
|
||||
pushFault(walk, malformedDirective(`a closing fence closes no open container; ${directiveLineEscape}`))
|
||||
return
|
||||
}
|
||||
if (colons < enclosing.container.colons) {
|
||||
@@ -403,16 +403,20 @@ function closeLeaf(walk: Walk): void {
|
||||
function pipeTableBlock(rows: readonly [string[], ...string[][]], position: SourcePosition): Block {
|
||||
const [header, delimiter, ...body] = rows
|
||||
if (delimiter !== undefined && delimiter.some(isPipeAlignment)) {
|
||||
return faultedBlock('a pipe table carries no column alignment ADF could hold', position)
|
||||
return faultedBlock('a pipe table carries no column alignment ADF could hold: this delimiter row holds an alignment colon', position)
|
||||
}
|
||||
if (delimiter === undefined || !delimiter.every(isPipeDelimiter)) {
|
||||
return faultedBlock('a pipe table underlines its header with a row of `-` runs', position)
|
||||
return faultedBlock('a pipe table underlines its header with a row of `-` runs: this one has none; \\| at the start of every row keeps them literal text', position)
|
||||
}
|
||||
const ragged = [delimiter, ...body].find((row) => row.length !== header.length)
|
||||
if (ragged !== undefined) return faultedBlock(`a pipe table row holds ${ragged.length} cells where its header holds ${header.length}`, position)
|
||||
if (ragged !== undefined) return faultedBlock(`a pipe table row holds ${cellCount(ragged.length)} where its header holds ${cellCount(header.length)}`, position)
|
||||
return { kind: 'table', position, rows: [header, ...body] }
|
||||
}
|
||||
|
||||
function cellCount(count: number): string {
|
||||
return `${count} cell${count === 1 ? '' : 's'}`
|
||||
}
|
||||
|
||||
function faultedBlock(message: string, position: SourcePosition): Block {
|
||||
return { fault: malformedPipeTable(message), kind: 'fault', position }
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ export function readVocabulary(
|
||||
for (const [key, spelled] of attributes) {
|
||||
if (key === elsewhere?.key) {
|
||||
const place = elsewhere.slot === 'argument' ? 'as the directive argument' : 'in the content slot'
|
||||
return failure('unsupported-node-shape', `${type} spells its ${key} attribute ${place}`, path)
|
||||
return failure('unsupported-node-shape', `${type} spells its ${key} attribute ${place}, never in {attrs}`, path)
|
||||
}
|
||||
const kind = Object.hasOwn(vocabulary, key) ? vocabulary[key] : undefined
|
||||
if (kind === undefined) return failure('unsupported-node-shape', `${type} holds no ${key} attribute`, path)
|
||||
if (kind === undefined) return failure('unsupported-node-shape', `${type} holds no ${key} attribute: this one spells it`, path)
|
||||
const read = attributeValue(spelled.decoded, kind)
|
||||
if (read === undefined) return failure('unsupported-node-shape', `the ${key} attribute of ${type} is no ${kind}`, path)
|
||||
const spelling = spellAttributeValue(read)
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { BlockDirective } from '../../adf/block-directives.ts'
|
||||
import type { ConvertFault } from '../../result.ts'
|
||||
import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
|
||||
import type { Elsewhere } from './directive-attributes.ts'
|
||||
import { attributeValue, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||
import { attributeValue, directiveLineEscape, inlineDirectiveEscape, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||
import { blockArgument } from '../block-directive-arguments.ts'
|
||||
import { blockDirective } from '../../adf/block-directives.ts'
|
||||
import { carryName } from '../opaque-carry.ts'
|
||||
@@ -27,7 +27,7 @@ export function readBlockDirectiveNode(
|
||||
return failure('malformed-directive', `the name ${carryName} is reserved for the opaque carry, whose block form is the fence`, path)
|
||||
}
|
||||
const directive = blockDirective(name)
|
||||
if (directive === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name), path)
|
||||
if (directive === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name, directiveLineEscape), path)
|
||||
const argumentKey = blockArgument(name)
|
||||
const rest = new Map(attributes)
|
||||
rest.delete(marksAttribute)
|
||||
@@ -35,7 +35,7 @@ export function readBlockDirectiveNode(
|
||||
const attrs = readVocabulary(name, rest, directive.attributes, elsewhere, path)
|
||||
if (!attrs.ok) return attrs
|
||||
if (argument !== undefined) {
|
||||
if (argumentKey === undefined) return failure('unsupported-node-shape', `${name} takes no argument`, path)
|
||||
if (argumentKey === undefined) return failure('unsupported-node-shape', `${name} takes no argument: this one spells one`, path)
|
||||
attrs.value[argumentKey] = argument
|
||||
}
|
||||
const spelled = attributes.get(marksAttribute)
|
||||
@@ -51,15 +51,15 @@ export function readInlineDirectiveNode(
|
||||
path: ConvertErrorPath,
|
||||
): Result<AdfNode> {
|
||||
const directive = inlineDirective(name)
|
||||
if (directive === undefined) return faulted(blockSpellingFault(name) ?? unknownDirectiveFault(name), path)
|
||||
if (directive === undefined) return faulted(blockSpellingFault(name) ?? unknownDirectiveFault(name, inlineDirectiveEscape), path)
|
||||
const slot = directive.textAttribute
|
||||
if (slot === undefined && content !== undefined) return failure('unsupported-node-shape', `${name} takes no content`, path)
|
||||
if (slot === undefined && content !== undefined) return failure('unsupported-node-shape', `${name} takes no content: this one holds some`, path)
|
||||
const elsewhere: Elsewhere | undefined = slot === undefined ? undefined : { key: slot, slot: 'content' }
|
||||
const attrs = readVocabulary(name, attributes, directive.attributes, elsewhere, path)
|
||||
if (!attrs.ok) return attrs
|
||||
if (slot !== undefined && content !== undefined) {
|
||||
const text = slotText(content)
|
||||
if (text === undefined) return failure('unsupported-node-shape', `the ${name} content slot holds one unmarked text node`, path)
|
||||
if (text === undefined) return failure('unsupported-node-shape', `the ${name} content slot holds one unmarked text node: this one holds something else`, path)
|
||||
const spans = slotLineEndingFault(name, text)
|
||||
if (spans !== undefined) return faulted(spans, path)
|
||||
attrs.value[slot] = text
|
||||
@@ -72,14 +72,14 @@ function inlineSpellingFault(name: string): ConvertFault | undefined {
|
||||
const mark = inlineMarkSpellingFault(name)
|
||||
if (mark !== undefined) return mark
|
||||
if (inlineDirective(name) === undefined && name !== textDirectiveName) return undefined
|
||||
return { code: 'unsupported-node-shape', message: `${name} takes the inline form, :${name}{…}` }
|
||||
return { code: 'unsupported-node-shape', message: `${name} takes the inline form, :${name}{…}, never the block form` }
|
||||
}
|
||||
|
||||
function blockSpellingFault(name: string): ConvertFault | undefined {
|
||||
const directive = blockDirective(name)
|
||||
if (directive === undefined) return undefined
|
||||
const form = directive.contentModel === 'none' ? `::${name}` : `:::${name}`
|
||||
return { code: 'unsupported-node-shape', message: `${name} takes the block form, ${form}` }
|
||||
return { code: 'unsupported-node-shape', message: `${name} takes the block form, ${form}, never the inline form` }
|
||||
}
|
||||
|
||||
// spec/flavour.md, Inline nodes: the slot is plain text, its adjacent nodes already merged.
|
||||
@@ -94,7 +94,7 @@ function readMarks(type: string, spelled: DirectiveValue, path: ConvertErrorPath
|
||||
const read = attributeValue(spelled.decoded, 'json')
|
||||
const marks = read === undefined || spellAttributeValue(read) !== spelled.spelling ? undefined : readMarkValues(read.value)
|
||||
if (marks === undefined) {
|
||||
return failure('unsupported-node-shape', `the ${marksAttribute} attribute of ${type} is its marks array in canonical JSON`, path)
|
||||
return failure('unsupported-node-shape', `the ${marksAttribute} attribute of ${type} is its marks array in canonical JSON: this one is not`, path)
|
||||
}
|
||||
return success(marks)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ type Scan = { definitions: LinkDefinitions; path: ConvertErrorPath; pending: str
|
||||
type SlotContent = { carry: boolean; nodes: AdfNode[] }
|
||||
|
||||
const carriedInMark = 'no mark spelling wraps an opaque carry: the carried node restores exactly, marks included'
|
||||
const imageAlone = 'an image fits only as a paragraph of its own'
|
||||
const imageAlone = 'an image fits only as a paragraph of its own: this one sits inside other content'
|
||||
|
||||
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath): Result<InlineContent> {
|
||||
return parseInline(source, definitions, path, true)
|
||||
@@ -133,7 +133,7 @@ function readAngle(scan: Scan, index: number): Result<number> {
|
||||
return success(index + autolink.length)
|
||||
}
|
||||
const construct = inlineHtmlConstruct(scan.source, index)
|
||||
if (construct !== undefined) return failure('unmappable-html', `no ADF node carries ${construct}`, scan.path)
|
||||
if (construct !== undefined) return failure('unmappable-html', `no raw HTML converts at this version: ${construct}`, scan.path)
|
||||
scan.pending += '<'
|
||||
return success(index + 1)
|
||||
}
|
||||
@@ -179,7 +179,7 @@ function directivePiece(scan: Scan, span: DirectiveSpan): Result<Piece> {
|
||||
if (mark !== undefined) {
|
||||
if (!mark.ok) return mark
|
||||
if (slot.value === undefined || slot.value.nodes.length === 0) {
|
||||
return failure('unsupported-node-shape', `the ${span.name} mark wraps the [content] it marks`, scan.path)
|
||||
return failure('unsupported-node-shape', `the ${span.name} mark wraps the [content] it marks: this one wraps none`, scan.path)
|
||||
}
|
||||
if (slot.value.carry) return failure('unsupported-node-shape', carriedInMark, scan.path)
|
||||
return success({ kind: 'nodes', nodes: applyMark(slot.value.nodes, mark.value) })
|
||||
|
||||
@@ -155,7 +155,7 @@ test('names the slot a codeBlock spells its language outside of', () => {
|
||||
assert.equal(content(markdownToAdf(':::codeBlock {language=rust}\n```sql\nx\n```\n:::\n')), slot)
|
||||
assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\n```adf\nx\n```\n:::\n')), slot)
|
||||
assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\n```a\\b\nx\n```\n:::\n')), slot)
|
||||
assert.equal(content(markdownToAdf('::codeBlock {wrap=true}\n')), 'unsupported-node-shape: codeBlock spells its body in the container form, :::')
|
||||
assert.equal(content(markdownToAdf('::codeBlock {wrap=true}\n')), 'unsupported-node-shape: codeBlock spells its body in the container form, :::, never the leaf form')
|
||||
})
|
||||
|
||||
test('reads a pipe table into the header row and the body rows under it', () => {
|
||||
@@ -182,11 +182,11 @@ test('claims the line a pipe opens and gives the rest back to the block walk', (
|
||||
})
|
||||
|
||||
test('names the pipe table a claimed line does not spell', () => {
|
||||
assert.equal(content(markdownToAdf('| a | b |\n')), 'malformed-pipe-table: a pipe table underlines its header with a row of `-` runs')
|
||||
assert.equal(content(markdownToAdf('| a |\n| x |\n')), 'malformed-pipe-table: a pipe table underlines its header with a row of `-` runs')
|
||||
assert.equal(content(markdownToAdf('| a | b |\n| :--- | ---: |\n')), 'malformed-pipe-table: a pipe table carries no column alignment ADF could hold')
|
||||
assert.equal(content(markdownToAdf('| a | b |\n| --- |\n')), 'malformed-pipe-table: a pipe table row holds 1 cells where its header holds 2')
|
||||
assert.equal(content(markdownToAdf('| a |\n| --- |\n| b | c |\n')), 'malformed-pipe-table: a pipe table row holds 2 cells where its header holds 1')
|
||||
assert.equal(content(markdownToAdf('| a | b |\n')), 'malformed-pipe-table: a pipe table underlines its header with a row of `-` runs: this one has none; \\| at the start of every row keeps them literal text')
|
||||
assert.equal(content(markdownToAdf('| a |\n| x |\n')), 'malformed-pipe-table: a pipe table underlines its header with a row of `-` runs: this one has none; \\| at the start of every row keeps them literal text')
|
||||
assert.equal(content(markdownToAdf('| a | b |\n| :--- | ---: |\n')), 'malformed-pipe-table: a pipe table carries no column alignment ADF could hold: this delimiter row holds an alignment colon')
|
||||
assert.equal(content(markdownToAdf('| a | b |\n| --- |\n')), 'malformed-pipe-table: a pipe table row holds 1 cell where its header holds 2 cells')
|
||||
assert.equal(content(markdownToAdf('| a |\n| --- |\n| b | c |\n')), 'malformed-pipe-table: a pipe table row holds 2 cells where its header holds 1 cell')
|
||||
assert.deepEqual(path(markdownToAdf('Part.\n\n| a |\n')), ['content', 1])
|
||||
})
|
||||
|
||||
@@ -248,8 +248,9 @@ test('gives back the refusal the CommonMark spelling itself raises', () => {
|
||||
|
||||
test('names the directive name no node reads back to', () => {
|
||||
assert.equal(code(markdownToAdf(':::widget info\nx\n:::\n')), 'unknown-directive-name')
|
||||
assert.equal(content(markdownToAdf('::widget\n')), 'unknown-directive-name: the directive name widget reads back to no node')
|
||||
assert.equal(code(markdownToAdf(':widget[x]\n')), 'unknown-directive-name')
|
||||
assert.equal(content(markdownToAdf('::widget\n')), 'unknown-directive-name: the directive name widget reads back to no node; \\::: keeps the line literal text')
|
||||
assert.equal(content(markdownToAdf(':widget[x]\n')), 'unknown-directive-name: the directive name widget reads back to no node; \\: keeps the colon literal')
|
||||
assert.equal(content(markdownToAdf('ratio a:b[c]{d}\n')), 'malformed-directive: an attribute reads key=value, the value bare or double-quoted: this one does not; \\: keeps the colon literal')
|
||||
assert.deepEqual(path(markdownToAdf('Part.\n\n::widget\n')), ['content', 1])
|
||||
assert.equal(content(markdownToAdf('Part.\n:::x\n')), 'malformed-directive: a container fenced with 3 colons is unclosed')
|
||||
assert.deepEqual(path(markdownToAdf('Part.\n:::x\n')), ['content', 1])
|
||||
@@ -258,10 +259,10 @@ test('names the directive name no node reads back to', () => {
|
||||
test('names the position a directive name the other one spells belongs to', () => {
|
||||
assert.equal(content(markdownToAdf(':::em\na\n:::\n')), 'unsupported-node-shape: em is spelled _x_, never as a block directive')
|
||||
assert.equal(content(markdownToAdf('::underline\n')), 'unsupported-node-shape: underline is spelled :underline[…], never as a block directive')
|
||||
assert.equal(content(markdownToAdf('::text {text=" "}\n')), 'unsupported-node-shape: text takes the inline form, :text{…}')
|
||||
assert.equal(content(markdownToAdf('::date {timestamp=1}\n')), 'unsupported-node-shape: date takes the inline form, :date{…}')
|
||||
assert.equal(content(markdownToAdf(':paragraph[a]\n')), 'unsupported-node-shape: paragraph takes the block form, :::paragraph')
|
||||
assert.equal(content(markdownToAdf(':rule[a]\n')), 'unsupported-node-shape: rule takes the block form, ::rule')
|
||||
assert.equal(content(markdownToAdf('::text {text=" "}\n')), 'unsupported-node-shape: text takes the inline form, :text{…}, never the block form')
|
||||
assert.equal(content(markdownToAdf('::date {timestamp=1}\n')), 'unsupported-node-shape: date takes the inline form, :date{…}, never the block form')
|
||||
assert.equal(content(markdownToAdf(':paragraph[a]\n')), 'unsupported-node-shape: paragraph takes the block form, :::paragraph, never the inline form')
|
||||
assert.equal(content(markdownToAdf(':rule[a]\n')), 'unsupported-node-shape: rule takes the block form, ::rule, never the inline form')
|
||||
assert.equal(code(markdownToAdf(':::widget\na\n:::\n')), 'unknown-directive-name')
|
||||
assert.equal(code(markdownToAdf(':widget[a]\n')), 'unknown-directive-name')
|
||||
})
|
||||
@@ -303,16 +304,16 @@ test('names the canonical spelling a carried JSON reads alone', () => {
|
||||
})
|
||||
|
||||
test('names the node JSON an opaque carry restores alone', () => {
|
||||
const node = "unsupported-node-shape: the opaque carry holds one ADF node's JSON"
|
||||
const node = "unsupported-node-shape: the opaque carry holds one ADF node's JSON: this JSON is no ADF node"
|
||||
assert.equal(content(markdownToAdf('```adf\n[]\n```\n')), node)
|
||||
assert.equal(content(markdownToAdf(':adf{json=null}\n')), node)
|
||||
assert.equal(content(markdownToAdf(':adf{json="{\\"kind\\":\\"x\\"}"}\n')), node)
|
||||
})
|
||||
|
||||
test('names the shape the inline carry reads alone', () => {
|
||||
assert.equal(content(markdownToAdf(':adf[x]{json="{}"}\n')), 'unsupported-node-shape: adf takes no content')
|
||||
assert.equal(content(markdownToAdf(':adf{}\n')), 'unsupported-node-shape: adf holds one json attribute alone')
|
||||
assert.equal(content(markdownToAdf(':adf{json="{}" localId=x}\n')), 'unsupported-node-shape: adf holds one json attribute alone')
|
||||
assert.equal(content(markdownToAdf(':adf[x]{json="{}"}\n')), 'unsupported-node-shape: adf takes no content: this one holds some')
|
||||
assert.equal(content(markdownToAdf(':adf{}\n')), 'unsupported-node-shape: adf holds one json attribute alone: this one does not')
|
||||
assert.equal(content(markdownToAdf(':adf{json="{}" localId=x}\n')), 'unsupported-node-shape: adf holds one json attribute alone: this one does not')
|
||||
assert.equal(content(markdownToAdf(':adf{json="null"}\n')), 'unsupported-node-shape: adf spells its json attribute as json=null')
|
||||
})
|
||||
|
||||
@@ -372,7 +373,7 @@ test('reads the reserved marks key as the node array it spells', () => {
|
||||
})
|
||||
|
||||
test('names the marks key no marks array reads back from', () => {
|
||||
const named = 'unsupported-node-shape: the marks attribute of rule is its marks array in canonical JSON'
|
||||
const named = 'unsupported-node-shape: the marks attribute of rule is its marks array in canonical JSON: this one is not'
|
||||
assert.equal(content(markdownToAdf('::rule {marks="[]"}\n')), named)
|
||||
assert.equal(content(markdownToAdf('::rule {marks="[1]"}\n')), named)
|
||||
assert.equal(content(markdownToAdf('::rule {marks="{}"}\n')), named)
|
||||
@@ -381,15 +382,15 @@ test('names the marks key no marks array reads back from', () => {
|
||||
})
|
||||
|
||||
test('names the attribute a node holds no reading for', () => {
|
||||
assert.equal(content(markdownToAdf('::rule {bogus=1}\n')), 'unsupported-node-shape: rule holds no bogus attribute')
|
||||
assert.equal(content(markdownToAdf('::rule {bogus=1}\n')), 'unsupported-node-shape: rule holds no bogus attribute: this one spells it')
|
||||
assert.equal(content(markdownToAdf('::media {width=wide}\n')), 'unsupported-node-shape: the width attribute of media is no number')
|
||||
assert.equal(content(markdownToAdf(':::table {isNumberColumnEnabled=yes}\n:::\n')), 'unsupported-node-shape: the isNumberColumnEnabled attribute of table is no boolean')
|
||||
assert.equal(content(markdownToAdf('::media {width=true}\n')), 'unsupported-node-shape: the width attribute of media is no number')
|
||||
assert.equal(content(markdownToAdf(':::tableCell {colwidth="[340,"}\n:::\n')), 'unsupported-node-shape: the colwidth attribute of tableCell is no json')
|
||||
const deep = `${'['.repeat(largestNesting + 2)}${']'.repeat(largestNesting + 2)}`
|
||||
assert.equal(content(markdownToAdf(`:::tableCell {colwidth="${deep}"}\n:::\n`)), 'unsupported-node-shape: the colwidth attribute of tableCell is no json')
|
||||
assert.equal(content(markdownToAdf(':::panel info {panelType=note}\nx\n:::\n')), 'unsupported-node-shape: panel spells its panelType attribute as the directive argument')
|
||||
assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot')
|
||||
assert.equal(content(markdownToAdf(':::panel info {panelType=note}\nx\n:::\n')), 'unsupported-node-shape: panel spells its panelType attribute as the directive argument, never in {attrs}')
|
||||
assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot, never in {attrs}')
|
||||
})
|
||||
|
||||
test('names the attribute value spelled outside the canonical form', () => {
|
||||
@@ -399,15 +400,15 @@ test('names the attribute value spelled outside the canonical form', () => {
|
||||
})
|
||||
|
||||
test('names the argument and the body a node takes no reading for', () => {
|
||||
assert.equal(content(markdownToAdf('::rule x\n')), 'unsupported-node-shape: rule takes no argument')
|
||||
assert.equal(content(markdownToAdf(':::rule\nPart.\n:::\n')), 'unsupported-node-shape: rule holds no content')
|
||||
assert.equal(content(markdownToAdf('::bulletList\n')), 'unsupported-node-shape: bulletList spells its body in the container form, :::')
|
||||
assert.equal(content(markdownToAdf(':::paragraph\n:::\n')), 'unsupported-node-shape: an empty paragraph takes the leaf form, ::')
|
||||
assert.equal(content(markdownToAdf(':::paragraph\nOne.\n\nTwo.\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body')
|
||||
assert.equal(content(markdownToAdf(':::paragraph\n---\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body')
|
||||
assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\nx\n:::\n')), 'unsupported-node-shape: codeBlock takes one code block as its body')
|
||||
assert.equal(content(markdownToAdf('::rule x\n')), 'unsupported-node-shape: rule takes no argument: this one spells one')
|
||||
assert.equal(content(markdownToAdf(':::rule\nPart.\n:::\n')), 'unsupported-node-shape: rule holds no content: this one holds some')
|
||||
assert.equal(content(markdownToAdf('::bulletList\n')), 'unsupported-node-shape: bulletList spells its body in the container form, :::, never the leaf form')
|
||||
assert.equal(content(markdownToAdf(':::paragraph\n:::\n')), 'unsupported-node-shape: an empty paragraph takes the leaf form, ::, never an empty container')
|
||||
assert.equal(content(markdownToAdf(':::paragraph\nOne.\n\nTwo.\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body: this body is not one')
|
||||
assert.equal(content(markdownToAdf(':::paragraph\n---\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body: this body is not one')
|
||||
assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\nx\n:::\n')), 'unsupported-node-shape: codeBlock takes one code block as its body: this body is not one')
|
||||
assert.equal(content(markdownToAdf(':::paragraph\n\n:::\n')), 'unmappable-image: no ADF node carries an image inside a paragraph')
|
||||
assert.equal(content(markdownToAdf('Part :date[now]{timestamp=1}.\n')), 'unsupported-node-shape: date takes no content')
|
||||
assert.equal(content(markdownToAdf('Part :date[now]{timestamp=1}.\n')), 'unsupported-node-shape: date takes no content: this one holds some')
|
||||
})
|
||||
|
||||
test('leaves the colon that opens no directive the text it is', () => {
|
||||
@@ -417,7 +418,7 @@ test('leaves the colon that opens no directive the text it is', () => {
|
||||
})
|
||||
|
||||
test('names the inline directive left unclosed at the end of its line', () => {
|
||||
assert.equal(code(markdownToAdf('Part :mention[@A\n')), 'malformed-directive')
|
||||
assert.equal(content(markdownToAdf('Part :mention[@A\n')), 'malformed-directive: an inline directive [content] is unclosed; \\: keeps the colon literal')
|
||||
assert.equal(code(markdownToAdf('Part :mention[@A]{id=\n')), 'malformed-directive')
|
||||
assert.deepEqual(path(markdownToAdf('> Part :mention[@A\n')), ['content', 0, 'content', 0])
|
||||
})
|
||||
@@ -637,14 +638,14 @@ test('decodes the fenced info string the block walk leaves raw', () => {
|
||||
})
|
||||
|
||||
test('refuses the raw inline HTML no element mapping carries, naming it', () => {
|
||||
assert.equal(content(markdownToAdf('Part <span> here.\n')), 'unmappable-html: no ADF node carries <span>')
|
||||
assert.equal(content(markdownToAdf('Part </div> here.\n')), 'unmappable-html: no ADF node carries <div>')
|
||||
assert.equal(content(markdownToAdf('Part <!-- note --> here.\n')), 'unmappable-html: no ADF node carries an HTML comment')
|
||||
assert.equal(content(markdownToAdf('Part <?php ?> here.\n')), 'unmappable-html: no ADF node carries an HTML processing instruction')
|
||||
assert.equal(content(markdownToAdf('Part <!DOCTYPE html> here.\n')), 'unmappable-html: no ADF node carries an HTML declaration')
|
||||
assert.equal(content(markdownToAdf('Part <![CDATA[x]]> here.\n')), 'unmappable-html: no ADF node carries a CDATA section')
|
||||
assert.equal(content(markdownToAdf('Part <!--> here.\n')), 'unmappable-html: no ADF node carries an HTML comment')
|
||||
assert.equal(content(markdownToAdf('Part <!---> here.\n')), 'unmappable-html: no ADF node carries an HTML comment')
|
||||
assert.equal(content(markdownToAdf('Part <span> here.\n')), 'unmappable-html: no raw HTML converts at this version: <span>')
|
||||
assert.equal(content(markdownToAdf('Part </div> here.\n')), 'unmappable-html: no raw HTML converts at this version: <div>')
|
||||
assert.equal(content(markdownToAdf('Part <!-- note --> here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML comment')
|
||||
assert.equal(content(markdownToAdf('Part <?php ?> here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML processing instruction')
|
||||
assert.equal(content(markdownToAdf('Part <!DOCTYPE html> here.\n')), 'unmappable-html: no raw HTML converts at this version: an HTML declaration')
|
||||
assert.equal(content(markdownToAdf('Part <![CDATA[x]]> 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 <a href="/x" disabled\nid=y> b\n')), 'unmappable-html')
|
||||
assert.equal(code(markdownToAdf('Part.\n<span>\n')), 'unmappable-html')
|
||||
assert.deepEqual(path(markdownToAdf('Part.\n\nA <b>b</b>.\n')), ['content', 1])
|
||||
@@ -781,7 +782,7 @@ test('leaves the brackets of an empty link text the text they are', () => {
|
||||
|
||||
test('refuses the image no ADF node carries where it sits', () => {
|
||||
assert.equal(content(markdownToAdf('\n')), 'unmappable-image: no media node carries a link title')
|
||||
assert.equal(content(markdownToAdf('See .\n')), 'unmappable-image: an image fits only as a paragraph of its own')
|
||||
assert.equal(content(markdownToAdf('See .\n')), 'unmappable-image: an image fits only as a paragraph of its own: this one sits inside other content')
|
||||
assert.equal(code(markdownToAdf('# \n')), 'unmappable-image')
|
||||
assert.equal(code(markdownToAdf('**\n')), 'unmappable-image')
|
||||
assert.equal(code(markdownToAdf('[](/v)\n')), 'unmappable-image')
|
||||
@@ -814,10 +815,10 @@ test('reads the content slot as the text attribute the node spells there', () =>
|
||||
})
|
||||
|
||||
test('names the content slot no one unmarked text node reads back from', () => {
|
||||
assert.equal(content(markdownToAdf(':status[**A**]{color=yellow}\n')), 'unsupported-node-shape: the status content slot holds one unmarked text node')
|
||||
assert.equal(content(markdownToAdf(':status[**A**]{color=yellow}\n')), 'unsupported-node-shape: the status content slot holds one unmarked text node: this one holds something else')
|
||||
assert.equal(code(markdownToAdf(':status[a`b`]{color=yellow}\n')), 'unsupported-node-shape')
|
||||
assert.equal(code(markdownToAdf(':status[:date{timestamp=1}]{color=yellow}\n')), 'unsupported-node-shape')
|
||||
assert.equal(content(markdownToAdf(':status[]{color=yellow}\n')), 'unmappable-image: an image fits only as a paragraph of its own')
|
||||
assert.equal(content(markdownToAdf(':status[]{color=yellow}\n')), 'unmappable-image: an image fits only as a paragraph of its own: this one sits inside other content')
|
||||
assert.equal(code(markdownToAdf(':status[<div>]{color=yellow}\n')), 'unmappable-html')
|
||||
assert.equal(code(markdownToAdf(':date[<div>]{timestamp=1}\n')), 'unmappable-html')
|
||||
assert.equal(code(markdownToAdf(':widget[<div>]\n')), 'unmappable-html')
|
||||
@@ -825,7 +826,7 @@ test('names the content slot no one unmarked text node reads back from', () => {
|
||||
assert.equal(content(markdownToAdf(':status[:text{text="\\n"}]{color=yellow}\n')), spans)
|
||||
assert.equal(content(markdownToAdf(':status[a b]{color=yellow}\n')), spans)
|
||||
assert.equal(content(markdownToAdf(':status[a b]{color=yellow}\n')), spans)
|
||||
assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot')
|
||||
assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot, never in {attrs}')
|
||||
})
|
||||
|
||||
test('reads the whitespace the reserved text directive carries', () => {
|
||||
@@ -836,13 +837,13 @@ test('reads the whitespace the reserved text directive carries', () => {
|
||||
})
|
||||
|
||||
test('names the text directive spelling no whitespace run reads back from', () => {
|
||||
const named = 'unsupported-node-shape: text spells one run of spaces and tabs, or one run of newlines'
|
||||
const named = 'unsupported-node-shape: text spells one run of spaces and tabs, or one run of newlines: this one spells neither'
|
||||
assert.equal(content(markdownToAdf(':text{text=hi}\n')), named)
|
||||
assert.equal(content(markdownToAdf(':text{text=" \\n"}\n')), named)
|
||||
assert.equal(content(markdownToAdf(':text{text=""}\n')), named)
|
||||
assert.equal(content(markdownToAdf(':text{}\n')), 'unsupported-node-shape: text holds one text attribute alone')
|
||||
assert.equal(content(markdownToAdf(':text{localId=a text=" "}\n')), 'unsupported-node-shape: text holds one text attribute alone')
|
||||
assert.equal(content(markdownToAdf(':text[a]{text=" "}\n')), 'unsupported-node-shape: text takes no content')
|
||||
assert.equal(content(markdownToAdf(':text{}\n')), 'unsupported-node-shape: text holds one text attribute alone: this one does not')
|
||||
assert.equal(content(markdownToAdf(':text{localId=a text=" "}\n')), 'unsupported-node-shape: text holds one text attribute alone: this one does not')
|
||||
assert.equal(content(markdownToAdf(':text[a]{text=" "}\n')), 'unsupported-node-shape: text takes no content: this one holds some')
|
||||
assert.equal(content(markdownToAdf(':text{text="\\u0020"}\n')), 'unsupported-node-shape: text spells its text attribute as text=" "')
|
||||
})
|
||||
|
||||
@@ -873,7 +874,7 @@ test('names the mark markdown spells, never a directive', () => {
|
||||
})
|
||||
|
||||
test('names the directive mark left without the content it wraps', () => {
|
||||
const named = 'unsupported-node-shape: the underline mark wraps the [content] it marks'
|
||||
const named = 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps none'
|
||||
assert.equal(content(markdownToAdf(':underline[]\n')), named)
|
||||
assert.equal(content(markdownToAdf(':underline{}\n')), named)
|
||||
})
|
||||
|
||||
@@ -46,7 +46,7 @@ function blockNode(block: Block, definitions: LinkDefinitions, path: ConvertErro
|
||||
case 'heading':
|
||||
return contentNode({ attrs: { level: block.level }, type: 'heading' }, block.text, definitions, path)
|
||||
case 'html':
|
||||
return failure('unmappable-html', `no ADF node carries ${block.construct}`, path)
|
||||
return failure('unmappable-html', `no raw HTML converts at this version: ${block.construct}`, path)
|
||||
case 'orderedList':
|
||||
return listNode({ attrs: { order: block.start }, type: 'orderedList' }, block.items, definitions, path, depth)
|
||||
case 'paragraph':
|
||||
@@ -73,9 +73,9 @@ function directiveBody(read: BlockDirectiveNode, blocks: Block[] | undefined, de
|
||||
const { contentModel, node } = read
|
||||
if (blocks === undefined) {
|
||||
if (contentModel === 'none' || contentModel === 'inline') return success(node)
|
||||
return failure('unsupported-node-shape', `${node.type} spells its body in the container form, :::`, path)
|
||||
return failure('unsupported-node-shape', `${node.type} spells its body in the container form, :::, never the leaf form`, path)
|
||||
}
|
||||
if (contentModel === 'none') return failure('unsupported-node-shape', `${node.type} holds no content`, path)
|
||||
if (contentModel === 'none') return failure('unsupported-node-shape', `${node.type} holds no content: this one holds some`, path)
|
||||
if (contentModel === 'code') return codeDirectiveNode(node, blocks, path)
|
||||
if (contentModel === 'block') return containerNode(node, blocks, definitions, path, depth)
|
||||
return inlineBodyNode(node, blocks, definitions, path)
|
||||
@@ -83,7 +83,7 @@ function directiveBody(read: BlockDirectiveNode, blocks: Block[] | undefined, de
|
||||
|
||||
function codeDirectiveNode(node: AdfNode, blocks: readonly Block[], path: ConvertErrorPath): Result<AdfNode> {
|
||||
const only = blocks.length === 1 ? blocks[0] : undefined
|
||||
if (only?.kind !== 'code') return failure('unsupported-node-shape', `${node.type} takes one code block as its body`, path)
|
||||
if (only?.kind !== 'code') return failure('unsupported-node-shape', `${node.type} takes one code block as its body: this body is not one`, path)
|
||||
const attribute = node.attrs?.['language']
|
||||
const fromFence = only.language !== ''
|
||||
const slot = languageSlot(fromFence ? only.language : attribute)
|
||||
@@ -110,9 +110,9 @@ function tableNode(rows: readonly string[][], definitions: LinkDefinitions, path
|
||||
}
|
||||
|
||||
function inlineBodyNode(node: AdfNode, blocks: readonly Block[], definitions: LinkDefinitions, path: ConvertErrorPath): Result<AdfNode> {
|
||||
if (blocks.length === 0) return failure('unsupported-node-shape', `an empty ${node.type} takes the leaf form, ::`, path)
|
||||
if (blocks.length === 0) return failure('unsupported-node-shape', `an empty ${node.type} takes the leaf form, ::, never an empty container`, path)
|
||||
const only = blocks.length === 1 ? blocks[0] : undefined
|
||||
if (only?.kind !== 'paragraph') return failure('unsupported-node-shape', `${node.type} takes one paragraph as its body`, path)
|
||||
if (only?.kind !== 'paragraph') return failure('unsupported-node-shape', `${node.type} takes one paragraph as its body: this body is not one`, path)
|
||||
return positioned(contentNode(node, only.text, definitions, path), only.position)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,6 @@ export function readTextDirective(span: DirectiveSpan): Read<string> | undefined
|
||||
if (span.name !== name) return undefined
|
||||
const spelled = readSoleStringAttribute(span, name)
|
||||
if (spelled.fault !== undefined) return spelled
|
||||
if (!whitespaceRun.test(spelled.value)) return { fault: unsupportedNodeShape(`${name} spells one run of spaces and tabs, or one run of newlines`) }
|
||||
if (!whitespaceRun.test(spelled.value)) return { fault: unsupportedNodeShape(`${name} spells one run of spaces and tabs, or one run of newlines: this one spells neither`) }
|
||||
return spelled
|
||||
}
|
||||
|
||||
@@ -412,3 +412,20 @@ Under **3 — `markdownToAdf` (`0.1.0`)**:
|
||||
`unsupported-node-shape` stays one code across the two directions, `unmappable-html` names
|
||||
the version rather than the element, and a direction that reads a source returns the
|
||||
narrowed error type.
|
||||
- [x] **5b2 — The error messages.** Most state the rule and leave the violation to be inferred —
|
||||
`a text node holds text` for a node holding none — so `rule: violation` becomes house style
|
||||
across the sites that do. `not-an-adf-document` gives one sentence of eight words to `null`,
|
||||
a string, a missing `version`, a `type` that is not `doc` and a REST envelope around the
|
||||
document; naming the check that failed makes the highest-frequency integrator mistake
|
||||
self-diagnosing without the library naming a REST shape (§7). The three carve-out claim
|
||||
messages name the escape that unclaims the line — `\|`, `\~~`, `\:::` — which today only
|
||||
`spec/flavour.md` holds. `unmappable-html` reads as §8 now frames it: this version converts
|
||||
no raw HTML, never a permanent judgment on the element.
|
||||
Thirty-odd sites gained the violation clause and §8 gained the house style. `isAdfDocument`
|
||||
parts into `adfDocumentFault`, the guard reading it, so the first failing check is the
|
||||
message — the wrapper mistake names the key it found. Two carve-outs claim a line, not
|
||||
three: a matched `~~` pair spells `strike` silently, so nothing refuses it and no message
|
||||
names `\~~`. The escape lands on the refusals a prose line hits, in the form that was
|
||||
claimed — `\:::` on the directive line's, `\|` on the pipe table's, `\:` on the inline
|
||||
directive's, the attribute-pair and unknown-name faults taking whichever form read them.
|
||||
`a pipe table row holds 1 cells` gained its plural.
|
||||
|
||||
@@ -146,15 +146,7 @@ The numbering is the order the work was planned in, not the order it ships.
|
||||
5 because §8 freezes the code list at `0.1.0` and 5b3's table is what reads the list before
|
||||
the freeze closes it.
|
||||
- [x] **5b1 — The error's source position.**
|
||||
- [ ] **5b2 — The error messages.** Most state the rule and leave the violation to be inferred —
|
||||
`a text node holds text` for a node holding none — so `rule: violation` becomes house style
|
||||
across the sites that do. `not-an-adf-document` gives one sentence of eight words to `null`,
|
||||
a string, a missing `version`, a `type` that is not `doc` and a REST envelope around the
|
||||
document; naming the check that failed makes the highest-frequency integrator mistake
|
||||
self-diagnosing without the library naming a REST shape (§7). The three carve-out claim
|
||||
messages name the escape that unclaims the line — `\|`, `\~~`, `\:::` — which today only
|
||||
`spec/flavour.md` holds. `unmappable-html` reads as §8 now frames it: this version converts
|
||||
no raw HTML, never a permanent judgment on the element.
|
||||
- [x] **5b2 — The error messages.**
|
||||
- [ ] **5b3 — The README's consumer surface.** §8 invites an exhaustive switch on `code` and no
|
||||
code name appears in the README, so it gains a table — code, when it fires, what the
|
||||
consumer does — grouped by direction; drafting it is the audit that reads the fifteen names
|
||||
|
||||
Reference in New Issue
Block a user