102 lines
4.0 KiB
TypeScript
102 lines
4.0 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import { dirname, join } from 'node:path'
|
|
import test from 'node:test'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
import type { AttributeKind, AttributeVocabulary } from './adf/attribute-vocabulary.ts'
|
|
import { blockDirectives } from './adf/block-directives.ts'
|
|
import { inlineDirectives } from './adf/inline-directives.ts'
|
|
import { markAttributes } from './adf/mark-attributes.ts'
|
|
|
|
type Declared = { attributes: AttributeVocabulary }
|
|
|
|
const specPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'spec', 'flavour.md')
|
|
const introducer = 'Attributes: '
|
|
const codeFence = /^`{3,}/
|
|
const directiveName = /`([a-z][A-Za-z0-9]*)`/g
|
|
const namedType = /^`([a-z][A-Za-z0-9]*)` \(([^)]*)\)/
|
|
const owned = ' — '
|
|
|
|
function bullets(heading: string): string[] {
|
|
const items: string[] = []
|
|
let fence: string | undefined
|
|
let item: string | undefined
|
|
let inside = false
|
|
for (const line of readFileSync(specPath, 'utf8').split('\n')) {
|
|
if (line.startsWith('## ')) inside = line === `## ${heading}`
|
|
if (!inside) continue
|
|
const marker = codeFence.exec(line)?.[0]
|
|
if (fence !== undefined) {
|
|
if (marker !== undefined && marker.length >= fence.length) fence = undefined
|
|
continue
|
|
}
|
|
if (marker !== undefined) {
|
|
fence = marker
|
|
continue
|
|
}
|
|
if (line.startsWith('- ')) {
|
|
if (item !== undefined) items.push(item)
|
|
item = line.slice(2)
|
|
} else if (item !== undefined && line.startsWith(' ')) item += ` ${line.trim()}`
|
|
else if (item !== undefined) {
|
|
items.push(item)
|
|
item = undefined
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
function declarations(heading: string): Record<string, Declared> {
|
|
const declared: Record<string, Declared> = {}
|
|
for (const item of bullets(heading)) {
|
|
const cut = item.indexOf(owned)
|
|
assert.notEqual(cut, -1, `${heading}: the bullet ${item} names no node ahead of a ${owned.trim()}`)
|
|
const attributes = attributeList(heading, item.slice(cut))
|
|
for (const [, name] of item.slice(0, cut).matchAll(directiveName)) {
|
|
assert.equal(declared[name ?? ''], undefined, `${heading}: ${name ?? ''} is declared twice`)
|
|
declared[name ?? ''] = { attributes }
|
|
}
|
|
}
|
|
return declared
|
|
}
|
|
|
|
function attributeList(heading: string, prose: string): AttributeVocabulary {
|
|
const at = prose.indexOf(introducer)
|
|
assert.notEqual(at, -1, `${heading}: ${prose} lists no attributes`)
|
|
let rest = prose.slice(at + introducer.length)
|
|
if (rest.startsWith('none')) return {}
|
|
const attributes: Record<string, AttributeKind> = {}
|
|
for (;;) {
|
|
const pair = namedType.exec(rest)
|
|
assert.notEqual(pair, null, `${heading}: ${rest} reads no \`name\` (type) pair`)
|
|
attributes[pair?.[1] ?? ''] = attributeKind(heading, pair?.[2] ?? '')
|
|
rest = rest.slice(pair?.[0].length ?? 0)
|
|
if (!rest.startsWith(', ')) return attributes
|
|
rest = rest.slice(2)
|
|
}
|
|
}
|
|
|
|
function attributeKind(heading: string, parenthesized: string): AttributeKind {
|
|
const first = parenthesized.split(/[\s,]/)[0] ?? ''
|
|
if (first === 'boolean' || first === 'json' || first === 'number' || first === 'string') return first
|
|
assert.ok(first.startsWith('`'), `${heading}: ${first} is neither an attribute kind nor a value set`)
|
|
return 'string'
|
|
}
|
|
|
|
function vocabularies(table: Readonly<Record<string, Declared>>): Record<string, Declared> {
|
|
return Object.fromEntries(Object.entries(table).map(([type, entry]) => [type, { attributes: entry.attributes }]))
|
|
}
|
|
|
|
test('the block node table holds the attributes spec/flavour.md gives each node', () => {
|
|
assert.deepEqual(declarations('Block nodes'), vocabularies(blockDirectives))
|
|
})
|
|
|
|
test('the inline node table holds the attributes spec/flavour.md gives each node', () => {
|
|
assert.deepEqual(declarations('Inline nodes'), vocabularies(inlineDirectives))
|
|
})
|
|
|
|
test('the mark table holds the attributes spec/flavour.md gives each mark', () => {
|
|
assert.deepEqual(declarations('Marks'), vocabularies(Object.fromEntries(Object.entries(markAttributes).map(([type, attributes]) => [type, { attributes }]))))
|
|
})
|