diff --git a/src/markdown/empty-keys.ts b/src/markdown/empty-keys.ts index b59008d..4bbbf97 100644 --- a/src/markdown/empty-keys.ts +++ b/src/markdown/empty-keys.ts @@ -16,7 +16,8 @@ export function spellsEmpty(value: DirectiveValue | undefined): boolean { return value?.spelling === emptyValue } -export function readEmptyKeys(attributes: DirectiveAttributes, keys: readonly EmptyKey[]): Read { +// `held` is whether the directive spells an attribute outside {attrs}: an argument or a content slot. +export function readEmptyKeys(type: string, attributes: DirectiveAttributes, keys: readonly EmptyKey[], held: boolean): Read { const rest = new Map(attributes) const empty = new Set() for (const key of keys) { @@ -26,5 +27,6 @@ export function readEmptyKeys(attributes: DirectiveAttributes, keys: readonly Em empty.add(key) rest.delete(key) } + if (empty.has('attrs') && (held || [...rest.keys()].some((key) => key !== 'marks'))) return { fault: unsupportedNodeShape(`${type} spells attrs=empty beside an attribute it holds`) } return { value: { empty, rest } } } diff --git a/src/markdown/parse/directive-marks.ts b/src/markdown/parse/directive-marks.ts index 5d42ecd..22e5c7a 100644 --- a/src/markdown/parse/directive-marks.ts +++ b/src/markdown/parse/directive-marks.ts @@ -13,11 +13,11 @@ export function readDirectiveMark(name: string, attributes: DirectiveAttributes, if (spelling === undefined) return undefined const markdown = markdownForm(spelling) if (markdown !== undefined) return failure('unsupported-node-shape', `${name} is spelled ${markdown}, never as a directive`, path) - const empty = readEmptyKeys(attributes, ['attrs']) + const empty = readEmptyKeys(name, attributes, ['attrs'], false) if (empty.fault !== undefined) return faulted(empty.fault, path) const attrs = readVocabulary(name, empty.value.rest, spelling.attributes, undefined, path) if (!attrs.ok) return attrs - if (empty.value.empty.has('attrs')) return Object.keys(attrs.value).length === 0 ? success({ attrs: {}, type: name }) : failure('unsupported-node-shape', `${name} spells attrs=empty beside an attribute it holds`, path) + if (empty.value.empty.has('attrs')) return success({ attrs: {}, type: name }) return success(Object.keys(attrs.value).length === 0 ? { type: name } : { attrs: attrs.value, type: name }) } diff --git a/src/markdown/parse/directive-nodes.ts b/src/markdown/parse/directive-nodes.ts index 3aab5db..63e1e56 100644 --- a/src/markdown/parse/directive-nodes.ts +++ b/src/markdown/parse/directive-nodes.ts @@ -17,8 +17,7 @@ import { slotLineEndingFault } from '../directive-syntax.ts' import { textBreakName } from '../text-break.ts' import { textDirectiveName } from '../text-directive.ts' -// `emptyContent` is whether the directive spells content=empty, which holds no body. -export type BlockDirectiveNode = { contentModel: BlockNodeModel['contentModel']; emptyContent: boolean; node: AdfNode } +export type BlockDirectiveNode = { contentModel: BlockNodeModel['contentModel']; node: AdfNode } export function readBlockDirectiveNode( name: string, @@ -33,7 +32,7 @@ export function readBlockDirectiveNode( if (model === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name), path) const argumentKey = blockArgument(name) const spelled = attributes.get(marksAttribute) - const empty = readEmptyKeys(attributes, spellsEmpty(spelled) ? ['attrs', 'content', 'marks'] : ['attrs', 'content']) + const empty = readEmptyKeys(name, attributes, spellsEmpty(spelled) ? ['attrs', 'content', 'marks'] : ['attrs', 'content'], argument !== undefined) if (empty.fault !== undefined) return faulted(empty.fault, path) const { rest } = empty.value rest.delete(marksAttribute) @@ -46,9 +45,7 @@ export function readBlockDirectiveNode( } const marks: Result = spelled === undefined || spellsEmpty(spelled) ? success(undefined) : readMarks(name, spelled, path) if (!marks.ok) return marks - const node = namedNode(name, attrs.value, marks.value, empty.value.empty, path) - if (!node.ok) return node - return success({ contentModel: model.contentModel, emptyContent: empty.value.empty.has('content'), node: node.value }) + return success({ contentModel: model.contentModel, node: namedNode(name, attrs.value, marks.value, empty.value.empty) }) } export function readInlineDirectiveNode( @@ -62,7 +59,7 @@ export function readInlineDirectiveNode( const slot = model.textAttribute 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 empty = readEmptyKeys(attributes, ['attrs', 'content', 'marks']) + const empty = readEmptyKeys(name, attributes, ['attrs', 'content', 'marks'], slot !== undefined && content !== undefined) if (empty.fault !== undefined) return faulted(empty.fault, path) const attrs = readVocabulary(name, empty.value.rest, model.attributes, elsewhere, path) if (!attrs.ok) return attrs @@ -75,7 +72,7 @@ export function readInlineDirectiveNode( if (spans !== undefined) return faulted(spans, path) attrs.value[slot] = text } - return namedNode(name, attrs.value, undefined, empty.value.empty, path) + return success(namedNode(name, attrs.value, undefined, empty.value.empty)) } // A name the other position spells names that spelling, never the code a later MINOR may fill (docs/decisions.md §Which code a cause takes). @@ -109,11 +106,9 @@ function readMarks(type: string, spelled: DirectiveValue, path: ConvertErrorPath return success(marks) } -function namedNode(type: string, attrs: AdfAttributes, marks: readonly AdfMark[] | undefined, empty: ReadonlySet, path: ConvertErrorPath): Result { - const held = Object.keys(attrs).length > 0 - if (held && empty.has('attrs')) return failure('unsupported-node-shape', `${type} spells attrs=empty beside an attribute it holds`, path) - const node: AdfNode = held || empty.has('attrs') ? { attrs, type } : { type } +function namedNode(type: string, attrs: AdfAttributes, marks: readonly AdfMark[] | undefined, empty: ReadonlySet): AdfNode { + const node: AdfNode = Object.keys(attrs).length > 0 || empty.has('attrs') ? { attrs, type } : { type } if (empty.has('content')) node.content = [] if (marks !== undefined || empty.has('marks')) node.marks = [...(marks ?? [])] - return success(node) + return node } diff --git a/src/markdown/parse/markdown-to-adf.ts b/src/markdown/parse/markdown-to-adf.ts index 55c42f0..7521fbc 100644 --- a/src/markdown/parse/markdown-to-adf.ts +++ b/src/markdown/parse/markdown-to-adf.ts @@ -229,9 +229,10 @@ function directiveNode(block: DirectiveBlock, reading: Reading, path: ConvertErr } function directiveBody(read: BlockDirectiveNode, blocks: Block[] | undefined, reading: Reading, path: ConvertErrorPath, depth: number): Result { - const { contentModel, emptyContent, node } = read + const { contentModel, node } = read if (blocks === undefined) return success(node) - if (emptyContent) return blocks.length === 0 ? success(node) : failure('unsupported-node-shape', `${node.type} spells content=empty, which holds no body: this one holds one`, path) + // A directive builds a content key only from content=empty, which holds no body. + if (node.content !== undefined) return blocks.length === 0 ? success(node) : failure('unsupported-node-shape', `${node.type} spells content=empty, which holds no body: this one holds one`, path) if (contentModel === 'code') return codeDirectiveNode(node, blocks, path) if (contentModel === 'inline') return inlineBodyNode(node, blocks, reading, path) return containerNode(node, blocks, reading, path, depth)