`- titel: X` zerfiel in Zeichenketten wie "titel: X". Jede Kanalpunkt- Belohnung in der Konfiguration war damit ohne jede Meldung unwirksam, auch das Beispiel in config.example.yaml. Ein Listeneintrag der Form `schluessel: wert` beginnt jetzt eine Map; die folgenden, tiefer eingerueckten Zeilen gehoeren dazu. `- http://x` bleibt Text, weil nach dem Doppelpunkt kein Leerzeichen steht. layout.yaml, config.example.yaml und die Hub-Konfiguration werden nachweislich gleich gelesen wie vorher. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
140 lines
5.0 KiB
TypeScript
140 lines
5.0 KiB
TypeScript
// 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;
|
||
}
|