export type DistinguisherParser = (distinguisher: string) => T; export type DocumentParser = (document: string[]) => T; const LEADING_WHITESPACE = /^([ \t]+)/; const ENTRY = /^(.+?):\s*(.*)/; export class Idv { collections: Record = {}; public static parse(input: string): Idv { const idv = new Idv(); return idv.import(input); } public static parseLines(input: string[]): Idv { const idv = new Idv(); return idv.importLines(input); } public import(input: string): Idv { const lines = input.split("\n").map((line) => line.trimEnd()); return this.importLines(lines); } public importLines(input: string[]): Idv { let currentDocument: string[] | undefined = undefined; let currentIndent: string | undefined = undefined; let bufferedBlankLines: string[] = []; input.forEach((line) => { const indent = LEADING_WHITESPACE.exec(line)?.[1]; if (indent) { if (currentDocument == undefined) { throw new Error("Indented document found before an entry"); } if (currentIndent == undefined) { currentIndent = indent; } if (line.startsWith(currentIndent)) { currentDocument.push(...bufferedBlankLines); bufferedBlankLines = []; currentDocument.push(line.substring(currentIndent.length)); } else { throw new Error( "Inconsistent indentation- line indented less than the first line of its document" ); } } else if (line == "") { bufferedBlankLines.push(""); } else if (line[0] == "#") { // skip } else { const matches = ENTRY.exec(line); if (matches) { const [, collection, distinguisher] = matches; if (this.collections[collection] == undefined) { this.collections[collection] = []; } currentDocument = []; currentIndent = undefined; this.collections[collection].push([distinguisher, currentDocument]); } else { throw new Error("Failed to parse a property"); } } }); return this; } public getProperty( name: string, parseDistinguisher: DistinguisherParser, parseDocument: DocumentParser ): T | undefined { const firstEntry = this.collections[name]?.[0]; return ( firstEntry && (firstEntry[1].length > 0 ? parseDocument(firstEntry[1]) : parseDistinguisher(firstEntry[0])) ); } public getList( name: string, parseDistinguisher: DistinguisherParser, parseDocument: DocumentParser ): T[] { return (this.collections[name] ?? []).map(([distinguisher, document]) => document.length > 0 ? parseDocument(document) : parseDistinguisher(distinguisher) ); } public getMap( name: string, parseDocument: DocumentParser ): Record { return Object.fromEntries( (this.collections[name] ?? []).map(([distinguisher, document]) => [ distinguisher, parseDocument(document), ]) ); } } export const StringFromDocument = (lines: string[]) => lines.join("\n");