diff options
Diffstat (limited to 'packages/system-prompt/src/parser.ts')
| -rw-r--r-- | packages/system-prompt/src/parser.ts | 346 |
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; } |
