Files
abdulive/packages/core/src/yaml.ts
T
ZeasyAlexandClaude Opus 5.5 b1cf0f8fab YAML: Listen aus Maps
`- 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>
2026-09-23 22:58:35 +02:00

140 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}