From c2acee7a1d64f03590b95ad2b8fbf6dede23b1b4 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 21 Sep 2026 21:39:59 +0200 Subject: [PATCH] 21 - the ADF tables carry ADF's nouns --- src/adf-schema.test.ts | 8 +++--- .../{block-directives.ts => block-nodes.ts} | 14 +++++----- .../{inline-directives.ts => inline-nodes.ts} | 8 +++--- src/flavour.test.ts | 10 +++---- src/markdown-property.test.ts | 22 +++++++-------- src/markdown/block-directive-arguments.ts | 2 +- src/markdown/block-directive-forms.ts | 8 +++--- src/markdown/emit/adf-to-markdown.ts | 28 +++++++++---------- src/markdown/emit/block-directive-spelling.ts | 6 ++-- .../emit/inline-directive-spelling.ts | 6 ++-- src/markdown/emit/inline-line.ts | 24 ++++++++-------- src/markdown/parse/directive-nodes.ts | 26 ++++++++--------- src/markdown/parse/inline-content.ts | 4 +-- src/property-harness.ts | 28 +++++++++---------- todo-history.md | 13 +++++++++ todo.md | 6 +--- 16 files changed, 111 insertions(+), 102 deletions(-) rename src/adf/{block-directives.ts => block-nodes.ts} (91%) rename src/adf/{inline-directives.ts => inline-nodes.ts} (78%) diff --git a/src/adf-schema.test.ts b/src/adf-schema.test.ts index 377b007..c0d8325 100644 --- a/src/adf-schema.test.ts +++ b/src/adf-schema.test.ts @@ -7,8 +7,8 @@ import test from 'node:test' import type { AttributeKind, AttributeVocabulary } from './adf/attribute-vocabulary.ts' import { blockArgument } from './markdown/block-directive-arguments.ts' -import { blockDirectives } from './adf/block-directives.ts' -import { inlineDirectives } from './adf/inline-directives.ts' +import { blockNodes } from './adf/block-nodes.ts' +import { inlineNodes } from './adf/inline-nodes.ts' import { markAttributes } from './adf/mark-attributes.ts' type Held = Map> @@ -78,8 +78,8 @@ test("the ADF JSON Schemas hold no type the tables leave unspelled, the pinned c function spelled(): Spelled[] { return [ - ...Object.entries(blockDirectives).map(([type, entry]) => spelledType(type, entry.attributes, blockArgument(type))), - ...Object.entries(inlineDirectives).map(([type, entry]) => spelledType(type, entry.attributes)), + ...Object.entries(blockNodes).map(([type, entry]) => spelledType(type, entry.attributes, blockArgument(type))), + ...Object.entries(inlineNodes).map(([type, entry]) => spelledType(type, entry.attributes)), ...Object.entries(markAttributes).map(([type, attributes]) => spelledType(type, attributes)), ] } diff --git a/src/adf/block-directives.ts b/src/adf/block-nodes.ts similarity index 91% rename from src/adf/block-directives.ts rename to src/adf/block-nodes.ts index 20c10b2..1a39620 100644 --- a/src/adf/block-directives.ts +++ b/src/adf/block-nodes.ts @@ -1,6 +1,6 @@ import type { AttributeVocabulary } from './attribute-vocabulary.ts' -export type BlockDirective = { +export type BlockNode = { attributes: AttributeVocabulary contentModel: 'block' | 'code' | 'inline' | 'none' } @@ -41,7 +41,7 @@ const mediaAttributes: AttributeVocabulary = { const syncBlockAttributes: AttributeVocabulary = { localId: 'string', resourceId: 'string' } -export const blockDirectives = { +export const blockNodes = { blockTaskItem: { attributes: localIdAttributes, contentModel: 'block' }, blockquote: { attributes: localIdAttributes, contentModel: 'block' }, bodiedExtension: { attributes: extensionAttributes, contentModel: 'block' }, @@ -80,14 +80,14 @@ export const blockDirectives = { tableRow: { attributes: localIdAttributes, contentModel: 'block' }, taskItem: { attributes: localIdAttributes, contentModel: 'inline' }, taskList: { attributes: localIdAttributes, contentModel: 'block' }, -} satisfies Readonly> +} satisfies Readonly> -export type BlockType = keyof typeof blockDirectives +export type BlockType = keyof typeof blockNodes -export function blockDirective(type: string): BlockDirective | undefined { - return isBlockType(type) ? blockDirectives[type] : undefined +export function blockNodeNamed(type: string): BlockNode | undefined { + return isBlockType(type) ? blockNodes[type] : undefined } function isBlockType(type: string): type is BlockType { - return Object.hasOwn(blockDirectives, type) + return Object.hasOwn(blockNodes, type) } diff --git a/src/adf/inline-directives.ts b/src/adf/inline-nodes.ts similarity index 78% rename from src/adf/inline-directives.ts rename to src/adf/inline-nodes.ts index 6636b2f..1850018 100644 --- a/src/adf/inline-directives.ts +++ b/src/adf/inline-nodes.ts @@ -1,11 +1,11 @@ import type { AttributeVocabulary } from './attribute-vocabulary.ts' -export type InlineDirective = { +export type InlineNode = { attributes: AttributeVocabulary textAttribute?: string } -export const inlineDirectives: Readonly> = { +export const inlineNodes: Readonly> = { date: { attributes: { localId: 'string', timestamp: 'string' } }, emoji: { attributes: { id: 'string', localId: 'string', shortName: 'string', text: 'string' }, textAttribute: 'text' }, hardBreak: { attributes: { localId: 'string', text: 'string' } }, @@ -27,6 +27,6 @@ export const inlineDirectives: Readonly> = { status: { attributes: { color: 'string', localId: 'string', style: 'string', text: 'string' }, textAttribute: 'text' }, } -export function inlineDirective(type: string): InlineDirective | undefined { - return Object.hasOwn(inlineDirectives, type) ? inlineDirectives[type] : undefined +export function inlineNodeNamed(type: string): InlineNode | undefined { + return Object.hasOwn(inlineNodes, type) ? inlineNodes[type] : undefined } diff --git a/src/flavour.test.ts b/src/flavour.test.ts index bad497a..4577df7 100644 --- a/src/flavour.test.ts +++ b/src/flavour.test.ts @@ -5,8 +5,8 @@ import { readFileSync } from 'node:fs' import test from 'node:test' import type { AttributeKind, AttributeVocabulary } from './adf/attribute-vocabulary.ts' -import { blockDirectives } from './adf/block-directives.ts' -import { inlineDirectives } from './adf/inline-directives.ts' +import { blockNodes } from './adf/block-nodes.ts' +import { inlineNodes } from './adf/inline-nodes.ts' import { markAttributes } from './adf/mark-attributes.ts' import { textDirectiveName } from './markdown/text-directive.ts' @@ -90,16 +90,16 @@ function vocabularies(table: Readonly>): Record { - assert.deepEqual(declarations('Block nodes'), vocabularies(blockDirectives)) + assert.deepEqual(declarations('Block nodes'), vocabularies(blockNodes)) }) test('the inline node table holds the attributes spec/flavour.md gives each node', () => { - assert.deepEqual(declarations('Inline nodes'), vocabularies(inlineDirectives)) + assert.deepEqual(declarations('Inline nodes'), vocabularies(inlineNodes)) }) // 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] + const names = [...Object.keys(blockNodes), ...Object.keys(inlineNodes), ...Object.keys(markAttributes), textDirectiveName] assert.equal(new Set(names).size, names.length) }) diff --git a/src/markdown-property.test.ts b/src/markdown-property.test.ts index 1cc7933..f42e142 100644 --- a/src/markdown-property.test.ts +++ b/src/markdown-property.test.ts @@ -10,7 +10,7 @@ import type { Result } from './result.ts' import { adfDocument, attributes, jsonKey, jsonValue, markdownPieces, propertyRuns, propertyTimeout, textOf } from './property-harness.ts' import { adfToMarkdown } from './markdown/emit/adf-to-markdown.ts' import { blockArgument } from './markdown/block-directive-arguments.ts' -import { blockDirectives } from './adf/block-directives.ts' +import { blockNodes } from './adf/block-nodes.ts' import { carryName } from './markdown/opaque-carry.ts' import { directivePrefix, @@ -24,7 +24,7 @@ import { spellVocabulary, } from './markdown/directive-syntax.ts' import { fencedCodeBlock } from './markdown/commonmark/backtick-runs.ts' -import { inlineDirectives } from './adf/inline-directives.ts' +import { inlineNodes } from './adf/inline-nodes.ts' import { listBreakName } from './markdown/list-break.ts' import { markAttributes } from './adf/mark-attributes.ts' import { markSpelling } from './markdown/mark-spellings.ts' @@ -50,11 +50,11 @@ const fixpointFloor = 600 const gateRuns = 1000 const markdownMarkTypes = new Set(Object.keys(markAttributes).filter((type) => markSpelling(type)?.kind !== 'directive')) -const vocabularies = [...Object.values(blockDirectives).map((directive) => directive.attributes), ...Object.values(inlineDirectives).map((directive) => directive.attributes), ...Object.values(markAttributes)] +const vocabularies = [...Object.values(blockNodes).map((blockNode) => blockNode.attributes), ...Object.values(inlineNodes).map((inlineNode) => inlineNode.attributes), ...Object.values(markAttributes)] const attributeKeys = [ - ...new Set([...vocabularies.flatMap((vocabulary) => Object.keys(vocabulary)), ...Object.keys(blockDirectives).flatMap((type) => blockArgument(type) ?? []), marksAttribute, 'json', textDirectiveName]), + ...new Set([...vocabularies.flatMap((vocabulary) => Object.keys(vocabulary)), ...Object.keys(blockNodes).flatMap((type) => blockArgument(type) ?? []), marksAttribute, 'json', textDirectiveName]), ] -const directiveNames = [...Object.keys(blockDirectives), ...Object.keys(inlineDirectives), ...Object.keys(markAttributes), carryName, listBreakName, textDirectiveName] +const directiveNames = [...Object.keys(blockNodes), ...Object.keys(inlineNodes), ...Object.keys(markAttributes), carryName, listBreakName, textDirectiveName] // Hostile generation reaches refusals; clean generation holds none a single piece would trip, so a whole document reaches the emitter. function choose(hostile: boolean, choices: readonly Choice[], depth?: { depthIdentifier: DepthIdentifier; maxDepth: number }): Arbitrary { @@ -233,9 +233,9 @@ function inlineMarkdown(hostile: boolean): InlineMarkdown { }, { arbitrary: fc.oneof( - ...Object.entries(inlineDirectives).map(([name, directive]) => + ...Object.entries(inlineNodes).map(([name, inlineNode]) => fc - .tuple(directive.textAttribute === undefined ? fc.constant(null) : fc.option(hostile ? word : prose), tableAttributes(directive.attributes, directive.textAttribute)) + .tuple(inlineNode.textAttribute === undefined ? fc.constant(null) : fc.option(hostile ? word : prose), tableAttributes(inlineNode.attributes, inlineNode.textAttribute)) .map(([slot, attrs]) => (slot === null ? spellInlineLeafDirective(name, attrs) : `${spellInlineDirectiveOpener(name)}${slot}]${attrs}`)), ), ...Object.entries(markAttributes) @@ -328,15 +328,15 @@ function blockMarkdown(hostile: boolean, { inlines, oneLine }: InlineMarkdown, { const blockDepth = fc.createDepthIdentifier() const { blocks } = fc.letrec<{ block: string; blocks: string }>((tie) => { const bodyByModel = { block: fc.oneof(tie('blocks'), fc.constant('')), code: fencedCode, inline: fc.oneof(oneLine, fc.constant('')) } - const tableDirectives = Object.entries(blockDirectives).map(([name, directive]) => { + const tableDirectives = Object.entries(blockNodes).map(([name, blockNode]) => { const argument = blockArgument(name) === undefined ? fc.constant(undefined) : 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) - if (directive.contentModel === 'none') return fc.tuple(argument, attrs).map(([held, spelled]) => spellDirectiveOpener(name, held, spelled)) + const attrs = hostile ? fc.oneof({ arbitrary: tableAttributes(blockNode.attributes), weight: 4 }, { arbitrary: hostileAttributes, weight: 1 }) : tableAttributes(blockNode.attributes) + if (blockNode.contentModel === 'none') return fc.tuple(argument, attrs).map(([held, spelled]) => spellDirectiveOpener(name, held, spelled)) return fc - .tuple(argument, attrs, bodyByModel[directive.contentModel], hostile ? closerDrift : fc.constant(null)) + .tuple(argument, attrs, bodyByModel[blockNode.contentModel], hostile ? closerDrift : fc.constant(null)) .map(([held, spelled, body, closer]) => container(spellDirectiveOpener(name, held, spelled), body, closer ?? spellDirectiveCloser(name))) }) return { diff --git a/src/markdown/block-directive-arguments.ts b/src/markdown/block-directive-arguments.ts index 9087549..1d1bf29 100644 --- a/src/markdown/block-directive-arguments.ts +++ b/src/markdown/block-directive-arguments.ts @@ -1,4 +1,4 @@ -import type { BlockType } from '../adf/block-directives.ts' +import type { BlockType } from '../adf/block-nodes.ts' const argumentByType = new Map( Object.entries({ diff --git a/src/markdown/block-directive-forms.ts b/src/markdown/block-directive-forms.ts index 5bf827e..2ce50a0 100644 --- a/src/markdown/block-directive-forms.ts +++ b/src/markdown/block-directive-forms.ts @@ -1,9 +1,9 @@ -import { blockDirective } from '../adf/block-directives.ts' +import { blockNodeNamed } from '../adf/block-nodes.ts' import { listBreakName } from './list-break.ts' export function blockDirectiveForm(name: string): 'container' | 'leaf' | undefined { if (name === listBreakName) return 'leaf' - const directive = blockDirective(name) - if (directive === undefined) return undefined - return directive.contentModel === 'none' ? 'leaf' : 'container' + const blockNode = blockNodeNamed(name) + if (blockNode === undefined) return undefined + return blockNode.contentModel === 'none' ? 'leaf' : 'container' } diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts index 299f248..08e6762 100644 --- a/src/markdown/emit/adf-to-markdown.ts +++ b/src/markdown/emit/adf-to-markdown.ts @@ -1,8 +1,8 @@ import type { AdfDocument, AdfNode } from '../../adf/document.ts' -import type { BlockDirective } from '../../adf/block-directives.ts' +import type { BlockNode } from '../../adf/block-nodes.ts' import { adfDocumentFault, carriesOnly, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts' -import { blockDirective, blockDirectives } from '../../adf/block-directives.ts' import { blockDirectiveForm } from '../block-directive-forms.ts' +import { blockNodeNamed, blockNodes } from '../../adf/block-nodes.ts' import { carriedBlock } from '../opaque-carry.ts' import { emitInlineLine } from './inline-line.ts' import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts' @@ -87,11 +87,11 @@ function interruptsParagraph(node: AdfNode): boolean { } function emitBlock(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result { - const directive = blockDirective(node.type) - if (directive === undefined) return commonMarkLine(carriedBlock(node, path, depth)) + const blockNode = blockNodeNamed(node.type) + if (blockNode === undefined) return commonMarkLine(carriedBlock(node, path, depth)) const readable = readableBlock(node, path, depth, memo) if (readable !== undefined) return readable - return emitDirectiveBlock(node, directive, path, depth, () => walkBlocks(nodeContent(node), path, depth + 1, memo)) + return emitDirectiveBlock(node, blockNode, path, depth, () => walkBlocks(nodeContent(node), path, depth + 1, memo)) } export function commonMarkSpelling(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo): Result | undefined { @@ -143,18 +143,18 @@ function directivePair(node: AdfNode, opener: string, body: string, headroom: nu return { headroom, spelling: 'directive', text: `${opener}\n${body === '' ? '' : `${body}\n`}${spellDirectiveCloser(node.type)}` } } -function emitDirectiveBlock(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath, depth: number, walkBody: () => Result): Result { +function emitDirectiveBlock(node: AdfNode, blockNode: BlockNode, path: ConvertErrorPath, depth: number, walkBody: () => Result): Result { if (node.text !== undefined) return failure('unsupported-node-shape', `a ${node.type} carries no text: this one holds text`, 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 (blockNode.contentModel === 'code') return emitCodeDirective(node, blockNode, path, depth) + const opener = spellBlockDirectiveOpener(node, blockNode) if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth)) - return emitDirectiveBody(node, directive, opener, path, walkBody) + return emitDirectiveBody(node, blockNode, opener, path, walkBody) } -function emitDirectiveBody(node: AdfNode, directive: BlockDirective, opener: string, path: ConvertErrorPath, walkBody: () => Result): Result { +function emitDirectiveBody(node: AdfNode, blockNode: BlockNode, opener: string, path: ConvertErrorPath, walkBody: () => Result): Result { if (blockDirectiveForm(node.type) === 'leaf') return success({ headroom: Number.POSITIVE_INFINITY, spelling: 'directive', text: opener }) - if (directive.contentModel === 'inline') { + if (blockNode.contentModel === 'inline') { const line = emitInlineLine(nodeContent(node), 'paragraph', path) if (!line.ok) return line return success(directivePair(node, opener, line.value)) @@ -184,9 +184,9 @@ function tryCodeBlock(node: AdfNode, path: ConvertErrorPath): Result { +function emitCodeDirective(node: AdfNode, blockNode: BlockNode, path: ConvertErrorPath, depth: number): Result { const slot = languageSlot(nodeAttrs(node)['language']) - const opener = spellBlockDirectiveOpener(node, directive, slot.kind === 'attribute' ? [] : ['language']) + const opener = spellBlockDirectiveOpener(node, blockNode, slot.kind === 'attribute' ? [] : ['language']) if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth)) const text = codeBlockText(node, path) if (!text.ok) return text @@ -246,7 +246,7 @@ function tryList(node: AdfNode, path: ConvertErrorPath, depth: number, memo: Spe const line = tryListItemLines(item.walk.blocks, ordered ? `${start + offset}. ` : '- ') if (line === undefined) { if (headroom < 1) return tooDeep(path) - return emitDirectiveBlock(node, ordered ? blockDirectives.orderedList : blockDirectives.bulletList, path, depth, () => success({ blocks: directiveItems(walked), headroom: headroom - 1 })) + return emitDirectiveBlock(node, ordered ? blockNodes.orderedList : blockNodes.bulletList, path, depth, () => success({ blocks: directiveItems(walked), headroom: headroom - 1 })) } lines.push(line) } diff --git a/src/markdown/emit/block-directive-spelling.ts b/src/markdown/emit/block-directive-spelling.ts index dbead55..317acb9 100644 --- a/src/markdown/emit/block-directive-spelling.ts +++ b/src/markdown/emit/block-directive-spelling.ts @@ -1,17 +1,17 @@ import type { AdfNode } from '../../adf/document.ts' -import type { BlockDirective } from '../../adf/block-directives.ts' +import type { BlockNode } from '../../adf/block-nodes.ts' import { blockArgument } from '../block-directive-arguments.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 { +export function spellBlockDirectiveOpener(node: AdfNode, blockNode: BlockNode, spelledByBody: readonly string[] = []): string | undefined { const argumentAttribute = blockArgument(node.type) 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) + const pairs = vocabularyPairs(nodeAttrs(node), blockNode.attributes, spelled) if (pairs === undefined) return undefined const spelledPairs = spellVocabulary(pairs) const marks = nodeMarks(node) diff --git a/src/markdown/emit/inline-directive-spelling.ts b/src/markdown/emit/inline-directive-spelling.ts index 5d867a4..90b75ad 100644 --- a/src/markdown/emit/inline-directive-spelling.ts +++ b/src/markdown/emit/inline-directive-spelling.ts @@ -1,10 +1,10 @@ import type { AdfNode } from '../../adf/document.ts' -import type { InlineDirective } from '../../adf/inline-directives.ts' +import type { InlineNode } from '../../adf/inline-nodes.ts' import { nodeAttrs } from '../../adf/document.ts' import { spellAttributes, spellVocabulary } from '../directive-syntax.ts' import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts' -export function spellInlineNodeAttributes(node: AdfNode, directive: InlineDirective): string | undefined { - const pairs = vocabularyPairs(nodeAttrs(node), directive.attributes, directive.textAttribute === undefined ? [] : [directive.textAttribute]) +export function spellInlineNodeAttributes(node: AdfNode, inlineNode: InlineNode): string | undefined { + const pairs = vocabularyPairs(nodeAttrs(node), inlineNode.attributes, inlineNode.textAttribute === undefined ? [] : [inlineNode.textAttribute]) return pairs === undefined ? undefined : spellAttributes(spellVocabulary(pairs)) } diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index 1c2034c..79e2efe 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -1,12 +1,12 @@ import type { AdfMark, AdfNode } from '../../adf/document.ts' -import type { InlineDirective } from '../../adf/inline-directives.ts' +import type { InlineNode } from '../../adf/inline-nodes.ts' import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts' import { carriedInline } from '../opaque-carry.ts' import { claimsLine, holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts' import { commonMarkLink, linkHref, markSpelling, spellMarkAttributes } from '../mark-spellings.ts' import { escapeUnbalanced, spellDestination } from '../commonmark/link-syntax.ts' import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts' -import { inlineDirective } from '../../adf/inline-directives.ts' +import { inlineNodeNamed } from '../../adf/inline-nodes.ts' import { largestNesting } from '../../nesting.ts' import { longestBacktickRun } from '../commonmark/backtick-runs.ts' import { nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts' @@ -196,7 +196,7 @@ function nodePath(context: InlineContext, index: number): ConvertErrorPath { function carries(node: AdfNode, carried: ReadonlySet, index: number): boolean { if (carried.has(index)) return true - return node.type !== 'text' && inlineDirective(node.type) === undefined + return node.type !== 'text' && inlineNodeNamed(node.type) === undefined } function emitLeaf(node: AdfNode, context: InlineContext, index: number): Result { @@ -208,27 +208,27 @@ function emitLeaf(node: AdfNode, context: InlineContext, index: number): Result< } const types = nodeMarks(node).map((mark) => mark.type) if (new Set(types).size !== types.length) return failure('unsupported-node-shape', `a ${node.type} node carries one mark type twice`, path) - const directive = inlineDirective(node.type) - if (directive === undefined) return emitText(node, context, index, path) - if (node.type === 'hardBreak') return emitHardBreak(node, directive, context, index, path) - return emitInlineDirective(node, directive, index, path) + const inlineNode = inlineNodeNamed(node.type) + if (inlineNode === undefined) return emitText(node, context, index, path) + if (node.type === 'hardBreak') return emitHardBreak(node, inlineNode, context, index, path) + return emitInlineDirective(node, inlineNode, index, path) } -function emitHardBreak(node: AdfNode, directive: InlineDirective, context: InlineContext, index: number, path: ConvertErrorPath): Result { +function emitHardBreak(node: AdfNode, inlineNode: InlineNode, context: InlineContext, index: number, path: ConvertErrorPath): Result { const empty = refuseContentAndText(node, path) if (!empty.ok) return empty - const attributes = spellInlineNodeAttributes(node, directive) + const attributes = spellInlineNodeAttributes(node, inlineNode) if (attributes === undefined) return success({ carry: { first: index, last: index } }) if (attributes === '' && context.spansLines && !context.atBlockEnd) return success({ segments: [syntax('\\\n')] }) return success({ segments: [syntax(spellInlineLeafDirective('hardBreak', attributes))] }) } -function emitInlineDirective(node: AdfNode, directive: InlineDirective, index: number, path: ConvertErrorPath): Result { +function emitInlineDirective(node: AdfNode, inlineNode: InlineNode, index: number, path: ConvertErrorPath): Result { const empty = refuseContentAndText(node, path) if (!empty.ok) return empty - const attributes = spellInlineNodeAttributes(node, directive) + const attributes = spellInlineNodeAttributes(node, inlineNode) if (attributes === undefined) return success({ carry: { first: index, last: index } }) - const slot = directive.textAttribute === undefined ? undefined : nodeAttrs(node)[directive.textAttribute] + const slot = inlineNode.textAttribute === undefined ? undefined : nodeAttrs(node)[inlineNode.textAttribute] if (slot === undefined) return success({ segments: [syntax(spellInlineLeafDirective(node.type, attributes))] }) if (typeof slot !== 'string') return success({ carry: { first: index, last: index } }) const spans = slotLineEndingFault(node.type, slot) diff --git a/src/markdown/parse/directive-nodes.ts b/src/markdown/parse/directive-nodes.ts index 8ee1a9a..c97cc2e 100644 --- a/src/markdown/parse/directive-nodes.ts +++ b/src/markdown/parse/directive-nodes.ts @@ -1,23 +1,23 @@ import type { AdfAttributes, AdfMark, AdfNode } from '../../adf/document.ts' -import type { BlockDirective } from '../../adf/block-directives.ts' +import type { BlockNode } from '../../adf/block-nodes.ts' import type { ConvertFault } from '../../result.ts' import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts' import type { Elsewhere } from './directive-attributes.ts' import { attributeNestingMessage, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts' import { attributeValue, directivePrefix, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts' import { blockArgument } from '../block-directive-arguments.ts' -import { blockDirective } from '../../adf/block-directives.ts' import { blockDirectiveForm } from '../block-directive-forms.ts' +import { blockNodeNamed } from '../../adf/block-nodes.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 { inlineMarkSpellingFault } from './directive-marks.ts' +import { inlineNodeNamed } from '../../adf/inline-nodes.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 } +export type BlockDirectiveNode = { contentModel: BlockNode['contentModel']; node: AdfNode } export function readBlockDirectiveNode( name: string, @@ -28,13 +28,13 @@ export function readBlockDirectiveNode( if (name === carryName) { return failure('malformed-directive', `the name ${carryName} is reserved for the opaque carry, whose block form is the ${carryName} fence`, path) } - const directive = blockDirective(name) - if (directive === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name), path) + const blockNode = blockNodeNamed(name) + if (blockNode === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name), path) const argumentKey = blockArgument(name) const rest = new Map(attributes) rest.delete(marksAttribute) const elsewhere: Elsewhere | undefined = argumentKey === undefined ? undefined : { key: argumentKey, slot: 'argument' } - const attrs = readVocabulary(name, rest, directive.attributes, elsewhere, path) + const attrs = readVocabulary(name, rest, blockNode.attributes, elsewhere, path) if (!attrs.ok) return attrs if (argument !== undefined) { if (argumentKey === undefined) return failure('unsupported-node-shape', `${name} takes no argument: this one spells one`, path) @@ -43,7 +43,7 @@ export function readBlockDirectiveNode( const spelled = attributes.get(marksAttribute) const marks: Result = spelled === undefined ? success(undefined) : readMarks(name, spelled, path) if (!marks.ok) return marks - return success({ contentModel: directive.contentModel, node: namedNode(name, attrs.value, marks.value) }) + return success({ contentModel: blockNode.contentModel, node: namedNode(name, attrs.value, marks.value) }) } export function readInlineDirectiveNode( @@ -52,12 +52,12 @@ export function readInlineDirectiveNode( content: readonly AdfNode[] | undefined, path: ConvertErrorPath, ): Result { - const directive = inlineDirective(name) - if (directive === undefined) return faulted(blockSpellingFault(name) ?? unknownDirectiveFault(name), path) - const slot = directive.textAttribute + const inlineNode = inlineNodeNamed(name) + if (inlineNode === undefined) return faulted(blockSpellingFault(name) ?? unknownDirectiveFault(name), path) + const slot = inlineNode.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 attrs = readVocabulary(name, attributes, directive.attributes, elsewhere, path) + const attrs = readVocabulary(name, attributes, inlineNode.attributes, elsewhere, path) if (!attrs.ok) return attrs if (slot !== undefined && content !== undefined) { const text = slotText(content) @@ -75,7 +75,7 @@ export function readInlineDirectiveNode( function inlineSpellingFault(name: string): ConvertFault | undefined { const mark = inlineMarkSpellingFault(name) if (mark !== undefined) return mark - if (inlineDirective(name) === undefined && name !== textDirectiveName) return undefined + if (inlineNodeNamed(name) === undefined && name !== textDirectiveName) return undefined return { code: 'unsupported-node-shape', message: `${name} takes the inline form, ${directivePrefix}${name}{โ€ฆ}, never the block form` } } diff --git a/src/markdown/parse/inline-content.ts b/src/markdown/parse/inline-content.ts index 036e7a7..ba53f4e 100644 --- a/src/markdown/parse/inline-content.ts +++ b/src/markdown/parse/inline-content.ts @@ -8,7 +8,7 @@ import { backtickRun, closingBacktickRun } from '../commonmark/backtick-runs.ts' import { commonMarkLink, linkHref } from '../mark-spellings.ts' import { delimiterFlags, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts' import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts' -import { inlineDirective } from '../../adf/inline-directives.ts' +import { inlineNodeNamed } from '../../adf/inline-nodes.ts' import { mergeAdjacentText } from '../../adf/editor-normal.ts' import { noSpans, readInlineDirective } from '../directive-syntax.ts' import { nodeAttrs, nodeMarks } from '../../adf/document.ts' @@ -407,7 +407,7 @@ function imageAlt(inner: readonly Piece[], path: ConvertErrorPath): Result?@[\\]^_`{|}~รฉ\xa0๐ŸŽ‰', 'ab:', 'http://', directivePrefix, `${directivePrefix}a[`, `${directivePrefix}a{`) const nestingCommonMarkShapeWeight = 21 -const spelledTypes = new Set(['text', ...Object.keys(blockDirectives), ...Object.keys(inlineDirectives), ...Object.keys(markAttributes)]) +const spelledTypes = new Set(['text', ...Object.keys(blockNodes), ...Object.keys(inlineNodes), ...Object.keys(markAttributes)]) export function textOf(minLength: number): Arbitrary { return fc.oneof( @@ -94,8 +94,8 @@ const autolinkTextNode = fc .record({ href: fc.tuple(fc.constantFrom('ab:', 'http://'), textOf(0)).map(([scheme, rest]) => `${scheme}${rest}`), marks }) .map(({ href, marks: held }): AdfNode => ({ marks: [...held.filter((outer) => outer.type !== 'link'), { attrs: { href }, type: 'link' }], text: href, type: 'text' })) -const inlineNodes = Object.entries(inlineDirectives).map(([type, directive]) => - fc.record({ attrs: attributes(directive.attributes), marks }).map((held): AdfNode => ({ ...held, type })), +const tableInlineNodes = Object.entries(inlineNodes).map(([type, inlineNode]) => + fc.record({ attrs: attributes(inlineNode.attributes), marks }).map((held): AdfNode => ({ ...held, type })), ) function weighted(arbitraries: readonly Arbitrary[], weight: number): { arbitrary: Arbitrary; weight: number }[] { @@ -112,17 +112,17 @@ const positions = fc.letrec((tie) => { none: fc.constant([]), } const blockMarks = fc.oneof({ arbitrary: fc.constant([]), weight: 4 }, { arbitrary: marks, weight: 1 }) - const blockNodes = Object.entries(blockDirectives).map(([type, directive]) => { + const tableBlockNodes = Object.entries(blockNodes).map(([type, blockNode]) => { const argument = blockArgument(type) - const vocabulary: AttributeVocabulary = argument === undefined ? directive.attributes : { ...directive.attributes, [argument]: 'string' } - const node = fc.record({ attrs: attributes(vocabulary), content: contentByModel[directive.contentModel], marks: blockMarks }).map((held): AdfNode => ({ ...held, type })) - return { leaf: directive.contentModel === 'code' || directive.contentModel === 'none', node } + const vocabulary: AttributeVocabulary = argument === undefined ? blockNode.attributes : { ...blockNode.attributes, [argument]: 'string' } + const node = fc.record({ attrs: attributes(vocabulary), content: contentByModel[blockNode.contentModel], marks: blockMarks }).map((held): AdfNode => ({ ...held, type })) + return { leaf: blockNode.contentModel === 'code' || blockNode.contentModel === 'none', node } }) const unknownNode = fc .record({ attrs: fc.dictionary(jsonKey, jsonValue, { maxKeys: 2, noNullPrototype: true }), content: fc.array(tie('inline'), { depthIdentifier, maxLength: 2 }), marks, type: unknownType }) .map((held): AdfNode => held) - const leafBlocks = blockNodes.filter((entry) => entry.leaf).map((entry) => entry.node) - const containerBlocks = blockNodes.filter((entry) => !entry.leaf).map((entry) => entry.node) + const leafBlocks = tableBlockNodes.filter((entry) => entry.leaf).map((entry) => entry.node) + const containerBlocks = tableBlockNodes.filter((entry) => !entry.leaf).map((entry) => entry.node) const misplacedWeight = 7 const paragraph = fc.oneof({ arbitrary: inlineContent, weight: 3 }, { arbitrary: fc.array(backtickRunNode, { maxLength: 4, minLength: 2 }), weight: 1 }).map((content): AdfNode => ({ content, type: 'paragraph' })) const cell = (type: string) => paragraph.map((held): AdfNode => ({ content: [held], type })) @@ -148,7 +148,7 @@ const positions = fc.letrec((tie) => { { depthIdentifier, depthSize: 'small', maxDepth: 4 }, { arbitrary: fc.oneof(...flatBlocks), weight: flatBlocks.reduce((sum, entry) => sum + entry.weight, 0) }, { arbitrary: fc.oneof(...containerBlocks), weight: containerBlocks.length * 2 }, - { arbitrary: fc.oneof(textNode, ...inlineNodes, unknownNode), weight: misplacedWeight }, + { arbitrary: fc.oneof(textNode, ...tableInlineNodes, unknownNode), weight: misplacedWeight }, { arbitrary: fc.oneof(...nestingCommonMarkShapes), weight: nestingCommonMarkShapes.length * nestingCommonMarkShapeWeight }, ), inline: fc.oneof( @@ -156,8 +156,8 @@ const positions = fc.letrec((tie) => { { arbitrary: textNode, weight: 12 }, { arbitrary: autolinkTextNode, weight: 2 }, { arbitrary: backtickRunNode, weight: 3 }, - { arbitrary: fc.oneof(...inlineNodes), weight: 7 }, - { arbitrary: fc.oneof(...blockNodes.map((entry) => entry.node), unknownNode), weight: 2 }, + { arbitrary: fc.oneof(...tableInlineNodes), weight: 7 }, + { arbitrary: fc.oneof(...tableBlockNodes.map((entry) => entry.node), unknownNode), weight: 2 }, ), } }) diff --git a/todo-history.md b/todo-history.md index 8ab98bd..6b330a1 100644 --- a/todo-history.md +++ b/todo-history.md @@ -994,6 +994,19 @@ The done `todo.md` items in full, as they were written. `todo.md` keeps a one-li go whole. The gate's seconds is confirmed stated nowhere: `docker-runner.sh` measures each leg's elapsed time and no number bounds it. +- [x] **21 โ€” The ADF tables carry ADF's nouns (`0.2.0`).** `adf/block-directives.ts` and + `adf/inline-directives.ts` hold the ADF node tables โ€” `paragraph`, `heading`, `blockquote` + and `rule` among them โ€” under the markdown flavour's word, inside the directory ยง11 forbids + to know a format. Rename to the noun `spec/flavour.md` uses, types and accessors with them. + Before 7 doubles the import sites. + **Done** (2026-09-21): the tables are `adf/block-nodes.ts` and `adf/inline-nodes.ts`, + `blockNodes` and `inlineNodes` holding `BlockNode` and `InlineNode`, read by + `blockNodeNamed` and `inlineNodeNamed` โ€” the accessor named apart from the entry so every + call site's local is the one name `blockNode`/`inlineNode` rather than shadowing its own + lookup. `BlockType` stays: it names a node type, the parallel of `mark-attributes.ts`'s + `MarkType`. `markdown/`'s `blockDirectiveForm`, `blockArgument` and the `spell*Directive*` + family keep the word, naming the spelling rather than the node. + ## 5 โ€” Ship `0.1.0` - [ ] **5 โ€” Ship `0.1.0`.** Only the maintainer's own acts are left (ยง15): make the Gitea repo diff --git a/todo.md b/todo.md index d0cc238..36c7a69 100644 --- a/todo.md +++ b/todo.md @@ -55,11 +55,6 @@ number under the pipeline. upward, so the first raise to the measured figure reddens a run that changed nothing. Make the measurement repeatable, or state the number the floor may be raised to and why it is not the measured one. -- [ ] **21 โ€” The ADF tables carry ADF's nouns (`0.2.0`).** `adf/block-directives.ts` and - `adf/inline-directives.ts` hold the ADF node tables โ€” `paragraph`, `heading`, `blockquote` - and `rule` among them โ€” under the markdown flavour's word, inside the directory ยง11 forbids - to know a format. Rename to the noun `spec/flavour.md` uses, types and accessors with them. - Before 7 doubles the import sites. - [ ] **22 โ€” `LineContainer` sits at the markdown level (`0.2.0`).** Two of the four `parse/` โ†’ `emit/` imports fetch this type from `emit/line-escaping.ts`, camouflaging the two that are the deliberate spelling consultation. Move it, and name those two in ยง11 as the whole @@ -287,6 +282,7 @@ number under the pipeline. - [x] **18 โ€” The subtree the directive spelling asks about (`0.2.0`).** - [x] **19 โ€” A home for what both formats read (`0.2.0`).** - [x] **20 โ€” The give-way channel is unmistakable (`0.2.0`).** +- [x] **21 โ€” The ADF tables carry ADF's nouns (`0.2.0`).** - [x] **28 โ€” `emitLine`'s retry loop cannot spin (`0.2.0`).** - [x] **29 โ€” The README reads raw HTML as refused for good (`0.2.0`).** - [x] **30 โ€” AGENTS.md says each thing once (`0.2.0`).**