2024.js/lib/idv.ts

125 lines
3.8 KiB
TypeScript

export type DistinguisherParser<T> = (distinguisher: string) => T;
export type DocumentParser<T> = (document: string[]) => T;
export type UnionParser<T> = (distinguisher: string, document: string[]) => T;
export type IdvConverter<T> = (idv: Idv) => T;
const LEADING_WHITESPACE = /^([ \t]+)/;
const ENTRY = /^(.+?):\s*(.*)/;
export class Idv {
collections: Map<string, undefined | [string, string[]][]> = 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<T>(name: string, parse: UnionParser<T>): T | undefined {
const firstEntry = this.collections.get(name)?.[0];
return firstEntry && parse(firstEntry[0], firstEntry[1]);
}
public getList<T>(name: string, parse: UnionParser<T>): T[] {
return (this.collections.get(name) ?? []).map(([distinguisher, document]) =>
parse(distinguisher, document)
);
}
public getMap<T>(
name: string,
parseDocument: DocumentParser<T>
): Record<string, T> {
return Object.fromEntries(
(this.collections.get(name) ?? []).map(([distinguisher, document]) => [
distinguisher,
parseDocument(document),
])
);
}
public getMergedMap<T>(
name: string,
convertIdv: IdvConverter<T>
): Record<string, T> {
const idvMap: Map<string, Idv> = 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<string, T> = {};
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");