Derive a block directive's empty content from its node, and let readEmptyKeys own the attrs=empty-stands-alone rule
This commit is contained in:
@@ -16,7 +16,8 @@ export function spellsEmpty(value: DirectiveValue | undefined): boolean {
|
||||
return value?.spelling === emptyValue
|
||||
}
|
||||
|
||||
export function readEmptyKeys(attributes: DirectiveAttributes, keys: readonly EmptyKey[]): Read<EmptyKeysRead> {
|
||||
// `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<EmptyKeysRead> {
|
||||
const rest = new Map(attributes)
|
||||
const empty = new Set<EmptyKey>()
|
||||
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 } }
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AdfMark[] | undefined> = 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<EmptyKey>, path: ConvertErrorPath): Result<AdfNode> {
|
||||
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<EmptyKey>): 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
|
||||
}
|
||||
|
||||
@@ -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<AdfNode> {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user