@cosmicdrift/kumiko-renderer-mail-html 0.374.0 → 0.378.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/document-kit.d.ts +2 -0
- package/dist/document-kit.js +23 -4
- package/dist/mail-chrome.js +1 -1
- package/dist/mail-footer.js +1 -1
- package/dist/render.js +24 -10
- package/dist/structured-layout.js +1 -1
- package/package.json +7 -7
- package/src/changes.json +18 -0
package/dist/document-kit.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import Handlebars from "handlebars";
|
|
|
4
4
|
export type DocumentFormat = {
|
|
5
5
|
readonly locale: string;
|
|
6
6
|
readonly currency: string;
|
|
7
|
+
/** Tenant zone for `{{date x time=true}}`; absent = UTC. */
|
|
8
|
+
readonly timeZone?: string;
|
|
7
9
|
};
|
|
8
10
|
export declare function createDocumentHandlebars(format: DocumentFormat): typeof Handlebars;
|
|
9
11
|
export declare function compileDocumentSource(hbs: typeof Handlebars, source: string, options?: {
|
package/dist/document-kit.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { BRANDING_QN } from "@cosmicdrift/kumiko-bundled-features/managed-pages";
|
|
2
2
|
import { isSafeHttpsUrl } from "@cosmicdrift/kumiko-bundled-features/page-render";
|
|
3
3
|
import { collectDocumentResourceKeys, flattenParagraphsToLineBreaks, isHexColor, sanitizeDocumentHtml, TemplateNotFoundError, } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
4
|
+
import { decryptTenantNames, TENANT_FEATURE, tenantTable, } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
4
5
|
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
5
6
|
import { isUuid, SYSTEM_TENANT_ID, TENANT_CURRENCY_CONFIG_KEY, } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
7
|
import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
|
|
7
8
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
8
|
-
import {
|
|
9
|
+
import { TENANT_TIMEZONE_CONFIG_KEY } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
10
|
+
import { isValidIanaTimeZone } from "@cosmicdrift/kumiko-framework/time";
|
|
11
|
+
import { DOCUMENT_FALLBACK_CURRENCY, formatDocumentDate, formatDocumentMoney, formatDocumentPercent, } from "@cosmicdrift/kumiko-renderer/headless";
|
|
9
12
|
import Handlebars from "handlebars";
|
|
10
13
|
// Handlebars instance, template resolution and resource lookup shared by the
|
|
11
14
|
// mail and PDF renderers, so both format money/dates and find images the same way.
|
|
@@ -14,7 +17,10 @@ const SAFE_LINK_PROTOCOL = /^(https?:|mailto:)/i;
|
|
|
14
17
|
export function createDocumentHandlebars(format) {
|
|
15
18
|
const hbs = Handlebars.create();
|
|
16
19
|
hbs.registerHelper("money", (value) => formatDocumentMoney(value, format.currency, format.locale));
|
|
17
|
-
hbs.registerHelper("date", (value) => formatDocumentDate(value, format.locale
|
|
20
|
+
hbs.registerHelper("date", (value, options) => formatDocumentDate(value, format.locale, {
|
|
21
|
+
time: options?.hash?.["time"] === true,
|
|
22
|
+
...(format.timeZone !== undefined && { timeZone: format.timeZone }),
|
|
23
|
+
}));
|
|
18
24
|
hbs.registerHelper("percent", (value) => formatDocumentPercent(value, format.locale));
|
|
19
25
|
hbs.registerHelper("richText", (value, options) => {
|
|
20
26
|
if (typeof value !== "string")
|
|
@@ -79,10 +85,23 @@ async function readConfigText(ctx, key) {
|
|
|
79
85
|
// "Oct 1, 2026". The currency is a tenant fact and still comes from its settings.
|
|
80
86
|
export async function readDocumentFormat(ctx, templateLocale) {
|
|
81
87
|
const currency = await readConfigText(ctx, TENANT_CURRENCY_CONFIG_KEY);
|
|
82
|
-
|
|
88
|
+
const zone = await readConfigText(ctx, TENANT_TIMEZONE_CONFIG_KEY);
|
|
89
|
+
return {
|
|
90
|
+
locale: templateLocale,
|
|
91
|
+
currency: currency ?? DOCUMENT_FALLBACK_CURRENCY,
|
|
92
|
+
...(zone !== undefined && isValidIanaTimeZone(zone) && { timeZone: zone }),
|
|
93
|
+
};
|
|
83
94
|
}
|
|
95
|
+
// Without managed-pages there is no branding title; the tenant record's own name is the fallback.
|
|
84
96
|
export async function readTenantName(ctx) {
|
|
85
|
-
|
|
97
|
+
const branded = await readConfigText(ctx, BRANDING_QN.title);
|
|
98
|
+
if (branded !== undefined)
|
|
99
|
+
return branded;
|
|
100
|
+
if (ctx.registry.getFeature(TENANT_FEATURE) === undefined)
|
|
101
|
+
return "";
|
|
102
|
+
const rows = await selectMany(ctx.db, tenantTable, { id: ctx.tenantId });
|
|
103
|
+
const [row] = await decryptTenantNames(rows, "renderer-mail-html:tenant-name");
|
|
104
|
+
return typeof row?.name === "string" ? row.name : "";
|
|
86
105
|
}
|
|
87
106
|
export async function readBrandingAccentColor(ctx) {
|
|
88
107
|
const value = await readConfigText(ctx, BRANDING_QN.accentColor);
|
package/dist/mail-chrome.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isHexColor, MAIL_TONES, } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
2
|
-
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-renderer/headless";
|
|
3
3
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
4
4
|
const FIXED_TONES = {
|
|
5
5
|
info: { bar: "#7C8DB5", fg: "#3E4C70", soft: "#EEF1F7" },
|
package/dist/mail-footer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
1
|
+
import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-renderer/headless";
|
|
2
2
|
import { mailChromeText } from "./mail-strings.js";
|
|
3
3
|
const SAFE_FOOTER_URL = /^(https?:|mailto:)/i;
|
|
4
4
|
const BAND_LINK_COLOR = "#4A515E";
|
package/dist/render.js
CHANGED
|
@@ -22,6 +22,8 @@ import { buildMailBandLinksHtml, buildMailFooterHtml, layoutLinksToPlaceholder,
|
|
|
22
22
|
import { mailChromeText } from "./mail-strings.js";
|
|
23
23
|
import { MAIL_IMAGE_MAX_WIDTH_PX, renderStructuredLayoutMjml } from "./structured-layout.js";
|
|
24
24
|
const LINE_BREAKS = /[\r\n]+/g;
|
|
25
|
+
const PLAIN_REFERENCE = /\{\{\s*([\w.]+)\s*\}\}/g;
|
|
26
|
+
const FORMATTED_VARIABLE_TYPES = new Set(["money", "date", "percent"]);
|
|
25
27
|
function describe(e) {
|
|
26
28
|
return e instanceof Error ? e.message : String(e);
|
|
27
29
|
}
|
|
@@ -211,24 +213,35 @@ async function renderMailBody(tplApi, ctx, template, hbs, context, buttonInlineS
|
|
|
211
213
|
throw new RendererError(`renderer-mail-html: rendering the body of "${template.slug}" failed — ${describe(e)}`, "other");
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
// Meta lines only hold plain {{name}} references (checked on save); typed values
|
|
217
|
+
// need the same format helpers the body compiler adds, or money renders as "[object Object]".
|
|
218
|
+
function withFormatHelpers(source, variables) {
|
|
219
|
+
return source.replace(PLAIN_REFERENCE, (reference, name) => {
|
|
220
|
+
const type = variables[name]?.type;
|
|
221
|
+
return type !== undefined && FORMATTED_VARIABLE_TYPES.has(type)
|
|
222
|
+
? `{{${type} ${name}}}`
|
|
223
|
+
: reference;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function renderSubjectLine(hbs, source, context, variables) {
|
|
227
|
+
return compileDocumentSource(hbs, withFormatHelpers(source, variables), { noEscape: true })(context).replace(LINE_BREAKS, "");
|
|
216
228
|
}
|
|
217
229
|
function renderFileNamePattern(hbs, pattern, slug, context) {
|
|
218
230
|
try {
|
|
219
|
-
|
|
231
|
+
// File names keep raw values: a formatted date or amount would put spaces into the name.
|
|
232
|
+
return renderSubjectLine(hbs, pattern, context, {});
|
|
220
233
|
}
|
|
221
234
|
catch (e) {
|
|
222
235
|
throw new RendererError(`renderer-mail-html: rendering the attachment file name "${pattern}" of "${slug}" failed — ${describe(e)}`, "other");
|
|
223
236
|
}
|
|
224
237
|
}
|
|
225
|
-
function renderSubjectLines(hbs, meta, slug, context) {
|
|
238
|
+
function renderSubjectLines(hbs, meta, slug, context, variables) {
|
|
226
239
|
try {
|
|
227
240
|
return {
|
|
228
|
-
subject: renderSubjectLine(hbs, meta.subject, context),
|
|
241
|
+
subject: renderSubjectLine(hbs, meta.subject, context, variables),
|
|
229
242
|
previewText: meta.previewText === undefined
|
|
230
243
|
? undefined
|
|
231
|
-
: renderSubjectLine(hbs, meta.previewText, context),
|
|
244
|
+
: renderSubjectLine(hbs, meta.previewText, context, variables),
|
|
232
245
|
};
|
|
233
246
|
}
|
|
234
247
|
catch (e) {
|
|
@@ -236,8 +249,8 @@ function renderSubjectLines(hbs, meta, slug, context) {
|
|
|
236
249
|
}
|
|
237
250
|
}
|
|
238
251
|
// Chip and tone are author-chosen per mail; a chip without a known tone takes the accent tone.
|
|
239
|
-
function renderMailChrome(hbs, meta, slug, context, brandColor) {
|
|
240
|
-
const render = (source) => source === undefined ? "" : renderSubjectLine(hbs, source, context).trim();
|
|
252
|
+
function renderMailChrome(hbs, meta, slug, context, variables, brandColor) {
|
|
253
|
+
const render = (source) => source === undefined ? "" : renderSubjectLine(hbs, source, context, variables).trim();
|
|
241
254
|
try {
|
|
242
255
|
const headerChip = render(meta.headerChip);
|
|
243
256
|
const tone = readMailTone(render(meta.tone));
|
|
@@ -483,8 +496,9 @@ export async function mailHtmlRender(req, ctx, options = {}) {
|
|
|
483
496
|
unsubscribeUrl: unsubscribeUrl ?? "",
|
|
484
497
|
};
|
|
485
498
|
const context = { ...variables, ...systemPlaceholders };
|
|
499
|
+
const variableDefs = resolveTemplateVariables(ctx.registry, template.slug, template.variableSchema);
|
|
486
500
|
const { appearance, accentColor, brandColor } = await resolveMailBranding(ctx, layout);
|
|
487
|
-
const chrome = renderMailChrome(hbs, meta, slug, context, brandColor);
|
|
501
|
+
const chrome = renderMailChrome(hbs, meta, slug, context, variableDefs, brandColor);
|
|
488
502
|
const body = await renderMailBody(tplApi, ctx, template, hbs, context, appearance?.buttonStyle, chrome.tone?.bar ?? brandColor ?? DEFAULT_ACCENT_COLOR, brandingLogoUrl);
|
|
489
503
|
const { contentHtml } = body;
|
|
490
504
|
const layoutFileRefs = await resolveLayoutFileRefs(tplApi, ctx, layout);
|
|
@@ -498,7 +512,7 @@ export async function mailHtmlRender(req, ctx, options = {}) {
|
|
|
498
512
|
locale,
|
|
499
513
|
renderFileName: (pattern) => renderFileNamePattern(hbs, pattern, slug, context),
|
|
500
514
|
});
|
|
501
|
-
const { subject, previewText } = renderSubjectLines(hbs, meta, slug, context);
|
|
515
|
+
const { subject, previewText } = renderSubjectLines(hbs, meta, slug, context, variableDefs);
|
|
502
516
|
const mjmlSource = compileLayoutMjml({
|
|
503
517
|
layout,
|
|
504
518
|
hbs,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Renders a structured (html) mail layout: the tenant-editable regions go into
|
|
2
2
|
// a fixed in-code MJML frame, so a tenant layout never contributes MJML source.
|
|
3
3
|
import { compileDocumentBodyToHandlebars, isHexColor, mailLayoutAppearance, readLayoutRegions, } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
4
|
-
import { escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
4
|
+
import { escapeHtmlAttr } from "@cosmicdrift/kumiko-renderer/headless";
|
|
5
5
|
import { headerRowHtml } from "./mail-chrome.js";
|
|
6
6
|
// The logo of a mail layout and of a mail body is capped here; the editors show the same width.
|
|
7
7
|
export const MAIL_IMAGE_MAX_WIDTH_PX = 180;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-mail-html",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.378.0",
|
|
4
4
|
"description": "Renderer-Foundation plugin for branded HTML emails: mail-kind templates (document HTML + subject meta) rendered with isolated Handlebars into an MJML layout, plus plain text via html-to-text. Templates resolved from template-resolver.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -19,17 +19,17 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
23
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
24
|
-
"@cosmicdrift/kumiko-
|
|
25
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
22
|
+
"@cosmicdrift/kumiko-bundled-features": "0.378.0",
|
|
23
|
+
"@cosmicdrift/kumiko-framework": "0.378.0",
|
|
24
|
+
"@cosmicdrift/kumiko-renderer": "0.378.0",
|
|
25
|
+
"@cosmicdrift/kumiko-types": "0.378.0",
|
|
26
26
|
"handlebars": "^4.7.8",
|
|
27
27
|
"html-to-text": "^9.0.5",
|
|
28
28
|
"mjml": "^5.2.2"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
32
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
31
|
+
"@cosmicdrift/kumiko-locale-de": "0.378.0",
|
|
32
|
+
"@cosmicdrift/kumiko-testing": "0.378.0",
|
|
33
33
|
"@types/html-to-text": "^9.0.4",
|
|
34
34
|
"@types/mjml": "^5.0.0"
|
|
35
35
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.378.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Typed placeholders in mail subject lines are formatted",
|
|
6
|
+
"detail": "A money placeholder in the subject or preview text rendered as \"[object Object]\" and a date as its raw ISO string, because only the body compiler added the format helpers. Subject, preview text, header chip, tone and footer note now use the same money, date and percent formatting as the body. Attachment file names keep the raw values."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.377.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Document dates can print the time in the tenant time zone",
|
|
12
|
+
"detail": "The date helper accepts time=true ({{date value time=true}}). Instants print date and time in the tenant's configured IANA zone (tenant timezone config, UTC without one), offset date-times keep their own wall clock, date-only values print no time. Without time=true nothing changes. formatDocumentDate takes an optional { time, timeZone } argument, DocumentFormat gets timeZone, and TENANT_TIMEZONE_CONFIG_KEY is exported from framework/pipeline."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.377.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "tenantName in the document context falls back to the tenant record",
|
|
18
|
+
"detail": "Without managed-pages there is no branding title, so tenantName was empty. It now falls back to the decrypted name of the tenant record when the tenant feature is mounted."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.367.0",
|
|
4
22
|
"type": "improvement",
|