21 - the ADF tables carry ADF's nouns #119
@@ -7,8 +7,8 @@ import test from 'node:test'
|
|||||||
|
|
||||||
import type { AttributeKind, AttributeVocabulary } from './adf/attribute-vocabulary.ts'
|
import type { AttributeKind, AttributeVocabulary } from './adf/attribute-vocabulary.ts'
|
||||||
import { blockArgument } from './markdown/block-directive-arguments.ts'
|
import { blockArgument } from './markdown/block-directive-arguments.ts'
|
||||||
import { blockDirectives } from './adf/block-directives.ts'
|
import { blockNodes } from './adf/block-nodes.ts'
|
||||||
import { inlineDirectives } from './adf/inline-directives.ts'
|
import { inlineNodes } from './adf/inline-nodes.ts'
|
||||||
import { markAttributes } from './adf/mark-attributes.ts'
|
import { markAttributes } from './adf/mark-attributes.ts'
|
||||||
|
|
||||||
type Held = Map<string, Set<AttributeKind>>
|
type Held = Map<string, Set<AttributeKind>>
|
||||||
@@ -78,8 +78,8 @@ test("the ADF JSON Schemas hold no type the tables leave unspelled, the pinned c
|
|||||||
|
|
||||||
function spelled(): Spelled[] {
|
function spelled(): Spelled[] {
|
||||||
return [
|
return [
|
||||||
...Object.entries(blockDirectives).map(([type, entry]) => spelledType(type, entry.attributes, blockArgument(type))),
|
...Object.entries(blockNodes).map(([type, model]) => spelledType(type, model.attributes, blockArgument(type))),
|
||||||
...Object.entries(inlineDirectives).map(([type, entry]) => spelledType(type, entry.attributes)),
|
...Object.entries(inlineNodes).map(([type, model]) => spelledType(type, model.attributes)),
|
||||||
...Object.entries(markAttributes).map(([type, attributes]) => spelledType(type, attributes)),
|
...Object.entries(markAttributes).map(([type, attributes]) => spelledType(type, attributes)),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { AttributeVocabulary } from './attribute-vocabulary.ts'
|
import type { AttributeVocabulary } from './attribute-vocabulary.ts'
|
||||||
|
|
||||||
export type BlockDirective = {
|
export type BlockNodeModel = {
|
||||||
attributes: AttributeVocabulary
|
attributes: AttributeVocabulary
|
||||||
contentModel: 'block' | 'code' | 'inline' | 'none'
|
contentModel: 'block' | 'code' | 'inline' | 'none'
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ const mediaAttributes: AttributeVocabulary = {
|
|||||||
|
|
||||||
const syncBlockAttributes: AttributeVocabulary = { localId: 'string', resourceId: 'string' }
|
const syncBlockAttributes: AttributeVocabulary = { localId: 'string', resourceId: 'string' }
|
||||||
|
|
||||||
export const blockDirectives = {
|
export const blockNodes = {
|
||||||
blockTaskItem: { attributes: localIdAttributes, contentModel: 'block' },
|
blockTaskItem: { attributes: localIdAttributes, contentModel: 'block' },
|
||||||
blockquote: { attributes: localIdAttributes, contentModel: 'block' },
|
blockquote: { attributes: localIdAttributes, contentModel: 'block' },
|
||||||
bodiedExtension: { attributes: extensionAttributes, contentModel: 'block' },
|
bodiedExtension: { attributes: extensionAttributes, contentModel: 'block' },
|
||||||
@@ -80,14 +80,14 @@ export const blockDirectives = {
|
|||||||
tableRow: { attributes: localIdAttributes, contentModel: 'block' },
|
tableRow: { attributes: localIdAttributes, contentModel: 'block' },
|
||||||
taskItem: { attributes: localIdAttributes, contentModel: 'inline' },
|
taskItem: { attributes: localIdAttributes, contentModel: 'inline' },
|
||||||
taskList: { attributes: localIdAttributes, contentModel: 'block' },
|
taskList: { attributes: localIdAttributes, contentModel: 'block' },
|
||||||
} satisfies Readonly<Record<string, BlockDirective>>
|
} satisfies Readonly<Record<string, BlockNodeModel>>
|
||||||
|
|
||||||
export type BlockType = keyof typeof blockDirectives
|
export type BlockType = keyof typeof blockNodes
|
||||||
|
|
||||||
export function blockDirective(type: string): BlockDirective | undefined {
|
export function blockNodeModel(type: string): BlockNodeModel | undefined {
|
||||||
return isBlockType(type) ? blockDirectives[type] : undefined
|
return isBlockType(type) ? blockNodes[type] : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function isBlockType(type: string): type is BlockType {
|
function isBlockType(type: string): type is BlockType {
|
||||||
return Object.hasOwn(blockDirectives, type)
|
return Object.hasOwn(blockNodes, type)
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { AttributeVocabulary } from './attribute-vocabulary.ts'
|
import type { AttributeVocabulary } from './attribute-vocabulary.ts'
|
||||||
|
|
||||||
export type InlineDirective = {
|
export type InlineNodeModel = {
|
||||||
attributes: AttributeVocabulary
|
attributes: AttributeVocabulary
|
||||||
textAttribute?: string
|
textAttribute?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const inlineDirectives: Readonly<Record<string, InlineDirective>> = {
|
export const inlineNodes: Readonly<Record<string, InlineNodeModel>> = {
|
||||||
date: { attributes: { localId: 'string', timestamp: 'string' } },
|
date: { attributes: { localId: 'string', timestamp: 'string' } },
|
||||||
emoji: { attributes: { id: 'string', localId: 'string', shortName: 'string', text: 'string' }, textAttribute: 'text' },
|
emoji: { attributes: { id: 'string', localId: 'string', shortName: 'string', text: 'string' }, textAttribute: 'text' },
|
||||||
hardBreak: { attributes: { localId: 'string', text: 'string' } },
|
hardBreak: { attributes: { localId: 'string', text: 'string' } },
|
||||||
@@ -27,6 +27,6 @@ export const inlineDirectives: Readonly<Record<string, InlineDirective>> = {
|
|||||||
status: { attributes: { color: 'string', localId: 'string', style: 'string', text: 'string' }, textAttribute: 'text' },
|
status: { attributes: { color: 'string', localId: 'string', style: 'string', text: 'string' }, textAttribute: 'text' },
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inlineDirective(type: string): InlineDirective | undefined {
|
export function inlineNodeModel(type: string): InlineNodeModel | undefined {
|
||||||
return Object.hasOwn(inlineDirectives, type) ? inlineDirectives[type] : undefined
|
return Object.hasOwn(inlineNodes, type) ? inlineNodes[type] : undefined
|
||||||
}
|
}
|
||||||
+5
-5
@@ -5,8 +5,8 @@ import { readFileSync } from 'node:fs'
|
|||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
|
|
||||||
import type { AttributeKind, AttributeVocabulary } from './adf/attribute-vocabulary.ts'
|
import type { AttributeKind, AttributeVocabulary } from './adf/attribute-vocabulary.ts'
|
||||||
import { blockDirectives } from './adf/block-directives.ts'
|
import { blockNodes } from './adf/block-nodes.ts'
|
||||||
import { inlineDirectives } from './adf/inline-directives.ts'
|
import { inlineNodes } from './adf/inline-nodes.ts'
|
||||||
import { markAttributes } from './adf/mark-attributes.ts'
|
import { markAttributes } from './adf/mark-attributes.ts'
|
||||||
import { textDirectiveName } from './markdown/text-directive.ts'
|
import { textDirectiveName } from './markdown/text-directive.ts'
|
||||||
|
|
||||||
@@ -90,16 +90,16 @@ function vocabularies(table: Readonly<Record<string, Declared>>): Record<string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
test('the block node table holds the attributes spec/flavour.md gives each node', () => {
|
test('the block node table holds the attributes spec/flavour.md gives each node', () => {
|
||||||
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', () => {
|
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.
|
// 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', () => {
|
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)
|
assert.equal(new Set(names).size, names.length)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { Result } from './result.ts'
|
|||||||
import { adfDocument, attributes, jsonKey, jsonValue, markdownPieces, propertyRuns, propertyTimeout, textOf } from './property-harness.ts'
|
import { adfDocument, attributes, jsonKey, jsonValue, markdownPieces, propertyRuns, propertyTimeout, textOf } from './property-harness.ts'
|
||||||
import { adfToMarkdown } from './markdown/emit/adf-to-markdown.ts'
|
import { adfToMarkdown } from './markdown/emit/adf-to-markdown.ts'
|
||||||
import { blockArgument } from './markdown/block-directive-arguments.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 { carryName } from './markdown/opaque-carry.ts'
|
||||||
import {
|
import {
|
||||||
directivePrefix,
|
directivePrefix,
|
||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
spellVocabulary,
|
spellVocabulary,
|
||||||
} from './markdown/directive-syntax.ts'
|
} from './markdown/directive-syntax.ts'
|
||||||
import { fencedCodeBlock } from './markdown/commonmark/backtick-runs.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 { listBreakName } from './markdown/list-break.ts'
|
||||||
import { markAttributes } from './adf/mark-attributes.ts'
|
import { markAttributes } from './adf/mark-attributes.ts'
|
||||||
import { markSpelling } from './markdown/mark-spellings.ts'
|
import { markSpelling } from './markdown/mark-spellings.ts'
|
||||||
@@ -50,11 +50,11 @@ const fixpointFloor = 600
|
|||||||
const gateRuns = 1000
|
const gateRuns = 1000
|
||||||
const markdownMarkTypes = new Set(Object.keys(markAttributes).filter((type) => markSpelling(type)?.kind !== 'directive'))
|
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((model) => model.attributes), ...Object.values(inlineNodes).map((model) => model.attributes), ...Object.values(markAttributes)]
|
||||||
const attributeKeys = [
|
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.
|
// 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<string> {
|
function choose(hostile: boolean, choices: readonly Choice[], depth?: { depthIdentifier: DepthIdentifier; maxDepth: number }): Arbitrary<string> {
|
||||||
@@ -233,9 +233,9 @@ function inlineMarkdown(hostile: boolean): InlineMarkdown {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
arbitrary: fc.oneof(
|
arbitrary: fc.oneof(
|
||||||
...Object.entries(inlineDirectives).map(([name, directive]) =>
|
...Object.entries(inlineNodes).map(([name, model]) =>
|
||||||
fc
|
fc
|
||||||
.tuple(directive.textAttribute === undefined ? fc.constant(null) : fc.option(hostile ? word : prose), tableAttributes(directive.attributes, directive.textAttribute))
|
.tuple(model.textAttribute === undefined ? fc.constant(null) : fc.option(hostile ? word : prose), tableAttributes(model.attributes, model.textAttribute))
|
||||||
.map(([slot, attrs]) => (slot === null ? spellInlineLeafDirective(name, attrs) : `${spellInlineDirectiveOpener(name)}${slot}]${attrs}`)),
|
.map(([slot, attrs]) => (slot === null ? spellInlineLeafDirective(name, attrs) : `${spellInlineDirectiveOpener(name)}${slot}]${attrs}`)),
|
||||||
),
|
),
|
||||||
...Object.entries(markAttributes)
|
...Object.entries(markAttributes)
|
||||||
@@ -328,15 +328,15 @@ function blockMarkdown(hostile: boolean, { inlines, oneLine }: InlineMarkdown, {
|
|||||||
const blockDepth = fc.createDepthIdentifier()
|
const blockDepth = fc.createDepthIdentifier()
|
||||||
const { blocks } = fc.letrec<{ block: string; blocks: string }>((tie) => {
|
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 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, model]) => {
|
||||||
const argument =
|
const argument =
|
||||||
blockArgument(name) === undefined
|
blockArgument(name) === undefined
|
||||||
? fc.constant(undefined)
|
? fc.constant(undefined)
|
||||||
: fc.oneof({ arbitrary: fc.constantFrom('DONE', 'TODO', 'custom', 'info', 'warning'), weight: 3 }, { arbitrary: bareToken, 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(model.attributes), weight: 4 }, { arbitrary: hostileAttributes, weight: 1 }) : tableAttributes(model.attributes)
|
||||||
if (directive.contentModel === 'none') return fc.tuple(argument, attrs).map(([held, spelled]) => spellDirectiveOpener(name, held, spelled))
|
if (model.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[model.contentModel], hostile ? closerDrift : fc.constant(null))
|
||||||
.map(([held, spelled, body, closer]) => container(spellDirectiveOpener(name, held, spelled), body, closer ?? spellDirectiveCloser(name)))
|
.map(([held, spelled, body, closer]) => container(spellDirectiveOpener(name, held, spelled), body, closer ?? spellDirectiveCloser(name)))
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { BlockType } from '../adf/block-directives.ts'
|
import type { BlockType } from '../adf/block-nodes.ts'
|
||||||
|
|
||||||
const argumentByType = new Map(
|
const argumentByType = new Map(
|
||||||
Object.entries({
|
Object.entries({
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { blockDirective } from '../adf/block-directives.ts'
|
import { blockNodeModel } from '../adf/block-nodes.ts'
|
||||||
import { listBreakName } from './list-break.ts'
|
import { listBreakName } from './list-break.ts'
|
||||||
|
|
||||||
export function blockDirectiveForm(name: string): 'container' | 'leaf' | undefined {
|
export function blockDirectiveForm(name: string): 'container' | 'leaf' | undefined {
|
||||||
if (name === listBreakName) return 'leaf'
|
if (name === listBreakName) return 'leaf'
|
||||||
const directive = blockDirective(name)
|
const model = blockNodeModel(name)
|
||||||
if (directive === undefined) return undefined
|
if (model === undefined) return undefined
|
||||||
return directive.contentModel === 'none' ? 'leaf' : 'container'
|
return model.contentModel === 'none' ? 'leaf' : 'container'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||||
import type { BlockDirective } from '../../adf/block-directives.ts'
|
import type { BlockNodeModel } from '../../adf/block-nodes.ts'
|
||||||
import { adfDocumentFault, carriesOnly, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.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 { blockDirectiveForm } from '../block-directive-forms.ts'
|
||||||
|
import { blockNodeModel, blockNodes } from '../../adf/block-nodes.ts'
|
||||||
import { carriedBlock } from '../opaque-carry.ts'
|
import { carriedBlock } from '../opaque-carry.ts'
|
||||||
import { emitInlineLine } from './inline-line.ts'
|
import { emitInlineLine } from './inline-line.ts'
|
||||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.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<EmittedBlock> {
|
function emitBlock(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result<EmittedBlock> {
|
||||||
const directive = blockDirective(node.type)
|
const model = blockNodeModel(node.type)
|
||||||
if (directive === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
if (model === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
||||||
const readable = readableBlock(node, path, depth, memo)
|
const readable = readableBlock(node, path, depth, memo)
|
||||||
if (readable !== undefined) return readable
|
if (readable !== undefined) return readable
|
||||||
return emitDirectiveBlock(node, directive, path, depth, () => walkBlocks(nodeContent(node), path, depth + 1, memo))
|
return emitDirectiveBlock(node, model, path, depth, () => walkBlocks(nodeContent(node), path, depth + 1, memo))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function commonMarkSpelling(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo): Result<null> | undefined {
|
export function commonMarkSpelling(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo): Result<null> | 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)}` }
|
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<Walk>): Result<EmittedBlock> {
|
function emitDirectiveBlock(node: AdfNode, model: BlockNodeModel, path: ConvertErrorPath, depth: number, walkBody: () => Result<Walk>): 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)
|
||||||
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 (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)
|
if (model.contentModel === 'code') return emitCodeDirective(node, model, path, depth)
|
||||||
const opener = spellBlockDirectiveOpener(node, directive)
|
const opener = spellBlockDirectiveOpener(node, model)
|
||||||
if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
||||||
return emitDirectiveBody(node, directive, opener, path, walkBody)
|
return emitDirectiveBody(node, model, opener, path, walkBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitDirectiveBody(node: AdfNode, directive: BlockDirective, opener: string, path: ConvertErrorPath, walkBody: () => Result<Walk>): Result<EmittedBlock> {
|
function emitDirectiveBody(node: AdfNode, model: BlockNodeModel, opener: string, path: ConvertErrorPath, walkBody: () => Result<Walk>): Result<EmittedBlock> {
|
||||||
if (blockDirectiveForm(node.type) === 'leaf') return success({ headroom: Number.POSITIVE_INFINITY, spelling: 'directive', text: opener })
|
if (blockDirectiveForm(node.type) === 'leaf') return success({ headroom: Number.POSITIVE_INFINITY, spelling: 'directive', text: opener })
|
||||||
if (directive.contentModel === 'inline') {
|
if (model.contentModel === 'inline') {
|
||||||
const line = emitInlineLine(nodeContent(node), 'paragraph', path)
|
const line = emitInlineLine(nodeContent(node), 'paragraph', path)
|
||||||
if (!line.ok) return line
|
if (!line.ok) return line
|
||||||
return success(directivePair(node, opener, line.value))
|
return success(directivePair(node, opener, line.value))
|
||||||
@@ -184,9 +184,9 @@ function tryCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBloc
|
|||||||
return success(commonMarkText(fencedCodeBlock(slot.kind === 'fence' ? slot.info : '', text.value)))
|
return success(commonMarkText(fencedCodeBlock(slot.kind === 'fence' ? slot.info : '', text.value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitCodeDirective(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
|
function emitCodeDirective(node: AdfNode, model: BlockNodeModel, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
|
||||||
const slot = languageSlot(nodeAttrs(node)['language'])
|
const slot = languageSlot(nodeAttrs(node)['language'])
|
||||||
const opener = spellBlockDirectiveOpener(node, directive, slot.kind === 'attribute' ? [] : ['language'])
|
const opener = spellBlockDirectiveOpener(node, model, slot.kind === 'attribute' ? [] : ['language'])
|
||||||
if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
||||||
const text = codeBlockText(node, path)
|
const text = codeBlockText(node, path)
|
||||||
if (!text.ok) return text
|
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}. ` : '- ')
|
const line = tryListItemLines(item.walk.blocks, ordered ? `${start + offset}. ` : '- ')
|
||||||
if (line === undefined) {
|
if (line === undefined) {
|
||||||
if (headroom < 1) return tooDeep(path)
|
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)
|
lines.push(line)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import type { AdfNode } from '../../adf/document.ts'
|
import type { AdfNode } from '../../adf/document.ts'
|
||||||
import type { BlockDirective } from '../../adf/block-directives.ts'
|
import type { BlockNodeModel } from '../../adf/block-nodes.ts'
|
||||||
import { blockArgument } from '../block-directive-arguments.ts'
|
import { blockArgument } from '../block-directive-arguments.ts'
|
||||||
import { isBareToken, spellAttributes, spellDirectiveOpener, 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, model: BlockNodeModel, spelledByBody: readonly string[] = []): string | undefined {
|
||||||
const argumentAttribute = blockArgument(node.type)
|
const argumentAttribute = blockArgument(node.type)
|
||||||
const slot = bareArgument(node, argumentAttribute)
|
const slot = bareArgument(node, argumentAttribute)
|
||||||
if (slot === 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), model.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)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { AdfNode } from '../../adf/document.ts'
|
import type { AdfNode } from '../../adf/document.ts'
|
||||||
import type { InlineDirective } from '../../adf/inline-directives.ts'
|
import type { InlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||||
import { nodeAttrs } from '../../adf/document.ts'
|
import { nodeAttrs } from '../../adf/document.ts'
|
||||||
import { spellAttributes, spellVocabulary } from '../directive-syntax.ts'
|
import { spellAttributes, spellVocabulary } from '../directive-syntax.ts'
|
||||||
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
|
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
|
||||||
|
|
||||||
export function spellInlineNodeAttributes(node: AdfNode, directive: InlineDirective): string | undefined {
|
export function spellInlineNodeAttributes(node: AdfNode, model: InlineNodeModel): string | undefined {
|
||||||
const pairs = vocabularyPairs(nodeAttrs(node), directive.attributes, directive.textAttribute === undefined ? [] : [directive.textAttribute])
|
const pairs = vocabularyPairs(nodeAttrs(node), model.attributes, model.textAttribute === undefined ? [] : [model.textAttribute])
|
||||||
return pairs === undefined ? undefined : spellAttributes(spellVocabulary(pairs))
|
return pairs === undefined ? undefined : spellAttributes(spellVocabulary(pairs))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
||||||
import type { InlineDirective } from '../../adf/inline-directives.ts'
|
import type { InlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||||
import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts'
|
import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts'
|
||||||
import { carriedInline } from '../opaque-carry.ts'
|
import { carriedInline } from '../opaque-carry.ts'
|
||||||
import { claimsLine, holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts'
|
import { claimsLine, holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts'
|
||||||
import { commonMarkLink, linkHref, markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
import { commonMarkLink, linkHref, markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
||||||
import { escapeUnbalanced, spellDestination } from '../commonmark/link-syntax.ts'
|
import { escapeUnbalanced, spellDestination } from '../commonmark/link-syntax.ts'
|
||||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { inlineDirective } from '../../adf/inline-directives.ts'
|
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { longestBacktickRun } from '../commonmark/backtick-runs.ts'
|
import { longestBacktickRun } from '../commonmark/backtick-runs.ts'
|
||||||
import { nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.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<number>, index: number): boolean {
|
function carries(node: AdfNode, carried: ReadonlySet<number>, index: number): boolean {
|
||||||
if (carried.has(index)) return true
|
if (carried.has(index)) return true
|
||||||
return node.type !== 'text' && inlineDirective(node.type) === undefined
|
return node.type !== 'text' && inlineNodeModel(node.type) === undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitLeaf(node: AdfNode, context: InlineContext, index: number): Result<Emission> {
|
function emitLeaf(node: AdfNode, context: InlineContext, index: number): Result<Emission> {
|
||||||
@@ -208,27 +208,27 @@ function emitLeaf(node: AdfNode, context: InlineContext, index: number): Result<
|
|||||||
}
|
}
|
||||||
const types = nodeMarks(node).map((mark) => mark.type)
|
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)
|
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)
|
const model = inlineNodeModel(node.type)
|
||||||
if (directive === undefined) return emitText(node, context, index, path)
|
if (model === undefined) return emitText(node, context, index, path)
|
||||||
if (node.type === 'hardBreak') return emitHardBreak(node, directive, context, index, path)
|
if (node.type === 'hardBreak') return emitHardBreak(node, model, context, index, path)
|
||||||
return emitInlineDirective(node, directive, index, path)
|
return emitInlineDirective(node, model, index, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitHardBreak(node: AdfNode, directive: InlineDirective, context: InlineContext, index: number, path: ConvertErrorPath): Result<Emission> {
|
function emitHardBreak(node: AdfNode, model: InlineNodeModel, context: InlineContext, index: number, path: ConvertErrorPath): Result<Emission> {
|
||||||
const empty = refuseContentAndText(node, path)
|
const empty = refuseContentAndText(node, path)
|
||||||
if (!empty.ok) return empty
|
if (!empty.ok) return empty
|
||||||
const attributes = spellInlineNodeAttributes(node, directive)
|
const attributes = spellInlineNodeAttributes(node, model)
|
||||||
if (attributes === undefined) return success({ carry: { first: index, last: index } })
|
if (attributes === undefined) return success({ carry: { first: index, last: index } })
|
||||||
if (attributes === '' && context.spansLines && !context.atBlockEnd) return success({ segments: [syntax('\\\n')] })
|
if (attributes === '' && context.spansLines && !context.atBlockEnd) return success({ segments: [syntax('\\\n')] })
|
||||||
return success({ segments: [syntax(spellInlineLeafDirective('hardBreak', attributes))] })
|
return success({ segments: [syntax(spellInlineLeafDirective('hardBreak', attributes))] })
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitInlineDirective(node: AdfNode, directive: InlineDirective, index: number, path: ConvertErrorPath): Result<Emission> {
|
function emitInlineDirective(node: AdfNode, model: InlineNodeModel, index: number, path: ConvertErrorPath): Result<Emission> {
|
||||||
const empty = refuseContentAndText(node, path)
|
const empty = refuseContentAndText(node, path)
|
||||||
if (!empty.ok) return empty
|
if (!empty.ok) return empty
|
||||||
const attributes = spellInlineNodeAttributes(node, directive)
|
const attributes = spellInlineNodeAttributes(node, model)
|
||||||
if (attributes === undefined) return success({ carry: { first: index, last: index } })
|
if (attributes === undefined) return success({ carry: { first: index, last: index } })
|
||||||
const slot = directive.textAttribute === undefined ? undefined : nodeAttrs(node)[directive.textAttribute]
|
const slot = model.textAttribute === undefined ? undefined : nodeAttrs(node)[model.textAttribute]
|
||||||
if (slot === undefined) return success({ segments: [syntax(spellInlineLeafDirective(node.type, attributes))] })
|
if (slot === undefined) return success({ segments: [syntax(spellInlineLeafDirective(node.type, attributes))] })
|
||||||
if (typeof slot !== 'string') return success({ carry: { first: index, last: index } })
|
if (typeof slot !== 'string') return success({ carry: { first: index, last: index } })
|
||||||
const spans = slotLineEndingFault(node.type, slot)
|
const spans = slotLineEndingFault(node.type, slot)
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import type { AdfAttributes, AdfMark, AdfNode } from '../../adf/document.ts'
|
import type { AdfAttributes, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||||
import type { BlockDirective } from '../../adf/block-directives.ts'
|
import type { BlockNodeModel } from '../../adf/block-nodes.ts'
|
||||||
import type { ConvertFault } from '../../result.ts'
|
import type { ConvertFault } from '../../result.ts'
|
||||||
import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
|
import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
|
||||||
import type { Elsewhere } from './directive-attributes.ts'
|
import type { Elsewhere } from './directive-attributes.ts'
|
||||||
import { attributeNestingMessage, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
import { attributeNestingMessage, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||||
import { attributeValue, directivePrefix, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
import { attributeValue, directivePrefix, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||||
import { blockArgument } from '../block-directive-arguments.ts'
|
import { blockArgument } from '../block-directive-arguments.ts'
|
||||||
import { blockDirective } from '../../adf/block-directives.ts'
|
|
||||||
import { blockDirectiveForm } from '../block-directive-forms.ts'
|
import { blockDirectiveForm } from '../block-directive-forms.ts'
|
||||||
|
import { blockNodeModel } from '../../adf/block-nodes.ts'
|
||||||
import { carryName } from '../opaque-carry.ts'
|
import { carryName } from '../opaque-carry.ts'
|
||||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.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 { inlineMarkSpellingFault } from './directive-marks.ts'
|
||||||
|
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||||
import { marksAttribute, readMarkValues } from '../block-directive-marks.ts'
|
import { marksAttribute, readMarkValues } from '../block-directive-marks.ts'
|
||||||
import { readVocabulary } from './directive-attributes.ts'
|
import { readVocabulary } from './directive-attributes.ts'
|
||||||
import { slotLineEndingFault } from '../directive-syntax.ts'
|
import { slotLineEndingFault } from '../directive-syntax.ts'
|
||||||
import { textDirectiveName } from '../text-directive.ts'
|
import { textDirectiveName } from '../text-directive.ts'
|
||||||
|
|
||||||
export type BlockDirectiveNode = { contentModel: BlockDirective['contentModel']; node: AdfNode }
|
export type BlockDirectiveNode = { contentModel: BlockNodeModel['contentModel']; node: AdfNode }
|
||||||
|
|
||||||
export function readBlockDirectiveNode(
|
export function readBlockDirectiveNode(
|
||||||
name: string,
|
name: string,
|
||||||
@@ -28,13 +28,13 @@ export function readBlockDirectiveNode(
|
|||||||
if (name === carryName) {
|
if (name === carryName) {
|
||||||
return failure('malformed-directive', `the name ${carryName} is reserved for the opaque carry, whose block form is the ${carryName} fence`, path)
|
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)
|
const model = blockNodeModel(name)
|
||||||
if (directive === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name), path)
|
if (model === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name), path)
|
||||||
const argumentKey = blockArgument(name)
|
const argumentKey = blockArgument(name)
|
||||||
const rest = new Map(attributes)
|
const rest = new Map(attributes)
|
||||||
rest.delete(marksAttribute)
|
rest.delete(marksAttribute)
|
||||||
const elsewhere: Elsewhere | undefined = argumentKey === undefined ? undefined : { key: argumentKey, slot: 'argument' }
|
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, model.attributes, elsewhere, path)
|
||||||
if (!attrs.ok) return attrs
|
if (!attrs.ok) return attrs
|
||||||
if (argument !== undefined) {
|
if (argument !== undefined) {
|
||||||
if (argumentKey === undefined) return failure('unsupported-node-shape', `${name} takes no argument: this one spells one`, path)
|
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 spelled = attributes.get(marksAttribute)
|
||||||
const marks: Result<AdfMark[] | undefined> = spelled === undefined ? success(undefined) : readMarks(name, spelled, path)
|
const marks: Result<AdfMark[] | undefined> = spelled === undefined ? success(undefined) : readMarks(name, spelled, path)
|
||||||
if (!marks.ok) return marks
|
if (!marks.ok) return marks
|
||||||
return success({ contentModel: directive.contentModel, node: namedNode(name, attrs.value, marks.value) })
|
return success({ contentModel: model.contentModel, node: namedNode(name, attrs.value, marks.value) })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readInlineDirectiveNode(
|
export function readInlineDirectiveNode(
|
||||||
@@ -52,12 +52,12 @@ export function readInlineDirectiveNode(
|
|||||||
content: readonly AdfNode[] | undefined,
|
content: readonly AdfNode[] | undefined,
|
||||||
path: ConvertErrorPath,
|
path: ConvertErrorPath,
|
||||||
): Result<AdfNode> {
|
): Result<AdfNode> {
|
||||||
const directive = inlineDirective(name)
|
const model = inlineNodeModel(name)
|
||||||
if (directive === undefined) return faulted(blockSpellingFault(name) ?? unknownDirectiveFault(name), path)
|
if (model === undefined) return faulted(blockSpellingFault(name) ?? unknownDirectiveFault(name), path)
|
||||||
const slot = directive.textAttribute
|
const slot = model.textAttribute
|
||||||
if (slot === undefined && content !== undefined) return failure('unsupported-node-shape', `${name} takes no content: this one holds some`, path)
|
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 elsewhere: Elsewhere | undefined = slot === undefined ? undefined : { key: slot, slot: 'content' }
|
||||||
const attrs = readVocabulary(name, attributes, directive.attributes, elsewhere, path)
|
const attrs = readVocabulary(name, attributes, model.attributes, elsewhere, path)
|
||||||
if (!attrs.ok) return attrs
|
if (!attrs.ok) return attrs
|
||||||
if (slot !== undefined && content !== undefined) {
|
if (slot !== undefined && content !== undefined) {
|
||||||
const text = slotText(content)
|
const text = slotText(content)
|
||||||
@@ -75,7 +75,7 @@ export function readInlineDirectiveNode(
|
|||||||
function inlineSpellingFault(name: string): ConvertFault | undefined {
|
function inlineSpellingFault(name: string): ConvertFault | undefined {
|
||||||
const mark = inlineMarkSpellingFault(name)
|
const mark = inlineMarkSpellingFault(name)
|
||||||
if (mark !== undefined) return mark
|
if (mark !== undefined) return mark
|
||||||
if (inlineDirective(name) === undefined && name !== textDirectiveName) return undefined
|
if (inlineNodeModel(name) === undefined && name !== textDirectiveName) return undefined
|
||||||
return { code: 'unsupported-node-shape', message: `${name} takes the inline form, ${directivePrefix}${name}{…}, never the block form` }
|
return { code: 'unsupported-node-shape', message: `${name} takes the inline form, ${directivePrefix}${name}{…}, never the block form` }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { backtickRun, closingBacktickRun } from '../commonmark/backtick-runs.ts'
|
|||||||
import { commonMarkLink, linkHref } from '../mark-spellings.ts'
|
import { commonMarkLink, linkHref } from '../mark-spellings.ts'
|
||||||
import { delimiterFlags, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts'
|
import { delimiterFlags, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts'
|
||||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { inlineDirective } from '../../adf/inline-directives.ts'
|
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||||
import { mergeAdjacentText } from '../../adf/editor-normal.ts'
|
import { mergeAdjacentText } from '../../adf/editor-normal.ts'
|
||||||
import { noSpans, readInlineDirective } from '../directive-syntax.ts'
|
import { noSpans, readInlineDirective } from '../directive-syntax.ts'
|
||||||
import { nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
import { nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
||||||
@@ -407,7 +407,7 @@ function imageAlt(inner: readonly Piece[], path: ConvertErrorPath): Result<strin
|
|||||||
// spec/flavour.md, The CommonMark image: the description's plain text, the content slot included.
|
// spec/flavour.md, The CommonMark image: the description's plain text, the content slot included.
|
||||||
function altText(node: AdfNode): string {
|
function altText(node: AdfNode): string {
|
||||||
if (node.type === 'hardBreak') return ' '
|
if (node.type === 'hardBreak') return ' '
|
||||||
const slot = inlineDirective(node.type)?.textAttribute
|
const slot = inlineNodeModel(node.type)?.textAttribute
|
||||||
const spelled = slot === undefined ? undefined : nodeAttrs(node)[slot]
|
const spelled = slot === undefined ? undefined : nodeAttrs(node)[slot]
|
||||||
return typeof spelled === 'string' ? spelled : (node.text ?? '')
|
return typeof spelled === 'string' ? spelled : (node.text ?? '')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ const documentStart: SourcePosition = { line: 1, offset: 0 }
|
|||||||
|
|
||||||
export function markdownToAdf(markdown: string): Result<AdfDocument, ParseError> {
|
export function markdownToAdf(markdown: string): Result<AdfDocument, ParseError> {
|
||||||
const parsed = parseBlocks(markdown)
|
const parsed = parseBlocks(markdown)
|
||||||
const content = positioned(blockNodes(parsed.blocks, parsed.definitions, [], 0, new Map()), documentStart)
|
const content = positioned(readBlocks(parsed.blocks, parsed.definitions, [], 0, new Map()), documentStart)
|
||||||
if (!content.ok) return content
|
if (!content.ok) return content
|
||||||
return success(content.value.length === 0 ? { type: 'doc', version: 1 } : { content: content.value, type: 'doc', version: 1 })
|
return success(content.value.length === 0 ? { type: 'doc', version: 1 } : { content: content.value, type: 'doc', version: 1 })
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockNodes(blocks: readonly Block[], definitions: LinkDefinitions, path: ConvertErrorPath, depth: number, memo: SpellingMemo): Result<AdfNode[]> {
|
function readBlocks(blocks: readonly Block[], definitions: LinkDefinitions, path: ConvertErrorPath, depth: number, memo: SpellingMemo): Result<AdfNode[]> {
|
||||||
if (depth > largestNesting) return failure('unsupported-nesting-depth', `the input nests deeper than the ${largestNesting} levels the parser carries`, path)
|
if (depth > largestNesting) return failure('unsupported-nesting-depth', `the input nests deeper than the ${largestNesting} levels the parser carries`, path)
|
||||||
const content: AdfNode[] = []
|
const content: AdfNode[] = []
|
||||||
for (const [index, block] of blocks.entries()) {
|
for (const [index, block] of blocks.entries()) {
|
||||||
@@ -35,7 +35,7 @@ function blockNodes(blocks: readonly Block[], definitions: LinkDefinitions, path
|
|||||||
if (fault !== undefined) return positioned(faulted(fault, nodePath), block.position)
|
if (fault !== undefined) return positioned(faulted(fault, nodePath), block.position)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const node = positioned(blockNode(block, definitions, nodePath, depth, memo), block.position)
|
const node = positioned(readBlock(block, definitions, nodePath, depth, memo), block.position)
|
||||||
if (!node.ok) return node
|
if (!node.ok) return node
|
||||||
content.push(node.value)
|
content.push(node.value)
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ function partsFault(): ConvertFault {
|
|||||||
return unsupportedNodeShape(`${listBreakName} parts two adjacent lists of one type: this one parts something else`)
|
return unsupportedNodeShape(`${listBreakName} parts two adjacent lists of one type: this one parts something else`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockNode(block: Block, definitions: LinkDefinitions, path: ConvertErrorPath, depth: number, memo: SpellingMemo): Result<AdfNode> {
|
function readBlock(block: Block, definitions: LinkDefinitions, path: ConvertErrorPath, depth: number, memo: SpellingMemo): Result<AdfNode> {
|
||||||
switch (block.kind) {
|
switch (block.kind) {
|
||||||
case 'blockquote':
|
case 'blockquote':
|
||||||
return containerNode({ type: 'blockquote' }, block.blocks, definitions, path, depth, memo)
|
return containerNode({ type: 'blockquote' }, block.blocks, definitions, path, depth, memo)
|
||||||
@@ -138,7 +138,7 @@ function inlineBodyNode(node: AdfNode, blocks: readonly Block[], definitions: Li
|
|||||||
}
|
}
|
||||||
|
|
||||||
function containerNode(node: AdfNode, blocks: readonly Block[], definitions: LinkDefinitions, path: ConvertErrorPath, depth: number, memo: SpellingMemo): Result<AdfNode> {
|
function containerNode(node: AdfNode, blocks: readonly Block[], definitions: LinkDefinitions, path: ConvertErrorPath, depth: number, memo: SpellingMemo): Result<AdfNode> {
|
||||||
const content = blockNodes(blocks, definitions, path, depth + 1, memo)
|
const content = readBlocks(blocks, definitions, path, depth + 1, memo)
|
||||||
if (!content.ok) return content
|
if (!content.ok) return content
|
||||||
return success(withContent(node, content.value))
|
return success(withContent(node, content.value))
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-14
@@ -7,9 +7,9 @@ import type { Arbitrary } from 'fast-check'
|
|||||||
import type { AttributeKind, AttributeVocabulary } from './adf/attribute-vocabulary.ts'
|
import type { AttributeKind, AttributeVocabulary } from './adf/attribute-vocabulary.ts'
|
||||||
import type { JsonValue } from './json-value.ts'
|
import type { JsonValue } from './json-value.ts'
|
||||||
import { blockArgument } from './markdown/block-directive-arguments.ts'
|
import { blockArgument } from './markdown/block-directive-arguments.ts'
|
||||||
import { blockDirectives } from './adf/block-directives.ts'
|
import { blockNodes } from './adf/block-nodes.ts'
|
||||||
import { directivePrefix } from './markdown/directive-syntax.ts'
|
import { directivePrefix } from './markdown/directive-syntax.ts'
|
||||||
import { inlineDirectives } from './adf/inline-directives.ts'
|
import { inlineNodes } from './adf/inline-nodes.ts'
|
||||||
import { markAttributes } from './adf/mark-attributes.ts'
|
import { markAttributes } from './adf/mark-attributes.ts'
|
||||||
import { toEditorNormal } from './adf/editor-normal.ts'
|
import { toEditorNormal } from './adf/editor-normal.ts'
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ const emptyCell: AdfNode = { content: [{ type: 'paragraph' }], type: 'tableCell'
|
|||||||
const flatCommonMarkShapeWeight = 4
|
const flatCommonMarkShapeWeight = 4
|
||||||
export const markdownPieces = fc.constantFrom(...'aZ09 \t\n!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~é\xa0🎉', 'ab:', 'http://', directivePrefix, `${directivePrefix}a[`, `${directivePrefix}a{`)
|
export const markdownPieces = fc.constantFrom(...'aZ09 \t\n!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~é\xa0🎉', 'ab:', 'http://', directivePrefix, `${directivePrefix}a[`, `${directivePrefix}a{`)
|
||||||
const nestingCommonMarkShapeWeight = 21
|
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<string> {
|
export function textOf(minLength: number): Arbitrary<string> {
|
||||||
return fc.oneof(
|
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 })
|
.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' }))
|
.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]) =>
|
const inlineArbitraries = Object.entries(inlineNodes).map(([type, model]) =>
|
||||||
fc.record({ attrs: attributes(directive.attributes), marks }).map((held): AdfNode => ({ ...held, type })),
|
fc.record({ attrs: attributes(model.attributes), marks }).map((held): AdfNode => ({ ...held, type })),
|
||||||
)
|
)
|
||||||
|
|
||||||
function weighted(arbitraries: readonly Arbitrary<AdfNode>[], weight: number): { arbitrary: Arbitrary<AdfNode>; weight: number }[] {
|
function weighted(arbitraries: readonly Arbitrary<AdfNode>[], weight: number): { arbitrary: Arbitrary<AdfNode>; weight: number }[] {
|
||||||
@@ -112,17 +112,17 @@ const positions = fc.letrec<Positions>((tie) => {
|
|||||||
none: fc.constant<AdfNode[]>([]),
|
none: fc.constant<AdfNode[]>([]),
|
||||||
}
|
}
|
||||||
const blockMarks = fc.oneof({ arbitrary: fc.constant<AdfMark[]>([]), weight: 4 }, { arbitrary: marks, weight: 1 })
|
const blockMarks = fc.oneof({ arbitrary: fc.constant<AdfMark[]>([]), weight: 4 }, { arbitrary: marks, weight: 1 })
|
||||||
const blockNodes = Object.entries(blockDirectives).map(([type, directive]) => {
|
const blockArbitraries = Object.entries(blockNodes).map(([type, model]) => {
|
||||||
const argument = blockArgument(type)
|
const argument = blockArgument(type)
|
||||||
const vocabulary: AttributeVocabulary = argument === undefined ? directive.attributes : { ...directive.attributes, [argument]: 'string' }
|
const vocabulary: AttributeVocabulary = argument === undefined ? model.attributes : { ...model.attributes, [argument]: 'string' }
|
||||||
const node = fc.record({ attrs: attributes(vocabulary), content: contentByModel[directive.contentModel], marks: blockMarks }).map((held): AdfNode => ({ ...held, type }))
|
const node = fc.record({ attrs: attributes(vocabulary), content: contentByModel[model.contentModel], marks: blockMarks }).map((held): AdfNode => ({ ...held, type }))
|
||||||
return { leaf: directive.contentModel === 'code' || directive.contentModel === 'none', node }
|
return { leaf: model.contentModel === 'code' || model.contentModel === 'none', node }
|
||||||
})
|
})
|
||||||
const unknownNode = fc
|
const unknownNode = fc
|
||||||
.record({ attrs: fc.dictionary(jsonKey, jsonValue, { maxKeys: 2, noNullPrototype: true }), content: fc.array(tie('inline'), { depthIdentifier, maxLength: 2 }), marks, type: unknownType })
|
.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)
|
.map((held): AdfNode => held)
|
||||||
const leafBlocks = blockNodes.filter((entry) => entry.leaf).map((entry) => entry.node)
|
const leafBlocks = blockArbitraries.filter((entry) => entry.leaf).map((entry) => entry.node)
|
||||||
const containerBlocks = blockNodes.filter((entry) => !entry.leaf).map((entry) => entry.node)
|
const containerBlocks = blockArbitraries.filter((entry) => !entry.leaf).map((entry) => entry.node)
|
||||||
const misplacedWeight = 7
|
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 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 }))
|
const cell = (type: string) => paragraph.map((held): AdfNode => ({ content: [held], type }))
|
||||||
@@ -148,7 +148,7 @@ const positions = fc.letrec<Positions>((tie) => {
|
|||||||
{ depthIdentifier, depthSize: 'small', maxDepth: 4 },
|
{ depthIdentifier, depthSize: 'small', maxDepth: 4 },
|
||||||
{ arbitrary: fc.oneof(...flatBlocks), weight: flatBlocks.reduce((sum, entry) => sum + entry.weight, 0) },
|
{ arbitrary: fc.oneof(...flatBlocks), weight: flatBlocks.reduce((sum, entry) => sum + entry.weight, 0) },
|
||||||
{ arbitrary: fc.oneof(...containerBlocks), weight: containerBlocks.length * 2 },
|
{ arbitrary: fc.oneof(...containerBlocks), weight: containerBlocks.length * 2 },
|
||||||
{ arbitrary: fc.oneof(textNode, ...inlineNodes, unknownNode), weight: misplacedWeight },
|
{ arbitrary: fc.oneof(textNode, ...inlineArbitraries, unknownNode), weight: misplacedWeight },
|
||||||
{ arbitrary: fc.oneof(...nestingCommonMarkShapes), weight: nestingCommonMarkShapes.length * nestingCommonMarkShapeWeight },
|
{ arbitrary: fc.oneof(...nestingCommonMarkShapes), weight: nestingCommonMarkShapes.length * nestingCommonMarkShapeWeight },
|
||||||
),
|
),
|
||||||
inline: fc.oneof(
|
inline: fc.oneof(
|
||||||
@@ -156,8 +156,8 @@ const positions = fc.letrec<Positions>((tie) => {
|
|||||||
{ arbitrary: textNode, weight: 12 },
|
{ arbitrary: textNode, weight: 12 },
|
||||||
{ arbitrary: autolinkTextNode, weight: 2 },
|
{ arbitrary: autolinkTextNode, weight: 2 },
|
||||||
{ arbitrary: backtickRunNode, weight: 3 },
|
{ arbitrary: backtickRunNode, weight: 3 },
|
||||||
{ arbitrary: fc.oneof(...inlineNodes), weight: 7 },
|
{ arbitrary: fc.oneof(...inlineArbitraries), weight: 7 },
|
||||||
{ arbitrary: fc.oneof(...blockNodes.map((entry) => entry.node), unknownNode), weight: 2 },
|
{ arbitrary: fc.oneof(...blockArbitraries.map((entry) => entry.node), unknownNode), weight: 2 },
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -994,6 +994,22 @@ 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
|
go whole. The gate's seconds is confirmed stated nowhere: `docker-runner.sh` measures each
|
||||||
leg's elapsed time and no number bounds it.
|
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` — `spec/flavour.md`'s own headings — each row a
|
||||||
|
`BlockNodeModel` or `InlineNodeModel` read by `blockNodeModel` and `inlineNodeModel`. The row
|
||||||
|
is the node's model, never the node: `BlockNode` beside `AdfNode` put two kinds of thing one
|
||||||
|
lookup apart in every signature the rename touched, where §8 already calls the content model
|
||||||
|
the model. `markdown-to-adf.ts`'s own `blockNodes`/`blockNode` walk became
|
||||||
|
`readBlocks`/`readBlock`, so the table's name means one thing repo-wide. `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`
|
||||||
|
|
||||||
- [ ] **5 — Ship `0.1.0`.** Only the maintainer's own acts are left (§15): make the Gitea repo
|
- [ ] **5 — Ship `0.1.0`.** Only the maintainer's own acts are left (§15): make the Gitea repo
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Start a session with: `Read AGENTS.md and todo.md, then do what todo.md's "Next
|
|||||||
## Milestones
|
## Milestones
|
||||||
|
|
||||||
Shipping order: 3h, 3i, 3j, 5a, 5b, 5c, 5d, 5 → `0.1.0` (shipped 2026-09-05); 3k, 11, 4, 12, 13, 4b,
|
Shipping order: 3h, 3i, 3j, 5a, 5b, 5c, 5d, 5 → `0.1.0` (shipped 2026-09-05); 3k, 11, 4, 12, 13, 4b,
|
||||||
4c, 14, 15, 16, 18, 4d, 28, 17, 29, 19, 20, 21, 22, 23, 24, 25, 30, 26, 27, 10, 6, 7, 31, 5f, 5g →
|
4c, 14, 15, 16, 18, 4d, 28, 17, 29, 19, 20, 21, 22, 32, 23, 24, 25, 30, 26, 27, 10, 6, 7, 31, 5f, 5g →
|
||||||
`0.2.0`;
|
`0.2.0`;
|
||||||
8, 9 → TBD; 5e last.
|
8, 9 → TBD; 5e last.
|
||||||
The numbering is the order the work was planned in, not the order it ships. Everything known and
|
The numbering is the order the work was planned in, not the order it ships. Everything known and
|
||||||
@@ -44,7 +44,8 @@ panel says the next reader pays for.
|
|||||||
29 and 30 come from 17's prose pass (2026-09-20). 29 reads first because every goal is what a later
|
29 and 30 come from 17's prose pass (2026-09-20). 29 reads first because every goal is what a later
|
||||||
ask is settled against, 19's included; 30 sits beside 25, the other chunk rereading AGENTS.md.
|
ask is settled against, 19's included; 30 sits beside 25, the other chunk rereading AGENTS.md.
|
||||||
31 comes from 20's gate runs (2026-09-21) and reads beside 5f, the other chunk putting a measured
|
31 comes from 20's gate runs (2026-09-21) and reads beside 5f, the other chunk putting a measured
|
||||||
number under the pipeline.
|
number under the pipeline. 32 comes from 21's review (2026-09-21) and reads beside 22, the other
|
||||||
|
chunk clearing a §11 seam.
|
||||||
|
|
||||||
- [ ] **31 — The branch figure the floor is read against is stable (`0.2.0`).** Three Node test
|
- [ ] **31 — The branch figure the floor is read against is stable (`0.2.0`).** Three Node test
|
||||||
legs over one unchanged tree reported `emit/inline-line.ts` at 95.83%, 96.23% and 96.23%,
|
legs over one unchanged tree reported `emit/inline-line.ts` at 95.83%, 96.23% and 96.23%,
|
||||||
@@ -55,15 +56,18 @@ number under the pipeline.
|
|||||||
upward, so the first raise to the measured figure reddens a run that changed nothing. Make
|
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 measurement repeatable, or state the number the floor may be raised to and why it is not
|
||||||
the measured one.
|
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
|
- [ ] **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
|
`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
|
that are the deliberate spelling consultation. Move it, and name those two in §11 as the whole
|
||||||
of that surface, so a reviewer checks the seam with one grep.
|
of that surface, so a reviewer checks the seam with one grep.
|
||||||
|
- [ ] **32 — The mark depth `adf/` counts is stated in ADF's terms (`0.2.0`).**
|
||||||
|
`document.ts`'s `markAttributeNesting` is `largestNesting - 3`, and both the comment above it
|
||||||
|
and AGENTS.md §8 give the reason as markdown's: the block directive spells the whole mark set
|
||||||
|
as one JSON attribute, so the parser reads the value at the bottom of array, mark and
|
||||||
|
`attrs`. That is a format's spelling deciding a constant inside the directory §11 forbids to
|
||||||
|
know a format. Move the derivation to where that spelling lives, or state the three levels in
|
||||||
|
ADF's own vocabulary. Before 7 gives the constant a second format whose spelling may not
|
||||||
|
spend the same three levels.
|
||||||
- [ ] **23 — The block-directive fragments are one file (`0.2.0`).** `block-directive-arguments.ts`,
|
- [ ] **23 — The block-directive fragments are one file (`0.2.0`).** `block-directive-arguments.ts`,
|
||||||
`-forms.ts` and `-marks.ts` are three files under 25 lines answering one question. Fold them,
|
`-forms.ts` and `-marks.ts` are three files under 25 lines answering one question. Fold them,
|
||||||
and take `src/markdown/` — the worst level both architects named, 13 entries with no
|
and take `src/markdown/` — the worst level both architects named, 13 entries with no
|
||||||
@@ -287,6 +291,7 @@ number under the pipeline.
|
|||||||
- [x] **18 — The subtree the directive spelling asks about (`0.2.0`).**
|
- [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] **19 — A home for what both formats read (`0.2.0`).**
|
||||||
- [x] **20 — The give-way channel is unmistakable (`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] **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] **29 — The README reads raw HTML as refused for good (`0.2.0`).**
|
||||||
- [x] **30 — AGENTS.md says each thing once (`0.2.0`).**
|
- [x] **30 — AGENTS.md says each thing once (`0.2.0`).**
|
||||||
|
|||||||
Reference in New Issue
Block a user