2024.js/lib/idv.ts

124 lines
3.7 KiB
TypeScript
Raw Normal View History

2024-07-07 21:00:20 -04:00
export type DistinguisherParser<T> = (distinguisher: string) => T;
export type DocumentParser<T> = (document: string[]) => T;
export type UnionParser<T> = (distinguisher: string, document: string[]) => T;
2024-07-14 22:19:26 -04:00
export type IdvConverter<T> = (idv: Idv) => T;
2024-07-07 21:00:20 -04:00
const LEADING_WHITESPACE = /^([ \t]+)/;
const ENTRY = /^(.+?):\s*(.*)/;
export class Idv {
collections: Record<string, undefined | [string, string[]][]> = {};
public static parse(input: string): Idv {
const idv = new Idv();
return idv.import(input);
2024-07-07 21:00:20 -04:00
}
public static parseLines(input: string[]): Idv {
2024-07-07 21:00:20 -04:00
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 {
2024-07-10 00:17:33 -04:00
let currentDocument: string[] | undefined = undefined;
let currentIndent: string | undefined = undefined;
let bufferedBlankLines: string[] = [];
2024-07-07 21:00:20 -04:00
input.forEach((line) => {
const indent = LEADING_WHITESPACE.exec(line)?.[1];
if (indent) {
2024-07-10 00:17:33 -04:00
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"
);
}
2024-07-07 21:00:20 -04:00
} else if (line == "") {
2024-07-10 00:17:33 -04:00
bufferedBlankLines.push("");
2024-07-07 21:00:20 -04:00
} 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] = [];
2024-07-07 21:00:20 -04:00
}
currentDocument = [];
2024-07-10 00:17:33 -04:00
currentIndent = undefined;
// TODO: implement backslash escaping in the distinguisher
this.collections[collection].push([distinguisher, currentDocument]);
2024-07-07 21:00:20 -04:00
} else {
throw new Error("Failed to parse a property");
}
}
});
return this;
2024-07-07 21:00:20 -04:00
}
public getProperty<T>(name: string, parse: UnionParser<T>): T | undefined {
2024-07-07 21:00:20 -04:00
const firstEntry = this.collections[name]?.[0];
return firstEntry && parse(firstEntry[0], firstEntry[1]);
2024-07-07 21:00:20 -04:00
}
public getList<T>(name: string, parse: UnionParser<T>): T[] {
2024-07-07 21:00:20 -04:00
return (this.collections[name] ?? []).map(([distinguisher, document]) =>
parse(distinguisher, document)
2024-07-07 21:00:20 -04:00
);
}
2024-07-10 00:31:33 -04:00
public getMap<T>(
name: string,
parseDocument: DocumentParser<T>
): Record<string, T> {
return Object.fromEntries(
(this.collections[name] ?? []).map(([distinguisher, document]) => [
distinguisher,
parseDocument(document),
])
);
}
2024-07-14 22:19:26 -04:00
public getMergedMap<T>(
name: string,
convertIdv: IdvConverter<T>
): Record<string, T> {
const idvMap: Map<string, Idv> = new Map();
(this.collections[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;
}
2024-07-07 21:00:20 -04:00
}
// TODO: implement backslash-escaping in the document?
export const StringProperty = (distinguisher: string, lines: string[]) =>
lines.length == 0 ? distinguisher : lines.join("\n");