Name the position a directive name the other one spells belongs to
CI / gate (push) Successful in 8s

This commit is contained in:
2026-09-01 23:04:23 +02:00
parent 1429f4ce3c
commit 5defa89b8d
7 changed files with 55 additions and 4 deletions
+7
View File
@@ -1,4 +1,5 @@
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 { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
@@ -15,6 +16,12 @@ export function readDirectiveMark(name: string, attributes: DirectiveAttributes,
return success(Object.keys(attrs.value).length === 0 ? { type: name } : { attrs: attrs.value, type: name })
}
export function markSpellingFault(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` }
}
function markdownForm(spelling: MarkSpelling): string | undefined {
switch (spelling.kind) {
case 'code':
+18 -2
View File
@@ -1,5 +1,6 @@
import type { AdfAttributes, AdfMark, AdfNode } from '../../adf/document.ts'
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 { attributeValue, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
@@ -8,9 +9,11 @@ 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 { markSpellingFault } from './directive-marks.ts'
import { marksAttribute, readMarkValues } from '../block-directive-marks.ts'
import { readVocabulary } from './directive-attributes.ts'
import { slotLineEndingFault } from '../directive-syntax.ts'
import { textDirectiveName } from '../text-directive.ts'
export type BlockDirectiveNode = { contentModel: BlockDirective['contentModel']; node: AdfNode }
@@ -24,7 +27,7 @@ export function readBlockDirectiveNode(
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)
if (directive === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name), path)
const argumentKey = blockArgument(name)
const rest = new Map(attributes)
rest.delete(marksAttribute)
@@ -48,7 +51,7 @@ export function readInlineDirectiveNode(
path: ConvertErrorPath,
): Result<AdfNode> {
const directive = inlineDirective(name)
if (directive === undefined) return faulted(unknownDirectiveFault(name), path)
if (directive === undefined) return faulted(blockSpellingFault(name) ?? unknownDirectiveFault(name), path)
const slot = directive.textAttribute
if (slot === undefined && content !== undefined) return failure('unsupported-node-shape', `${name} takes no content`, path)
const elsewhere: Elsewhere | undefined = slot === undefined ? undefined : { key: slot, slot: 'content' }
@@ -64,6 +67,19 @@ export function readInlineDirectiveNode(
return success(namedNode(name, attrs.value, undefined))
}
// A name the other position spells names that spelling, never the code a later MINOR may fill (AGENTS.md §8).
function inlineSpellingFault(name: string): ConvertFault | undefined {
const mark = markSpellingFault(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}` }
}
function blockSpellingFault(name: string): ConvertFault | undefined {
if (blockDirective(name) === undefined) return undefined
return { code: 'unsupported-node-shape', message: `${name} takes the block form, ::${name}` }
}
// spec/flavour.md, Inline nodes: the slot is plain text, its adjacent nodes already merged.
function slotText(content: readonly AdfNode[]): string | undefined {
if (content.length === 0) return ''
@@ -251,6 +251,16 @@ test('names the directive name no node reads back to', () => {
assert.deepEqual(path(markdownToAdf('Part.\n:::x\n')), ['content', 1])
})
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')
assert.equal(content(markdownToAdf('::date {timestamp=1}\n')), 'unsupported-node-shape: date takes the inline form, :date')
assert.equal(content(markdownToAdf(':paragraph[a]\n')), 'unsupported-node-shape: paragraph takes the block form, ::paragraph')
assert.equal(code(markdownToAdf(':::widget\na\n:::\n')), 'unknown-directive-name')
assert.equal(code(markdownToAdf(':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)
+4 -1
View File
@@ -2,14 +2,17 @@ import type { ConvertFault } from '../result.ts'
import type { DirectiveSpan, Read } from './directive-syntax.ts'
import { spellAttributes, spellLeafDirective, spellStringAttribute } from './directive-syntax.ts'
const name = 'text'
export const textDirectiveName = 'text'
const whitespaceRun = /^(?:[ \t]+|\n+)$/
export function spellTextDirective(text: string): string {
const name = textDirectiveName
return spellLeafDirective(name, spellAttributes([[name, spellStringAttribute(text)]]))
}
export function readTextDirective(span: DirectiveSpan): Read<string> | undefined {
const name = textDirectiveName
if (span.name !== name) return undefined
if (span.content !== undefined) return { fault: unsupported(`${name} takes no content`) }
const spelled = span.attributes.get(name)
+7
View File
@@ -8,6 +8,7 @@ import type { AttributeKind, AttributeVocabulary } from './adf/attribute-vocabul
import { blockDirectives } from './adf/block-directives.ts'
import { inlineDirectives } from './adf/inline-directives.ts'
import { markAttributes } from './adf/mark-attributes.ts'
import { textDirectiveName } from './markdown/text-directive.ts'
type Declared = { attributes: AttributeVocabulary }
@@ -96,6 +97,12 @@ test('the inline node table holds the attributes spec/flavour.md gives each node
assert.deepEqual(declarations('Inline nodes'), vocabularies(inlineDirectives))
})
// A name in two tables would make the position a directive is read in ambiguous.
test('no name is spelled in more than one position', () => {
const names = [...Object.keys(blockDirectives), ...Object.keys(inlineDirectives), ...Object.keys(markAttributes), textDirectiveName]
assert.equal(new Set(names).size, names.length)
})
test('the mark table holds the attributes spec/flavour.md gives each mark', () => {
assert.deepEqual(declarations('Marks'), vocabularies(Object.fromEntries(Object.entries(markAttributes).map(([type, attributes]) => [type, { attributes }]))))
})