Implement AXIS parity UI and execution baseline
This commit is contained in:
120
web/workspace/ini-parser.mjs
Normal file
120
web/workspace/ini-parser.mjs
Normal file
@@ -0,0 +1,120 @@
|
||||
import { fnv1a64 } from "../worker/replay.mjs";
|
||||
import { normalizeWorkspacePath } from "./runtime.mjs";
|
||||
|
||||
export const INI_SCHEMA = "ini-resource-graph.v1";
|
||||
export const INI_ERRORS = Object.freeze({
|
||||
missing: "ini_source_missing",
|
||||
cycle: "ini_include_cycle",
|
||||
depth: "ini_include_depth",
|
||||
section: "ini_section_invalid",
|
||||
key: "ini_key_invalid",
|
||||
assignment: "ini_assignment_invalid",
|
||||
variable: "ini_variable_missing",
|
||||
path: "ini_path_invalid",
|
||||
});
|
||||
|
||||
function clone(value) { return structuredClone(value); }
|
||||
function error(code, path, line, message = code, extra = {}) {
|
||||
return Object.assign(new Error(message), { code, source: path, line, column: 1, ...extra });
|
||||
}
|
||||
function stripComment(line) {
|
||||
let quote = null;
|
||||
for (let index = 0; index < line.length; index += 1) {
|
||||
const char = line[index];
|
||||
if ((char === "\"" || char === "'") && (!quote || quote === char)) quote = quote ? null : char;
|
||||
if (!quote && (char === ";" || char === "#")) return line.slice(0, index);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
function resolveInclude(basePath, includePath) {
|
||||
if (includePath.startsWith("/") || includePath.includes("\\")) throw error(INI_ERRORS.path, basePath, 0, "ini_include_path_invalid");
|
||||
const baseParts = basePath.split("/");
|
||||
baseParts.pop();
|
||||
return normalizeWorkspacePath(`/workspace/${[...baseParts, includePath].join("/")}`);
|
||||
}
|
||||
function expand(value, variables, path, line) {
|
||||
return value.replace(/\$\{([^}]+)\}|\$\(([^)]+)\)|\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, brace, paren, plain) => {
|
||||
const name = brace ?? paren ?? plain;
|
||||
if (!(name in variables)) throw error(INI_ERRORS.variable, path, line, "ini_variable_missing", { variable: name });
|
||||
return String(variables[name]);
|
||||
});
|
||||
}
|
||||
|
||||
export function parseIni({ workspace, entrypoint, variables = {}, maxDepth = 32 } = {}) {
|
||||
if (!workspace || typeof workspace.read !== "function") throw new Error("ini_workspace_required");
|
||||
const root = normalizeWorkspacePath(entrypoint);
|
||||
const sections = {};
|
||||
const entries = [];
|
||||
const includes = [];
|
||||
const diagnostics = [];
|
||||
const graph = [];
|
||||
const active = [];
|
||||
const vars = { ...variables };
|
||||
|
||||
function parseFile(path, depth) {
|
||||
if (depth > maxDepth) throw error(INI_ERRORS.depth, path, 0, "ini_include_depth", { maxDepth });
|
||||
if (active.includes(path)) throw error(INI_ERRORS.cycle, path, 0, "ini_include_cycle", { cycle: [...active, path] });
|
||||
active.push(path);
|
||||
let source;
|
||||
try { source = workspace.read(`/workspace/${path}`); } catch (cause) {
|
||||
active.pop();
|
||||
throw error(INI_ERRORS.missing, path, 0, "ini_source_missing", { cause: String(cause?.message ?? cause) });
|
||||
}
|
||||
graph.push(path);
|
||||
let section = "";
|
||||
const lines = source.split(/\r?\n/);
|
||||
lines.forEach((rawLine, offset) => {
|
||||
const lineNumber = offset + 1;
|
||||
const hashInclude = rawLine.trim().match(/^#INCLUDE\s+(.+)$/i);
|
||||
if (hashInclude) {
|
||||
const rawInclude = hashInclude[1].trim().replace(/^['"]|['"]$/g, "");
|
||||
let child;
|
||||
try { child = resolveInclude(path, expand(rawInclude, vars, path, lineNumber)); } catch (cause) { throw cause.code ? cause : error(INI_ERRORS.path, path, lineNumber, "ini_include_path_invalid"); }
|
||||
includes.push({ source: path, line: lineNumber, path: child });
|
||||
parseFile(child, depth + 1);
|
||||
return;
|
||||
}
|
||||
const clean = stripComment(rawLine).trim();
|
||||
if (!clean) return;
|
||||
const includeMatch = clean.match(/^(?:include|INCLUDE)\s*(?:=\s*)?(.+)$/);
|
||||
if (includeMatch) {
|
||||
const rawInclude = includeMatch[1].trim().replace(/^['"]|['"]$/g, "");
|
||||
let child;
|
||||
try { child = resolveInclude(path, expand(rawInclude, vars, path, lineNumber)); } catch (cause) { throw cause.code ? cause : error(INI_ERRORS.path, path, lineNumber, "ini_include_path_invalid"); }
|
||||
includes.push({ source: path, line: lineNumber, path: child });
|
||||
parseFile(child, depth + 1);
|
||||
return;
|
||||
}
|
||||
const sectionMatch = clean.match(/^\[([^\]]+)\]$/);
|
||||
if (sectionMatch) {
|
||||
section = sectionMatch[1].trim();
|
||||
if (!section || /[\0\r\n]/.test(section)) throw error(INI_ERRORS.section, path, lineNumber, "ini_section_invalid");
|
||||
if (!sections[section]) sections[section] = {};
|
||||
return;
|
||||
}
|
||||
const assignment = clean.match(/^([^=:#]+?)\s*(?:=|:)\s*(.*)$/);
|
||||
if (!assignment || !section) throw error(!section ? INI_ERRORS.section : INI_ERRORS.assignment, path, lineNumber, !section ? "ini_section_invalid" : "ini_assignment_invalid");
|
||||
const key = assignment[1].trim();
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(key)) throw error(INI_ERRORS.key, path, lineNumber, "ini_key_invalid");
|
||||
const value = expand(assignment[2].trim(), vars, path, lineNumber);
|
||||
sections[section][key] = value;
|
||||
if (section.toUpperCase() === "VAR" || section.toUpperCase() === "VARIABLES") vars[key] = value;
|
||||
entries.push({ source: path, line: lineNumber, section, key, value });
|
||||
});
|
||||
active.pop();
|
||||
}
|
||||
|
||||
parseFile(root, 0);
|
||||
const normalizedSections = Object.fromEntries(Object.keys(sections).sort().map((name) => [name, Object.fromEntries(Object.keys(sections[name]).sort().map((key) => [key, sections[name][key]]))]));
|
||||
const result = {
|
||||
schema: INI_SCHEMA,
|
||||
entrypoint: root,
|
||||
sections: normalizedSections,
|
||||
entries,
|
||||
includes,
|
||||
graph: [...new Set(graph)],
|
||||
variables: Object.fromEntries(Object.keys(vars).sort().map((key) => [key, vars[key]])),
|
||||
diagnostics,
|
||||
};
|
||||
return { ...result, state_hash: fnv1a64(result) };
|
||||
}
|
||||
Reference in New Issue
Block a user