export type DistinguisherParser = (distinguisher: string) => T; export type DocumentParser = (document: string[]) => T; export type UnionParser = (distinguisher: string, document: string[]) => T; export type IdvConverter = (idv: Idv) => T; const LEADING_WHITESPACE = /^([ \t]+)/; const ENTRY = /^(.+?):\s*(.*)/; export class Idv { collections: Map = new Map(); 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.has(collection)) { this.collections.set(collection, []); } currentDocument = []; currentIndent = undefined; // TODO: implement backslash escaping in the distinguisher this.collections .get(collection)! .push([distinguisher, currentDocument]); } else { throw new Error("Failed to parse a property"); } } }); return this; } public getProperty(name: string, parse: UnionParser): T | undefined { const firstEntry = this.collections.get(name)?.[0]; return firstEntry && parse(firstEntry[0], firstEntry[1]); } public getList(name: string, parse: UnionParser): T[] { return (this.collections.get(name) ?? []).map(([distinguisher, document]) => parse(distinguisher, document) ); } public getMap( name: string, parseDocument: DocumentParser ): Record { return Object.fromEntries( (this.collections.get(name) ?? []).map(([distinguisher, document]) => [ distinguisher, parseDocument(document), ]) ); } public getMergedMap( name: string, convertIdv: IdvConverter ): Record { const idvMap: Map = new Map(); (this.collections.get(name) ?? []).forEach(([distinguisher, document]) => { let idv = idvMap.get(distinguisher); if (idv == undefined) { idvMap.set(distinguisher, (idv = new Idv())); } idv.importLines(document); }); const result: Record = {}; for (const [distinguisher, idv] of idvMap.entries()) { result[distinguisher] = convertIdv(idv); } return result; } } // TODO: implement backslash-escaping in the document? export const StringProperty = (distinguisher: string, lines: string[]) => lines.length == 0 ? distinguisher : lines.join("\n");