@cosmicdrift/kumiko-renderer-mail-html 0.386.0 → 0.388.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/LICENSE +21 -57
- package/dist/document-kit.js +18 -0
- package/dist/mail-footer.js +1 -1
- package/dist/render.js +31 -7
- package/dist/seeding.js +4 -3
- package/package.json +8 -8
- package/src/changes.json +6 -0
package/LICENSE
CHANGED
|
@@ -1,57 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
controlling, or under common control with the Licensor ("Affiliates"), or
|
|
23
|
-
contractors acting on their behalf. The Licensor remains free to use the
|
|
24
|
-
Licensed Work for any purpose, including for the operation of kumiko.rocks.
|
|
25
|
-
|
|
26
|
-
Change Date: 2030-05-05
|
|
27
|
-
Change License: Apache License, Version 2.0
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Terms
|
|
31
|
-
|
|
32
|
-
The Licensor hereby grants you the right to copy, modify, create derivative works,
|
|
33
|
-
redistribute, and make non-production use of the Licensed Work. The Licensor may
|
|
34
|
-
make an Additional Use Grant, above, permitting limited production use.
|
|
35
|
-
|
|
36
|
-
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
37
|
-
available distribution of the Licensed Work under this License, whichever comes
|
|
38
|
-
first, this License will convert to the Change License.
|
|
39
|
-
|
|
40
|
-
This Business Source License governs use of the Licensed Work in all cases, except
|
|
41
|
-
as to any use that is explicitly granted in the Additional Use Grant above or
|
|
42
|
-
under the Change License after the Change Date.
|
|
43
|
-
|
|
44
|
-
If your use of the Licensed Work does not comply with the requirements of this
|
|
45
|
-
License, you must cease use of the Licensed Work immediately.
|
|
46
|
-
|
|
47
|
-
All copies of the Licensed Work, and all derivative works thereof, must include
|
|
48
|
-
this License.
|
|
49
|
-
|
|
50
|
-
This License does not grant you any right, title, or interest in any trademark,
|
|
51
|
-
logo, or branding of the Licensor, except as required to comply with this License.
|
|
52
|
-
|
|
53
|
-
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN
|
|
54
|
-
“AS IS” BASIS. LICENSOR DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
|
|
55
|
-
WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
56
|
-
TITLE, AND NON-INFRINGEMENT. IN NO EVENT WILL LICENSOR BE LIABLE FOR ANY DAMAGES
|
|
57
|
-
ARISING OUT OF OR RELATED TO THIS LICENSE OR THE USE OF THE LICENSED WORK.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marc Frost
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/document-kit.js
CHANGED
|
@@ -10,6 +10,7 @@ import { TENANT_TIMEZONE_CONFIG_KEY } from "@cosmicdrift/kumiko-framework/pipeli
|
|
|
10
10
|
import { isValidIanaTimeZone } from "@cosmicdrift/kumiko-framework/time";
|
|
11
11
|
import { DOCUMENT_FALLBACK_CURRENCY, formatDocumentDate, formatDocumentMoney, formatDocumentPercent, } from "@cosmicdrift/kumiko-renderer/headless";
|
|
12
12
|
import Handlebars from "handlebars";
|
|
13
|
+
import { convert as htmlToText } from "html-to-text";
|
|
13
14
|
// Handlebars instance, template resolution and resource lookup shared by the
|
|
14
15
|
// mail and PDF renderers, so both format money/dates and find images the same way.
|
|
15
16
|
const documentKitLogger = createFallbackLogger("renderer-mail-html");
|
|
@@ -37,6 +38,15 @@ export function createDocumentHandlebars(format) {
|
|
|
37
38
|
});
|
|
38
39
|
return value;
|
|
39
40
|
});
|
|
41
|
+
hbs.registerHelper("richTextPlain", (value) => typeof value === "string"
|
|
42
|
+
? htmlToText(value, {
|
|
43
|
+
wordwrap: false,
|
|
44
|
+
selectors: [
|
|
45
|
+
{ selector: "a", options: { ignoreHref: true } },
|
|
46
|
+
{ selector: "img", format: "skip" },
|
|
47
|
+
],
|
|
48
|
+
})
|
|
49
|
+
: "");
|
|
40
50
|
hbs.registerHelper("link", (value) => typeof value === "string" && SAFE_LINK_PROTOCOL.test(value.trim()) ? value : "#");
|
|
41
51
|
hbs.registerHelper("stepIs", function stepIs(value, index, keys, state, options) {
|
|
42
52
|
return isStepInState(value, index, keys, state) ? options.fn(this) : options.inverse(this);
|
|
@@ -63,6 +73,7 @@ const KNOWN_HELPERS = {
|
|
|
63
73
|
richText: true,
|
|
64
74
|
stepIs: true,
|
|
65
75
|
percent: true,
|
|
76
|
+
richTextPlain: true,
|
|
66
77
|
};
|
|
67
78
|
export function compileDocumentSource(hbs, source, options = {}) {
|
|
68
79
|
return hbs.compile(source, {
|
|
@@ -86,6 +97,13 @@ async function readConfigText(ctx, key) {
|
|
|
86
97
|
export async function readDocumentFormat(ctx, templateLocale) {
|
|
87
98
|
const currency = await readConfigText(ctx, TENANT_CURRENCY_CONFIG_KEY);
|
|
88
99
|
const zone = await readConfigText(ctx, TENANT_TIMEZONE_CONFIG_KEY);
|
|
100
|
+
if (zone !== undefined && !isValidIanaTimeZone(zone)) {
|
|
101
|
+
documentKitLogger.warn("tenant time zone is not a valid IANA zone, documents print UTC times", {
|
|
102
|
+
tenantId: ctx.tenantId,
|
|
103
|
+
configKey: TENANT_TIMEZONE_CONFIG_KEY,
|
|
104
|
+
zone,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
89
107
|
return {
|
|
90
108
|
locale: templateLocale,
|
|
91
109
|
currency: currency ?? DOCUMENT_FALLBACK_CURRENCY,
|
package/dist/mail-footer.js
CHANGED
|
@@ -80,5 +80,5 @@ export function buildMailFooterHtml(input) {
|
|
|
80
80
|
return lines.join("<br>");
|
|
81
81
|
}
|
|
82
82
|
export function layoutLinksToPlaceholder(layoutHtml, name) {
|
|
83
|
-
return new RegExp(
|
|
83
|
+
return new RegExp(`<a\\b[^>]*href="\\{\\{\\s*${name}\\s*\\}\\}"[^>]*>\\s*[^<\\s]`).test(layoutHtml);
|
|
84
84
|
}
|
package/dist/render.js
CHANGED
|
@@ -23,7 +23,13 @@ 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
25
|
const PLAIN_REFERENCE = /\{\{\s*([\w.]+)\s*\}\}/g;
|
|
26
|
-
|
|
26
|
+
// richText becomes plain text: meta lines render unescaped into subject and layout rows.
|
|
27
|
+
const META_FORMAT_HELPER_BY_TYPE = {
|
|
28
|
+
money: "money",
|
|
29
|
+
date: "date",
|
|
30
|
+
percent: "percent",
|
|
31
|
+
richText: "richTextPlain",
|
|
32
|
+
};
|
|
27
33
|
function describe(e) {
|
|
28
34
|
return e instanceof Error ? e.message : String(e);
|
|
29
35
|
}
|
|
@@ -218,9 +224,10 @@ async function renderMailBody(tplApi, ctx, template, hbs, context, buttonInlineS
|
|
|
218
224
|
function withFormatHelpers(source, variables) {
|
|
219
225
|
return source.replace(PLAIN_REFERENCE, (reference, name) => {
|
|
220
226
|
const type = variables[name]?.type;
|
|
221
|
-
|
|
222
|
-
?
|
|
223
|
-
:
|
|
227
|
+
const helper = type !== undefined && Object.hasOwn(META_FORMAT_HELPER_BY_TYPE, type)
|
|
228
|
+
? META_FORMAT_HELPER_BY_TYPE[type]
|
|
229
|
+
: undefined;
|
|
230
|
+
return helper === undefined ? reference : `{{${helper} ${name}}}`;
|
|
224
231
|
});
|
|
225
232
|
}
|
|
226
233
|
function renderSubjectLine(hbs, source, context, variables) {
|
|
@@ -302,6 +309,17 @@ async function assembleMailAttachments(args) {
|
|
|
302
309
|
});
|
|
303
310
|
return combineMailAttachments(slug, inlineAttachments, mergeInheritedAttachments(template.fileAttachments ?? [], inherited.fileAttachments ?? [], (file) => file.fileName), renderedPdfs);
|
|
304
311
|
}
|
|
312
|
+
const MJML_FOOTER_SECTION = '<mj-section><mj-column><mj-text font-size="12px" color="#5A6170" align="center">{{{mailFooterHtml}}}</mj-text></mj-column></mj-section>';
|
|
313
|
+
// The platform footer (imprint, privacy, unsubscribe) must not depend on the layout author
|
|
314
|
+
// remembering the placeholder.
|
|
315
|
+
function withPlatformFooter(mjmlSource) {
|
|
316
|
+
if (mjmlSource.includes("mailFooterHtml"))
|
|
317
|
+
return mjmlSource;
|
|
318
|
+
const bodyEnd = mjmlSource.lastIndexOf("</mj-body>");
|
|
319
|
+
return bodyEnd === -1
|
|
320
|
+
? mjmlSource
|
|
321
|
+
: `${mjmlSource.slice(0, bodyEnd)}{{#if mailFooterHtml}}${MJML_FOOTER_SECTION}{{/if}}${mjmlSource.slice(bodyEnd)}`;
|
|
322
|
+
}
|
|
305
323
|
function compileLayoutMjml(args) {
|
|
306
324
|
const { layout, hbs, contentHtml, appearance, accentColor, resourceFileRefs, layoutContext } = args;
|
|
307
325
|
try {
|
|
@@ -321,7 +339,7 @@ function compileLayoutMjml(args) {
|
|
|
321
339
|
bandLinksHtml: args.bandLinksHtml,
|
|
322
340
|
footerHtml: args.footerHtml,
|
|
323
341
|
})
|
|
324
|
-
: compileDocumentSource(hbs, layout.content)({
|
|
342
|
+
: compileDocumentSource(hbs, withPlatformFooter(layout.content))({
|
|
325
343
|
...layoutContext,
|
|
326
344
|
content: contentHtml,
|
|
327
345
|
mailFooterHtml: args.footerHtml,
|
|
@@ -421,17 +439,23 @@ async function resolveMailSiteUrl(options, ctx, locale) {
|
|
|
421
439
|
throw new RendererError(`renderer-mail-html: the site URL resolver failed — ${describe(e)}`, "other");
|
|
422
440
|
}
|
|
423
441
|
}
|
|
442
|
+
// Only header, signature and footer reach the mail; a link elsewhere in the layout is never shown.
|
|
443
|
+
function layoutRegionsSource(layoutHtml) {
|
|
444
|
+
const { header, signature, footer } = readLayoutRegions(layoutHtml);
|
|
445
|
+
return [header, signature, footer].join("");
|
|
446
|
+
}
|
|
424
447
|
// A structured layout shows the site and unsubscribe links inside the card and only the
|
|
425
448
|
// platform line below it; an mjml layout gets everything in one line.
|
|
426
449
|
function renderFooterParts(args) {
|
|
427
450
|
const { layout, footer, unsubscribeUrl, locale } = args;
|
|
428
451
|
const structured = layout.format === "html";
|
|
429
|
-
const
|
|
452
|
+
const linkedRegions = structured ? layoutRegionsSource(layout.content) : "";
|
|
453
|
+
const layoutLinksUnsubscribe = layoutLinksToPlaceholder(linkedRegions, "unsubscribeUrl");
|
|
430
454
|
const footerHtml = buildMailFooterHtml({
|
|
431
455
|
footer,
|
|
432
456
|
unsubscribeUrl: structured ? "" : unsubscribeUrl,
|
|
433
457
|
locale,
|
|
434
|
-
layoutLinksImprint:
|
|
458
|
+
layoutLinksImprint: layoutLinksToPlaceholder(linkedRegions, "imprint"),
|
|
435
459
|
layoutLinksUnsubscribe,
|
|
436
460
|
});
|
|
437
461
|
return {
|
package/dist/seeding.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// system layout (slug "default", kind "mail-layout", tenantId=SYSTEM_TENANT_ID)
|
|
3
3
|
// into the template-resource table.
|
|
4
4
|
//
|
|
5
|
-
// **When to use?** When the app author wants
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// **When to use?** When the app author wants an editable layout in the tenant admin
|
|
6
|
+
// UI. Without seeding the renderer falls back to the structured in-code layout with
|
|
7
|
+
// the tenant branding. A seeded MJML layout has its own look and must contain
|
|
8
|
+
// {{{mailFooterHtml}}}, or the platform footer is appended after its body.
|
|
8
9
|
//
|
|
9
10
|
// Idempotent: a second call with unchanged content writes no event, a changed
|
|
10
11
|
// layout updates the existing row.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-mail-html",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.388.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
|
-
"license": "
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"kumiko": {
|
|
@@ -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-renderer": "0.
|
|
25
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
22
|
+
"@cosmicdrift/kumiko-bundled-features": "0.388.0",
|
|
23
|
+
"@cosmicdrift/kumiko-framework": "0.388.0",
|
|
24
|
+
"@cosmicdrift/kumiko-renderer": "0.388.0",
|
|
25
|
+
"@cosmicdrift/kumiko-types": "0.388.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.388.0",
|
|
32
|
+
"@cosmicdrift/kumiko-testing": "0.388.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,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Mail footer cannot be dropped, richText in subject is plain text",
|
|
6
|
+
"detail": "An mjml layout without the mailFooterHtml placeholder now gets the platform footer appended before the end of the body. Layout links to the imprint or unsubscribe URL only count when they sit in the header, signature or footer region and have link text. richText variables in subject, preview and other meta lines render as plain text instead of raw HTML. An invalid tenant time zone is logged as a warning."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.378.0",
|
|
4
10
|
"type": "fix",
|