12c - The block directive forms read and spell !adf: openers and closers #86

Merged
lilleman merged 6 commits from 12c into main 2026-09-16 21:48:08 +02:00
5 changed files with 51 additions and 7 deletions
Showing only changes of commit 4aa9e88238 - Show all commits
+1
View File
@@ -0,0 +1 @@
malformed-directive
+7
View File
@@ -0,0 +1,7 @@
- Bolt M8
!adf:listBreak
Packed.
!adf:/listBreak
- Nut M8
+2 -1
View File
@@ -115,5 +115,6 @@ test('closes the containers a closer names past as unclosed, and crosses no list
assert.deepEqual(faults('> !adf:panel info\n> Part.\n!adf:/panel\n'), [unclosed('panel'), unopened('panel')])
assert.deepEqual(faults('!adf:panel info\n> !adf:/panel\n!adf:/panel\n'), [unopened('panel')])
assert.deepEqual(faults('Part.\n\n!adf:/panel\n'), [unopened('panel')])
assert.deepEqual(faults('!adf:rule {localId=a-1}\nPart.\n!adf:/rule\n'), ['rule takes no body, so no !adf:/rule closes it'])
assert.deepEqual(kinds('!adf:rule {localId=a-1}\nPart.\n!adf:/rule\n'), ['fault', 'paragraph'])
assert.deepEqual(kinds('!adf:rule {localId=a-1}\n!adf:/rule\n!adf:/rule\n'), ['fault', 'fault'])
})
+29 -5
View File
@@ -60,14 +60,16 @@ type ContainerStart = { kind: 'blockquote'; rest: Line } | { fresh: boolean; ind
// The line from an absolute column on: a tab a cut splits keeps the stop it is measured against.
type Line = { column: number; text: string }
type Walk = ParsedBlocks & { leaf: OpenLeaf | undefined; position: SourcePosition; stack: OpenContainer[] }
type LeafOpener = { index: number; position: SourcePosition }
type Walk = ParsedBlocks & { leaf: OpenLeaf | undefined; leafOpeners: Map<Block[], Map<string, LeafOpener>>; position: SourcePosition; stack: OpenContainer[] }
const indentedCodeColumns = 4
const largestOpenerIndentation = 3
const tabStop = 4
export function parseBlocks(markdown: string): ParsedBlocks {
const walk: Walk = { blocks: [], definitions: new Map(), leaf: undefined, position: { line: 1, offset: 0 }, stack: [] }
const walk: Walk = { blocks: [], definitions: new Map(), leaf: undefined, leafOpeners: new Map(), position: { line: 1, offset: 0 }, stack: [] }
for (const line of sourceLines(markdown)) {
walk.position = line.position
readLine(walk, { column: 0, text: line.text })
@@ -229,15 +231,16 @@ function openDirective(walk: Walk, directive: Extract<DirectiveLine, { kind: 'op
position: walk.position,
}
const parent = currentBlocks(walk)
parent.push(block)
const index = parent.push(block) - 1
const { position } = block
if (block.blocks !== undefined) walk.stack.push({ blocks: block.blocks, index: parent.length - 1, kind: 'directive', name, parent, position })
if (block.blocks !== undefined) walk.stack.push({ blocks: block.blocks, index, kind: 'directive', name, parent, position })
else leafOpenersIn(walk, parent).set(name, { index, position })
}
function closeDirective(walk: Walk, name: string): void {
const closer = spellDirectiveCloser(name)
if (blockDirectiveForm(name) === 'leaf') {
pushFault(walk, malformedDirective(`${name} takes no body, so no ${closer} closes it`))
faultLeafOpener(walk, name, malformedDirective(`${name} takes no body, so no ${closer} closes it`))
return
}
const depth = openDirectiveDepth(walk, name)
@@ -259,6 +262,27 @@ function openDirectiveDepth(walk: Walk, name: string): number | undefined {
return undefined
}
// A leaf given a body faults at its opener, as a container missing its closer does.
function faultLeafOpener(walk: Walk, name: string, fault: ConvertFault): void {
const blocks = currentBlocks(walk)
const openers = walk.leafOpeners.get(blocks)
const opener = openers?.get(name)
if (openers === undefined || opener === undefined) {
pushFault(walk, fault)
return
}
openers.delete(name)
blocks[opener.index] = { fault, kind: 'fault', position: opener.position }
}
function leafOpenersIn(walk: Walk, blocks: Block[]): Map<string, LeafOpener> {
const known = walk.leafOpeners.get(blocks)
if (known !== undefined) return known
const openers = new Map<string, LeafOpener>()
walk.leafOpeners.set(blocks, openers)
return openers
}
function pushFault(walk: Walk, fault: ConvertFault): void {
currentBlocks(walk).push({ fault, kind: 'fault', position: walk.position })
}
+12 -1
View File
@@ -315,7 +315,18 @@ test('names the closer that finds no container open where it stands', () => {
assert.equal(content(markdownToAdf('!adf:panel info\n> !adf:/panel\n!adf:/panel\n')), unopened('panel'))
assert.equal(content(markdownToAdf('!adf:panel info\n- !adf:/panel\n!adf:/panel\n')), unopened('panel'))
assert.equal(content(markdownToAdf('!adf:panel info\nPart.\n!adf:/expand\n!adf:/panel\n')), unopened('expand'))
assert.equal(content(markdownToAdf('!adf:rule {localId=a-1}\nPart.\n!adf:/rule\n')), 'malformed-directive: rule takes no body, so no !adf:/rule closes it')
})
test('names the leaf given a body at its opener, ahead of any refusal the leaf holds itself', () => {
const body = (name: string): string => `malformed-directive: ${name} takes no body, so no !adf:/${name} closes it`
assert.equal(content(markdownToAdf('!adf:rule\nPart.\n!adf:/rule\n')), body('rule'))
assert.deepEqual(position(markdownToAdf('Part.\n\n!adf:rule\nPart.\n!adf:/rule\n')), { line: 3, offset: 7 })
assert.equal(content(markdownToAdf('- a\n\n!adf:listBreak\n!adf:/listBreak\n\n- b\n')), body('listBreak'))
assert.equal(content(markdownToAdf('- a\n\n!adf:listBreak\nPart.\n!adf:/listBreak\n\n- b\n')), body('listBreak'))
assert.deepEqual(path(markdownToAdf('!adf:rule {localId=a-1}\n!adf:rule {localId=a-2}\nPart.\n!adf:/rule\n')), ['content', 1])
assert.deepEqual(path(markdownToAdf('!adf:rule {localId=a-1}\n!adf:/rule\n!adf:/rule\n')), ['content', 0])
assert.deepEqual(path(markdownToAdf('Part.\n\n!adf:/rule\n')), ['content', 1])
assert.deepEqual(path(markdownToAdf('!adf:panel info\n!adf:rule {localId=a-1}\n!adf:/panel\n!adf:/rule\n')), ['content', 1])
})
test('names the position a directive name the other one spells belongs to', () => {