@hostwebhook/template-engine 1.0.0 → 1.1.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/index.d.mts CHANGED
@@ -74,9 +74,14 @@ declare function tryNullCoalesce(expr: string, ctx: TemplateContext): string | n
74
74
  */
75
75
  declare function tryTernary(expr: string, ctx: TemplateContext): string | null;
76
76
  /**
77
- * Apply a pipe transform: | json, | upper, | lower, | trim
77
+ * Apply a pipe transform.
78
+ *
79
+ * String: | json, | upper, | lower, | trim
80
+ * Date: | formatDate, | formatTime, | formatDateTime, | toISO, | toEpoch
81
+ * | diffDays(otherPath), | diffHours(otherPath)
82
+ * Number: | currency, | percent, | round(decimals)
78
83
  */
79
- declare function applyPipe(pipe: string, val: unknown): string;
84
+ declare function applyPipe(pipe: string, val: unknown, arg?: string, ctx?: TemplateContext): string;
80
85
  /**
81
86
  * Evaluate a single {{...}} expression and return a string.
82
87
  */
package/dist/index.d.ts CHANGED
@@ -74,9 +74,14 @@ declare function tryNullCoalesce(expr: string, ctx: TemplateContext): string | n
74
74
  */
75
75
  declare function tryTernary(expr: string, ctx: TemplateContext): string | null;
76
76
  /**
77
- * Apply a pipe transform: | json, | upper, | lower, | trim
77
+ * Apply a pipe transform.
78
+ *
79
+ * String: | json, | upper, | lower, | trim
80
+ * Date: | formatDate, | formatTime, | formatDateTime, | toISO, | toEpoch
81
+ * | diffDays(otherPath), | diffHours(otherPath)
82
+ * Number: | currency, | percent, | round(decimals)
78
83
  */
79
- declare function applyPipe(pipe: string, val: unknown): string;
84
+ declare function applyPipe(pipe: string, val: unknown, arg?: string, ctx?: TemplateContext): string;
80
85
  /**
81
86
  * Evaluate a single {{...}} expression and return a string.
82
87
  */
package/dist/index.js CHANGED
@@ -218,7 +218,8 @@ var TRUTHY_RE = new RegExp(
218
218
  `^(!?)(.+?)\\s*\\?\\s*(${BRANCH})\\s*:\\s*(${BRANCH})$`
219
219
  );
220
220
  var NULL_COALESCE_RE = /^(.+?)\s*\?\?\s*("(?:[^"]*)"$|'(?:[^']*)'$|\S+$)/;
221
- var PIPE_RE = /^(.+?)\s*\|\s*(json|upper|lower|trim)$/;
221
+ var PIPE_NAMES = "json|upper|lower|trim|diffDays|diffHours|formatDate|formatTime|formatDateTime|toISO|toEpoch|currency|percent|round";
222
+ var PIPE_RE = new RegExp(`^(.+?)\\s*\\|\\s*(${PIPE_NAMES})(?:\\((.+?)\\))?$`);
222
223
  function tryNullCoalesce(expr, ctx) {
223
224
  const m = expr.match(NULL_COALESCE_RE);
224
225
  if (!m) return null;
@@ -245,8 +246,21 @@ function tryTernary(expr, ctx) {
245
246
  if (negate === "!") truthy = !truthy;
246
247
  return truthy ? resolveBranch(trueRaw, ctx) : resolveBranch(falseRaw, ctx);
247
248
  }
248
- function applyPipe(pipe, val) {
249
+ function toDate(val) {
250
+ if (val instanceof Date) return val;
251
+ if (typeof val === "number") return new Date(val);
252
+ if (typeof val === "string") {
253
+ const d = new Date(val);
254
+ return isNaN(d.getTime()) ? null : d;
255
+ }
256
+ return null;
257
+ }
258
+ function pad2(n) {
259
+ return n < 10 ? `0${n}` : String(n);
260
+ }
261
+ function applyPipe(pipe, val, arg, ctx) {
249
262
  switch (pipe) {
263
+ // ── String ───────────────────────────────────────────────
250
264
  case "json":
251
265
  return JSON.stringify(val, null, 2);
252
266
  case "upper":
@@ -255,6 +269,59 @@ function applyPipe(pipe, val) {
255
269
  return String(val ?? "").toLowerCase();
256
270
  case "trim":
257
271
  return String(val ?? "").trim();
272
+ // ── Date formatting ──────────────────────────────────────
273
+ case "formatDate": {
274
+ const d = toDate(val);
275
+ if (!d) return "";
276
+ return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
277
+ }
278
+ case "formatTime": {
279
+ const d = toDate(val);
280
+ if (!d) return "";
281
+ return `${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}`;
282
+ }
283
+ case "formatDateTime": {
284
+ const d = toDate(val);
285
+ if (!d) return "";
286
+ return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}`;
287
+ }
288
+ case "toISO": {
289
+ const d = toDate(val);
290
+ return d ? d.toISOString() : "";
291
+ }
292
+ case "toEpoch": {
293
+ const d = toDate(val);
294
+ return d ? String(d.getTime()) : "";
295
+ }
296
+ // ── Date diff ────────────────────────────────────────────
297
+ case "diffDays":
298
+ case "diffHours": {
299
+ const d1 = toDate(val);
300
+ if (!d1 || !arg || !ctx) return "";
301
+ const otherVal = resolvePath(arg.trim(), ctx);
302
+ const d2 = toDate(otherVal);
303
+ if (!d2) return "";
304
+ const diffMs = Math.abs(d1.getTime() - d2.getTime());
305
+ if (pipe === "diffDays") return String(Math.floor(diffMs / 864e5));
306
+ return String(Math.floor(diffMs / 36e5));
307
+ }
308
+ // ── Number ───────────────────────────────────────────────
309
+ case "currency": {
310
+ const n = Number(val);
311
+ if (isNaN(n)) return "";
312
+ return `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
313
+ }
314
+ case "percent": {
315
+ const n = Number(val);
316
+ if (isNaN(n)) return "";
317
+ return `${n}%`;
318
+ }
319
+ case "round": {
320
+ const n = Number(val);
321
+ if (isNaN(n)) return "";
322
+ const decimals = arg ? parseInt(arg, 10) : 0;
323
+ return n.toFixed(isNaN(decimals) ? 0 : decimals);
324
+ }
258
325
  default:
259
326
  return valueToString(val);
260
327
  }
@@ -271,9 +338,9 @@ function evaluate(expr, ctx, opts) {
271
338
  }
272
339
  const pipeM = trimmed.match(PIPE_RE);
273
340
  if (pipeM) {
274
- const [, path, pipe] = pipeM;
341
+ const [, path, pipe, arg] = pipeM;
275
342
  const val = resolvePath(path.trim(), ctx);
276
- return applyPipe(pipe, val);
343
+ return applyPipe(pipe, val, arg, ctx);
277
344
  }
278
345
  const ncResult = tryNullCoalesce(trimmed, ctx);
279
346
  if (ncResult !== null) return ncResult;
@@ -309,7 +376,7 @@ function resolveJsonValue(value, ctx, opts) {
309
376
  const val = resolvePath(expr.slice(1).trim(), ctx);
310
377
  return !val;
311
378
  }
312
- if (/\|\s*(json|upper|lower|trim)$/.test(expr)) {
379
+ if (/\|\s*\w+/.test(expr)) {
313
380
  return evaluate(expr, ctx, opts);
314
381
  }
315
382
  const resolved = resolvePath(expr, ctx);
package/dist/index.mjs CHANGED
@@ -176,7 +176,8 @@ var TRUTHY_RE = new RegExp(
176
176
  `^(!?)(.+?)\\s*\\?\\s*(${BRANCH})\\s*:\\s*(${BRANCH})$`
177
177
  );
178
178
  var NULL_COALESCE_RE = /^(.+?)\s*\?\?\s*("(?:[^"]*)"$|'(?:[^']*)'$|\S+$)/;
179
- var PIPE_RE = /^(.+?)\s*\|\s*(json|upper|lower|trim)$/;
179
+ var PIPE_NAMES = "json|upper|lower|trim|diffDays|diffHours|formatDate|formatTime|formatDateTime|toISO|toEpoch|currency|percent|round";
180
+ var PIPE_RE = new RegExp(`^(.+?)\\s*\\|\\s*(${PIPE_NAMES})(?:\\((.+?)\\))?$`);
180
181
  function tryNullCoalesce(expr, ctx) {
181
182
  const m = expr.match(NULL_COALESCE_RE);
182
183
  if (!m) return null;
@@ -203,8 +204,21 @@ function tryTernary(expr, ctx) {
203
204
  if (negate === "!") truthy = !truthy;
204
205
  return truthy ? resolveBranch(trueRaw, ctx) : resolveBranch(falseRaw, ctx);
205
206
  }
206
- function applyPipe(pipe, val) {
207
+ function toDate(val) {
208
+ if (val instanceof Date) return val;
209
+ if (typeof val === "number") return new Date(val);
210
+ if (typeof val === "string") {
211
+ const d = new Date(val);
212
+ return isNaN(d.getTime()) ? null : d;
213
+ }
214
+ return null;
215
+ }
216
+ function pad2(n) {
217
+ return n < 10 ? `0${n}` : String(n);
218
+ }
219
+ function applyPipe(pipe, val, arg, ctx) {
207
220
  switch (pipe) {
221
+ // ── String ───────────────────────────────────────────────
208
222
  case "json":
209
223
  return JSON.stringify(val, null, 2);
210
224
  case "upper":
@@ -213,6 +227,59 @@ function applyPipe(pipe, val) {
213
227
  return String(val ?? "").toLowerCase();
214
228
  case "trim":
215
229
  return String(val ?? "").trim();
230
+ // ── Date formatting ──────────────────────────────────────
231
+ case "formatDate": {
232
+ const d = toDate(val);
233
+ if (!d) return "";
234
+ return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
235
+ }
236
+ case "formatTime": {
237
+ const d = toDate(val);
238
+ if (!d) return "";
239
+ return `${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}`;
240
+ }
241
+ case "formatDateTime": {
242
+ const d = toDate(val);
243
+ if (!d) return "";
244
+ return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}`;
245
+ }
246
+ case "toISO": {
247
+ const d = toDate(val);
248
+ return d ? d.toISOString() : "";
249
+ }
250
+ case "toEpoch": {
251
+ const d = toDate(val);
252
+ return d ? String(d.getTime()) : "";
253
+ }
254
+ // ── Date diff ────────────────────────────────────────────
255
+ case "diffDays":
256
+ case "diffHours": {
257
+ const d1 = toDate(val);
258
+ if (!d1 || !arg || !ctx) return "";
259
+ const otherVal = resolvePath(arg.trim(), ctx);
260
+ const d2 = toDate(otherVal);
261
+ if (!d2) return "";
262
+ const diffMs = Math.abs(d1.getTime() - d2.getTime());
263
+ if (pipe === "diffDays") return String(Math.floor(diffMs / 864e5));
264
+ return String(Math.floor(diffMs / 36e5));
265
+ }
266
+ // ── Number ───────────────────────────────────────────────
267
+ case "currency": {
268
+ const n = Number(val);
269
+ if (isNaN(n)) return "";
270
+ return `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
271
+ }
272
+ case "percent": {
273
+ const n = Number(val);
274
+ if (isNaN(n)) return "";
275
+ return `${n}%`;
276
+ }
277
+ case "round": {
278
+ const n = Number(val);
279
+ if (isNaN(n)) return "";
280
+ const decimals = arg ? parseInt(arg, 10) : 0;
281
+ return n.toFixed(isNaN(decimals) ? 0 : decimals);
282
+ }
216
283
  default:
217
284
  return valueToString(val);
218
285
  }
@@ -229,9 +296,9 @@ function evaluate(expr, ctx, opts) {
229
296
  }
230
297
  const pipeM = trimmed.match(PIPE_RE);
231
298
  if (pipeM) {
232
- const [, path, pipe] = pipeM;
299
+ const [, path, pipe, arg] = pipeM;
233
300
  const val = resolvePath(path.trim(), ctx);
234
- return applyPipe(pipe, val);
301
+ return applyPipe(pipe, val, arg, ctx);
235
302
  }
236
303
  const ncResult = tryNullCoalesce(trimmed, ctx);
237
304
  if (ncResult !== null) return ncResult;
@@ -267,7 +334,7 @@ function resolveJsonValue(value, ctx, opts) {
267
334
  const val = resolvePath(expr.slice(1).trim(), ctx);
268
335
  return !val;
269
336
  }
270
- if (/\|\s*(json|upper|lower|trim)$/.test(expr)) {
337
+ if (/\|\s*\w+/.test(expr)) {
271
338
  return evaluate(expr, ctx, opts);
272
339
  }
273
340
  const resolved = resolvePath(expr, ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostwebhook/template-engine",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Template interpolation and sandboxed JS execution engine for HostWebhook",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",