TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1/**2 * Prompt-library template variables — pure helpers shared by the server (render on `/use`) and the client3 * (live detection in the editor, fill-in sheet). No React, no DB, no `server-only`.4 *5 * Syntax: `{{name}}` (whitespace inside the braces is tolerated: `{{ name }}`). Names start with a letter or6 * underscore and may contain letters, digits, `_`, `-` and `.`. Anything else is left untouched, so JSON examples7 * and Jinja-like blocks (`{% … %}`) inside a prompt are safe.8 */910export const VARIABLE_RE = /\{\{\s*([A-Za-z_][\w.-]*)\s*\}\}/g;1112export interface PromptVariable {13 name: string;14 label?: string;15 default?: string;16 required?: boolean;17 /** When set, the fill-in UI shows a select instead of a free text input. */18 options?: string[];19}2021/** Unique variable names in order of first appearance. */22export function parseVariables(content: string): string[] {23 const out: string[] = [];24 const seen = new Set<string>();25 if (!content) return out;26 for (const m of content.matchAll(VARIABLE_RE)) {27 const name = m[1];28 if (!seen.has(name)) {29 seen.add(name);30 out.push(name);31 }32 }33 return out;34}3536/**37 * Reconciles the declared variable list with the names actually present in the content: keeps38 * label/default/options for names that still exist, appends new names, drops removed ones. Order = content order.39 */40export function mergeVariables(content: string, declared: PromptVariable[] | null | undefined): PromptVariable[] {41 const byName = new Map((declared ?? []).map((v) => [v.name, v]));42 return parseVariables(content).map((name) => {43 const prev = byName.get(name);44 return prev ? { ...prev, name } : { name };45 });46}4748/** Human label fallback: `customer_name` → `Customer name`, `lang.code` → `Lang code`. */49export function labelFor(v: PromptVariable): string {50 if (v.label?.trim()) return v.label.trim();51 const words = v.name.replace(/[_.-]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim().toLowerCase();52 return words.charAt(0).toUpperCase() + words.slice(1);53}5455export interface RenderResult {56 text: string;57 /** Variables that had neither a value nor a default (left as empty strings). */58 missing: string[];59 /** Variables that were filled with their declared default. */60 defaulted: string[];61}6263/**64 * Replaces every `{{name}}` with `values[name]`, falling back to the declared default, then to `""`.65 * Unknown names (not declared) still render from `values` when provided.66 */67export function renderTemplate(content: string, values: Record<string, string | undefined> = {}, declared: PromptVariable[] = []): RenderResult {68 const defaults = new Map(declared.map((v) => [v.name, v.default]));69 const missing = new Set<string>();70 const defaulted = new Set<string>();71 const text = content.replace(VARIABLE_RE, (_m, name: string) => {72 const v = values[name];73 if (v !== undefined && v !== null && v !== "") return String(v);74 const d = defaults.get(name);75 if (d !== undefined && d !== "") {76 defaulted.add(name);77 return d;78 }79 missing.add(name);80 return "";81 });82 return { text, missing: [...missing], defaulted: [...defaulted] };83}8485/** Variables that the fill-in sheet must ask for: everything without a default (or explicitly required). */86export function variablesToAsk(vars: PromptVariable[]): PromptVariable[] {87 return vars.filter((v) => v.required || v.default === undefined || v.default === "");88}89