12b: the inline directive forms read and spell the !adf: prefix
CI / gate (push) Successful in 26s
CI / publish (push) Has been skipped

This commit is contained in:
2026-09-16 09:52:52 +02:00
parent fe064efdb9
commit 302804cc4e
66 changed files with 397 additions and 329 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ import type { AdfMark } from '../../adf/document.ts'
import type { ConvertFault } from '../../result.ts'
import type { DirectiveAttributes } from '../directive-syntax.ts'
import type { MarkSpelling } from '../mark-spellings.ts'
import { directivePrefix } from '../directive-syntax.ts'
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { markSpelling } from '../mark-spellings.ts'
import { readVocabulary } from './directive-attributes.ts'
@@ -19,7 +20,7 @@ export function readDirectiveMark(name: string, attributes: DirectiveAttributes,
export function inlineMarkSpellingFault(name: string): ConvertFault | undefined {
const spelling = markSpelling(name)
if (spelling === undefined) return undefined
return { code: 'unsupported-node-shape', message: `${name} is spelled ${markdownForm(spelling) ?? `:${name}[…]`}, never as a block directive` }
return { code: 'unsupported-node-shape', message: `${name} is spelled ${markdownForm(spelling) ?? `${directivePrefix}${name}[…]`}, never as a block directive` }
}
function markdownForm(spelling: MarkSpelling): string | undefined {
+7 -5
View File
@@ -3,8 +3,8 @@ 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 { attributeNestingMessage, nodeMarks } from '../../adf/document.ts'
import { attributeValue, directiveLineEscape, inlineDirectiveEscape, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
import { attributeNestingMessage, nodeAttrs, nodeMarks } from '../../adf/document.ts'
import { attributeValue, directiveLineEscape, directivePrefix, 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'
@@ -61,7 +61,9 @@ export function readInlineDirectiveNode(
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: this one holds something else`, path)
if (text === undefined) {
return failure('unsupported-node-shape', `the ${name} content slot holds one text node carrying neither marks nor attributes: this one holds something else`, path)
}
const spans = slotLineEndingFault(name, text)
if (spans !== undefined) return faulted(spans, path)
attrs.value[slot] = text
@@ -74,7 +76,7 @@ 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}{…}, never the block form` }
return { code: 'unsupported-node-shape', message: `${name} takes the inline form, ${directivePrefix}${name}{…}, never the block form` }
}
function blockSpellingFault(name: string): ConvertFault | undefined {
@@ -89,7 +91,7 @@ function blockSpellingFault(name: string): ConvertFault | undefined {
function slotText(content: readonly AdfNode[]): string | undefined {
if (content.length === 0) return ''
const only = content.length === 1 ? content[0] : undefined
if (only?.type !== 'text' || nodeMarks(only).length > 0 || typeof only.text !== 'string') return undefined
if (only?.type !== 'text' || nodeMarks(only).length > 0 || Object.keys(nodeAttrs(only)).length > 0 || typeof only.text !== 'string') return undefined
return only.text
}
+7 -7
View File
@@ -64,13 +64,16 @@ function parseInline(source: string, definitions: LinkDefinitions, path: Convert
index = angle.value
break
}
case ':': {
case '!': {
const directive = readDirective(scan, index)
if (directive === undefined) {
index = openBracket(scan, index)
break
}
if (!directive.ok) return directive
index = directive.value
break
}
case '!':
case '[':
index = openBracket(scan, index)
break
@@ -151,12 +154,9 @@ function openBracket(scan: Scan, index: number): number {
return index + width
}
function readDirective(scan: Scan, index: number): Result<number> {
function readDirective(scan: Scan, index: number): Result<number> | undefined {
const directive = readInlineDirective(scan.source, index)
if (directive === undefined) {
scan.pending += ':'
return success(index + 1)
}
if (directive === undefined) return undefined
if (directive.fault !== undefined) return faulted(directive.fault, scan.path)
const piece = directivePiece(scan, directive.value)
if (!piece.ok) return piece
+97 -86
View File
@@ -230,7 +230,7 @@ test('refuses the list separator that parts anything else', () => {
assert.equal(content(markdownToAdf('- a\n\n::listBreak x\n\n- b\n')), bare)
assert.equal(content(markdownToAdf('- a\n\n::listBreak {id=x}\n\n- b\n')), bare)
assert.equal(content(markdownToAdf(':::listBreak\n- a\n:::\n')), bare)
assert.equal(content(markdownToAdf(':listBreak{}\n')), 'unsupported-node-shape: listBreak takes the block form, ::listBreak, never the inline form')
assert.equal(content(markdownToAdf('!adf:listBreak{}\n')), 'unsupported-node-shape: listBreak takes the block form, ::listBreak, never the inline form')
assert.deepEqual(path(markdownToAdf('Part.\n\n::listBreak\n')), ['content', 1])
})
@@ -270,7 +270,7 @@ test('reads the three directive forms into the nodes the tables name', () => {
assert.deepEqual(content(markdownToAdf(':::heading {level=2 localId=a-1}\nPart.\n:::\n')), [
{ attrs: { level: 2, localId: 'a-1' }, content: [text('Part.')], type: 'heading' },
])
assert.deepEqual(content(markdownToAdf('Part:hardBreak{}.\n')), [{ content: [text('Part'), hardBreak(), text('.')], type: 'paragraph' }])
assert.deepEqual(content(markdownToAdf('Part!adf:hardBreak{}.\n')), [{ content: [text('Part'), hardBreak(), text('.')], type: 'paragraph' }])
})
test('names the directive form a node CommonMark spells refuses', () => {
@@ -293,8 +293,8 @@ 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; \\::: 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.equal(content(markdownToAdf('!adf:widget[x]\n')), 'unknown-directive-name: the directive name widget reads back to no node; \\!adf: keeps the prefix literal')
assert.equal(content(markdownToAdf('ratio a!adf:b[c]{d}\n')), 'malformed-directive: an attribute reads key=value, the value bare or double-quoted: this one does not; \\!adf: keeps the prefix 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])
@@ -302,23 +302,23 @@ 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{…}, 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(content(markdownToAdf('::underline\n')), 'unsupported-node-shape: underline is spelled !adf:underline[…], never as a block directive')
assert.equal(content(markdownToAdf('::text {text=" "}\n')), 'unsupported-node-shape: text takes the inline form, !adf:text{…}, never the block form')
assert.equal(content(markdownToAdf('::date {timestamp=1}\n')), 'unsupported-node-shape: date takes the inline form, !adf:date{…}, never the block form')
assert.equal(content(markdownToAdf('!adf:paragraph[a]\n')), 'unsupported-node-shape: paragraph takes the block form, :::paragraph, never the inline form')
assert.equal(content(markdownToAdf('!adf: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')
assert.equal(code(markdownToAdf('!adf:widget[a]\n')), 'unknown-directive-name')
})
test('names the reserved carry name a block directive spells', () => {
const reserved = 'malformed-directive: the name adf is reserved for the opaque carry, whose block form is the fence'
assert.equal(content(markdownToAdf('::adf\n')), reserved)
assert.equal(content(markdownToAdf(':::adf\nx\n:::\n')), reserved)
const reserved = 'malformed-directive: the name carry is reserved for the opaque carry, whose block form is the fence'
assert.equal(content(markdownToAdf('::carry\n')), reserved)
assert.equal(content(markdownToAdf(':::carry\nx\n:::\n')), reserved)
assert.deepEqual(content(markdownToAdf('```adfx\nx\n```\n')), [{ attrs: { language: 'adfx' }, content: [text('x')], type: 'codeBlock' }])
})
const carried = ':adf{json="{\\"type\\":\\"placeholder\\"}"}'
const carried = '!adf:carry{json="{\\"type\\":\\"placeholder\\"}"}'
test('reads the adf fence back to the node its JSON holds', () => {
assert.deepEqual(content(markdownToAdf('```adf\n{\n "attrs": {\n "url": "https://example.com/x"\n },\n "type": "blockCard"\n}\n```\n')), [
@@ -336,36 +336,36 @@ test('names the invalid JSON no opaque carry holds', () => {
const invalid = 'malformed-directive: the opaque carry holds invalid JSON'
assert.equal(content(markdownToAdf('```adf\n{"type":\n```\n')), invalid)
assert.equal(content(markdownToAdf('```adf\n```\n')), invalid)
assert.equal(content(markdownToAdf(':adf{json="{"}\n')), invalid)
assert.equal(content(markdownToAdf(':adf{json=abc}\n')), invalid)
assert.equal(content(markdownToAdf('!adf:carry{json="{"}\n')), invalid)
assert.equal(content(markdownToAdf('!adf:carry{json=abc}\n')), invalid)
})
test('names the canonical spelling a carried JSON reads alone', () => {
const canonically = "unsupported-node-shape: the opaque carry spells its node's JSON canonically: "
assert.equal(content(markdownToAdf('```adf\n{"type":"blockCard"}\n```\n')), `${canonically}two-space indent, keys sorted`)
assert.equal(content(markdownToAdf(':adf{json="{\\"type\\": \\"blockCard\\"}"}\n')), `${canonically}compact, keys sorted`)
assert.equal(content(markdownToAdf(':adf{json="{\\"type\\":\\"blockCard\\",\\"attrs\\":{}}"}\n')), `${canonically}compact, keys sorted`)
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"type\\": \\"blockCard\\"}"}\n')), `${canonically}compact, keys sorted`)
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"type\\":\\"blockCard\\",\\"attrs\\":{}}"}\n')), `${canonically}compact, keys sorted`)
})
test('names the node JSON an opaque carry restores alone', () => {
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)
assert.equal(content(markdownToAdf('!adf:carry{json=null}\n')), node)
assert.equal(content(markdownToAdf('!adf:carry{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: 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')
assert.equal(content(markdownToAdf('!adf:carry[x]{json="{}"}\n')), 'unsupported-node-shape: carry takes no content: this one holds some')
assert.equal(content(markdownToAdf('!adf:carry{}\n')), 'unsupported-node-shape: carry holds one json attribute alone: this one does not')
assert.equal(content(markdownToAdf('!adf:carry{json="{}" localId=x}\n')), 'unsupported-node-shape: carry holds one json attribute alone: this one does not')
assert.equal(content(markdownToAdf('!adf:carry{json="null"}\n')), 'unsupported-node-shape: carry spells its json attribute as json=null')
})
test('holds a carried JSON value to the nesting its position leaves', () => {
const nested = (levels: number): string => `${'['.repeat(levels)}${']'.repeat(levels)}`
const fence = (prefix: string, levels: number): string => `${prefix}\`\`\`adf\n${prefix}${nested(levels)}\n${prefix}\`\`\`\n`
const deeper = (levels: number): string => `unsupported-nesting-depth: a carried node's JSON nests deeper than the ${levels} levels its position leaves`
assert.equal(content(markdownToAdf(`:adf{json="${nested(largestNesting + 2)}"}\n`)), deeper(largestNesting))
assert.equal(content(markdownToAdf(`!adf:carry{json="${nested(largestNesting + 2)}"}\n`)), deeper(largestNesting))
assert.equal(
content(markdownToAdf(fence('', largestNesting + 1))),
"unsupported-node-shape: the opaque carry spells its node's JSON canonically: two-space indent, keys sorted",
@@ -375,7 +375,7 @@ test('holds a carried JSON value to the nesting its position leaves', () => {
test('names the number no JSON spelling carries in an opaque carry', () => {
const named = 'unsupported-node-shape: the opaque carry holds a number JSON cannot spell'
assert.equal(content(markdownToAdf(':adf{json="{\\"attrs\\":{\\"width\\":1e999},\\"type\\":\\"blockCard\\"}"}\n')), named)
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"attrs\\":{\\"width\\":1e999},\\"type\\":\\"blockCard\\"}"}\n')), named)
assert.equal(content(markdownToAdf('```adf\n1e999\n```\n')), named)
})
@@ -385,8 +385,8 @@ test('names the mark spelling no opaque carry sits inside', () => {
assert.equal(content(markdownToAdf(`**${carried}**\n`)), named)
assert.equal(content(markdownToAdf(`~~a ${carried}~~\n`)), named)
assert.equal(content(markdownToAdf(`[a ${carried} b](https://example.com/x)\n`)), named)
assert.equal(content(markdownToAdf(`:underline[${carried}]\n`)), named)
assert.equal(content(markdownToAdf(`:textColor[a ${carried}]{color="#ae2e24"}\n`)), named)
assert.equal(content(markdownToAdf(`!adf:underline[${carried}]\n`)), named)
assert.equal(content(markdownToAdf(`!adf:textColor[a ${carried}]{color="#ae2e24"}\n`)), named)
assert.equal(content(markdownToAdf(`![_a ${carried}_](https://example.com/i)\n`)), named)
})
@@ -432,7 +432,7 @@ test('names the attribute a node holds no reading for', () => {
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')
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}')
assert.equal(content(markdownToAdf('Part !adf:mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot, never in {attrs}')
})
test('names the depth an attribute value nests past, never the kind the JSON reads as', () => {
@@ -460,19 +460,28 @@ test('names the argument and the body a node takes no reading for', () => {
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![a](/u)\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: this one holds some')
assert.equal(content(markdownToAdf('Part !adf: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', () => {
test('leaves the text that opens no directive the text it is', () => {
assert.deepEqual(content(markdownToAdf('At 10:30 :smile: today.\n')), [paragraph('At 10:30 :smile: today.')])
assert.deepEqual(content(markdownToAdf('\\:mention[@A]\n')), [paragraph(':mention[@A]')])
assert.deepEqual(content(markdownToAdf('`:mention[@A]`\n')), [{ content: [codeSpan(':mention[@A]')], type: 'paragraph' }])
assert.deepEqual(content(markdownToAdf(':mention[@A] and !adfx and a!\n')), [paragraph(':mention[@A] and !adfx and a!')])
assert.deepEqual(content(markdownToAdf('\\!adf:mention[@A]\n')), [paragraph('!adf:mention[@A]')])
assert.deepEqual(content(markdownToAdf('`!adf:mention[@A]`\n')), [{ content: [codeSpan('!adf:mention[@A]')], type: 'paragraph' }])
})
test('names the unescaped prefix that completes no directive', () => {
const named = 'malformed-directive: an unescaped !adf: completes no directive; \\!adf: keeps the prefix literal'
assert.equal(content(markdownToAdf('Part !adf: here.\n')), named)
assert.equal(content(markdownToAdf('Part !adf:Mention[@A] here.\n')), named)
assert.equal(content(markdownToAdf('Part !adf:mention @A here.\n')), named)
assert.equal(content(markdownToAdf('!adf:underline[a !adf: b]\n')), named)
})
test('names the inline directive left unclosed at the end of its line', () => {
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])
assert.equal(content(markdownToAdf('Part !adf:mention[@A\n')), 'malformed-directive: an inline directive [content] is unclosed; \\!adf: keeps the prefix literal')
assert.equal(code(markdownToAdf('Part !adf:mention[@A]{id=\n')), 'malformed-directive')
assert.deepEqual(path(markdownToAdf('> Part !adf:mention[@A\n')), ['content', 0, 'content', 0])
})
test('claims a block-level pipe with no table to parse it', () => {
@@ -630,7 +639,7 @@ test('names the block the claim inside a container opens', () => {
test('refuses input nested deeper than the parser carries', () => {
assert.equal(code(markdownToAdf('> '.repeat(501))), 'unsupported-nesting-depth')
assert.ok(markdownToAdf('> '.repeat(500)).ok)
const marks = (levels: number): string => `${':underline['.repeat(levels)}a${']'.repeat(levels)}\n`
const marks = (levels: number): string => `${'!adf:underline['.repeat(levels)}a${']'.repeat(levels)}\n`
assert.equal(code(markdownToAdf(marks(largestNesting + 1))), 'unsupported-nesting-depth')
assert.deepEqual(content(markdownToAdf(marks(largestNesting))), [{ content: [marked('a', underline)], type: 'paragraph' }])
})
@@ -820,8 +829,8 @@ test('flattens the description of a lone image to the plain text alt holds', ()
assert.deepEqual(content(markdownToAdf('![a\nb](/u)\n')), [image('/u', 'a b')])
assert.deepEqual(content(markdownToAdf('![a \nb](/u)\n')), [image('/u', 'a b')])
assert.deepEqual(content(markdownToAdf('![a `b`](/u)\n')), [image('/u', 'a b')])
assert.deepEqual(content(markdownToAdf('![a :mention[@A]{id=b1c2} b](/u)\n')), [image('/u', 'a @A b')])
assert.deepEqual(content(markdownToAdf('![:mention[@A]{id=b1c2}](/u)\n')), [image('/u', '@A')])
assert.deepEqual(content(markdownToAdf('![a !adf:mention[@A]{id=b1c2} b](/u)\n')), [image('/u', 'a @A b')])
assert.deepEqual(content(markdownToAdf('![!adf:mention[@A]{id=b1c2}](/u)\n')), [image('/u', '@A')])
})
test('leaves the brackets of an empty link text the text they are', () => {
@@ -855,78 +864,80 @@ test('carries the mark a spelling nested inside its own kind names once', () =>
test('reads the content slot as the text attribute the node spells there', () => {
const status = (attrs: AdfAttributes): AdfNode[] => [{ content: [{ attrs, type: 'status' }], type: 'paragraph' }]
assert.deepEqual(content(markdownToAdf(':status[In review]{color=yellow}\n')), status({ color: 'yellow', text: 'In review' }))
assert.deepEqual(content(markdownToAdf(':status{color=neutral}\n')), status({ color: 'neutral' }))
assert.deepEqual(content(markdownToAdf(':status[]{color=neutral}\n')), status({ color: 'neutral', text: '' }))
assert.deepEqual(content(markdownToAdf(':status[ In review ]{color=yellow}\n')), status({ color: 'yellow', text: ' In review ' }))
assert.deepEqual(content(markdownToAdf(':status[In:text{text=" "}review]{color=yellow}\n')), status({ color: 'yellow', text: 'In review' }))
assert.deepEqual(content(markdownToAdf(':status[a\\]b]{color=yellow}\n')), status({ color: 'yellow', text: 'a]b' }))
assert.deepEqual(content(markdownToAdf('**:mention[@A]{id=b1c2}**\n')), [
assert.deepEqual(content(markdownToAdf('!adf:status[In review]{color=yellow}\n')), status({ color: 'yellow', text: 'In review' }))
assert.deepEqual(content(markdownToAdf('!adf:status{color=neutral}\n')), status({ color: 'neutral' }))
assert.deepEqual(content(markdownToAdf('!adf:status[]{color=neutral}\n')), status({ color: 'neutral', text: '' }))
assert.deepEqual(content(markdownToAdf('!adf:status[ In review ]{color=yellow}\n')), status({ color: 'yellow', text: ' In review ' }))
assert.deepEqual(content(markdownToAdf('!adf:status[In!adf:text{text=" "}review]{color=yellow}\n')), status({ color: 'yellow', text: 'In review' }))
assert.deepEqual(content(markdownToAdf('!adf:status[a\\]b]{color=yellow}\n')), status({ color: 'yellow', text: 'a]b' }))
assert.deepEqual(content(markdownToAdf('**!adf:mention[@A]{id=b1c2}**\n')), [
{ content: [{ attrs: { id: 'b1c2', text: '@A' }, marks: [strong], type: 'mention' }], type: 'paragraph' },
])
})
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: 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[![a](/u)]{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')
test('names the content slot no lone plain text node reads back from', () => {
const named = 'unsupported-node-shape: the status content slot holds one text node carrying neither marks nor attributes: this one holds something else'
assert.equal(content(markdownToAdf('!adf:status[**A**]{color=yellow}\n')), named)
assert.equal(content(markdownToAdf('!adf:status[!adf:carry{json="{\\"attrs\\":{\\"localId\\":\\"a\\"},\\"text\\":\\"A\\",\\"type\\":\\"text\\"}"}]{color=yellow}\n')), named)
assert.equal(code(markdownToAdf('!adf:status[a`b`]{color=yellow}\n')), 'unsupported-node-shape')
assert.equal(code(markdownToAdf('!adf:status[!adf:date{timestamp=1}]{color=yellow}\n')), 'unsupported-node-shape')
assert.equal(content(markdownToAdf('!adf:status[![a](/u)]{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('!adf:status[<div>]{color=yellow}\n')), 'unmappable-html')
assert.equal(code(markdownToAdf('!adf:date[<div>]{timestamp=1}\n')), 'unmappable-html')
assert.equal(code(markdownToAdf('!adf:widget[<div>]\n')), 'unmappable-html')
const spans = 'unspellable-whitespace: the status content slot holds a newline no inline directive spans'
assert.equal(content(markdownToAdf(':status[:text{text="\\n"}]{color=yellow}\n')), spans)
assert.equal(content(markdownToAdf(':status[a&#10;b]{color=yellow}\n')), spans)
assert.equal(content(markdownToAdf(':status[a&#13;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, never in {attrs}')
assert.equal(content(markdownToAdf('!adf:status[!adf:text{text="\\n"}]{color=yellow}\n')), spans)
assert.equal(content(markdownToAdf('!adf:status[a&#10;b]{color=yellow}\n')), spans)
assert.equal(content(markdownToAdf('!adf:status[a&#13;b]{color=yellow}\n')), spans)
assert.equal(content(markdownToAdf('Part !adf: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', () => {
assert.deepEqual(content(markdownToAdf(':text{text=" "}a\n')), [paragraph(' a')])
assert.deepEqual(content(markdownToAdf('a:text{text="\\n"}b\n')), [paragraph('a\nb')])
assert.deepEqual(content(markdownToAdf('a:text{text="\\t"}\n')), [paragraph('a\t')])
assert.deepEqual(content(markdownToAdf('_:text{text=" "}a_\n')), [{ content: [marked(' a', em)], type: 'paragraph' }])
assert.deepEqual(content(markdownToAdf('!adf:text{text=" "}a\n')), [paragraph(' a')])
assert.deepEqual(content(markdownToAdf('a!adf:text{text="\\n"}b\n')), [paragraph('a\nb')])
assert.deepEqual(content(markdownToAdf('a!adf:text{text="\\t"}\n')), [paragraph('a\t')])
assert.deepEqual(content(markdownToAdf('_!adf:text{text=" "}a_\n')), [{ content: [marked(' a', em)], type: 'paragraph' }])
})
test('names the text directive spelling no whitespace run reads back from', () => {
const named = '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: 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=" "')
assert.equal(content(markdownToAdf('!adf:text{text=hi}\n')), named)
assert.equal(content(markdownToAdf('!adf:text{text=" \\n"}\n')), named)
assert.equal(content(markdownToAdf('!adf:text{text=""}\n')), named)
assert.equal(content(markdownToAdf('!adf:text{}\n')), 'unsupported-node-shape: text holds one text attribute alone: this one does not')
assert.equal(content(markdownToAdf('!adf:text{localId=a text=" "}\n')), 'unsupported-node-shape: text holds one text attribute alone: this one does not')
assert.equal(content(markdownToAdf('!adf:text[a]{text=" "}\n')), 'unsupported-node-shape: text takes no content: this one holds some')
assert.equal(content(markdownToAdf('!adf:text{text="\\u0020"}\n')), 'unsupported-node-shape: text spells its text attribute as text=" "')
})
test('reads the directive marks, the nesting outermost first', () => {
const wrapped = (...marks: AdfMark[]): AdfNode[] => [{ content: [marked('a', ...marks)], type: 'paragraph' }]
assert.deepEqual(content(markdownToAdf(':underline[a]\n')), wrapped(underline))
assert.deepEqual(content(markdownToAdf('_:underline[a]_\n')), wrapped(em, underline))
assert.deepEqual(content(markdownToAdf(':underline[_a_]\n')), wrapped(underline, em))
assert.deepEqual(content(markdownToAdf(':underline[:underline[a]]\n')), wrapped(underline))
assert.deepEqual(content(markdownToAdf(':textColor[a]{color="#ae2e24"}\n')), wrapped({ attrs: { color: '#ae2e24' }, type: 'textColor' }))
assert.deepEqual(content(markdownToAdf(':subsup[a]{type=sub}\n')), wrapped({ attrs: { type: 'sub' }, type: 'subsup' }))
assert.deepEqual(content(markdownToAdf(':border[a]{color="#091e42" size=2}\n')), wrapped({ attrs: { color: '#091e42', size: 2 }, type: 'border' }))
assert.deepEqual(content(markdownToAdf(':underline[a:date{timestamp=1}]\n')), [
assert.deepEqual(content(markdownToAdf('!adf:underline[a]\n')), wrapped(underline))
assert.deepEqual(content(markdownToAdf('_!adf:underline[a]_\n')), wrapped(em, underline))
assert.deepEqual(content(markdownToAdf('!adf:underline[_a_]\n')), wrapped(underline, em))
assert.deepEqual(content(markdownToAdf('!adf:underline[!adf:underline[a]]\n')), wrapped(underline))
assert.deepEqual(content(markdownToAdf('!adf:textColor[a]{color="#ae2e24"}\n')), wrapped({ attrs: { color: '#ae2e24' }, type: 'textColor' }))
assert.deepEqual(content(markdownToAdf('!adf:subsup[a]{type=sub}\n')), wrapped({ attrs: { type: 'sub' }, type: 'subsup' }))
assert.deepEqual(content(markdownToAdf('!adf:border[a]{color="#091e42" size=2}\n')), wrapped({ attrs: { color: '#091e42', size: 2 }, type: 'border' }))
assert.deepEqual(content(markdownToAdf('!adf:underline[a!adf:date{timestamp=1}]\n')), [
{ content: [marked('a', underline), { attrs: { timestamp: '1' }, marks: [underline], type: 'date' }], type: 'paragraph' },
])
assert.deepEqual(content(markdownToAdf('_:underline[a:date{timestamp=1}]_\n')), [
assert.deepEqual(content(markdownToAdf('_!adf:underline[a!adf:date{timestamp=1}]_\n')), [
{ content: [marked('a', em, underline), { attrs: { timestamp: '1' }, marks: [em, underline], type: 'date' }], type: 'paragraph' },
])
assert.equal(content(markdownToAdf(':border[a]{color="#091e42" size=x}\n')), 'unsupported-node-shape: the size attribute of border is no number')
assert.equal(content(markdownToAdf('!adf:border[a]{color="#091e42" size=x}\n')), 'unsupported-node-shape: the size attribute of border is no number')
})
test('names the mark markdown spells, never a directive', () => {
assert.equal(content(markdownToAdf(':em[a]\n')), 'unsupported-node-shape: em is spelled _x_, never as a directive')
assert.equal(content(markdownToAdf(':strong[a]\n')), 'unsupported-node-shape: strong is spelled **x**, never as a directive')
assert.equal(content(markdownToAdf(':strike[a]\n')), 'unsupported-node-shape: strike is spelled ~~x~~, never as a directive')
assert.equal(content(markdownToAdf(':code[a]\n')), 'unsupported-node-shape: code is spelled `x`, never as a directive')
assert.equal(content(markdownToAdf(':link[a]{href="/u"}\n')), 'unsupported-node-shape: link is spelled [x](url), never as a directive')
assert.equal(content(markdownToAdf('!adf:em[a]\n')), 'unsupported-node-shape: em is spelled _x_, never as a directive')
assert.equal(content(markdownToAdf('!adf:strong[a]\n')), 'unsupported-node-shape: strong is spelled **x**, never as a directive')
assert.equal(content(markdownToAdf('!adf:strike[a]\n')), 'unsupported-node-shape: strike is spelled ~~x~~, never as a directive')
assert.equal(content(markdownToAdf('!adf:code[a]\n')), 'unsupported-node-shape: code is spelled `x`, never as a directive')
assert.equal(content(markdownToAdf('!adf:link[a]{href="/u"}\n')), 'unsupported-node-shape: link is spelled [x](url), never as 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: this one wraps none'
assert.equal(content(markdownToAdf(':underline[]\n')), named)
assert.equal(content(markdownToAdf(':underline{}\n')), named)
assert.equal(content(markdownToAdf('!adf:underline[]\n')), named)
assert.equal(content(markdownToAdf('!adf:underline{}\n')), named)
})
+2 -2
View File
@@ -3,7 +3,7 @@ import type { Block, DirectiveBlock } from './blocks.ts'
import type { BlockDirectiveNode } from './directive-nodes.ts'
import type { ConvertFault } from '../../result.ts'
import type { LinkDefinitions } from './inline-content.ts'
import { carryName, readCarriedBlock } from '../opaque-carry.ts'
import { carryFence, readCarriedBlock } from '../opaque-carry.ts'
import { commonMarkSpelling } from '../emit/adf-to-markdown.ts'
import { failure, faulted, positioned, success, type ConvertErrorPath, type ParseError, type Result, type SourcePosition } from '../../result.ts'
import { languageSlot } from '../code-language.ts'
@@ -161,7 +161,7 @@ function listNode(node: AdfNode, items: readonly Block[][], definitions: LinkDef
}
function codeBlockNode(language: string, text: string, path: ConvertErrorPath, depth: number): Result<AdfNode> {
if (language === carryName) {
if (language === carryFence) {
const carried = readCarriedBlock(text, depth)
if (carried.fault !== undefined) return faulted(carried.fault, path)
return success(carried.value)