@telorun/templating 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cel/catalog.d.ts.map +1 -1
- package/dist/cel/catalog.js +65 -16
- package/package.json +2 -1
- package/src/cel/catalog.ts +66 -19
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/cel/catalog.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/cel/catalog.ts"],"names":[],"mappings":"AAGA;;;0DAG0D;AAC1D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3B,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAClE,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;CAClC;AA2CD,MAAM,MAAM,mBAAmB,GAC3B,YAAY,GACZ,MAAM,GACN,MAAM,GACN,QAAQ,GACR,MAAM,GACN,YAAY,GACZ,MAAM,GACN,UAAU,GACV,SAAS,GACT,MAAM,CAAC;AAEX;;;qBAGqB;AACrB,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;0EACsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;yDAIqD;IACrD,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;wBACoB;IACpB,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;2EACuE;IACvE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC;CACjE;AAED,wEAAwE;AACxE,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;AAyE5D,eAAO,MAAM,aAAa,EAAE,SAAS,cAAc,EA8elD,CAAC;AAEF,oEAAoE;AACpE,wBAAgB,kBAAkB,IAAI,eAAe,EAAE,CAEtD"}
|
package/dist/cel/catalog.js
CHANGED
|
@@ -1,4 +1,45 @@
|
|
|
1
|
+
import { RE2JS } from "re2js";
|
|
1
2
|
import { v1, v3, v4, v5, v6, v7, validate as uuidValidate, version as uuidVersion } from "uuid";
|
|
3
|
+
/** RE2 regex engine for the CEL `regex*` functions — `re2js`, a pure-JS port of
|
|
4
|
+
* Google's RE2. The dialect is RE2 (linear-time, no backtracking, ReDoS-safe) —
|
|
5
|
+
* never JS `RegExp` — so a manifest's regex behaves the same across runtimes.
|
|
6
|
+
* Flags map a trailing option string to RE2 flags; inline `(?s)` etc. work in
|
|
7
|
+
* the pattern too.
|
|
8
|
+
*
|
|
9
|
+
* Why pure-JS and not a native RE2:
|
|
10
|
+
* - The Rust `regex` crate via napi (N-API) is faster, but shipping it means a
|
|
11
|
+
* cross-platform prebuild matrix + per-triple npm packages — a whole release
|
|
12
|
+
* subsystem for one function family.
|
|
13
|
+
* - The `re2` npm package (Google's C++ RE2) is simpler to depend on, but it's a
|
|
14
|
+
* nan/V8 addon and **does not load under Bun** (incomplete V8 C++ ABI →
|
|
15
|
+
* `undefined symbol`). Telo's CLI and test suite run on Bun, so that's a
|
|
16
|
+
* non-starter.
|
|
17
|
+
* `re2js` is pure JS: zero native addons, runs identically on Node, Bun, and the
|
|
18
|
+
* browser (keeps this package — and the analyzer — browser-safe), and needs no
|
|
19
|
+
* prebuilds. The cost is throughput vs. native, which is irrelevant for the
|
|
20
|
+
* small strings CEL manifests run regex over. */
|
|
21
|
+
const RE2_FLAG = {
|
|
22
|
+
i: RE2JS.CASE_INSENSITIVE,
|
|
23
|
+
m: RE2JS.MULTILINE,
|
|
24
|
+
s: RE2JS.DOTALL,
|
|
25
|
+
};
|
|
26
|
+
const compileRe2 = (fn, pattern, flags) => {
|
|
27
|
+
let bits = 0;
|
|
28
|
+
for (const c of flags ?? "") {
|
|
29
|
+
if (c === "g")
|
|
30
|
+
continue; // global is implicit in replaceAll / find-loop
|
|
31
|
+
const bit = RE2_FLAG[c];
|
|
32
|
+
if (bit === undefined)
|
|
33
|
+
throw new Error(`${fn}: unknown regex flag '${c}' (supported: i, m, s)`);
|
|
34
|
+
bits |= bit;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return RE2JS.compile(pattern, bits);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
throw new Error(`${fn}: invalid RE2 pattern ${JSON.stringify(pattern)}: ${e instanceof Error ? e.message : String(e)}`);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
2
43
|
const num = (x) => Number(x);
|
|
3
44
|
const minMax = (list, isMin) => {
|
|
4
45
|
if (!Array.isArray(list) || list.length === 0)
|
|
@@ -14,9 +55,6 @@ const minMax = (list, isMin) => {
|
|
|
14
55
|
}
|
|
15
56
|
return best;
|
|
16
57
|
};
|
|
17
|
-
/** Ensure a regex replaces every match: append the global flag unless the
|
|
18
|
-
* caller already passed it. */
|
|
19
|
-
const withGlobal = (flags) => flags && flags.includes("g") ? flags : `${flags ?? ""}g`;
|
|
20
58
|
const sortList = (list) => [...list].sort((a, b) => {
|
|
21
59
|
if (typeof a === "number" || typeof a === "bigint") {
|
|
22
60
|
const d = num(a) - num(b);
|
|
@@ -201,39 +239,50 @@ export const CEL_FUNCTIONS = [
|
|
|
201
239
|
name: "regexReplace",
|
|
202
240
|
signature: "regexReplace(string, string, string, string?): string",
|
|
203
241
|
category: "string",
|
|
204
|
-
summary: "Replace every regex match with a replacement ($1 backrefs); flags like 'i', 'm', 's'.",
|
|
242
|
+
summary: "Replace every regex match (RE2 syntax) with a replacement ($1 backrefs); flags like 'i', 'm', 's'.",
|
|
205
243
|
deterministic: true,
|
|
206
244
|
hostBacked: false,
|
|
207
|
-
build: () => (s, pattern, replacement, flags) =>
|
|
245
|
+
build: () => (s, pattern, replacement, flags) => compileRe2("regexReplace", pattern, flags).matcher(s).replaceAll(replacement),
|
|
208
246
|
},
|
|
209
247
|
{
|
|
210
248
|
name: "regexExtract",
|
|
211
|
-
signature: "regexExtract(string, string): string",
|
|
249
|
+
signature: "regexExtract(string, string, string?): string",
|
|
212
250
|
category: "string",
|
|
213
|
-
summary: "First whole match of a regex, or '' when there is none.",
|
|
251
|
+
summary: "First whole match of a regex (RE2 syntax), or '' when there is none.",
|
|
214
252
|
deterministic: true,
|
|
215
253
|
hostBacked: false,
|
|
216
|
-
build: () => (s, pattern) =>
|
|
254
|
+
build: () => (s, pattern, flags) => {
|
|
255
|
+
const m = compileRe2("regexExtract", pattern, flags).matcher(s);
|
|
256
|
+
return m.find() ? (m.group() ?? "") : "";
|
|
257
|
+
},
|
|
217
258
|
},
|
|
218
259
|
{
|
|
219
260
|
name: "regexExtractAll",
|
|
220
|
-
signature: "regexExtractAll(string, string): list<string>",
|
|
261
|
+
signature: "regexExtractAll(string, string, string?): list<string>",
|
|
221
262
|
category: "string",
|
|
222
|
-
summary: "Every whole match of a regex, in order.",
|
|
263
|
+
summary: "Every whole match of a regex (RE2 syntax), in order.",
|
|
223
264
|
deterministic: true,
|
|
224
265
|
hostBacked: false,
|
|
225
|
-
build: () => (s, pattern
|
|
266
|
+
build: () => (s, pattern, flags) => {
|
|
267
|
+
const m = compileRe2("regexExtractAll", pattern, flags).matcher(s);
|
|
268
|
+
const out = [];
|
|
269
|
+
while (m.find())
|
|
270
|
+
out.push(m.group() ?? "");
|
|
271
|
+
return out;
|
|
272
|
+
},
|
|
226
273
|
},
|
|
227
274
|
{
|
|
228
275
|
name: "regexGroups",
|
|
229
|
-
signature: "regexGroups(string, string): list<string>",
|
|
276
|
+
signature: "regexGroups(string, string, string?): list<string>",
|
|
230
277
|
category: "string",
|
|
231
|
-
summary: "Capture groups of the first regex match (empty list when there is no match
|
|
278
|
+
summary: "Capture groups of the first regex match (RE2 syntax); empty list when there is no match.",
|
|
232
279
|
deterministic: true,
|
|
233
280
|
hostBacked: false,
|
|
234
|
-
build: () => (s, pattern) => {
|
|
235
|
-
const m =
|
|
236
|
-
|
|
281
|
+
build: () => (s, pattern, flags) => {
|
|
282
|
+
const m = compileRe2("regexGroups", pattern, flags).matcher(s);
|
|
283
|
+
if (!m.find())
|
|
284
|
+
return [];
|
|
285
|
+
return Array.from({ length: m.groupCount() }, (_unused, i) => m.group(i + 1) ?? "");
|
|
237
286
|
},
|
|
238
287
|
},
|
|
239
288
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/templating",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Telo Templating - Engine registry and shared CEL core for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@marcbachmann/cel-js": "^7.6.1",
|
|
39
|
+
"re2js": "^2.8.3",
|
|
39
40
|
"uuid": "^10.0.0",
|
|
40
41
|
"yaml": "^2.8.3"
|
|
41
42
|
},
|
package/src/cel/catalog.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RE2JS } from "re2js";
|
|
1
2
|
import { v1, v3, v4, v5, v6, v7, validate as uuidValidate, version as uuidVersion } from "uuid";
|
|
2
3
|
|
|
3
4
|
/** Host-injected functions that need platform APIs the templating package must
|
|
@@ -15,6 +16,47 @@ export interface CelHandlers {
|
|
|
15
16
|
json: (value: unknown) => string;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/** RE2 regex engine for the CEL `regex*` functions — `re2js`, a pure-JS port of
|
|
20
|
+
* Google's RE2. The dialect is RE2 (linear-time, no backtracking, ReDoS-safe) —
|
|
21
|
+
* never JS `RegExp` — so a manifest's regex behaves the same across runtimes.
|
|
22
|
+
* Flags map a trailing option string to RE2 flags; inline `(?s)` etc. work in
|
|
23
|
+
* the pattern too.
|
|
24
|
+
*
|
|
25
|
+
* Why pure-JS and not a native RE2:
|
|
26
|
+
* - The Rust `regex` crate via napi (N-API) is faster, but shipping it means a
|
|
27
|
+
* cross-platform prebuild matrix + per-triple npm packages — a whole release
|
|
28
|
+
* subsystem for one function family.
|
|
29
|
+
* - The `re2` npm package (Google's C++ RE2) is simpler to depend on, but it's a
|
|
30
|
+
* nan/V8 addon and **does not load under Bun** (incomplete V8 C++ ABI →
|
|
31
|
+
* `undefined symbol`). Telo's CLI and test suite run on Bun, so that's a
|
|
32
|
+
* non-starter.
|
|
33
|
+
* `re2js` is pure JS: zero native addons, runs identically on Node, Bun, and the
|
|
34
|
+
* browser (keeps this package — and the analyzer — browser-safe), and needs no
|
|
35
|
+
* prebuilds. The cost is throughput vs. native, which is irrelevant for the
|
|
36
|
+
* small strings CEL manifests run regex over. */
|
|
37
|
+
const RE2_FLAG: Record<string, number> = {
|
|
38
|
+
i: RE2JS.CASE_INSENSITIVE,
|
|
39
|
+
m: RE2JS.MULTILINE,
|
|
40
|
+
s: RE2JS.DOTALL,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const compileRe2 = (fn: string, pattern: string, flags?: string): RE2JS => {
|
|
44
|
+
let bits = 0;
|
|
45
|
+
for (const c of flags ?? "") {
|
|
46
|
+
if (c === "g") continue; // global is implicit in replaceAll / find-loop
|
|
47
|
+
const bit = RE2_FLAG[c];
|
|
48
|
+
if (bit === undefined) throw new Error(`${fn}: unknown regex flag '${c}' (supported: i, m, s)`);
|
|
49
|
+
bits |= bit;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return RE2JS.compile(pattern, bits);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`${fn}: invalid RE2 pattern ${JSON.stringify(pattern)}: ${e instanceof Error ? e.message : String(e)}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
18
60
|
export type CelFunctionCategory =
|
|
19
61
|
| "conversion"
|
|
20
62
|
| "time"
|
|
@@ -73,11 +115,6 @@ const minMax = (list: unknown[], isMin: boolean): unknown => {
|
|
|
73
115
|
return best;
|
|
74
116
|
};
|
|
75
117
|
|
|
76
|
-
/** Ensure a regex replaces every match: append the global flag unless the
|
|
77
|
-
* caller already passed it. */
|
|
78
|
-
const withGlobal = (flags?: string): string =>
|
|
79
|
-
flags && flags.includes("g") ? flags : `${flags ?? ""}g`;
|
|
80
|
-
|
|
81
118
|
const sortList = (list: unknown[]): unknown[] =>
|
|
82
119
|
[...list].sort((a, b) => {
|
|
83
120
|
if (typeof a === "number" || typeof a === "bigint") {
|
|
@@ -270,41 +307,51 @@ export const CEL_FUNCTIONS: readonly CelFunctionDoc[] = [
|
|
|
270
307
|
name: "regexReplace",
|
|
271
308
|
signature: "regexReplace(string, string, string, string?): string",
|
|
272
309
|
category: "string",
|
|
273
|
-
summary:
|
|
310
|
+
summary:
|
|
311
|
+
"Replace every regex match (RE2 syntax) with a replacement ($1 backrefs); flags like 'i', 'm', 's'.",
|
|
274
312
|
deterministic: true,
|
|
275
313
|
hostBacked: false,
|
|
276
314
|
build: () => (s: string, pattern: string, replacement: string, flags?: string) =>
|
|
277
|
-
|
|
315
|
+
compileRe2("regexReplace", pattern, flags).matcher(s).replaceAll(replacement),
|
|
278
316
|
},
|
|
279
317
|
{
|
|
280
318
|
name: "regexExtract",
|
|
281
|
-
signature: "regexExtract(string, string): string",
|
|
319
|
+
signature: "regexExtract(string, string, string?): string",
|
|
282
320
|
category: "string",
|
|
283
|
-
summary: "First whole match of a regex, or '' when there is none.",
|
|
321
|
+
summary: "First whole match of a regex (RE2 syntax), or '' when there is none.",
|
|
284
322
|
deterministic: true,
|
|
285
323
|
hostBacked: false,
|
|
286
|
-
build: () => (s: string, pattern: string
|
|
324
|
+
build: () => (s: string, pattern: string, flags?: string) => {
|
|
325
|
+
const m = compileRe2("regexExtract", pattern, flags).matcher(s);
|
|
326
|
+
return m.find() ? (m.group() ?? "") : "";
|
|
327
|
+
},
|
|
287
328
|
},
|
|
288
329
|
{
|
|
289
330
|
name: "regexExtractAll",
|
|
290
|
-
signature: "regexExtractAll(string, string): list<string>",
|
|
331
|
+
signature: "regexExtractAll(string, string, string?): list<string>",
|
|
291
332
|
category: "string",
|
|
292
|
-
summary: "Every whole match of a regex, in order.",
|
|
333
|
+
summary: "Every whole match of a regex (RE2 syntax), in order.",
|
|
293
334
|
deterministic: true,
|
|
294
335
|
hostBacked: false,
|
|
295
|
-
build: () => (s: string, pattern: string) =>
|
|
296
|
-
|
|
336
|
+
build: () => (s: string, pattern: string, flags?: string) => {
|
|
337
|
+
const m = compileRe2("regexExtractAll", pattern, flags).matcher(s);
|
|
338
|
+
const out: string[] = [];
|
|
339
|
+
while (m.find()) out.push(m.group() ?? "");
|
|
340
|
+
return out;
|
|
341
|
+
},
|
|
297
342
|
},
|
|
298
343
|
{
|
|
299
344
|
name: "regexGroups",
|
|
300
|
-
signature: "regexGroups(string, string): list<string>",
|
|
345
|
+
signature: "regexGroups(string, string, string?): list<string>",
|
|
301
346
|
category: "string",
|
|
302
|
-
summary:
|
|
347
|
+
summary:
|
|
348
|
+
"Capture groups of the first regex match (RE2 syntax); empty list when there is no match.",
|
|
303
349
|
deterministic: true,
|
|
304
350
|
hostBacked: false,
|
|
305
|
-
build: () => (s: string, pattern: string) => {
|
|
306
|
-
const m =
|
|
307
|
-
|
|
351
|
+
build: () => (s: string, pattern: string, flags?: string) => {
|
|
352
|
+
const m = compileRe2("regexGroups", pattern, flags).matcher(s);
|
|
353
|
+
if (!m.find()) return [];
|
|
354
|
+
return Array.from({ length: m.groupCount() }, (_unused, i) => m.group(i + 1) ?? "");
|
|
308
355
|
},
|
|
309
356
|
},
|
|
310
357
|
{
|