12c: one block opener speller beside its reader, the line readers split along the union, and AGENTS.md records the leaf body
CI / gate (push) Successful in 26s
CI / publish (push) Has been skipped

This commit is contained in:
2026-09-16 19:21:06 +02:00
parent 4aa9e88238
commit ba91900b89
7 changed files with 69 additions and 54 deletions
+37 -28
View File
@@ -77,17 +77,7 @@ export function malformedDirective(message: string): ConvertFault {
export function readDirectiveLine(line: string): Read<DirectiveLine> | undefined {
if (!claimsDirectivePrefix(line, 0)) return undefined
const closer = line.charAt(directivePrefix.length) === '/'
const start = directivePrefix.length + (closer ? 1 : 0)
directiveName.lastIndex = start
const name = directiveName.exec(line)?.[0]
if (name === undefined) return undefined
const rest = line.slice(start + name.length)
if (closer) return lineEnd.test(rest) ? { value: { kind: 'closer', name } } : { fault: malformedDirective(closerFault) }
if (!rest.startsWith(' ') && !lineEnd.test(rest)) return undefined
const opener = readOpenerRest(rest)
if (opener.fault !== undefined) return { fault: opener.fault }
return { value: { argument: opener.value.argument, attributes: opener.value.attributes, kind: 'opener', name } }
return line.charAt(directivePrefix.length) === '/' ? readCloserLine(line) : readOpenerLine(line)
}
export function readInlineDirective(text: string, index: number): Read<DirectiveSpan> | undefined {
@@ -119,6 +109,10 @@ export function spellDirectiveCloser(name: string): string {
return `${directivePrefix}/${name}`
}
export function spellDirectiveOpener(name: string, argument: string | undefined, attributes: string): string {
return `${directivePrefix}${name}${argument === undefined ? '' : ` ${argument}`}${attributes === '' ? '' : ` ${attributes}`}`
}
export function spellJsonAttribute(value: JsonValue): string {
return quote(serializeCanonicalJson(value, 'compact'))
}
@@ -173,24 +167,39 @@ function inlineDirectiveName(text: string, index: number): string | undefined {
return opened === undefined ? undefined : opened.slice(directivePrefix.length, -1)
}
function readCloserLine(line: string): Read<DirectiveLine> | undefined {
const start = directivePrefix.length + 1
const name = readDirectiveName(line, start)
if (name === undefined) return undefined
return lineEnd.test(line.slice(start + name.length)) ? { value: { kind: 'closer', name } } : { fault: malformedDirective(closerFault) }
}
function readOpenerLine(line: string): Read<DirectiveLine> | undefined {
const name = readDirectiveName(line, directivePrefix.length)
if (name === undefined) return undefined
const rest = line.slice(directivePrefix.length + name.length)
if (!rest.startsWith(' ') && !lineEnd.test(rest)) return undefined
const opener = readOpenerRest(rest)
if (opener.fault !== undefined) return { fault: opener.fault }
return { value: { argument: opener.value.argument, attributes: opener.value.attributes, kind: 'opener', name } }
}
function readDirectiveName(text: string, index: number): string | undefined {
directiveName.lastIndex = index
return directiveName.exec(text)?.[0]
}
function readOpenerRest(rest: string): Read<{ argument: string | undefined; attributes: DirectiveAttributes }> {
let cursor = 0
let argument: string | undefined
let attributes = noAttributes
if (rest.charAt(0) === ' ' && rest.charAt(1) !== '{' && !lineEnd.test(rest)) {
bareRun.lastIndex = 1
argument = bareRun.exec(rest)?.[0]
if (argument === undefined) return { fault: malformedDirective(openerFault) }
cursor = 1 + argument.length
}
if (rest.charAt(cursor) === ' ' && rest.charAt(cursor + 1) === '{') {
const read = readAttributesAt(rest, cursor + 1, false)
if (read.fault !== undefined) return { fault: read.fault }
attributes = read.value.attributes
cursor = read.value.end
}
if (!lineEnd.test(rest.slice(cursor))) return { fault: malformedDirective(openerFault) }
return { value: { argument, attributes } }
bareRun.lastIndex = 1
const argument = bareRun.exec(rest)?.[0]
const attributes = readOpenerAttributes(rest, argument === undefined ? 0 : 1 + argument.length)
if (attributes.fault !== undefined) return { fault: attributes.fault }
if (!lineEnd.test(rest.slice(attributes.value.end))) return { fault: malformedDirective(openerFault) }
return { value: { argument, attributes: attributes.value.attributes } }
}
function readOpenerAttributes(rest: string, index: number): Read<Attributes> {
return rest.charAt(index) === ' ' ? readAttributesAt(rest, index + 1, false) : { value: { attributes: noAttributes, end: index } }
}
function readNestedDirective(text: string, index: number, depth: number): Read<DirectiveSpan> | undefined {
+7 -4
View File
@@ -113,13 +113,16 @@ function directivePair(node: AdfNode, opener: string, body: string): EmittedBloc
function emitDirectiveBlock(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
if (node.text !== undefined) return failure('unsupported-node-shape', `a ${node.type} carries no text: this one holds text`, path)
const content = nodeContent(node)
const leaf = blockDirectiveForm(node.type) === 'leaf'
if (leaf && content.length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content: this one holds some`, path)
if (blockDirectiveForm(node.type) === 'leaf' && nodeContent(node).length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content: this one holds some`, path)
if (directive.contentModel === 'code') return emitCodeDirective(node, directive, path, depth)
const opener = spellBlockDirectiveOpener(node, directive)
if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth))
if (leaf) return success({ spelling: 'directive', text: opener })
return emitDirectiveBody(node, directive, opener, path, depth)
}
function emitDirectiveBody(node: AdfNode, directive: BlockDirective, opener: string, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
if (blockDirectiveForm(node.type) === 'leaf') return success({ spelling: 'directive', text: opener })
const content = nodeContent(node)
const body = directive.contentModel === 'inline' ? emitInlineLine(content, 'paragraph', path) : emitBlocks(content, 'directive', path, depth + 1)
if (!body.ok) return body
return success(directivePair(node, opener, body.value))
@@ -1,28 +1,27 @@
import type { AdfNode } from '../../adf/document.ts'
import type { BlockDirective } from '../../adf/block-directives.ts'
import { blockArgument } from '../block-directive-arguments.ts'
import { directivePrefix, isBareToken, spellAttributes, spellJsonAttribute, spellVocabulary } from '../directive-syntax.ts'
import { isBareToken, spellAttributes, spellDirectiveOpener, spellJsonAttribute, spellVocabulary } from '../directive-syntax.ts'
import { markValues, marksAttribute } from '../block-directive-marks.ts'
import { nodeAttrs, nodeMarks } from '../../adf/document.ts'
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
export function spellBlockDirectiveOpener(node: AdfNode, directive: BlockDirective, spelledByBody: readonly string[] = []): string | undefined {
const argumentAttribute = blockArgument(node.type)
const argument = spellArgument(node, argumentAttribute)
if (argument === undefined) return undefined
const slot = bareArgument(node, argumentAttribute)
if (slot === undefined) return undefined
const spelled = argumentAttribute === undefined ? spelledByBody : [argumentAttribute, ...spelledByBody]
const pairs = vocabularyPairs(nodeAttrs(node), directive.attributes, spelled)
if (pairs === undefined) return undefined
const spelledPairs = spellVocabulary(pairs)
const marks = nodeMarks(node)
if (marks.length > 0) spelledPairs.push([marksAttribute, spellJsonAttribute(markValues(marks))])
const attributes = spellAttributes(spelledPairs)
return `${directivePrefix}${node.type}${argument}${attributes === '' ? '' : ` ${attributes}`}`
return spellDirectiveOpener(node.type, slot.argument, spellAttributes(spelledPairs))
}
function spellArgument(node: AdfNode, argumentAttribute: string | undefined): string | undefined {
// `undefined` where the argument slot holds a value no bare token spells.
function bareArgument(node: AdfNode, argumentAttribute: string | undefined): { argument: string | undefined } | undefined {
const value = argumentAttribute === undefined ? undefined : nodeAttrs(node)[argumentAttribute]
if (value === undefined) return ''
if (typeof value !== 'string' || !isBareToken(value)) return undefined
return ` ${value}`
if (value === undefined) return { argument: undefined }
return typeof value === 'string' && isBareToken(value) ? { argument: value } : undefined
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { directivePrefix } from './directive-syntax.ts'
import { spellDirectiveOpener } from './directive-syntax.ts'
export const listBreakName = 'listBreak'
export const listBreakSpelling = `${directivePrefix}${listBreakName}`
export const listBreakSpelling = spellDirectiveOpener(listBreakName, undefined, '')