Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

68 lines · 2.0 KBRaw History
1/** Splitting of strings containing `${{ expr }}` placeholders. */
2
3export type TemplatePart =
4 | { kind: "text"; text: string }
5 | { kind: "expr"; expr: string; start: number; end: number };
6
7export class TemplateError extends Error {
8 constructor(
9 message: string,
10 readonly offset: number,
11 ) {
12 super(message);
13 this.name = "TemplateError";
14 }
15}
16
17export function containsExpression(s: string): boolean {
18 return typeof s === "string" && s.includes("${{");
19}
20
21/**
22 * Splits a template into literal text and expressions. `}}` inside a
23 * single-quoted string literal does not close the expression.
24 * Throws TemplateError when a `${{` is not closed.
25 */
26export function splitTemplate(s: string): TemplatePart[] {
27 const parts: TemplatePart[] = [];
28 let pos = 0;
29 for (;;) {
30 const open = s.indexOf("${{", pos);
31 if (open < 0) {
32 if (pos < s.length) parts.push({ kind: "text", text: s.slice(pos) });
33 return parts;
34 }
35 if (open > pos) parts.push({ kind: "text", text: s.slice(pos, open) });
36 let i = open + 3;
37 let inString = false;
38 let close = -1;
39 while (i < s.length) {
40 const c = s[i];
41 if (c === "'") inString = !inString; // '' escapes toggle twice, which is a no-op
42 else if (!inString && c === "}" && s[i + 1] === "}") {
43 close = i;
44 break;
45 }
46 i++;
47 }
48 if (close < 0) {
49 throw new TemplateError(`Unclosed expression: '\${{' at offset ${open} has no matching '}}'`, open);
50 }
51 parts.push({ kind: "expr", expr: s.slice(open + 3, close).trim(), start: open, end: close + 2 });
52 pos = close + 2;
53 }
54}
55
56/** If the whole (trimmed) string is a single `${{ expr }}`, returns the inner expression. */
57export function soleExpression(s: string): string | undefined {
58 const t = s.trim();
59 if (!t.startsWith("${{")) return undefined;
60 let parts: TemplatePart[];
61 try {
62 parts = splitTemplate(t);
63 } catch {
64 return undefined;
65 }
66 const only = parts[0];
67 return parts.length === 1 && only?.kind === "expr" ? only.expr : undefined;
68}