// Minimal Valve KeyValues (VDF) text parser. // Handles nested objects and quoted string values. Sufficient for libraryfolders.vdf. export type VdfNode = { [key: string]: string | VdfNode } class Cursor { constructor( public src: string, public pos: number = 0 ) {} peek(): string { return this.src[this.pos] ?? '' } next(): string { return this.src[this.pos++] ?? '' } eof(): boolean { return this.pos >= this.src.length } } function skipWhitespaceAndComments(c: Cursor): void { for (;;) { while (!c.eof() && /\s/.test(c.peek())) c.next() if (c.peek() === '/' && c.src[c.pos + 1] === '/') { while (!c.eof() && c.next() !== '\n') { /* skip line */ } continue } return } } function readToken(c: Cursor): string { skipWhitespaceAndComments(c) if (c.eof()) return '' if (c.peek() === '"') { c.next() let out = '' while (!c.eof()) { const ch = c.next() if (ch === '\\') { const next = c.next() if (next === 'n') out += '\n' else if (next === 't') out += '\t' else out += next continue } if (ch === '"') return out out += ch } return out } if (c.peek() === '{' || c.peek() === '}') return c.next() let out = '' while ( !c.eof() && !/\s/.test(c.peek()) && c.peek() !== '{' && c.peek() !== '}' ) { out += c.next() } return out } function parseObject(c: Cursor): VdfNode { const node: VdfNode = {} for (;;) { skipWhitespaceAndComments(c) if (c.eof()) return node if (c.peek() === '}') { c.next() return node } const key = readToken(c) if (!key) return node skipWhitespaceAndComments(c) if (c.peek() === '{') { c.next() node[key] = parseObject(c) } else { node[key] = readToken(c) } } } export function parseVdf(src: string): VdfNode { const c = new Cursor(src) const root: VdfNode = {} for (;;) { skipWhitespaceAndComments(c) if (c.eof()) break const key = readToken(c) if (!key) break skipWhitespaceAndComments(c) if (c.peek() === '{') { c.next() root[key] = parseObject(c) } else { root[key] = readToken(c) } } return root } function escapeVdfString(s: string): string { return s .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\n/g, '\\n') .replace(/\t/g, '\\t') } export function stringifyVdf(node: VdfNode, indent: number = 0): string { const pad = '\t'.repeat(indent) let out = '' for (const key of Object.keys(node)) { const value = node[key] if (typeof value === 'string') { out += `${pad}"${escapeVdfString(key)}"\t\t"${escapeVdfString(value)}"\n` } else { out += `${pad}"${escapeVdfString(key)}"\n${pad}{\n` out += stringifyVdf(value, indent + 1) out += `${pad}}\n` } } return out }