12c - The block directive forms read and spell !adf: openers and closers #86
@@ -133,9 +133,11 @@ reserved name is a known name, so never that code, and the two the flavour reser
|
|||||||
a form the grammar does not have is a claim code — `!adf:carry`, whose carry is the fence — and a
|
a form the grammar does not have is a claim code — `!adf:carry`, whose carry is the fence — and a
|
||||||
well-formed form in the wrong place is `unsupported-node-shape`, `!adf:listBreak` parting anything
|
well-formed form in the wrong place is `unsupported-node-shape`, `!adf:listBreak` parting anything
|
||||||
but two adjacent lists of one type. What
|
but two adjacent lists of one type. What
|
||||||
the grammar itself refuses stays a claim code, key order among it; a well-formed directive the
|
the grammar itself refuses stays a claim code, key order among it, and a leaf given a body is refused
|
||||||
node tables refuse — an attribute a node does not hold or spells elsewhere, a value outside its
|
at its opener, as a container missing its closer is (the maintainer, 2026-09-16); a well-formed
|
||||||
kind or its canonical spelling, an argument or a body its content model does not take — is
|
directive the node tables refuse — an attribute a node does not hold or spells elsewhere, a value
|
||||||
|
outside its kind or its canonical spelling, an argument, or a body of a shape its content model does
|
||||||
|
not take — is
|
||||||
`unsupported-node-shape`, the emitter's code for the same mismatch read the other way — one code
|
`unsupported-node-shape`, the emitter's code for the same mismatch read the other way — one code
|
||||||
across both directions for good, since the call site knows which direction it called and parting
|
across both directions for good, since the call site knows which direction it called and parting
|
||||||
them after `0.1.0` is MAJOR. `unmappable-html` names the version rather than the element: this one
|
them after `0.1.0` is MAJOR. `unmappable-html` names the version rather than the element: this one
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
directivePrefix,
|
directivePrefix,
|
||||||
spellAttributes,
|
spellAttributes,
|
||||||
spellDirectiveCloser,
|
spellDirectiveCloser,
|
||||||
|
spellDirectiveOpener,
|
||||||
spellInlineDirectiveOpener,
|
spellInlineDirectiveOpener,
|
||||||
spellInlineLeafDirective,
|
spellInlineLeafDirective,
|
||||||
spellJsonAttribute,
|
spellJsonAttribute,
|
||||||
@@ -160,7 +161,7 @@ const hostileArgument = fc.oneof(
|
|||||||
{ arbitrary: fc.oneof(bareToken.map((held) => ` ${held}`), fc.constantFrom(' info', ' a b', ' "a"')), weight: 1 },
|
{ arbitrary: fc.oneof(bareToken.map((held) => ` ${held}`), fc.constantFrom(' info', ' a b', ' "a"')), weight: 1 },
|
||||||
)
|
)
|
||||||
|
|
||||||
function directiveOpener(name: string, argument: string, attrs: string): string {
|
function hostileOpener(name: string, argument: string, attrs: string): string {
|
||||||
return `${directivePrefix}${name}${argument}${attrs === '' ? '' : ` ${attrs}`}`
|
return `${directivePrefix}${name}${argument}${attrs === '' ? '' : ` ${attrs}`}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +311,7 @@ function leafBlocks(hostile: boolean, { destination, inlines, label, oneLine, te
|
|||||||
weight: 1,
|
weight: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
arbitrary: fc.tuple(directiveName, hostileArgument, hostileAttributes).map(([name, argument, attrs]) => directiveOpener(name, argument, attrs)),
|
arbitrary: fc.tuple(directiveName, hostileArgument, hostileAttributes).map(([name, argument, attrs]) => hostileOpener(name, argument, attrs)),
|
||||||
hostile: true,
|
hostile: true,
|
||||||
weight: 1,
|
weight: 1,
|
||||||
},
|
},
|
||||||
@@ -330,13 +331,13 @@ function blockMarkdown(hostile: boolean, { inlines, oneLine }: InlineMarkdown, {
|
|||||||
const tableDirectives = Object.entries(blockDirectives).map(([name, directive]) => {
|
const tableDirectives = Object.entries(blockDirectives).map(([name, directive]) => {
|
||||||
const argument =
|
const argument =
|
||||||
blockArgument(name) === undefined
|
blockArgument(name) === undefined
|
||||||
? fc.constant('')
|
? fc.constant(undefined)
|
||||||
: fc.oneof({ arbitrary: fc.constantFrom(' DONE', ' TODO', ' custom', ' info', ' warning'), weight: 3 }, { arbitrary: bareToken.map((held) => ` ${held}`), weight: 1 })
|
: fc.oneof({ arbitrary: fc.constantFrom('DONE', 'TODO', 'custom', 'info', 'warning'), weight: 3 }, { arbitrary: bareToken, weight: 1 })
|
||||||
const attrs = hostile ? fc.oneof({ arbitrary: tableAttributes(directive.attributes), weight: 4 }, { arbitrary: hostileAttributes, weight: 1 }) : tableAttributes(directive.attributes)
|
const attrs = hostile ? fc.oneof({ arbitrary: tableAttributes(directive.attributes), weight: 4 }, { arbitrary: hostileAttributes, weight: 1 }) : tableAttributes(directive.attributes)
|
||||||
if (directive.contentModel === 'none') return fc.tuple(argument, attrs).map(([held, spelled]) => directiveOpener(name, held, spelled))
|
if (directive.contentModel === 'none') return fc.tuple(argument, attrs).map(([held, spelled]) => spellDirectiveOpener(name, held, spelled))
|
||||||
return fc
|
return fc
|
||||||
.tuple(argument, attrs, bodyByModel[directive.contentModel], hostile ? closerDrift : fc.constant(null))
|
.tuple(argument, attrs, bodyByModel[directive.contentModel], hostile ? closerDrift : fc.constant(null))
|
||||||
.map(([held, spelled, body, closer]) => container(directiveOpener(name, held, spelled), body, closer ?? spellDirectiveCloser(name)))
|
.map(([held, spelled, body, closer]) => container(spellDirectiveOpener(name, held, spelled), body, closer ?? spellDirectiveCloser(name)))
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
block: choose(
|
block: choose(
|
||||||
@@ -365,7 +366,7 @@ function blockMarkdown(hostile: boolean, { inlines, oneLine }: InlineMarkdown, {
|
|||||||
{
|
{
|
||||||
arbitrary: fc
|
arbitrary: fc
|
||||||
.tuple(directiveName, hostileArgument, hostileAttributes, tie('blocks'), closerDrift)
|
.tuple(directiveName, hostileArgument, hostileAttributes, tie('blocks'), closerDrift)
|
||||||
.map(([name, argument, attrs, body, closer]) => container(directiveOpener(name, argument, attrs), body, closer ?? spellDirectiveCloser(name))),
|
.map(([name, argument, attrs, body, closer]) => container(hostileOpener(name, argument, attrs), body, closer ?? spellDirectiveCloser(name))),
|
||||||
hostile: true,
|
hostile: true,
|
||||||
weight: 1,
|
weight: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -77,17 +77,7 @@ export function malformedDirective(message: string): ConvertFault {
|
|||||||
|
|
||||||
export function readDirectiveLine(line: string): Read<DirectiveLine> | undefined {
|
export function readDirectiveLine(line: string): Read<DirectiveLine> | undefined {
|
||||||
if (!claimsDirectivePrefix(line, 0)) return undefined
|
if (!claimsDirectivePrefix(line, 0)) return undefined
|
||||||
const closer = line.charAt(directivePrefix.length) === '/'
|
return line.charAt(directivePrefix.length) === '/' ? readCloserLine(line) : readOpenerLine(line)
|
||||||
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 } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readInlineDirective(text: string, index: number): Read<DirectiveSpan> | undefined {
|
export function readInlineDirective(text: string, index: number): Read<DirectiveSpan> | undefined {
|
||||||
@@ -119,6 +109,10 @@ export function spellDirectiveCloser(name: string): string {
|
|||||||
return `${directivePrefix}/${name}`
|
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 {
|
export function spellJsonAttribute(value: JsonValue): string {
|
||||||
return quote(serializeCanonicalJson(value, 'compact'))
|
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)
|
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 }> {
|
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
|
bareRun.lastIndex = 1
|
||||||
argument = bareRun.exec(rest)?.[0]
|
const argument = bareRun.exec(rest)?.[0]
|
||||||
if (argument === undefined) return { fault: malformedDirective(openerFault) }
|
const attributes = readOpenerAttributes(rest, argument === undefined ? 0 : 1 + argument.length)
|
||||||
cursor = 1 + argument.length
|
if (attributes.fault !== undefined) return { fault: attributes.fault }
|
||||||
}
|
if (!lineEnd.test(rest.slice(attributes.value.end))) return { fault: malformedDirective(openerFault) }
|
||||||
if (rest.charAt(cursor) === ' ' && rest.charAt(cursor + 1) === '{') {
|
return { value: { argument, attributes: attributes.value.attributes } }
|
||||||
const read = readAttributesAt(rest, cursor + 1, false)
|
}
|
||||||
if (read.fault !== undefined) return { fault: read.fault }
|
|
||||||
attributes = read.value.attributes
|
function readOpenerAttributes(rest: string, index: number): Read<Attributes> {
|
||||||
cursor = read.value.end
|
return rest.charAt(index) === ' ' ? readAttributesAt(rest, index + 1, false) : { value: { attributes: noAttributes, end: index } }
|
||||||
}
|
|
||||||
if (!lineEnd.test(rest.slice(cursor))) return { fault: malformedDirective(openerFault) }
|
|
||||||
return { value: { argument, attributes } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function readNestedDirective(text: string, index: number, depth: number): Read<DirectiveSpan> | undefined {
|
function readNestedDirective(text: string, index: number, depth: number): Read<DirectiveSpan> | undefined {
|
||||||
|
|||||||
@@ -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> {
|
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)
|
if (node.text !== undefined) return failure('unsupported-node-shape', `a ${node.type} carries no text: this one holds text`, path)
|
||||||
const content = nodeContent(node)
|
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)
|
||||||
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 (directive.contentModel === 'code') return emitCodeDirective(node, directive, path, depth)
|
if (directive.contentModel === 'code') return emitCodeDirective(node, directive, path, depth)
|
||||||
const opener = spellBlockDirectiveOpener(node, directive)
|
const opener = spellBlockDirectiveOpener(node, directive)
|
||||||
if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
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)
|
const body = directive.contentModel === 'inline' ? emitInlineLine(content, 'paragraph', path) : emitBlocks(content, 'directive', path, depth + 1)
|
||||||
if (!body.ok) return body
|
if (!body.ok) return body
|
||||||
return success(directivePair(node, opener, body.value))
|
return success(directivePair(node, opener, body.value))
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
import type { AdfNode } from '../../adf/document.ts'
|
import type { AdfNode } from '../../adf/document.ts'
|
||||||
import type { BlockDirective } from '../../adf/block-directives.ts'
|
import type { BlockDirective } from '../../adf/block-directives.ts'
|
||||||
import { blockArgument } from '../block-directive-arguments.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 { markValues, marksAttribute } from '../block-directive-marks.ts'
|
||||||
import { nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
import { nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
||||||
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
|
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
|
||||||
|
|
||||||
export function spellBlockDirectiveOpener(node: AdfNode, directive: BlockDirective, spelledByBody: readonly string[] = []): string | undefined {
|
export function spellBlockDirectiveOpener(node: AdfNode, directive: BlockDirective, spelledByBody: readonly string[] = []): string | undefined {
|
||||||
const argumentAttribute = blockArgument(node.type)
|
const argumentAttribute = blockArgument(node.type)
|
||||||
const argument = spellArgument(node, argumentAttribute)
|
const slot = bareArgument(node, argumentAttribute)
|
||||||
if (argument === undefined) return undefined
|
if (slot === undefined) return undefined
|
||||||
const spelled = argumentAttribute === undefined ? spelledByBody : [argumentAttribute, ...spelledByBody]
|
const spelled = argumentAttribute === undefined ? spelledByBody : [argumentAttribute, ...spelledByBody]
|
||||||
const pairs = vocabularyPairs(nodeAttrs(node), directive.attributes, spelled)
|
const pairs = vocabularyPairs(nodeAttrs(node), directive.attributes, spelled)
|
||||||
if (pairs === undefined) return undefined
|
if (pairs === undefined) return undefined
|
||||||
const spelledPairs = spellVocabulary(pairs)
|
const spelledPairs = spellVocabulary(pairs)
|
||||||
const marks = nodeMarks(node)
|
const marks = nodeMarks(node)
|
||||||
if (marks.length > 0) spelledPairs.push([marksAttribute, spellJsonAttribute(markValues(marks))])
|
if (marks.length > 0) spelledPairs.push([marksAttribute, spellJsonAttribute(markValues(marks))])
|
||||||
const attributes = spellAttributes(spelledPairs)
|
return spellDirectiveOpener(node.type, slot.argument, spellAttributes(spelledPairs))
|
||||||
return `${directivePrefix}${node.type}${argument}${attributes === '' ? '' : ` ${attributes}`}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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]
|
const value = argumentAttribute === undefined ? undefined : nodeAttrs(node)[argumentAttribute]
|
||||||
if (value === undefined) return ''
|
if (value === undefined) return { argument: undefined }
|
||||||
if (typeof value !== 'string' || !isBareToken(value)) return undefined
|
return typeof value === 'string' && isBareToken(value) ? { argument: value } : undefined
|
||||||
return ` ${value}`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { directivePrefix } from './directive-syntax.ts'
|
import { spellDirectiveOpener } from './directive-syntax.ts'
|
||||||
|
|
||||||
export const listBreakName = 'listBreak'
|
export const listBreakName = 'listBreak'
|
||||||
|
|
||||||
export const listBreakSpelling = `${directivePrefix}${listBreakName}`
|
export const listBreakSpelling = spellDirectiveOpener(listBreakName, undefined, '')
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ proves 12, 13 spells 11's gaps in 12's grammar, and 12 rewrites code 4b and 4c c
|
|||||||
re-scans the region below it and nested inline directives cost O(depth × content) — 3f's
|
re-scans the region below it and nested inline directives cost O(depth × content) — 3f's
|
||||||
cost, which 3i's slot parse doubles rather than changes in class, bounded by the 500-level
|
cost, which 3i's slot parse doubles rather than changes in class, bounded by the 500-level
|
||||||
guard. §11's scanning rule is the whole argument; the pipeline persona feeds documents
|
guard. §11's scanning rule is the whole argument; the pipeline persona feeds documents
|
||||||
nobody typed.
|
nobody typed. `readDirectiveContent`'s scan splits into named steps with that fix rather
|
||||||
|
than keeping its complexity (the maintainer, 2026-09-16).
|
||||||
- [ ] **4d — What the gate says while it runs (`0.2.1`).** `ci.sh` runs nine legs and announces
|
- [ ] **4d — What the gate says while it runs (`0.2.1`).** `ci.sh` runs nine legs and announces
|
||||||
none of them, so five minutes of a Gitea run read as silence and a hang cannot be told from
|
none of them, so five minutes of a Gitea run read as silence and a hang cannot be told from
|
||||||
a slow pull — the maintainer hit exactly this on the `0.1.0` release. Three causes, each its
|
a slow pull — the maintainer hit exactly this on the `0.1.0` release. Three causes, each its
|
||||||
|
|||||||
Reference in New Issue
Block a user