summaryrefslogtreecommitdiffhomepage
path: root/packages/system-prompt/src/parser.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/system-prompt/src/parser.ts')
-rw-r--r--packages/system-prompt/src/parser.ts346
1 files changed, 173 insertions, 173 deletions
diff --git a/packages/system-prompt/src/parser.ts b/packages/system-prompt/src/parser.ts
index 5b39b7f..d01e6a8 100644
--- a/packages/system-prompt/src/parser.ts
+++ b/packages/system-prompt/src/parser.ts
@@ -19,48 +19,48 @@
// ─── Token model ─────────────────────────────────────────────────────────────
interface TextToken {
- readonly kind: "text";
- readonly value: string;
+ readonly kind: "text";
+ readonly value: string;
}
interface VarToken {
- readonly kind: "var";
- readonly key: string;
- readonly raw: string;
+ readonly kind: "var";
+ readonly key: string;
+ readonly raw: string;
}
interface IfToken {
- readonly kind: "if";
- readonly key: string;
- readonly negated: boolean;
- readonly raw: string;
+ readonly kind: "if";
+ readonly key: string;
+ readonly negated: boolean;
+ readonly raw: string;
}
interface ElseToken {
- readonly kind: "else";
- readonly raw: string;
+ readonly kind: "else";
+ readonly raw: string;
}
interface EndifToken {
- readonly kind: "endif";
- readonly raw: string;
+ readonly kind: "endif";
+ readonly raw: string;
}
type Token = TextToken | VarToken | IfToken | ElseToken | EndifToken;
// ─── Node model (AST) ────────────────────────────────────────────────────────
interface TextNode {
- readonly kind: "text";
- readonly value: string;
+ readonly kind: "text";
+ readonly value: string;
}
interface VarNode {
- readonly kind: "var";
- readonly key: string;
+ readonly kind: "var";
+ readonly key: string;
}
interface IfNode {
- kind: "if";
- key: string;
- negated: boolean;
- thenBranch: Node[];
- else: Node[] | null;
- matched: boolean;
- readonly raw: string;
+ kind: "if";
+ key: string;
+ negated: boolean;
+ thenBranch: Node[];
+ else: Node[] | null;
+ matched: boolean;
+ readonly raw: string;
}
type Node = TextNode | VarNode | IfNode;
@@ -71,23 +71,23 @@ type Node = TextNode | VarNode | IfNode;
* `null` when it is not a recognized tag (then it stays literal text).
*/
function classifyTag(content: string): Token | null {
- const trimmed = content.trim();
- if (trimmed === "endif") return { kind: "endif", raw: `[${content}]` };
- if (trimmed === "else") return { kind: "else", raw: `[${content}]` };
+ const trimmed = content.trim();
+ if (trimmed === "endif") return { kind: "endif", raw: `[${content}]` };
+ if (trimmed === "else") return { kind: "else", raw: `[${content}]` };
- // `[if type:name]` / `[if !type:name]`
- const ifMatch = /^if\s+(!?)(\w+:.*)$/.exec(trimmed);
- if (ifMatch) {
- const negated = (ifMatch[1] ?? "") === "!";
- const key = ifMatch[2] ?? "";
- return { kind: "if", key, negated, raw: `[${content}]` };
- }
+ // `[if type:name]` / `[if !type:name]`
+ const ifMatch = /^if\s+(!?)(\w+:.*)$/.exec(trimmed);
+ if (ifMatch) {
+ const negated = (ifMatch[1] ?? "") === "!";
+ const key = ifMatch[2] ?? "";
+ return { kind: "if", key, negated, raw: `[${content}]` };
+ }
- // `[type:name]` — variable insertion (any `word:rest`)
- const varMatch = /^(\w+:.*)$/.exec(trimmed);
- if (varMatch) return { kind: "var", key: trimmed, raw: `[${content}]` };
+ // `[type:name]` — variable insertion (any `word:rest`)
+ const varMatch = /^(\w+:.*)$/.exec(trimmed);
+ if (varMatch) return { kind: "var", key: trimmed, raw: `[${content}]` };
- return null;
+ return null;
}
/**
@@ -96,45 +96,45 @@ function classifyTag(content: string): Token | null {
* as literal text.
*/
function tokenize(template: string): Token[] {
- const tokens: Token[] = [];
- let buf = "";
- let i = 0;
- const n = template.length;
+ const tokens: Token[] = [];
+ let buf = "";
+ let i = 0;
+ const n = template.length;
- const flush = (): void => {
- if (buf.length > 0) {
- tokens.push({ kind: "text", value: buf });
- buf = "";
- }
- };
+ const flush = (): void => {
+ if (buf.length > 0) {
+ tokens.push({ kind: "text", value: buf });
+ buf = "";
+ }
+ };
- while (i < n) {
- const ch = template[i];
- if (ch === undefined) break;
- if (ch === "[") {
- const close = template.indexOf("]", i + 1);
- if (close === -1) {
- buf += "[";
- i++;
- continue;
- }
- const content = template.slice(i + 1, close);
- const tag = classifyTag(content);
- if (tag !== null) {
- flush();
- tokens.push(tag);
- i = close + 1;
- continue;
- }
- buf += "[";
- i++;
- } else {
- buf += ch;
- i++;
- }
- }
- flush();
- return tokens;
+ while (i < n) {
+ const ch = template[i];
+ if (ch === undefined) break;
+ if (ch === "[") {
+ const close = template.indexOf("]", i + 1);
+ if (close === -1) {
+ buf += "[";
+ i++;
+ continue;
+ }
+ const content = template.slice(i + 1, close);
+ const tag = classifyTag(content);
+ if (tag !== null) {
+ flush();
+ tokens.push(tag);
+ i = close + 1;
+ continue;
+ }
+ buf += "[";
+ i++;
+ } else {
+ buf += ch;
+ i++;
+ }
+ }
+ flush();
+ return tokens;
}
// ─── Parser (token stream → AST) ─────────────────────────────────────────────
@@ -147,99 +147,99 @@ const EMPTY: readonly Node[] = Object.freeze([]) as readonly Node[];
* stray `else`/`endif` (no open `if`) becomes a literal text node.
*/
function parse(tokens: readonly Token[]): Node[] {
- const root: Node[] = [];
- const stack: IfNode[] = [];
- let current: Node[] = root;
+ const root: Node[] = [];
+ const stack: IfNode[] = [];
+ let current: Node[] = root;
- for (const tok of tokens) {
- switch (tok.kind) {
- case "text":
- current.push({ kind: "text", value: tok.value });
- break;
- case "var":
- current.push({ kind: "var", key: tok.key });
- break;
- case "if": {
- const node: IfNode = {
- kind: "if",
- key: tok.key,
- negated: tok.negated,
- thenBranch: [],
- else: null,
- matched: true,
- raw: tok.raw,
- };
- current.push(node);
- stack.push(node);
- current = node.thenBranch;
- break;
- }
- case "else": {
- const top = stack[stack.length - 1];
- if (top !== undefined && top.else === null) {
- top.else = [];
- current = top.else;
- } else {
- // stray else (no open if, or if already has an else) → literal
- current.push({ kind: "text", value: tok.raw });
- }
- break;
- }
- case "endif": {
- const top = stack.pop();
- if (top === undefined) {
- // stray endif → literal
- current.push({ kind: "text", value: tok.raw });
- break;
- }
- const parent = stack[stack.length - 1];
- current = parent === undefined ? root : (parent.else ?? parent.thenBranch);
- break;
- }
- }
- }
+ for (const tok of tokens) {
+ switch (tok.kind) {
+ case "text":
+ current.push({ kind: "text", value: tok.value });
+ break;
+ case "var":
+ current.push({ kind: "var", key: tok.key });
+ break;
+ case "if": {
+ const node: IfNode = {
+ kind: "if",
+ key: tok.key,
+ negated: tok.negated,
+ thenBranch: [],
+ else: null,
+ matched: true,
+ raw: tok.raw,
+ };
+ current.push(node);
+ stack.push(node);
+ current = node.thenBranch;
+ break;
+ }
+ case "else": {
+ const top = stack[stack.length - 1];
+ if (top !== undefined && top.else === null) {
+ top.else = [];
+ current = top.else;
+ } else {
+ // stray else (no open if, or if already has an else) → literal
+ current.push({ kind: "text", value: tok.raw });
+ }
+ break;
+ }
+ case "endif": {
+ const top = stack.pop();
+ if (top === undefined) {
+ // stray endif → literal
+ current.push({ kind: "text", value: tok.raw });
+ break;
+ }
+ const parent = stack[stack.length - 1];
+ current = parent === undefined ? root : (parent.else ?? parent.thenBranch);
+ break;
+ }
+ }
+ }
- // Any `if` still on the stack never found its `endif` → unmatched.
- for (const node of stack) node.matched = false;
- return root;
+ // Any `if` still on the stack never found its `endif` → unmatched.
+ for (const node of stack) node.matched = false;
+ return root;
}
// ─── Renderer (AST → string) ────────────────────────────────────────────────
function variableExists(key: string, vars: ReadonlyMap<string, string | null>): boolean {
- return vars.has(key) && vars.get(key) !== null;
+ return vars.has(key) && vars.get(key) !== null;
}
function render(nodes: readonly Node[], vars: ReadonlyMap<string, string | null>): string {
- let out = "";
- for (const node of nodes) {
- switch (node.kind) {
- case "text":
- out += node.value;
- break;
- case "var":
- out += vars.get(node.key) ?? "";
- break;
- case "if": {
- if (node.matched) {
- const exists = variableExists(node.key, vars);
- const takeThen = node.negated ? !exists : exists;
- const branch = takeThen ? node.thenBranch : (node.else ?? EMPTY);
- out += render(branch, vars);
- } else {
- // Unmatched `if` → the tag is literal text; content still renders.
- out += node.raw;
- out += render(node.thenBranch, vars);
- if (node.else !== null) {
- out += "[else]";
- out += render(node.else, vars);
- }
- }
- break;
- }
- }
- }
- return out;
+ let out = "";
+ for (const node of nodes) {
+ switch (node.kind) {
+ case "text":
+ out += node.value;
+ break;
+ case "var":
+ out += vars.get(node.key) ?? "";
+ break;
+ case "if": {
+ if (node.matched) {
+ const exists = variableExists(node.key, vars);
+ const takeThen = node.negated ? !exists : exists;
+ const branch = takeThen ? node.thenBranch : (node.else ?? EMPTY);
+ out += render(branch, vars);
+ } else {
+ // Unmatched `if` → the tag is literal text; content still renders.
+ out += node.raw;
+ out += render(node.thenBranch, vars);
+ if (node.else !== null) {
+ out += "[else]";
+ out += render(node.else, vars);
+ }
+ }
+ break;
+ }
+ }
+ }
+ return out;
}
// ─── Public API ──────────────────────────────────────────────────────────────
@@ -254,9 +254,9 @@ function render(nodes: readonly Node[], vars: ReadonlyMap<string, string | null>
* - Unmatched `[if]`/`[endif]` tags pass through as literal text.
*/
export function parseTemplate(template: string, vars: ReadonlyMap<string, string | null>): string {
- const tokens = tokenize(template);
- const ast = parse(tokens);
- return render(ast, vars);
+ const tokens = tokenize(template);
+ const ast = parse(tokens);
+ return render(ast, vars);
}
/**
@@ -266,16 +266,16 @@ export function parseTemplate(template: string, vars: ReadonlyMap<string, string
* Returns unique keys in first-seen order.
*/
export function extractVariables(template: string): string[] {
- const tokens = tokenize(template);
- const seen = new Set<string>();
- const keys: string[] = [];
- for (const tok of tokens) {
- if (tok.kind === "var" || tok.kind === "if") {
- if (!seen.has(tok.key)) {
- seen.add(tok.key);
- keys.push(tok.key);
- }
- }
- }
- return keys;
+ const tokens = tokenize(template);
+ const seen = new Set<string>();
+ const keys: string[] = [];
+ for (const tok of tokens) {
+ if (tok.kind === "var" || tok.kind === "if") {
+ if (!seen.has(tok.key)) {
+ seen.add(tok.key);
+ keys.push(tok.key);
+ }
+ }
+ }
+ return keys;
}