Answer the architecture pass: the empty {attrs}, the reserved fence and where the reader lives
CI / gate (push) Successful in 5s

This commit is contained in:
2026-09-01 13:42:57 +02:00
parent cd0a4cb0e8
commit f10353dc78
8 changed files with 55 additions and 17 deletions
+10 -7
View File
@@ -12,7 +12,7 @@ import { marksAttribute, readMarkValues } from '../block-directive-marks.ts'
export type BlockDirectiveNode = { contentModel: BlockDirective['contentModel']; node: AdfNode }
type Elsewhere = { key: string; place: string }
type Elsewhere = { key: string; slot: 'argument' | 'content' }
export function readBlockDirectiveNode(
name: string,
@@ -28,7 +28,7 @@ export function readBlockDirectiveNode(
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 elsewhere: Elsewhere | undefined = argumentKey === undefined ? undefined : { key: argumentKey, slot: 'argument' }
const attrs = readVocabulary(name, rest, directive.attributes, elsewhere, path)
if (!attrs.ok) return attrs
if (argument !== undefined) {
@@ -38,7 +38,7 @@ export function readBlockDirectiveNode(
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) })
return success({ contentModel: directive.contentModel, node: namedNode(name, attrs.value, marks.value) })
}
export function readInlineDirectiveNode(span: DirectiveSpan, path: ConvertErrorPath): Result<AdfNode> {
@@ -49,10 +49,10 @@ export function readInlineDirectiveNode(span: DirectiveSpan, path: ConvertErrorP
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 elsewhere: Elsewhere | undefined = slot === undefined ? undefined : { key: slot, slot: 'content' }
const attrs = readVocabulary(span.name, span.attributes, directive.attributes, elsewhere, path)
if (!attrs.ok) return attrs
return success(directiveNode(span.name, attrs.value, undefined))
return success(namedNode(span.name, attrs.value, undefined))
}
function readVocabulary(
@@ -64,7 +64,10 @@ function readVocabulary(
): 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)
if (key === elsewhere?.key) {
const place = elsewhere.slot === 'argument' ? 'as the directive argument' : 'in the content slot'
return failure('unsupported-node-shape', `a ${type} spells its ${key} attribute ${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)
@@ -85,7 +88,7 @@ function readMarks(type: string, spelled: DirectiveValue, path: ConvertErrorPath
return success(marks)
}
function directiveNode(type: string, attrs: AdfAttributes, marks: readonly AdfMark[] | undefined): AdfNode {
function namedNode(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] }
}
@@ -159,6 +159,8 @@ 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)
assert.equal(content(markdownToAdf('```adf\nx\n```\n')), 'malformed-directive: the info string adf is reserved for the opaque carry')
assert.deepEqual(content(markdownToAdf('```adfx\nx\n```\n')), [{ attrs: { language: 'adfx' }, content: [text('x')], type: 'codeBlock' }])
})
test('reads each attribute value as the type its section assigns', () => {
+5 -3
View File
@@ -1,6 +1,7 @@
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
import type { Block, DirectiveBlock } from './blocks.ts'
import type { LinkDefinitions } from './inline-content.ts'
import { carryName } from '../opaque-carry.ts'
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { largestNesting } from '../../nesting.ts'
import { parseBlocks } from './blocks.ts'
@@ -32,7 +33,7 @@ function blockNode(block: Block, definitions: LinkDefinitions, path: ConvertErro
case 'bulletList':
return listNode({ type: 'bulletList' }, block.items, definitions, path, depth)
case 'code':
return success(codeBlockNode(block.language, block.text))
return codeBlockNode(block.language, block.text, path)
case 'directive':
return directiveNode(block, definitions, path, depth)
case 'fault':
@@ -92,9 +93,10 @@ function listNode(node: AdfNode, items: readonly Block[][], definitions: LinkDef
return success({ ...node, content })
}
function codeBlockNode(language: string, text: string): AdfNode {
function codeBlockNode(language: string, text: string, path: ConvertErrorPath): Result<AdfNode> {
if (language === carryName) return failure('malformed-directive', `the info string ${carryName} is reserved for the opaque carry`, path)
const node: AdfNode = language === '' ? { type: 'codeBlock' } : { attrs: { language }, type: 'codeBlock' }
return text === '' ? node : { ...node, content: [{ text, type: 'text' }] }
return success(text === '' ? node : { ...node, content: [{ text, type: 'text' }] })
}
// spec/flavour.md, The CommonMark image: only a plain paragraph gives an image the block it needs.