// Minimaler YAML-Leser fuer unsere eigenen Konfigurationsdateien. // // Deckt genau das ab, was config/*.yaml benutzt: verschachtelte Maps ueber // Einrueckung, Skalare, Inline-Maps `{a: 1, b: 2}`, Listen aus Skalaren oder // Maps (`- titel: X`) und Blockskalare (`>` / `|`). Bewusst kein vollstaendiges YAML - das Projekt // kommt ohne npm-Abhaengigkeiten aus, und der Sprachumfang, den wir selbst // schreiben, ist ueberschaubar. export type YamlValue = string | number | boolean | null | YamlValue[] | { [k: string]: YamlValue }; export type YamlMap = { [k: string]: YamlValue }; function scalar(raw: string): YamlValue { const s = raw.trim(); if (s === '' || s === '~' || s === 'null') return null; if (s === 'true') return true; if (s === 'false') return false; if (/^-?\d+$/.test(s)) return Number.parseInt(s, 10); if (/^-?\d*\.\d+$/.test(s)) return Number.parseFloat(s); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { return s.slice(1, -1); } return s; } function flowMap(raw: string): YamlMap { const out: YamlMap = {}; for (const part of raw.slice(1, -1).split(',')) { if (!part.trim()) continue; const i = part.indexOf(':'); if (i === -1) continue; out[part.slice(0, i).trim()] = scalar(part.slice(i + 1)); } return out; } function stripComment(line: string): string { let quote: string | null = null; for (let i = 0; i < line.length; i++) { const c = line[i]!; if (quote) { if (c === quote) quote = null; } else if (c === '"' || c === "'") { quote = c; } else if (c === '#' && (i === 0 || /\s/.test(line[i - 1]!))) { return line.slice(0, i); } } return line; } type Frame = { indent: number; target: YamlMap; listKey?: string }; export function parseYaml(text: string): YamlMap { // Byte Order Mark abschneiden. Notepad und `Set-Content -Encoding utf8` // unter Windows PowerShell 5.1 schreiben eines an den Dateianfang; ohne // diese Zeile heisst der erste Schluessel dann "channel" und die // Einstellung wird stillschweigend ignoriert. const lines = text.replace(/^/, '').split(/\r?\n/); const root: YamlMap = {}; const stack: Frame[] = [{ indent: -1, target: root }]; let i = 0; while (i < lines.length) { const line = stripComment(lines[i]!); if (!line.trim()) { i++; continue; } const indent = line.length - line.trimStart().length; const body = line.trim(); while (stack.length > 1 && indent <= stack[stack.length - 1]!.indent) stack.pop(); const frame = stack[stack.length - 1]!; if (body.startsWith('- ')) { const key = frame.listKey; const list = key ? frame.target[key] : undefined; const item = body.slice(2); // `- titel: X` beginnt eine Map als Listeneintrag. Die Zeile wird mit // der Einrueckung ihres Inhalts neu gelesen, die folgenden, tiefer // eingerueckten Zeilen landen in derselben Map. Ohne das zerfiel // `rewards.belohnungen` in einzelne Zeichenketten, und jede Belohnung // war ohne Fehlermeldung unwirksam. if (Array.isArray(list) && /^[^\s'"{[#-][^:]*:(\s|$)/.test(item)) { const map: YamlMap = {}; list.push(map); stack.push({ indent, target: map }); lines[i] = ' '.repeat(indent + 2) + item; continue; } if (Array.isArray(list)) list.push(scalar(item)); i++; continue; } const colon = body.indexOf(':'); if (colon === -1) { i++; continue; } const key = body.slice(0, colon).trim(); const rest = body.slice(colon + 1).trim(); if (rest === '') { // Verschachtelte Map, Liste oder leerer Wert - die naechste // inhaltstragende Zeile entscheidet let j = i + 1; while (j < lines.length && !stripComment(lines[j]!).trim()) j++; const next = j < lines.length ? stripComment(lines[j]!) : ''; const nextIndent = next ? next.length - next.trimStart().length : -1; if (next && nextIndent > indent && next.trim().startsWith('- ')) { frame.target[key] = []; stack.push({ indent, target: frame.target, listKey: key }); } else if (next && nextIndent > indent) { const child: YamlMap = {}; frame.target[key] = child; stack.push({ indent, target: child }); } else { frame.target[key] = null; } } else if (rest === '>' || rest === '|' || rest === '>-' || rest === '|-') { const parts: string[] = []; let j = i + 1; for (; j < lines.length; j++) { const raw = lines[j]!; if (!raw.trim()) { parts.push(''); continue; } if (raw.length - raw.trimStart().length <= indent) break; parts.push(raw.trim()); } frame.target[key] = rest.startsWith('>') ? parts.join(' ').trim() : parts.join('\n'); i = j; continue; } else if (rest.startsWith('{')) { frame.target[key] = flowMap(rest); } else if (rest.startsWith('[')) { frame.target[key] = rest.slice(1, -1).split(',').map(scalar).filter((v) => v !== null); } else { frame.target[key] = scalar(rest); } i++; } return root; }