@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
228 lines • 9.29 kB
JavaScript
import { compareCodePoints } from "../../../../utils/sort.js";
import { TEMPLATE_ACTION, TEMPLATE_DECLARATION, TEMPLATE_TOKEN } from "./constants.js";
function text(value) {
if (value == null)
return '<no value>';
if (Array.isArray(value))
return `[${value.map(text).join(' ')}]`;
if (typeof value === 'object')
return `map[${Object.keys(value)
.sort(compareCodePoints)
.map((key) => `${key}:${text(value[key])}`)
.join(' ')}]`;
return String(value);
}
function lookup(token, dot, root, variables) {
if (token.startsWith('"'))
return JSON.parse(token);
if (token.startsWith('`'))
return token.slice(1, -1);
if (token === 'true' || token === 'false')
return token === 'true';
if (/^-?\d+$/.test(token))
return Number(token);
if (token === '.')
return dot;
let value = dot, path = token.replace(/^\.+/, '');
if (token.startsWith('$')) {
const at = token.indexOf('.'), name = at < 0 ? token : token.slice(0, at);
path = at < 0 ? '' : token.slice(at + 1);
const bound = variables.get(name);
if (name !== '$' && bound === undefined)
throw new Error(`template: undefined variable "${name}"`);
value = bound === undefined ? root : bound.value;
}
for (const key of path.split('.').filter(Boolean))
value =
typeof value === 'object' && value !== null && !Array.isArray(value)
? (value[key] ?? null)
: null;
return value;
}
function call(name, args) {
const a = args[0], b = args[1];
switch (name) {
case 'len':
return typeof a === 'string' || Array.isArray(a)
? a.length
: Object.keys(a).length;
case 'join':
return b.map(text).join(text(a));
case 'pluck':
return b.map((row) => row[text(a)] ?? null);
case 'print':
return args.map(text).join('');
case 'println':
return `${args.map(text).join(' ')}\n`;
case 'printf': {
let i = 1;
return text(a).replace(/%(%|s|v|d|q|f)/g, (_, flag) => flag === '%'
? '%'
: flag === 'q'
? JSON.stringify(args[i++])
: flag === 'f'
? Number(args[i++]).toFixed(6)
: text(args[i++]));
}
case 'index': {
let v = a;
for (const key of args.slice(1))
v = Array.isArray(v) ? v[Number(key)] : v[text(key)];
return v ?? null;
}
case 'eq':
return args.slice(1).some((value) => value === a);
case 'ne':
return a !== b;
case 'not':
return !a;
case 'and':
return args.every(Boolean);
case 'or':
return args.some(Boolean);
case 'color':
case 'autocolor':
case 'hyperlink':
return args.at(-1) ?? '';
case 'truncate': {
const n = Number(a), s = text(b);
return s.length <= n ? s : s.slice(0, Math.max(0, n - 3)) + '.'.repeat(Math.min(n, 3));
}
case 'contains':
return text(b).includes(text(a));
case 'hasPrefix':
return text(b).startsWith(text(a));
case 'hasSuffix':
return text(b).endsWith(text(a));
default:
throw new Error(`template: function "${name}" not defined`);
}
}
function evaluate(expression, dot, root, variables) {
const tokens = expression.match(TEMPLATE_TOKEN) ?? [];
const parts = [[]];
for (const token of tokens) {
if (token === '|')
parts.push([]);
else
parts.at(-1)?.push(token);
}
let value = null;
parts.forEach((part, i) => {
const head = part[0];
if (head === undefined)
throw new Error('template: empty pipeline');
const args = part.slice(1).map((token) => lookup(token, dot, root, variables));
if (i > 0)
args.push(value);
value =
part.length === 1 && i === 0 && (/^[.$"`]/.test(head) || /^(true|false|-?\d+)$/.test(head))
? lookup(head, dot, root, variables)
: call(head, args);
});
return value;
}
function declaration(expression) {
const match = TEMPLATE_DECLARATION.exec(expression);
if (match === null)
return [[], expression, false];
return [
[match[1], match[2]].filter((name) => name !== undefined),
match[4] ?? '',
match[3] === '=',
];
}
/** Render selected JSON with Go-style actions, pipelines and blocks. */
export function renderTemplate(template, value) {
const tokens = [];
let end = 0, trim = false;
for (const match of template.matchAll(TEMPLATE_ACTION)) {
let segment = template.slice(end, match.index);
if (trim)
segment = segment.trimStart();
if (match[1])
segment = segment.trimEnd();
tokens.push(['text', segment], ['action', match[2] ?? '']);
trim = Boolean(match[3]);
end = match.index + match[0].length;
}
tokens.push(['text', trim ? template.slice(end).trimStart() : template.slice(end)]);
function render(start, stop, dot, variables) {
const output = [];
for (let i = start; i < stop;) {
const [tag, action] = tokens[i++] ?? ['', ''];
if (tag === 'text') {
output.push(action);
continue;
}
const command = action.split(' ')[0] ?? '', expression = action.slice(command.length + 1);
if (['range', 'if', 'with'].includes(command)) {
let depth = 1, cursor = i, alternate;
for (; cursor < stop; cursor++) {
const [nestedTag, nestedAction] = tokens[cursor] ?? ['', ''];
if (nestedTag !== 'action')
continue;
const nested = nestedAction.split(' ')[0] ?? '';
if (['range', 'if', 'with'].includes(nested))
depth++;
else if (nested === 'end') {
if (--depth === 0)
break;
}
else if (nested === 'else' && depth === 1)
alternate = cursor;
}
if (depth)
throw new Error('template: unexpected EOF');
const [names, pipeline] = declaration(expression);
if (names.length > 1 && command !== 'range')
throw new Error(`template: too many declarations in ${command}`);
const resolved = evaluate(pipeline, dot, value, variables), bodyEnd = alternate ?? cursor;
const scope = new Map(variables);
if (names[0] !== undefined && command !== 'range')
scope.set(names[0], { value: resolved });
const truthy = Boolean(resolved) && (!Array.isArray(resolved) || resolved.length > 0);
if (command === 'range' && truthy) {
const entries = Array.isArray(resolved)
? resolved.map((item, index) => [index, item])
: Object.keys(resolved)
.sort(compareCodePoints)
.map((key) => [key, resolved[key] ?? null]);
for (const [key, item] of entries) {
const bound = new Map(variables);
if (names.length === 2)
bound.set(names[0] ?? '', { value: key });
const last = names.at(-1);
if (last !== undefined)
bound.set(last, { value: item });
output.push(render(i, bodyEnd, item, bound));
}
}
else if (truthy)
output.push(render(i, bodyEnd, command === 'with' ? resolved : dot, scope));
else if (alternate !== undefined)
output.push(render(alternate + 1, cursor, dot, scope));
i = cursor + 1;
}
else if (command === 'end' || command === 'else')
throw new Error(`template: unexpected ${command}`);
else {
const [names, pipeline, assign] = declaration(action);
const result = evaluate(pipeline, dot, value, variables), name = names[0];
if (name === undefined)
output.push(text(result));
else if (!assign)
variables.set(name, { value: result });
else {
const bound = variables.get(name);
if (bound === undefined)
throw new Error(`template: undefined variable "${name}"`);
bound.value = result;
}
}
}
return output.join('');
}
return render(0, tokens.length, value, new Map());
}
//# sourceMappingURL=template.js.map