tridactyl.tridactyl/src/lib/metadata.ts

321 lines
10 KiB
TypeScript

/**
* Runtime metadata over `typedoc --json` output for src/excmds.ts and
* src/lib/config.ts. Exposes plain-object indexes (keyed by symbol name) plus
* a small set of free helpers that operate on raw typedoc nodes.
*
* @hidden symbols are dropped by typedoc itself before they reach this loader,
* so consumers never see them.
*/
import metadataJson from "../.metadata.generated.json"
import staticThemesJson from "../.themes.generated.json"
export const staticThemes: string[] = staticThemesJson
// =============================================================================
// Indexes — built once at module load by walking the typedoc tree.
// =============================================================================
type Node = any
const fileBuckets: Record<
string,
{ functions: Record<string, Node>; classes: Record<string, Node> }
> = {
"src/excmds.ts": { functions: {}, classes: {} },
"src/lib/config.ts": { functions: {}, classes: {} },
}
function inTargetFile(node: Node): string | undefined {
for (const src of node.sources || []) {
if (fileBuckets[src.fileName]) return src.fileName
}
return undefined
}
function walk(node: Node) {
if (!node || typeof node !== "object") return
if (Array.isArray(node)) {
for (const v of node) walk(v)
return
}
if (node.kindString === "Function") {
const f = inTargetFile(node)
if (f) fileBuckets[f].functions[node.name] = node
} else if (node.kindString === "Class") {
const f = inTargetFile(node)
if (f) fileBuckets[f].classes[node.name] = node
}
if (node.children) walk(node.children)
}
walk(metadataJson)
export const excmdsFunctions: Record<string, Node> =
fileBuckets["src/excmds.ts"].functions
const defaultConfig: Node | undefined =
fileBuckets["src/lib/config.ts"].classes["default_config"]
export const defaultConfigMembers: Record<string, Node> = (() => {
const out: Record<string, Node> = {}
for (const ch of defaultConfig?.children || []) out[ch.name] = ch
return out
})()
// =============================================================================
// Doc / type access on function and class-member nodes.
// =============================================================================
function readComment(c: Node | undefined): string {
if (!c) return ""
let s: string = c.shortText || ""
if (c.text) s += (s ? "\n\n" : "") + c.text
return s.replace(/\n+$/, "")
}
export function getDoc(node: Node | undefined): string {
if (!node) return ""
return (
readComment(node.signatures?.[0]?.comment) || readComment(node.comment)
)
}
export function memberDoc(node: Node | undefined): string {
if (!node) return ""
if (node.kindString === "Accessor") {
const sig = (node.getSignature || [])[0] || (node.setSignature || [])[0]
if (sig?.comment) return readComment(sig.comment)
}
return readComment(node.comment)
}
/**
* Returns a typedoc type node for a class member, synthesising an empty
* reflection for `Object literal` fields (which have no annotated type) so
* downstream helpers can treat them uniformly as objects.
*/
export function memberType(node: Node | undefined): Node | undefined {
if (!node) return undefined
if (node.type) return node.type
if (node.kindString === "Object literal") {
return { type: "reflection", declaration: { children: [] } }
}
if (node.kindString === "Accessor") {
const sig = (node.getSignature || [])[0] || (node.setSignature || [])[0]
return sig?.type
}
return undefined
}
/** Parameter list for a function node — each entry has `name`, `type`, `flags`. */
export function paramTypes(fnNode: Node | undefined): Node[] {
return fnNode?.signatures?.[0]?.parameters || []
}
// =============================================================================
// Type coercion + stringification over typedoc type nodes.
// =============================================================================
function intrinsicName(t) {
switch (t.name) {
case "string":
case "number":
case "boolean":
case "object":
return t.name
case "void":
case "undefined":
case "never":
return "void"
default:
return "any"
}
}
/** Normalised kind: "string" | "number" | "boolean" | "object" | "array" | "void" | "any" | ... */
export function typeKind(t: Node | undefined): string {
if (!t) return "any"
switch (t.type) {
case "intrinsic":
return intrinsicName(t)
case "array":
case "tuple":
case "union":
return t.type
case "reflection":
return t.declaration?.signatures?.length ? "function" : "object"
case "stringLiteral":
case "numberLiteral":
case "booleanLiteral":
return "LiteralType"
case "reference":
return t.name || "reference"
default:
return "any"
}
}
export function typeToString(t: Node | undefined): string {
if (!t) return "any"
switch (t.type) {
case "intrinsic":
return t.name
case "array":
return `${typeToString(t.elementType)}[]`
case "tuple":
return `[${(t.elements || []).map(typeToString).join(", ")}]`
case "union":
return (t.types || []).map(typeToString).join(" | ")
case "stringLiteral":
case "numberLiteral":
case "booleanLiteral":
return JSON.stringify(t.value)
case "reference": {
const args = (t.typeArguments || []).map(typeToString)
return args.length
? `${t.name}<${args.join(", ")}>`
: t.name || "reference"
}
case "reflection": {
const decl = t.declaration || {}
if (decl.signatures?.length) {
const sig = decl.signatures[0]
const args = (sig.parameters || []).map((p: Node) =>
typeToString(p.type),
)
return `(${args.join(", ")}) => ${typeToString(sig.type)}`
}
return "object"
}
default:
return "any"
}
}
function convertIntrinsic(t) {
switch (t.name) {
case "string":
if (typeof value === "string") return value
throw new Error(`Can't convert to string: ${value}`)
case "number": {
const n = parseFloat(value)
if (!Number.isNaN(n)) return n
throw new Error(`Can't convert to number: ${value}`)
}
case "boolean":
if (value === "true") return true
if (value === "false") return false
throw new Error(`Can't convert ${value} to boolean`)
case "object":
try {
return JSON.parse(value)
} catch {
throw new Error(`Can't convert to object: ${value}`)
}
case "void":
case "undefined":
case "never":
return null
default:
return value
}
}
export function convert(t: Node | undefined, value: any): any {
if (!t) return value
switch (t.type) {
case "intrinsic":
return convertIntrinsic(t)
case "array": {
let arr = value
if (!Array.isArray(arr)) {
try {
arr = JSON.parse(arr)
} catch {
throw new Error(`Can't convert ${value} to array:`)
}
if (!Array.isArray(arr))
throw new Error(`Can't convert ${value} to array:`)
}
return arr.map((v: any) => convert(t.elementType, v))
}
case "tuple": {
let arr = value
if (!Array.isArray(arr)) {
try {
arr = JSON.parse(arr)
} catch {
throw new Error(`Can't convert to tuple: ${value}`)
}
if (!Array.isArray(arr))
throw new Error(`Can't convert to tuple: ${value}`)
}
const elems = t.elements || []
if (arr.length !== elems.length) {
throw new Error(
`Error converting tuple: number of elements and type mismatch ${value}`,
)
}
return arr.map((v: any, i: number) => convert(elems[i], v))
}
case "union":
for (const u of t.types || []) {
try {
return convert(u, value)
} catch {}
}
throw new Error(
`Can't convert "${value}" to any of: ${(t.types || []).map(typeToString).join(", ")}`,
)
case "stringLiteral":
case "numberLiteral":
case "booleanLiteral":
if (value === t.value) return value
throw new Error(
`Argument does not match expected value (${t.value}): ${value}`,
)
case "reflection": {
const decl = t.declaration || {}
if (decl.signatures?.length) {
throw new Error(
`Conversion to function not implemented: ${value}`,
)
}
try {
return JSON.parse(value)
} catch {
throw new Error(`Can't convert to object: ${value}`)
}
}
case "reference":
throw new Error(
"Conversion of simple type references not implemented.",
)
default:
return value
}
}
/** Walk `path` through an object-type reflection, coercing `value` against the matched leaf. */
export function convertMember(
t: Node | undefined,
path: string[],
value: any,
): any {
const decl = t?.type === "reflection" ? t.declaration : undefined
const named: Record<string, Node> = {}
let indexSig: Node | undefined
for (const ch of decl?.children || []) {
if (ch.name && ch.type) named[ch.name] = ch.type
}
const idx = decl?.indexSignature
const idxArr = Array.isArray(idx) ? idx : idx ? [idx] : []
for (const sig of idxArr) if (sig?.type) indexSig = sig.type
const sub = named[path[0]] ?? indexSig
if (!sub) return value
if (typeKind(sub) === "object")
return convertMember(sub, path.slice(1), value)
return convert(sub, value)
}