Initial commit
This commit is contained in:
122
src/main/games/vdf.ts
Normal file
122
src/main/games/vdf.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user