Read the node tables backwards, a directive to the node and marks it names
CI / gate (push) Successful in 5s
CI / gate (push) Successful in 5s
This commit is contained in:
@@ -90,7 +90,7 @@ test('holds a directive container open until the fence that closes it', () => {
|
||||
assert.deepEqual(parseBlocks(':::panel info {panelColor="#ff0000"}\nPart.\n:::\n').blocks, [
|
||||
{
|
||||
argument: 'info',
|
||||
attributes: new Map([['panelColor', '#ff0000']]),
|
||||
attributes: new Map([['panelColor', { spelling: '"#ff0000"', text: '#ff0000' }]]),
|
||||
blocks: [{ kind: 'paragraph', text: 'Part.' }],
|
||||
kind: 'directive',
|
||||
name: 'panel',
|
||||
|
||||
@@ -31,7 +31,7 @@ export type Block =
|
||||
|
||||
export type ParsedBlocks = { blocks: Block[]; definitions: Map<string, LinkDefinition> }
|
||||
|
||||
type DirectiveBlock = Extract<Block, { kind: 'directive' }>
|
||||
export type DirectiveBlock = Extract<Block, { kind: 'directive' }>
|
||||
|
||||
type ListBlock = Extract<Block, { items: Block[][] }>
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { AdfAttributes, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { AttributeVocabulary } from '../../adf/attribute-vocabulary.ts'
|
||||
import type { BlockDirective } from '../../adf/block-directives.ts'
|
||||
import type { DirectiveAttributes, DirectiveSpan, DirectiveValue } from '../directive-syntax.ts'
|
||||
import { attributeValue, 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'
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { inlineDirective } from '../../adf/inline-directives.ts'
|
||||
import { marksAttribute, readMarkValues } from '../block-directive-marks.ts'
|
||||
|
||||
export type BlockDirectiveNode = { contentModel: BlockDirective['contentModel']; node: AdfNode }
|
||||
|
||||
type Elsewhere = { key: string; place: string }
|
||||
|
||||
export function readBlockDirectiveNode(
|
||||
name: string,
|
||||
argument: string | undefined,
|
||||
attributes: DirectiveAttributes,
|
||||
path: ConvertErrorPath,
|
||||
): Result<BlockDirectiveNode> {
|
||||
if (name === carryName) {
|
||||
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(unknownDirectiveFault(name), path)
|
||||
const argumentKey = blockArgument(name)
|
||||
const rest = new Map(attributes)
|
||||
rest.delete(marksAttribute)
|
||||
const elsewhere = argumentKey === undefined ? undefined : { key: argumentKey, place: 'as the directive argument' }
|
||||
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', `a ${name} takes no argument`, path)
|
||||
attrs.value[argumentKey] = argument
|
||||
}
|
||||
const spelled = attributes.get(marksAttribute)
|
||||
const marks: Result<AdfMark[] | undefined> = spelled === undefined ? success(undefined) : readMarks(name, spelled, path)
|
||||
if (!marks.ok) return marks
|
||||
return success({ contentModel: directive.contentModel, node: directiveNode(name, attrs.value, marks.value) })
|
||||
}
|
||||
|
||||
export function readInlineDirectiveNode(span: DirectiveSpan, path: ConvertErrorPath): Result<AdfNode> {
|
||||
const directive = inlineDirective(span.name)
|
||||
if (directive === undefined) return faulted(unknownDirectiveFault(span.name), path)
|
||||
const slot = directive.textAttribute
|
||||
if (span.content !== undefined) {
|
||||
const message = slot === undefined ? `a ${span.name} takes no content` : `the content slot a ${span.name} spells its ${slot} attribute in is unsupported`
|
||||
return failure('unsupported-node-shape', message, path)
|
||||
}
|
||||
const elsewhere = slot === undefined ? undefined : { key: slot, place: 'in the content slot' }
|
||||
const attrs = readVocabulary(span.name, span.attributes, directive.attributes, elsewhere, path)
|
||||
if (!attrs.ok) return attrs
|
||||
return success(directiveNode(span.name, attrs.value, undefined))
|
||||
}
|
||||
|
||||
function readVocabulary(
|
||||
type: string,
|
||||
attributes: DirectiveAttributes,
|
||||
vocabulary: AttributeVocabulary,
|
||||
elsewhere: Elsewhere | undefined,
|
||||
path: ConvertErrorPath,
|
||||
): Result<AdfAttributes> {
|
||||
const attrs: AdfAttributes = {}
|
||||
for (const [key, spelled] of attributes) {
|
||||
if (key === elsewhere?.key) return failure('unsupported-node-shape', `a ${type} spells its ${key} attribute ${elsewhere.place}`, path)
|
||||
const kind = Object.hasOwn(vocabulary, key) ? vocabulary[key] : undefined
|
||||
if (kind === undefined) return failure('unsupported-node-shape', `a ${type} holds no ${key} attribute`, path)
|
||||
const read = attributeValue(spelled.text, kind)
|
||||
if (read === undefined) return failure('unsupported-node-shape', `the ${key} attribute of a ${type} is a ${kind}`, path)
|
||||
const spelling = spellAttributeValue(read)
|
||||
if (spelling !== spelled.spelling) return failure('unsupported-node-shape', `a ${type} spells its ${key} attribute as ${key}=${spelling}`, path)
|
||||
attrs[key] = read.value
|
||||
}
|
||||
return success(attrs)
|
||||
}
|
||||
|
||||
function readMarks(type: string, spelled: DirectiveValue, path: ConvertErrorPath): Result<AdfMark[]> {
|
||||
const read = attributeValue(spelled.text, '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 a ${type} is its marks array in canonical JSON`, path)
|
||||
}
|
||||
return success(marks)
|
||||
}
|
||||
|
||||
function directiveNode(type: string, attrs: AdfAttributes, marks: readonly AdfMark[] | undefined): AdfNode {
|
||||
const named = Object.keys(attrs).length === 0 ? { type } : { attrs, type }
|
||||
return marks === undefined ? named : { ...named, marks: [...marks] }
|
||||
}
|
||||
@@ -7,7 +7,8 @@ import { delimiterFlags, matchEmphasis, runLength } from '../emphasis-matching.t
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { mergeAdjacentText } from '../../adf/editor-normal.ts'
|
||||
import { normalizeLabel, readInlineTarget, readLabel } from '../link-syntax.ts'
|
||||
import { readInlineDirective, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||
import { readInlineDirective } from '../directive-syntax.ts'
|
||||
import { readInlineDirectiveNode } from './directive-nodes.ts'
|
||||
|
||||
export type InlineContent = { image: AdfNode; nodes?: undefined } | { image?: undefined; nodes: AdfNode[] }
|
||||
|
||||
@@ -144,7 +145,12 @@ function readDirective(scan: Scan, index: number): Result<number> {
|
||||
scan.pending += ':'
|
||||
return success(index + 1)
|
||||
}
|
||||
return faulted(directive.fault ?? unknownDirectiveFault(directive.value.name), scan.path)
|
||||
if (directive.fault !== undefined) return faulted(directive.fault, scan.path)
|
||||
const node = readInlineDirectiveNode(directive.value, scan.path)
|
||||
if (!node.ok) return node
|
||||
flush(scan, false)
|
||||
pushNode(scan, node.value)
|
||||
return success(index + directive.value.length)
|
||||
}
|
||||
|
||||
function flush(scan: Scan, strip: boolean): void {
|
||||
|
||||
@@ -3,6 +3,7 @@ import test from 'node:test'
|
||||
|
||||
import type { AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { Result } from '../../result.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { markdownToAdf } from './markdown-to-adf.ts'
|
||||
|
||||
const em: AdfMark = { type: 'em' }
|
||||
@@ -134,16 +135,91 @@ test('claims a block-level colon run with no directive to parse it', () => {
|
||||
assert.deepEqual(content(markdownToAdf(':: two\n')), [paragraph(':: two')])
|
||||
})
|
||||
|
||||
test('reads the three directive forms, and names the node none of them reads back to', () => {
|
||||
assert.equal(code(markdownToAdf('::rule\n')), 'unknown-directive-name')
|
||||
assert.equal(code(markdownToAdf(' :::panel info\nx\n:::\n')), 'unknown-directive-name')
|
||||
assert.equal(code(markdownToAdf('Part :mention[@A]{id=b1c2}.\n')), 'unknown-directive-name')
|
||||
assert.equal(content(markdownToAdf('::rule\n')), 'unknown-directive-name: the directive name rule reads back to no node')
|
||||
assert.deepEqual(path(markdownToAdf('Part.\n\n::rule\n')), ['content', 1])
|
||||
test('reads the three directive forms into the nodes the tables name', () => {
|
||||
assert.deepEqual(content(markdownToAdf('::rule\n')), [{ type: 'rule' }])
|
||||
assert.deepEqual(content(markdownToAdf('::paragraph\n')), [{ type: 'paragraph' }])
|
||||
assert.deepEqual(content(markdownToAdf(' :::panel info\nPart.\n:::\n')), [
|
||||
{ attrs: { panelType: 'info' }, content: [paragraph('Part.')], type: 'panel' },
|
||||
])
|
||||
assert.deepEqual(content(markdownToAdf(':::blockquote\n:::\n')), [{ type: 'blockquote' }])
|
||||
assert.deepEqual(content(markdownToAdf(':::heading {level=2}\nPart.\n:::\n')), [{ attrs: { level: 2 }, content: [text('Part.')], type: 'heading' }])
|
||||
assert.deepEqual(content(markdownToAdf('Part:hardBreak{}.\n')), [{ content: [text('Part'), hardBreak(), text('.')], type: 'paragraph' }])
|
||||
})
|
||||
|
||||
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.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])
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
test('reads each attribute value as the type its section assigns', () => {
|
||||
assert.deepEqual(content(markdownToAdf('::media {height=10 id=a-1 type=file url="/x y" width="20.5"}\n')), [
|
||||
{ attrs: { height: 10, id: 'a-1', type: 'file', url: '/x y', width: 20.5 }, type: 'media' },
|
||||
])
|
||||
assert.deepEqual(content(markdownToAdf(':::table {isNumberColumnEnabled=true}\n:::\n')), [{ attrs: { isNumberColumnEnabled: true }, type: 'table' }])
|
||||
assert.deepEqual(content(markdownToAdf(':::tableCell {colwidth="[340,420]"}\n:::\n')), [{ attrs: { colwidth: [340, 420] }, type: 'tableCell' }])
|
||||
assert.deepEqual(content(markdownToAdf('::rule {localId=a-1}\n')), [{ attrs: { localId: 'a-1' }, type: 'rule' }])
|
||||
})
|
||||
|
||||
test('reads the reserved marks key as the node array it spells', () => {
|
||||
assert.deepEqual(content(markdownToAdf('::rule {marks="[{\\"type\\":\\"em\\"}]"}\n')), [{ marks: [em], type: 'rule' }])
|
||||
assert.deepEqual(content(markdownToAdf('::rule {localId=a-1 marks="[{\\"attrs\\":{\\"mode\\":\\"wide\\"},\\"type\\":\\"breakout\\"}]"}\n')), [
|
||||
{ attrs: { localId: 'a-1' }, marks: [{ attrs: { mode: 'wide' }, type: 'breakout' }], type: 'rule' },
|
||||
])
|
||||
})
|
||||
|
||||
test('names the marks key no marks array reads back from', () => {
|
||||
const named = 'unsupported-node-shape: the marks attribute of a rule is its marks array in canonical JSON'
|
||||
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)
|
||||
assert.equal(content(markdownToAdf('::rule {marks=x}\n')), named)
|
||||
assert.equal(content(markdownToAdf('::rule {marks="[{\\"attrs\\":{},\\"type\\":\\"em\\"}]"}\n')), named)
|
||||
})
|
||||
|
||||
test('names the attribute a node holds no reading for', () => {
|
||||
assert.equal(content(markdownToAdf('::rule {bogus=1}\n')), 'unsupported-node-shape: a rule holds no bogus attribute')
|
||||
assert.equal(content(markdownToAdf('::media {width=wide}\n')), 'unsupported-node-shape: the width attribute of a media is a number')
|
||||
assert.equal(content(markdownToAdf(':::table {isNumberColumnEnabled=yes}\n:::\n')), 'unsupported-node-shape: the isNumberColumnEnabled attribute of a table is a boolean')
|
||||
assert.equal(content(markdownToAdf('::media {width=true}\n')), 'unsupported-node-shape: the width attribute of a media is a number')
|
||||
assert.equal(content(markdownToAdf(':::tableCell {colwidth="[340,"}\n:::\n')), 'unsupported-node-shape: the colwidth attribute of a tableCell is a 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 a tableCell is a json')
|
||||
assert.equal(content(markdownToAdf(':::panel info {panelType=note}\nx\n:::\n')), 'unsupported-node-shape: a panel spells its panelType attribute as the directive argument')
|
||||
assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: a mention spells its text attribute in the content slot')
|
||||
})
|
||||
|
||||
test('names the attribute value spelled outside the canonical form', () => {
|
||||
assert.equal(content(markdownToAdf('::rule {localId="a-1"}\n')), 'unsupported-node-shape: a rule spells its localId attribute as localId=a-1')
|
||||
assert.equal(content(markdownToAdf('::media {width="20.0"}\n')), 'unsupported-node-shape: a media spells its width attribute as width=20')
|
||||
assert.equal(content(markdownToAdf(':::tableCell {colwidth="[340, 420]"}\n:::\n')), 'unsupported-node-shape: a tableCell spells its colwidth attribute as colwidth="[340,420]"')
|
||||
})
|
||||
|
||||
test('names the argument and the body a node takes no reading for', () => {
|
||||
assert.equal(content(markdownToAdf('::rule x\n')), 'unsupported-node-shape: a rule takes no argument')
|
||||
assert.equal(content(markdownToAdf(':::rule\nPart.\n:::\n')), 'unsupported-node-shape: a rule holds no content')
|
||||
assert.equal(content(markdownToAdf('::bulletList\n')), 'unsupported-node-shape: a bulletList spells its body in the container form :::bulletList')
|
||||
assert.equal(content(markdownToAdf(':::paragraph\n:::\n')), 'unsupported-node-shape: an empty paragraph is the leaf form ::paragraph')
|
||||
assert.equal(content(markdownToAdf(':::paragraph\nOne.\n\nTwo.\n:::\n')), 'unsupported-node-shape: a paragraph takes one paragraph as its body')
|
||||
assert.equal(content(markdownToAdf(':::paragraph\n---\n:::\n')), 'unsupported-node-shape: a paragraph takes one paragraph as its body')
|
||||
assert.equal(content(markdownToAdf(':::codeBlock\n```\nx\n```\n:::\n')), 'unsupported-node-shape: the fenced body of a codeBlock is unsupported')
|
||||
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: a date takes no content')
|
||||
assert.equal(
|
||||
content(markdownToAdf('Part :emoji[x]{shortName=":x:"}.\n')),
|
||||
'unsupported-node-shape: the content slot a emoji spells its text attribute in is unsupported',
|
||||
)
|
||||
})
|
||||
|
||||
test('leaves the colon 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]')])
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||
import type { Block } from './blocks.ts'
|
||||
import type { Block, DirectiveBlock } from './blocks.ts'
|
||||
import type { LinkDefinitions } from './inline-content.ts'
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { parseBlocks } from './blocks.ts'
|
||||
import { parseInlineContent } from './inline-content.ts'
|
||||
import { unknownDirectiveFault } from '../directive-syntax.ts'
|
||||
import { readBlockDirectiveNode } from './directive-nodes.ts'
|
||||
|
||||
export function markdownToAdf(markdown: string): Result<AdfDocument> {
|
||||
const parsed = parseBlocks(markdown)
|
||||
@@ -34,7 +34,7 @@ function blockNode(block: Block, definitions: LinkDefinitions, path: ConvertErro
|
||||
case 'code':
|
||||
return success(codeBlockNode(block.language, block.text))
|
||||
case 'directive':
|
||||
return faulted(unknownDirectiveFault(block.name), path)
|
||||
return directiveNode(block, definitions, path, depth)
|
||||
case 'fault':
|
||||
return faulted(block.fault, path)
|
||||
case 'heading':
|
||||
@@ -44,16 +44,42 @@ function blockNode(block: Block, definitions: LinkDefinitions, path: ConvertErro
|
||||
case 'orderedList':
|
||||
return listNode({ attrs: { order: block.start }, type: 'orderedList' }, block.items, definitions, path, depth)
|
||||
case 'paragraph':
|
||||
return contentNode({ type: 'paragraph' }, block.text, definitions, path)
|
||||
return paragraphNode(block.text, definitions, path)
|
||||
case 'rule':
|
||||
return success({ type: 'rule' })
|
||||
}
|
||||
}
|
||||
|
||||
function directiveNode(block: DirectiveBlock, definitions: LinkDefinitions, path: ConvertErrorPath, depth: number): Result<AdfNode> {
|
||||
const read = readBlockDirectiveNode(block.name, block.argument, block.attributes, path)
|
||||
if (!read.ok) return read
|
||||
const { contentModel, node } = read.value
|
||||
const blocks = block.blocks
|
||||
if (blocks === undefined) {
|
||||
if (contentModel === 'none' || contentModel === 'inline') return success(node)
|
||||
return failure('unsupported-node-shape', `a ${node.type} spells its body in the container form :::${node.type}`, path)
|
||||
}
|
||||
if (contentModel === 'none') return failure('unsupported-node-shape', `a ${node.type} holds no content`, path)
|
||||
if (contentModel === 'code') return failure('unsupported-node-shape', `the fenced body of a ${node.type} is unsupported`, path)
|
||||
if (contentModel === 'block') return containerNode(node, blocks, definitions, path, depth)
|
||||
return inlineBodyNode(node, blocks, definitions, 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} is the leaf form ::${node.type}`, path)
|
||||
const only = blocks.length === 1 ? blocks[0] : undefined
|
||||
if (only?.kind !== 'paragraph') return failure('unsupported-node-shape', `a ${node.type} takes one paragraph as its body`, path)
|
||||
return contentNode(node, only.text, definitions, path)
|
||||
}
|
||||
|
||||
function containerNode(node: AdfNode, blocks: readonly Block[], definitions: LinkDefinitions, path: ConvertErrorPath, depth: number): Result<AdfNode> {
|
||||
const content = blockNodes(blocks, definitions, path, depth + 1)
|
||||
if (!content.ok) return content
|
||||
return success(content.value.length === 0 ? node : { ...node, content: content.value })
|
||||
return success(withContent(node, content.value))
|
||||
}
|
||||
|
||||
function withContent(node: AdfNode, content: readonly AdfNode[]): AdfNode {
|
||||
return content.length === 0 ? node : { ...node, content: [...content] }
|
||||
}
|
||||
|
||||
function listNode(node: AdfNode, items: readonly Block[][], definitions: LinkDefinitions, path: ConvertErrorPath, depth: number): Result<AdfNode> {
|
||||
@@ -71,13 +97,17 @@ function codeBlockNode(language: string, text: string): AdfNode {
|
||||
return text === '' ? node : { ...node, content: [{ text, type: 'text' }] }
|
||||
}
|
||||
|
||||
// spec/flavour.md, The CommonMark image: only a paragraph gives an image the block it needs.
|
||||
function contentNode(node: AdfNode, text: string, definitions: LinkDefinitions, path: ConvertErrorPath): Result<AdfNode> {
|
||||
// spec/flavour.md, The CommonMark image: only a plain paragraph gives an image the block it needs.
|
||||
function paragraphNode(text: string, definitions: LinkDefinitions, path: ConvertErrorPath): Result<AdfNode> {
|
||||
const content = parseInlineContent(text, definitions, path)
|
||||
if (!content.ok) return content
|
||||
const image = content.value.image
|
||||
if (image !== undefined) {
|
||||
return node.type === 'paragraph' ? success(image) : failure('unmappable-image', `no ADF node carries an image inside a ${node.type}`, path)
|
||||
}
|
||||
return success(content.value.nodes.length === 0 ? node : { ...node, content: content.value.nodes })
|
||||
return success(image === undefined ? withContent({ type: 'paragraph' }, content.value.nodes) : image)
|
||||
}
|
||||
|
||||
function contentNode(node: AdfNode, text: string, definitions: LinkDefinitions, path: ConvertErrorPath): Result<AdfNode> {
|
||||
const content = parseInlineContent(text, definitions, path)
|
||||
if (!content.ok) return content
|
||||
if (content.value.image !== undefined) return failure('unmappable-image', `no ADF node carries an image inside a ${node.type}`, path)
|
||||
return success(withContent(node, content.value.nodes))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user