@pikku/addon-console 0.12.43 → 0.12.44
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/CHANGELOG.md +25 -0
- package/dist/.pikku/addon/agent/index.d.ts +1 -1
- package/dist/.pikku/addon/agent/index.js +1 -1
- package/dist/.pikku/addon/agent/pikku-agent-map.gen.d.ts +1 -1
- package/dist/.pikku/addon/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/agent/pikku-model-aliases.gen.js +1 -1
- package/dist/.pikku/addon/agent/pikku-scorer-names.gen.d.ts +1 -1
- package/dist/.pikku/addon/auth/auth.types.d.ts +1 -1
- package/dist/.pikku/addon/auth/auth.types.js +1 -1
- package/dist/.pikku/addon/auth/index.d.ts +1 -1
- package/dist/.pikku/addon/auth/index.js +1 -1
- package/dist/.pikku/addon/auth/pikku-auth-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/auth/pikku-auth-types.gen.js +1 -1
- package/dist/.pikku/addon/channel/index.d.ts +1 -1
- package/dist/.pikku/addon/channel/index.js +1 -1
- package/dist/.pikku/addon/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/addon/error/index.d.ts +1 -1
- package/dist/.pikku/addon/error/index.js +1 -1
- package/dist/.pikku/addon/error/pikku-error-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/error/pikku-error-types.gen.js +1 -1
- package/dist/.pikku/addon/function/index.d.ts +1 -1
- package/dist/.pikku/addon/function/index.js +1 -1
- package/dist/.pikku/addon/function/pikku-function-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/addon/function/pikku-functions-meta-verbose.gen.json +3 -3
- package/dist/.pikku/addon/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/addon/function/pikku-functions-meta.gen.json +1 -1
- package/dist/.pikku/addon/function/pikku-functions.gen.js +1 -1
- package/dist/.pikku/addon/http/index.d.ts +1 -1
- package/dist/.pikku/addon/http/index.js +1 -1
- package/dist/.pikku/addon/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/addon/mcp/index.d.ts +1 -1
- package/dist/.pikku/addon/mcp/index.js +1 -1
- package/dist/.pikku/addon/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/addon/middleware/index.d.ts +1 -1
- package/dist/.pikku/addon/middleware/index.js +1 -1
- package/dist/.pikku/addon/middleware/pikku-middleware-types.gen.d.ts +2 -2
- package/dist/.pikku/addon/middleware/pikku-middleware-types.gen.d.ts.map +1 -1
- package/dist/.pikku/addon/middleware/pikku-middleware-types.gen.js +1 -1
- package/dist/.pikku/addon/pikku-bootstrap-scenarios.gen.d.ts +1 -1
- package/dist/.pikku/addon/pikku-bootstrap-scenarios.gen.js +1 -1
- package/dist/.pikku/addon/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/addon/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/addon/pikku-package.gen.js +1 -1
- package/dist/.pikku/addon/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/addon/rpc/pikku-rpc-wirings-map.gen.d.ts +1 -1
- package/dist/.pikku/addon/rpc/pikku-rpc-wirings-map.internal.gen.d.ts +1 -1
- package/dist/.pikku/addon/rpc/pikku-rpc-wirings-map.remote.gen.d.ts +1 -1
- package/dist/.pikku/addon/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/addon/scenarios/index.d.ts +1 -1
- package/dist/.pikku/addon/scenarios/index.js +1 -1
- package/dist/.pikku/addon/scenarios/pikku-personas.gen.js +1 -1
- package/dist/.pikku/addon/scenarios/pikku-scenario-functions-meta.gen.js +1 -1
- package/dist/.pikku/addon/scenarios/pikku-scenario-functions.gen.d.ts +1 -1
- package/dist/.pikku/addon/scenarios/pikku-scenario-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/scenarios/pikku-scenario-wirings-meta.gen.js +1 -1
- package/dist/.pikku/addon/scenarios/pikku-scenario-wirings.gen.d.ts +1 -1
- package/dist/.pikku/addon/scenarios/schemas/register.gen.d.ts +1 -1
- package/dist/.pikku/addon/scenarios/schemas/register.gen.js +1 -1
- package/dist/.pikku/addon/schemas/register.gen.js +1 -1
- package/dist/.pikku/addon/scopes/index.d.ts +1 -1
- package/dist/.pikku/addon/scopes/index.js +1 -1
- package/dist/.pikku/addon/scopes/pikku-personas.gen.js +1 -1
- package/dist/.pikku/addon/scopes/pikku-roles.gen.d.ts +1 -1
- package/dist/.pikku/addon/scopes/pikku-scope-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/scopes/pikku-scope-types.gen.js +1 -1
- package/dist/.pikku/addon/scopes/pikku-scopes.gen.d.ts +1 -1
- package/dist/.pikku/addon/secrets/index.d.ts +1 -1
- package/dist/.pikku/addon/secrets/index.js +1 -1
- package/dist/.pikku/addon/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/addon/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/addon/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/addon/services/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/addon/services/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/addon/setup/index.d.ts +1 -1
- package/dist/.pikku/addon/setup/index.js +1 -1
- package/dist/.pikku/addon/setup/pikku-addon-setup-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/setup/pikku-addon-setup-types.gen.js +1 -1
- package/dist/.pikku/addon/setup/pikku-setup-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/setup/pikku-setup-types.gen.js +1 -1
- package/dist/.pikku/addon/trigger/index.d.ts +1 -1
- package/dist/.pikku/addon/trigger/index.js +1 -1
- package/dist/.pikku/addon/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/addon/variables/index.d.ts +1 -1
- package/dist/.pikku/addon/variables/index.js +1 -1
- package/dist/.pikku/addon/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/addon/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/addon/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/addon/workflow/index.d.ts +1 -1
- package/dist/.pikku/addon/workflow/index.js +1 -1
- package/dist/.pikku/addon/workflow/pikku-scenario-step-map.gen.d.ts +1 -1
- package/dist/.pikku/addon/workflow/pikku-workflow-map.gen.d.ts +1 -1
- package/dist/.pikku/addon/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/addon/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/addon/workflow/pikku-workflow-wirings.gen.d.ts +1 -1
- package/dist/.pikku/addon/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/src/functions/render-email-preview.function.d.ts.map +1 -1
- package/dist/src/functions/render-email-preview.function.js +6 -17
- package/dist/src/functions/render-email-preview.function.js.map +1 -1
- package/dist/src/functions/render-email-template.utils.d.ts +8 -3
- package/dist/src/functions/render-email-template.utils.d.ts.map +1 -1
- package/dist/src/functions/render-email-template.utils.js +71 -17
- package/dist/src/functions/render-email-template.utils.js.map +1 -1
- package/dist/src/functions/render-email-template.utils.test.js +56 -23
- package/dist/src/functions/render-email-template.utils.test.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* This file was generated by @pikku/cli@0.12.
|
|
2
|
+
* This file was generated by @pikku/cli@0.12.109
|
|
3
3
|
*/
|
|
4
4
|
import { WorkflowCancelledException } from '@pikku/core/workflow';
|
|
5
5
|
import type { PikkuWorkflowGraphConfig, PikkuWorkflowGraphResult } from '@pikku/core/workflow';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-email-preview.function.d.ts","sourceRoot":"","sources":["../../../src/functions/render-email-preview.function.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render-email-preview.function.d.ts","sourceRoot":"","sources":["../../../src/functions/render-email-preview.function.ts"],"names":[],"mappings":"AAQA,KAAK,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAA;AAClE,KAAK,kBAAkB,GACrB,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,CAAA;AAE3D,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAA;CAC1C;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,6FAA6F;IAC7F,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,eAAO,MAAM,kBAAkB,8eA4F7B,CAAA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { pikkuFunc } from '#pikku/addon/function';
|
|
2
2
|
import { NotFoundError } from '#pikku/addon/error';
|
|
3
|
-
import { getNestedValue, renderTemplate,
|
|
3
|
+
import { getNestedValue, renderTemplate, } from './render-email-template.utils.js';
|
|
4
4
|
export const renderEmailPreview = pikkuFunc({
|
|
5
5
|
title: 'Render Email Preview',
|
|
6
6
|
description: 'Renders an email template preview from emailTemplatesDir using a locale and variable payload.',
|
|
@@ -24,34 +24,23 @@ export const renderEmailPreview = pikkuFunc({
|
|
|
24
24
|
t: assets.strings,
|
|
25
25
|
appName,
|
|
26
26
|
};
|
|
27
|
-
const subject = renderTemplate(assets.subject, baseContext).trim();
|
|
28
|
-
const
|
|
29
|
-
let htmlBody = renderTemplate(htmlWithPartials, {
|
|
27
|
+
const subject = renderTemplate(assets.subject, baseContext, assets.partials, false).trim();
|
|
28
|
+
const htmlBody = renderTemplate(assets.html, {
|
|
30
29
|
...baseContext,
|
|
31
30
|
subject,
|
|
32
|
-
});
|
|
33
|
-
const partialMatches = [...htmlBody.matchAll(/@@PARTIAL:([^@]+)@@/g)];
|
|
34
|
-
for (const match of partialMatches) {
|
|
35
|
-
if (!match[1])
|
|
36
|
-
continue;
|
|
37
|
-
const rendered = renderPartial(match[1], assets.partials, {
|
|
38
|
-
...baseContext,
|
|
39
|
-
subject,
|
|
40
|
-
});
|
|
41
|
-
htmlBody = htmlBody.replace(match[0], rendered);
|
|
42
|
-
}
|
|
31
|
+
}, assets.partials, true);
|
|
43
32
|
const html = assets.layout
|
|
44
33
|
? renderTemplate(assets.layout, {
|
|
45
34
|
...baseContext,
|
|
46
35
|
subject,
|
|
47
36
|
content: htmlBody,
|
|
48
|
-
})
|
|
37
|
+
}, assets.partials, true)
|
|
49
38
|
: htmlBody;
|
|
50
39
|
const text = assets.text
|
|
51
40
|
? renderTemplate(assets.text, {
|
|
52
41
|
...baseContext,
|
|
53
42
|
subject,
|
|
54
|
-
}).trim()
|
|
43
|
+
}, assets.partials, false).trim()
|
|
55
44
|
: undefined;
|
|
56
45
|
return {
|
|
57
46
|
name: input.templateName,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-email-preview.function.js","sourceRoot":"","sources":["../../../src/functions/render-email-preview.function.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD,OAAO,EACL,cAAc,EACd,cAAc,
|
|
1
|
+
{"version":3,"file":"render-email-preview.function.js","sourceRoot":"","sources":["../../../src/functions/render-email-preview.function.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
export declare function escapeHtml(value: string): string;
|
|
1
2
|
export declare function getNestedValue(source: Record<string, unknown>, path: string): string;
|
|
2
|
-
export declare function
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
export declare function expandPartials(source: string, partials: Record<string, string>, depth?: number): string;
|
|
4
|
+
/**
|
|
5
|
+
* A single substitution pass — the replacement text is never rescanned, so a
|
|
6
|
+
* caller-supplied value can never be reinterpreted as a template.
|
|
7
|
+
*/
|
|
8
|
+
export declare function substitute(source: string, context: Record<string, unknown>, escape: boolean): string;
|
|
9
|
+
export declare function renderTemplate(source: string, context: Record<string, unknown>, partials: Record<string, string>, escape: boolean): string;
|
|
5
10
|
//# sourceMappingURL=render-email-template.utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-email-template.utils.d.ts","sourceRoot":"","sources":["../../../src/functions/render-email-template.utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render-email-template.utils.d.ts","sourceRoot":"","sources":["../../../src/functions/render-email-template.utils.ts"],"names":[],"mappings":"AAyBA,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,EAAE,MAAM,GACX,MAAM,CAYR;AAWD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,KAAK,SAAI,GACR,MAAM,CASR;AA0BD;;;GAGG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,MAAM,EAAE,OAAO,GACd,MAAM,CAYR;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,MAAM,EAAE,OAAO,GACd,MAAM,CAOR"}
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
const HTML_ESCAPES = {
|
|
2
|
+
'&': '&',
|
|
3
|
+
'<': '<',
|
|
4
|
+
'>': '>',
|
|
5
|
+
'"': '"',
|
|
6
|
+
"'": ''',
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Matches the raw `{{{ value }}}` form before the escaped `{{ value }}` form, so
|
|
10
|
+
* the opt-in escape hatch is never mistaken for a normal substitution.
|
|
11
|
+
*/
|
|
12
|
+
const TEMPLATE_TOKEN = /\{\{\{\s*([^{}]+?)\s*\}\}\}|\{\{\s*([^}]+?)\s*\}\}/g;
|
|
13
|
+
const PARTIAL_TOKEN = /\{\{\s*>\s*([a-zA-Z0-9-_/.]+)\s*\}\}/g;
|
|
14
|
+
const MAX_TEMPLATE_DEPTH = 5;
|
|
15
|
+
/**
|
|
16
|
+
* theme.json and the locale files ship with the templates, so they are treated
|
|
17
|
+
* as template-author input: expanded before caller data and allowed to contain
|
|
18
|
+
* their own placeholders. Everything else is caller-supplied.
|
|
19
|
+
*/
|
|
20
|
+
const TRUSTED_ROOTS = ['theme', 't'];
|
|
21
|
+
export function escapeHtml(value) {
|
|
22
|
+
return value.replace(/[&<>"']/g, (char) => HTML_ESCAPES[char] ?? char);
|
|
23
|
+
}
|
|
1
24
|
export function getNestedValue(source, path) {
|
|
2
25
|
const segments = path.split('.');
|
|
3
26
|
let current = source;
|
|
@@ -11,30 +34,61 @@ export function getNestedValue(source, path) {
|
|
|
11
34
|
? String(current)
|
|
12
35
|
: '';
|
|
13
36
|
}
|
|
14
|
-
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return
|
|
37
|
+
function isTrustedKey(key) {
|
|
38
|
+
return TRUSTED_ROOTS.includes(String(key.split('.')[0]));
|
|
39
|
+
}
|
|
40
|
+
function readToken(rawTriple, rawDouble) {
|
|
41
|
+
const raw = typeof rawTriple === 'string';
|
|
42
|
+
return { raw, key: String(raw ? rawTriple : rawDouble).trim() };
|
|
43
|
+
}
|
|
44
|
+
export function expandPartials(source, partials, depth = 0) {
|
|
45
|
+
if (depth >= MAX_TEMPLATE_DEPTH)
|
|
46
|
+
return source;
|
|
47
|
+
let found = false;
|
|
48
|
+
const expanded = source.replace(PARTIAL_TOKEN, (_match, partialName) => {
|
|
49
|
+
found = true;
|
|
50
|
+
const partial = partials[String(partialName).trim()];
|
|
51
|
+
return typeof partial === 'string' ? partial : '';
|
|
24
52
|
});
|
|
53
|
+
return found ? expandPartials(expanded, partials, depth + 1) : expanded;
|
|
25
54
|
}
|
|
26
|
-
|
|
55
|
+
function expandTrusted(source, context, escape) {
|
|
27
56
|
let rendered = source;
|
|
28
|
-
for (let i = 0; i <
|
|
29
|
-
|
|
30
|
-
|
|
57
|
+
for (let i = 0; i < MAX_TEMPLATE_DEPTH; i += 1) {
|
|
58
|
+
let found = false;
|
|
59
|
+
const next = rendered.replace(TEMPLATE_TOKEN, (match, rawTriple, rawDouble) => {
|
|
60
|
+
const { raw, key } = readToken(rawTriple, rawDouble);
|
|
61
|
+
if (!isTrustedKey(key))
|
|
62
|
+
return match;
|
|
63
|
+
found = true;
|
|
64
|
+
const value = getNestedValue(context, key);
|
|
65
|
+
return raw || !escape ? value : escapeHtml(value);
|
|
66
|
+
});
|
|
67
|
+
if (!found || next === rendered)
|
|
31
68
|
break;
|
|
32
69
|
rendered = next;
|
|
33
70
|
}
|
|
34
71
|
return rendered;
|
|
35
72
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
73
|
+
/**
|
|
74
|
+
* A single substitution pass — the replacement text is never rescanned, so a
|
|
75
|
+
* caller-supplied value can never be reinterpreted as a template.
|
|
76
|
+
*/
|
|
77
|
+
export function substitute(source, context, escape) {
|
|
78
|
+
return source.replace(TEMPLATE_TOKEN, (_match, rawTriple, rawDouble) => {
|
|
79
|
+
const { raw, key } = readToken(rawTriple, rawDouble);
|
|
80
|
+
if (key === 'content') {
|
|
81
|
+
return typeof context.content === 'string' ? context.content : '';
|
|
82
|
+
}
|
|
83
|
+
if (key.startsWith('>')) {
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
const value = getNestedValue(context, key);
|
|
87
|
+
return raw || !escape ? value : escapeHtml(value);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export function renderTemplate(source, context, partials, escape) {
|
|
91
|
+
const composed = expandTrusted(expandPartials(source, partials), context, escape);
|
|
92
|
+
return substitute(composed, context, escape);
|
|
39
93
|
}
|
|
40
94
|
//# sourceMappingURL=render-email-template.utils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-email-template.utils.js","sourceRoot":"","sources":["../../../src/functions/render-email-template.utils.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,cAAc,CAC5B,MAA+B,EAC/B,IAAY;IAEZ,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAChC,IAAI,OAAO,GAAY,MAAM,CAAA;IAC7B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,EAAE,CAAC;YACrE,OAAO,EAAE,CAAA;QACX,CAAC;QACD,OAAO,GAAI,OAAmC,CAAC,OAAO,CAAC,CAAA;IACzD,CAAC;IACD,OAAO,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,OAAO,KAAK,QAAQ;QAC/D,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;QACjB,CAAC,CAAC,EAAE,CAAA;AACR,CAAC;AAED,
|
|
1
|
+
{"version":3,"file":"render-email-template.utils.js","sourceRoot":"","sources":["../../../src/functions/render-email-template.utils.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import {
|
|
3
|
+
import { escapeHtml, expandPartials, getNestedValue, renderTemplate, substitute, } from './render-email-template.utils.js';
|
|
4
4
|
describe('getNestedValue', () => {
|
|
5
5
|
test('returns top-level string value', () => {
|
|
6
6
|
assert.equal(getNestedValue({ name: 'Alice' }, 'name'), 'Alice');
|
|
@@ -21,45 +21,78 @@ describe('getNestedValue', () => {
|
|
|
21
21
|
assert.equal(getNestedValue({ count: 42 }, 'count'), '42');
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
|
-
describe('
|
|
24
|
+
describe('escapeHtml', () => {
|
|
25
|
+
test('escapes the five html-significant characters', () => {
|
|
26
|
+
assert.equal(escapeHtml(`<a href="x" title='y'>&</a>`), '<a href="x" title='y'>&</a>');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('substitute', () => {
|
|
25
30
|
test('replaces simple placeholder', () => {
|
|
26
|
-
assert.equal(
|
|
31
|
+
assert.equal(substitute('Hello {{ name }}!', { name: 'Bob' }, false), 'Hello Bob!');
|
|
32
|
+
});
|
|
33
|
+
test('escapes the value when rendering html', () => {
|
|
34
|
+
assert.equal(substitute('<p>{{ name }}</p>', { name: '<b>Bob</b>' }, true), '<p><b>Bob</b></p>');
|
|
35
|
+
});
|
|
36
|
+
test('leaves the value raw when rendering plain text', () => {
|
|
37
|
+
assert.equal(substitute('{{ name }}', { name: 'Ada & Bob' }, false), 'Ada & Bob');
|
|
27
38
|
});
|
|
28
|
-
test('
|
|
29
|
-
assert.equal(
|
|
39
|
+
test('renders the triple-brace form raw even in html', () => {
|
|
40
|
+
assert.equal(substitute('<p>{{{ name }}}</p>', { name: '<b>Bob</b>' }, true), '<p><b>Bob</b></p>');
|
|
41
|
+
});
|
|
42
|
+
test('replaces {{content}} with context.content string, unescaped', () => {
|
|
43
|
+
assert.equal(substitute('Body: {{ content }}', { content: '<p>Hi</p>' }, true), 'Body: <p>Hi</p>');
|
|
30
44
|
});
|
|
31
45
|
test('replaces {{content}} with empty string when not a string', () => {
|
|
32
|
-
assert.equal(
|
|
46
|
+
assert.equal(substitute('{{ content }}', { content: 123 }, true), '');
|
|
33
47
|
});
|
|
34
48
|
test('strips partial tags (> prefix)', () => {
|
|
35
|
-
assert.equal(
|
|
49
|
+
assert.equal(substitute('{{ > header }}', {}, true), '');
|
|
36
50
|
});
|
|
37
51
|
test('returns empty string for missing placeholder', () => {
|
|
38
|
-
assert.equal(
|
|
52
|
+
assert.equal(substitute('{{ missing }}', {}, true), '');
|
|
39
53
|
});
|
|
40
54
|
test('resolves dot-path placeholders', () => {
|
|
41
|
-
assert.equal(
|
|
55
|
+
assert.equal(substitute('{{ user.name }}', { user: { name: 'Carol' } }, true), 'Carol');
|
|
56
|
+
});
|
|
57
|
+
test('does not rescan the substituted value', () => {
|
|
58
|
+
assert.equal(substitute('{{ a }}', { a: '{{ b }}', b: 'leaked' }, false), '{{ b }}');
|
|
42
59
|
});
|
|
43
60
|
});
|
|
44
|
-
describe('
|
|
45
|
-
test('
|
|
46
|
-
assert.equal(
|
|
61
|
+
describe('expandPartials', () => {
|
|
62
|
+
test('inlines a known partial', () => {
|
|
63
|
+
assert.equal(expandPartials('{{> header }}', { header: '<h1>{{ title }}</h1>' }), '<h1>{{ title }}</h1>');
|
|
47
64
|
});
|
|
48
|
-
test('
|
|
49
|
-
|
|
50
|
-
assert.equal(renderTemplate('{{ greeting }}', ctx), 'Hi Eve');
|
|
65
|
+
test('inlines nested partials', () => {
|
|
66
|
+
assert.equal(expandPartials('{{> a }}', { a: 'A{{> b }}', b: 'B' }), 'AB');
|
|
51
67
|
});
|
|
52
|
-
test('
|
|
53
|
-
assert.equal(
|
|
68
|
+
test('drops an unknown partial', () => {
|
|
69
|
+
assert.equal(expandPartials('x{{> missing }}y', {}), 'xy');
|
|
54
70
|
});
|
|
55
71
|
});
|
|
56
|
-
describe('
|
|
57
|
-
test('
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
describe('renderTemplate', () => {
|
|
73
|
+
test('resolves a caller variable in one pass', () => {
|
|
74
|
+
assert.equal(renderTemplate('Hi {{ name }}', { name: 'Dave' }, {}, false), 'Hi Dave');
|
|
75
|
+
});
|
|
76
|
+
test('expands a locale string that itself references a caller variable', () => {
|
|
77
|
+
const ctx = { t: { greeting: 'Hi {{ name }}' }, name: 'Eve' };
|
|
78
|
+
assert.equal(renderTemplate('{{ t.greeting }}', ctx, {}, false), 'Hi Eve');
|
|
79
|
+
});
|
|
80
|
+
test('never expands a caller value as a template', () => {
|
|
81
|
+
const ctx = { t: { note: 'secret' }, name: '{{ t.note }}' };
|
|
82
|
+
assert.equal(renderTemplate('{{ name }}', ctx, {}, false), '{{ t.note }}');
|
|
83
|
+
});
|
|
84
|
+
test('escapes a theme value landing in an attribute', () => {
|
|
85
|
+
const ctx = { theme: { fonts: { body: '"Segoe UI", sans-serif' } } };
|
|
86
|
+
assert.equal(renderTemplate('<p style="font-family:{{theme.fonts.body}};">', ctx, {}, true), '<p style="font-family:"Segoe UI", sans-serif;">');
|
|
60
87
|
});
|
|
61
|
-
test('
|
|
62
|
-
assert.equal(
|
|
88
|
+
test('renders partials before substitution', () => {
|
|
89
|
+
assert.equal(renderTemplate('{{> header }}', { title: 'Welcome' }, { header: '<h1>{{ title }}</h1>' }, true), '<h1>Welcome</h1>');
|
|
90
|
+
});
|
|
91
|
+
test('a caller value cannot forge a partial include', () => {
|
|
92
|
+
assert.equal(renderTemplate('{{ name }}', { name: '{{> header }}' }, { header: 'LEAKED' }, false), '{{> header }}');
|
|
93
|
+
});
|
|
94
|
+
test('stops early when no more changes', () => {
|
|
95
|
+
assert.equal(renderTemplate('static', {}, {}, true), 'static');
|
|
63
96
|
});
|
|
64
97
|
});
|
|
65
98
|
//# sourceMappingURL=render-email-template.utils.test.js.map
|