config and plugin
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
// Kino-style dedicated_sp.cfg files are line-oriented: `set KEY VALUE` lines
|
||||
// interleaved with `//` comments and blank lines. This module parses a
|
||||
// baseline, applies overlays (each a bag of `set KEY ...` lines that should
|
||||
// override the baseline's line for that KEY), and renders the composed file
|
||||
// back to text while preserving structure.
|
||||
//
|
||||
// Semantics:
|
||||
// - Overlays are matched to baseline lines by KEY only. Overlay's full raw
|
||||
// line replaces the baseline's line at that position.
|
||||
// - Overlay KEYs not present in the baseline are appended at the end.
|
||||
// - Multiple overlays apply in the order given. Later overlays override
|
||||
// earlier ones on the same KEY.
|
||||
// - Comments (// ...) and blank lines are preserved as-is.
|
||||
// - Line endings default to \n for the composed output. Baseline CRLF is
|
||||
// preserved if the baseline uses CRLF throughout.
|
||||
|
||||
interface CfgLine {
|
||||
kind: 'set' | 'raw';
|
||||
key?: string;
|
||||
line: string; // full line text without trailing newline
|
||||
}
|
||||
|
||||
interface ParsedCfg {
|
||||
order: CfgLine[];
|
||||
entries: Map<string, number>; // KEY → index in `order`
|
||||
eol: '\n' | '\r\n';
|
||||
}
|
||||
|
||||
// Matches `set KEY <rest of line>`. KEY is any non-whitespace token. Leading
|
||||
// whitespace and inline `//` comments after the value are preserved via the
|
||||
// captured full line.
|
||||
const SET_LINE_RE = /^\s*set\s+(\S+)\s+/;
|
||||
|
||||
export function parseCfg(text: string): ParsedCfg {
|
||||
// Detect CRLF vs LF. If either \r\n appears we treat the file as CRLF.
|
||||
const eol: '\n' | '\r\n' = /\r\n/.test(text) ? '\r\n' : '\n';
|
||||
const rawLines = text.split(/\r?\n/);
|
||||
// Trailing newline in the source produces an empty final split element —
|
||||
// drop it so we don't emit a spurious blank line on the round-trip.
|
||||
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === '') rawLines.pop();
|
||||
|
||||
const order: CfgLine[] = [];
|
||||
const entries = new Map<string, number>();
|
||||
for (const line of rawLines) {
|
||||
const m = SET_LINE_RE.exec(line);
|
||||
if (m) {
|
||||
const key = m[1];
|
||||
// If the same KEY appears twice in the baseline, later occurrence
|
||||
// wins (matches how the Plutonium runtime reads the cfg top-down).
|
||||
entries.set(key, order.length);
|
||||
order.push({ kind: 'set', key, line });
|
||||
} else {
|
||||
order.push({ kind: 'raw', line });
|
||||
}
|
||||
}
|
||||
return { order, entries, eol };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an overlay's text. Returns KEY → full replacement line. Blank/comment
|
||||
* lines are ignored. If the overlay lists the same KEY twice, the last wins.
|
||||
*/
|
||||
export function parseOverlay(text: string): Map<string, string> {
|
||||
const out = new Map<string, string>();
|
||||
const lines = text.split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
const m = SET_LINE_RE.exec(line);
|
||||
if (!m) continue;
|
||||
out.set(m[1], line);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export interface OverlayInput {
|
||||
/** Stable identifier used to determine application order (asc). */
|
||||
id: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose a baseline cfg with a set of overlays. Overlays apply in `id`
|
||||
* ascending order — deterministic across scopes.
|
||||
*/
|
||||
export function composeCfg(baselineText: string, overlays: OverlayInput[]): string {
|
||||
const parsed = parseCfg(baselineText);
|
||||
|
||||
// Determine application order deterministically.
|
||||
const sorted = [...overlays].sort((a, b) => a.id - b.id);
|
||||
|
||||
// Merge overlay entries: later overlays win per KEY.
|
||||
const merged = new Map<string, string>();
|
||||
for (const overlay of sorted) {
|
||||
const kvs = parseOverlay(overlay.text);
|
||||
for (const [k, line] of kvs) merged.set(k, line);
|
||||
}
|
||||
|
||||
// Apply: replace in-place where the KEY exists in the baseline, else
|
||||
// append. Appended lines preserve overlay order (Map iteration order is
|
||||
// insertion order — which matches overlay id ascending).
|
||||
const appended: string[] = [];
|
||||
for (const [k, line] of merged) {
|
||||
const idx = parsed.entries.get(k);
|
||||
if (idx !== undefined) {
|
||||
parsed.order[idx] = { kind: 'set', key: k, line };
|
||||
} else {
|
||||
appended.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
const bodyLines = parsed.order.map((l) => l.line);
|
||||
if (appended.length > 0) {
|
||||
// Ensure a visual separator before appended lines if the baseline
|
||||
// doesn't already end with a blank line — cheap ergonomics for
|
||||
// operators inspecting the composed file.
|
||||
const last = bodyLines[bodyLines.length - 1];
|
||||
if (last !== undefined && last.trim() !== '') bodyLines.push('');
|
||||
bodyLines.push('// --- appended by electric-cherry overlays ---');
|
||||
for (const line of appended) bodyLines.push(line);
|
||||
}
|
||||
|
||||
// Preserve trailing newline: Plutonium tolerates either, but adding one
|
||||
// matches upstream cfg conventions.
|
||||
return bodyLines.join(parsed.eol) + parsed.eol;
|
||||
}
|
||||
Reference in New Issue
Block a user