@broberg/mail-templates 0.1.0 → 0.2.2
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.cjs +12 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -431
- package/dist/index.d.ts +50 -431
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,9 @@ function isBrandColor(v) {
|
|
|
33
33
|
function isFontStack(v) {
|
|
34
34
|
return v.trim().length > 0 && v.length <= 200 && !/[<>"`;{}]/.test(v);
|
|
35
35
|
}
|
|
36
|
+
var MAX_BLOCKS = 100;
|
|
37
|
+
var MAX_FOOTER_LINES = 20;
|
|
38
|
+
var MAX_COPY_KEYS = 200;
|
|
36
39
|
var brandColor = zod.z.string().refine(isBrandColor, { message: "not a colour: use #rrggbb, rgb()/hsl(), or a CSS colour name" }).optional();
|
|
37
40
|
var brandFontStack = zod.z.string().refine(isFontStack, { message: "not a font stack: use single quotes for family names, e.g. 'DM Sans',Arial,sans-serif" }).optional();
|
|
38
41
|
var MailBrand = zod.z.object({
|
|
@@ -51,13 +54,15 @@ var MailBrand = zod.z.object({
|
|
|
51
54
|
fontSans: brandFontStack,
|
|
52
55
|
fontSerif: brandFontStack,
|
|
53
56
|
webfontHref: zod.z.string().optional(),
|
|
54
|
-
footerLines: zod.z.array(zod.z.string()).optional()
|
|
57
|
+
footerLines: zod.z.array(zod.z.string()).max(MAX_FOOTER_LINES).optional()
|
|
55
58
|
}).passthrough();
|
|
56
|
-
var MailCopy = zod.z.
|
|
59
|
+
var MailCopy = zod.z.unknown().refine((v) => v === null || typeof v !== "object" || Object.keys(v).length <= MAX_COPY_KEYS, {
|
|
60
|
+
message: `at most ${MAX_COPY_KEYS} phrases`
|
|
61
|
+
}).pipe(zod.z.record(zod.z.string(), zod.z.string()));
|
|
57
62
|
var MailTemplate = zod.z.object({
|
|
58
63
|
purpose: zod.z.string().min(1),
|
|
59
64
|
subject: zod.z.string().min(1),
|
|
60
|
-
blocks: zod.z.array(MailBlock).min(1),
|
|
65
|
+
blocks: zod.z.array(MailBlock).min(1).max(MAX_BLOCKS),
|
|
61
66
|
/** Required-or-derived, never silently empty — see derivePreheader. */
|
|
62
67
|
preheader: zod.z.string().optional(),
|
|
63
68
|
brand: MailBrand.optional(),
|
|
@@ -65,7 +70,7 @@ var MailTemplate = zod.z.object({
|
|
|
65
70
|
});
|
|
66
71
|
var MailTemplateOverride = zod.z.object({
|
|
67
72
|
subject: zod.z.string().min(1).optional(),
|
|
68
|
-
blocks: zod.z.array(MailBlock).min(1).optional(),
|
|
73
|
+
blocks: zod.z.array(MailBlock).min(1).max(MAX_BLOCKS).optional(),
|
|
69
74
|
preheader: zod.z.string().optional(),
|
|
70
75
|
brand: MailBrand.optional(),
|
|
71
76
|
copy: MailCopy.optional()
|
|
@@ -222,6 +227,9 @@ async function hashContent(canonical) {
|
|
|
222
227
|
}
|
|
223
228
|
|
|
224
229
|
exports.MAIL_CLASSES = MAIL_CLASSES;
|
|
230
|
+
exports.MAX_BLOCKS = MAX_BLOCKS;
|
|
231
|
+
exports.MAX_COPY_KEYS = MAX_COPY_KEYS;
|
|
232
|
+
exports.MAX_FOOTER_LINES = MAX_FOOTER_LINES;
|
|
225
233
|
exports.MailBlock = MailBlock;
|
|
226
234
|
exports.MailBrand = MailBrand;
|
|
227
235
|
exports.MailCopy = MailCopy;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["z"],"mappings":"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","file":"index.cjs","sourcesContent":["// F040.2 — @broberg/mail-templates: the ENGINE behind a mail template.\n//\n// Extracted from cardmem F298 (packages/shared/src/mail-template.ts, mail-render.ts,\n// mail-classes.ts), which aggregated four projects' MEASUREMENTS — fd-sundhed,\n// sanne, vn-leker, trail. Every rule below is here because someone paid for it in\n// production; the comments keep the reason, so a later \"simplification\" can see\n// what it would undo.\n//\n// What this package owns: the template SHAPE (blocks + brand), the one correct\n// render order, placeholder filling, the per-tenant override over a coded default,\n// and the content hash a host versions on. What it does NOT own: storage (the host\n// keeps versions and overrides), the shell (@broberg/mail-core) and sending\n// (@broberg/mail).\n\nimport { z } from \"zod\";\n\n// ─────────────────────────────────────────────────────────────── escaping\n\nconst ESCAPES: Record<string, string> = { \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\", \"'\": \"'\" };\n\n/** Escape text for HTML. Covers quotes too, so the same function is safe in an attribute. */\nexport function escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) => ESCAPES[c] ?? c);\n}\n\n/** Escape an attribute value. Same job as escapeHtml here; named so a call site says which it means. */\nexport function escapeAttr(s: string): string {\n return escapeHtml(s);\n}\n\n// ─────────────────────────────────────────────────────────────── the shape\n\n/** A body BLOCK. The template supplies structure; the shell supplies markup.\n *\n * vn-leker, reviewing their own v1: a body field that takes `<p class style>`\n * works while one person writes both halves — a foreign filler either loses all\n * formatting (plain text) or breaks the layout (HTML). */\nexport const MailBlock = z.discriminatedUnion(\"type\", [\n z.object({ type: z.literal(\"paragraph\"), text: z.string() }),\n z.object({ type: z.literal(\"heading\"), text: z.string() }),\n /** The note box with a coloured left edge. */\n z.object({ type: z.literal(\"note\"), text: z.string() }),\n z.object({ type: z.literal(\"button\"), text: z.string(), url: z.string() }),\n z.object({ type: z.literal(\"divider\") }),\n /** A slot for PRE-RENDERED html (a letter body with headings and lists) — the\n * only block that does not escape. Deliberately narrow: a value filling it is\n * still escaped unless the caller lists its key in rawValueKeys, so a template\n * author cannot make a plain value raw by choosing this block. */\n z.object({ type: z.literal(\"html\"), text: z.string() }),\n]);\nexport type MailBlock = z.infer<typeof MailBlock>;\n\n/** A brand COLOUR — REFUSED, not escaped, when it is not one.\n *\n * Proved against the built shell: an accent colour of\n * `#fff;\"></td></tr></table><a href=\"https://phish.example\">Log ind her</a>…`\n * put that anchor into the mail. An escaped non-colour still leaves the building\n * as a broken mail nobody sees an error for; a refusal is an error the project\n * reads while it still has the file open. */\nexport function isBrandColor(v: string): boolean {\n const s = v.trim();\n if (!s || s.length > 32) return false;\n if (/^#[0-9a-fA-F]{3}$|^#[0-9a-fA-F]{6}$|^#[0-9a-fA-F]{8}$/.test(s)) return true;\n // digits, commas, dots, spaces, % only — nothing that can close an attribute\n if (/^(rgb|rgba|hsl|hsla)\\(\\s*[0-9.,%\\s/]+\\)$/.test(s)) return true;\n // a CSS colour name: letters only (`rebeccapurple` is real)\n return /^[a-zA-Z]{3,20}$/.test(s);\n}\n\n/** A font STACK may hold quotes and commas, never a tag delimiter or a quote that closes its attribute. */\nexport function isFontStack(v: string): boolean {\n return v.trim().length > 0 && v.length <= 200 && !/[<>\"`;{}]/.test(v);\n}\n\nconst brandColor = z\n .string()\n .refine(isBrandColor, { message: \"not a colour: use #rrggbb, rgb()/hsl(), or a CSS colour name\" })\n .optional();\nconst brandFontStack = z\n .string()\n .refine(isFontStack, { message: \"not a font stack: use single quotes for family names, e.g. 'DM Sans',Arial,sans-serif\" })\n .optional();\n\n/** Brand values, ALL optional — and EMPTY is a value, not an absence (fd-sundhed\n * renders different, correct copy for an empty partner name). */\nexport const MailBrand = z\n .object({\n partner: z.string().optional(),\n senderName: z.string().optional(),\n senderTitle: z.string().optional(),\n senderEmail: z.string().optional(),\n /** Both delivery forms: a hosted URL works from any sender; a CID only if the\n * sender attaches the bytes with that Content-ID (sanne shipped a broken logo twice). */\n logoUrl: z.string().optional(),\n /** px. Outlook ignores CSS dimensions on an image; only the HTML attribute holds. */\n logoWidth: z.number().int().positive().max(600).optional(),\n logoCid: z.string().optional(),\n colorAccent: brandColor,\n colorInk: brandColor,\n fontSans: brandFontStack,\n fontSerif: brandFontStack,\n webfontHref: z.string().optional(),\n footerLines: z.array(z.string()).optional(),\n })\n .passthrough();\nexport type MailBrand = z.infer<typeof MailBrand>;\n\n/** Greeting, sign-off and other fixed phrases are DATA, per language (vn-leker runs da/no/sv). */\nexport const MailCopy = z.record(z.string(), z.string());\nexport type MailCopy = z.infer<typeof MailCopy>;\n\n/** One template, keyed on its PURPOSE (fd-sport's model: a project has a set, not one file). */\nexport const MailTemplate = z.object({\n purpose: z.string().min(1),\n subject: z.string().min(1),\n blocks: z.array(MailBlock).min(1),\n /** Required-or-derived, never silently empty — see derivePreheader. */\n preheader: z.string().optional(),\n brand: MailBrand.optional(),\n copy: MailCopy.optional(),\n});\nexport type MailTemplate = z.infer<typeof MailTemplate>;\n\n/** What a tenant stores to change a coded default. Every field optional; `purpose` is the key, not a field. */\nexport const MailTemplateOverride = z.object({\n subject: z.string().min(1).optional(),\n blocks: z.array(MailBlock).min(1).optional(),\n preheader: z.string().optional(),\n brand: MailBrand.optional(),\n copy: MailCopy.optional(),\n});\nexport type MailTemplateOverride = z.infer<typeof MailTemplateOverride>;\n\n/** The class vocabulary the shell styles — SMALL AND CLOSED. fd-sundhed's dark-mode\n * rules target classes IN the content, so the hooks have to be agreed names. */\nexport const MAIL_CLASSES = {\n bg: \"cm-bg\",\n card: \"cm-card\",\n p: \"cm-p\",\n sm: \"cm-sm\",\n btn: \"cm-btn\",\n box: \"cm-box\",\n} as const;\n\n// ─────────────────────────────────────────────────────────────── small rules\n\n/** The preheader, DERIVED when not given: the first paragraph or heading, cut on a\n * word. Omitted, the inbox shows whatever words fall first (vn-leker #6). */\nexport function derivePreheader(blocks: MailBlock[], given?: string): string {\n const explicit = (given ?? \"\").trim();\n if (explicit) return explicit;\n const first = blocks.find((b) => b.type === \"paragraph\" || b.type === \"heading\");\n const text = first && \"text\" in first ? first.text.trim() : \"\";\n if (text.length <= 120) return text;\n return `${text.slice(0, 117).replace(/\\s+\\S*$/, \"\")}…`;\n}\n\nconst TOKEN = /\\{\\{\\s*([a-zA-Z0-9_.]+)\\s*\\}\\}/g;\n\n/** A link in a mail must be ABSOLUTE — `/da/shop/bog` has no page to resolve\n * against (sanne's customer). Returns the reason so a refusal can NAME the link.\n *\n * `filled: true` at render time: a url that is EXACTLY one `{{token}}` is accepted\n * when a template is authored (it must not have to know the host's routes) and\n * refused once filling has happened, because a surviving token is a dead button. */\nexport function checkUrl(url: string, opts?: { filled?: boolean }): { ok: true } | { ok: false; reason: string } {\n const u = url.trim();\n if (!u) return { ok: false, reason: \"empty url\" };\n if (opts?.filled && /\\{\\{\\s*[a-zA-Z0-9_.]+\\s*\\}\\}/.test(u))\n return { ok: false, reason: `link still contains an unfilled placeholder \"${u}\" — the button would be dead, so it is shown as plain text instead` };\n if (/^\\{\\{\\s*[a-zA-Z0-9_.]+\\s*\\}\\}$/.test(u)) return { ok: true };\n if (/^https?:\\/\\//i.test(u)) return { ok: true };\n if (/^mailto:/i.test(u)) return { ok: true };\n if (u.startsWith(\"/\"))\n return { ok: false, reason: `relative link \"${u}\" — a mail has no page to resolve it against. Use an absolute https:// URL.` };\n return { ok: false, reason: `unsupported link \"${u}\" — use https:// or mailto:` };\n}\n\n// ─────────────────────────────────────────────────────────────── rendering\n\n/** The @broberg/mail-core primitives the body needs, INJECTED.\n *\n * mail-core reads files at module scope (node:fs, for the logo attachment), so\n * importing it here would break every browser bundle that renders a preview —\n * cardmem measured exactly that on `vite build`. The host passes mail-core's own\n * functions; there is no fallback, because a fallback IS a second implementation. */\nexport interface MailPrimitives {\n heading: (text: string, opts: { accentColor?: string; fontSerif?: string }) => string;\n paragraph: (text: string) => string;\n noteBox: (html: string, opts: { accentColor: string }) => string;\n cta: (href: string, label: string, opts: { accentColor: string }) => string;\n}\n\nfunction withClass(html: string, cls: string): string {\n return html.replace(/^\\s*<([a-z0-9]+)/i, (_m, tag) => `<${tag} class=\"${cls}\"`);\n}\n\n/** Blocks → body html, styled inline by mail-core and carrying the class hooks. */\nexport function renderBlocks(\n blocks: MailBlock[],\n opts: { accentColor: string; primitives: MailPrimitives; fontSerif?: string },\n): { html: string; notes: string[] } {\n const notes: string[] = [];\n const out: string[] = [];\n for (const b of blocks) {\n switch (b.type) {\n case \"heading\":\n out.push(withClass(opts.primitives.heading(b.text, { accentColor: opts.accentColor, ...(opts.fontSerif ? { fontSerif: opts.fontSerif } : {}) }), MAIL_CLASSES.p));\n break;\n case \"paragraph\":\n out.push(withClass(opts.primitives.paragraph(b.text), MAIL_CLASSES.p));\n break;\n case \"note\":\n // A coloured left EDGE, not a filled panel: in Outlook iOS card and footer\n // both became #484848, so a zone defined by fill stops existing.\n out.push(withClass(opts.primitives.noteBox(escapeHtml(b.text), { accentColor: opts.accentColor }), MAIL_CLASSES.box));\n break;\n case \"button\": {\n const check = checkUrl(b.url, { filled: true });\n if (!check.ok) {\n // Plain TEXT, not dropped: a missing button is a silent change; a visible\n // non-link is a visible one, and the note says why.\n notes.push(check.reason);\n out.push(withClass(opts.primitives.paragraph(b.text), MAIL_CLASSES.p));\n break;\n }\n out.push(withClass(opts.primitives.cta(b.url, b.text, { accentColor: opts.accentColor }), MAIL_CLASSES.btn));\n break;\n }\n case \"divider\":\n out.push(\"<hr>\");\n break;\n case \"html\":\n out.push(b.text);\n break;\n }\n }\n return { html: out.join(\"\\n\"), notes };\n}\n\n/** Fill `{{name}}` placeholders, escaping each value.\n *\n * A MISSING value stays as the literal placeholder and is named in `missing` —\n * a visible `{{navn}}` is caught; a silently blank sentence is not. An EMPTY value\n * is honoured as empty (`in values`, not truthiness). `rawKeys` names values that\n * are already HTML — opt-in per call, so a caller must say which key it trusts. */\nexport function fillPlaceholders(\n text: string,\n values: Record<string, string>,\n rawKeys: readonly string[] = [],\n): { text: string; missing: string[] } {\n const raw = new Set(rawKeys);\n const missing: string[] = [];\n const filled = text.replace(TOKEN, (whole, key: string) => {\n if (!(key in values)) {\n missing.push(key);\n return whole;\n }\n const v = values[key] ?? \"\";\n return raw.has(key) ? v : escapeHtml(v);\n });\n return { text: filled, missing: [...new Set(missing)] };\n}\n\n/** Fill WITHOUT escaping, for a destination that escapes for itself (a subject line,\n * a button url). A separate function, not an `escape: false` flag someone flips on a text field. */\nexport function fillPlaceholdersRaw(text: string, values: Record<string, string>): { text: string; missing: string[] } {\n const missing: string[] = [];\n const filled = text.replace(TOKEN, (whole, key: string) => {\n if (!(key in values)) {\n missing.push(key);\n return whole;\n }\n return values[key] ?? \"\";\n });\n return { text: filled, missing: [...new Set(missing)] };\n}\n\nexport interface RenderedTemplate {\n /** The BODY html — wrap it in @broberg/mail-core's renderShell. */\n html: string;\n /** Plain text, filled without escaping: a mail client never renders a subject as HTML. */\n subject: string;\n /** Explicit or derived — never silently empty. */\n preheader: string;\n /** Placeholders with no value, from body, subject and button urls. */\n missing: string[];\n /** Findings that did not stop the render but a person should see. */\n notes: string[];\n}\n\n/** The WHOLE render, in the one order that is correct: render blocks, THEN fill.\n *\n * Both steps escape, so the other order escapes twice:\n * fill THEN render → \"Sørensen & Søn\" → \"Sørensen &amp; Søn\"\n * render THEN fill → \"Sørensen & Søn\" → \"Sørensen & Søn\" ✓\n * The wrong order looks right for every customer without an `&` in their name.\n *\n * ONE exception, for the same reason: a button URL is filled FIRST, unescaped, so\n * it goes through checkUrl (a value resolving to a relative or javascript: link\n * degrades to text) and then the cta primitive's own attribute escaping. */\nexport function renderTemplate(\n template: Pick<MailTemplate, \"subject\" | \"blocks\" | \"preheader\">,\n opts: {\n /** Resolved by the caller (the shell needs the same value — two places deciding a brand colour disagree). */\n accentColor: string;\n fontSerif?: string;\n primitives: MailPrimitives;\n /** Value keys that are already HTML. See fillPlaceholders. */\n rawValueKeys?: readonly string[];\n },\n values: Record<string, string> = {},\n): RenderedTemplate {\n const urlMissing: string[] = [];\n const blocks = template.blocks.map((b) => {\n if (b.type !== \"button\") return b;\n const f = fillPlaceholdersRaw(b.url, values);\n urlMissing.push(...f.missing);\n return { ...b, url: f.text };\n });\n const rendered = renderBlocks(blocks, { accentColor: opts.accentColor, primitives: opts.primitives, ...(opts.fontSerif ? { fontSerif: opts.fontSerif } : {}) });\n // Brace text that is NOT a token (\"{{brevets brødtekst indsættes her}}\") matches\n // neither `missing` nor anything else — fd-sundhed shipped one for two days.\n // Checked BEFORE filling, where a filled token is not yet gone.\n for (const m of rendered.html.matchAll(/\\{\\{([^}]*)\\}\\}/g)) {\n if (/^\\s*[a-zA-Z0-9_.]+\\s*$/.test(m[1] ?? \"\")) continue;\n rendered.notes.push(`\"${m[0]}\" looks like a placeholder but is not one — it will be delivered to the recipient verbatim. A placeholder is a single key, e.g. {{body}}.`);\n }\n const body = fillPlaceholders(rendered.html, values, opts.rawValueKeys ?? []);\n const subject = fillPlaceholdersRaw(template.subject, values);\n const preheader = fillPlaceholdersRaw(derivePreheader(template.blocks, template.preheader), values);\n return {\n html: body.text,\n subject: subject.text,\n preheader: preheader.text,\n missing: [...new Set([...body.missing, ...subject.missing, ...urlMissing, ...preheader.missing])],\n notes: rendered.notes,\n };\n}\n\n// ─────────────────────────────────────────────────────────────── per-tenant overrides\n\n/** The EFFECTIVE template: a stored override over the coded default.\n *\n * subject, blocks and preheader REPLACE; brand and copy MERGE key by key, so a\n * tenant that sets only its accent keeps the default's sender and footer. */\nexport function resolveTemplate(standard: MailTemplate, override?: MailTemplateOverride | null): MailTemplate {\n if (!override) return standard;\n const out: MailTemplate = { ...standard };\n if (override.subject !== undefined) out.subject = override.subject;\n if (override.blocks !== undefined) out.blocks = override.blocks;\n if (override.preheader !== undefined) out.preheader = override.preheader;\n if (override.brand !== undefined) out.brand = { ...(standard.brand ?? {}), ...override.brand };\n if (override.copy !== undefined) out.copy = { ...(standard.copy ?? {}), ...override.copy };\n return out;\n}\n\nexport interface EffectiveTemplate {\n purpose: string;\n effective: MailTemplate;\n /** The coded default — what \"reset\" goes back to. */\n default: MailTemplate;\n /** True when the tenant's override changes at least one field. */\n isCustom: boolean;\n}\n\n/** Every coded template with its tenant's override applied — what an editor lists.\n *\n * An override whose purpose has no coded default is NAMED in `unknownOverrides`,\n * not dropped: a template renamed in code would otherwise lose a tenant's edits\n * with nothing to show for it. */\nexport function listEffectiveTemplates(\n defaults: readonly MailTemplate[],\n overrides: Readonly<Record<string, MailTemplateOverride | null | undefined>> = {},\n): { templates: EffectiveTemplate[]; unknownOverrides: string[] } {\n const known = new Set(defaults.map((d) => d.purpose));\n const templates = defaults.map((d) => {\n const o = overrides[d.purpose];\n const isCustom = !!o && Object.values(o).some((v) => v !== undefined);\n return { purpose: d.purpose, effective: resolveTemplate(d, o), default: d, isCustom };\n });\n const unknownOverrides = Object.keys(overrides).filter((p) => !known.has(p) && overrides[p]);\n return { templates, unknownOverrides };\n}\n\n// ─────────────────────────────────────────────────────────────── versioning\n\nfunction stable(v: unknown): unknown {\n if (Array.isArray(v)) return v.map(stable);\n if (v && typeof v === \"object\") {\n return Object.fromEntries(\n Object.entries(v as Record<string, unknown>)\n .filter(([, val]) => val !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, val]) => [k, stable(val)]),\n );\n }\n return v;\n}\n\n/** The canonical content a version's hash is taken over: stable key ORDER, so a\n * moved JSON key is not reported as a change (a hash that cries wolf teaches\n * everyone to ignore it). Extra fields a host adds to its template are included. */\nexport function canonicalContent(t: MailTemplate & Record<string, unknown>): string {\n return JSON.stringify(stable({ ...t, preheader: t.preheader ?? null, brand: t.brand ?? null, copy: t.copy ?? null }));\n}\n\n/** SHA-256 hex over canonical content. Submission and sync check must call THIS — two hashers drift. */\nexport async function hashContent(canonical: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(canonical));\n return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["z"],"mappings":"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","file":"index.cjs","sourcesContent":["// F040.2 — @broberg/mail-templates: the ENGINE behind a mail template.\n//\n// Extracted from cardmem F298 (packages/shared/src/mail-template.ts, mail-render.ts,\n// mail-classes.ts), which aggregated four projects' MEASUREMENTS — fd-sundhed,\n// sanne, vn-leker, trail. Every rule below is here because someone paid for it in\n// production; the comments keep the reason, so a later \"simplification\" can see\n// what it would undo.\n//\n// What this package owns: the template SHAPE (blocks + brand), the one correct\n// render order, placeholder filling, the per-tenant override over a coded default,\n// and the content hash a host versions on. What it does NOT own: storage (the host\n// keeps versions and overrides), the shell (@broberg/mail-core) and sending\n// (@broberg/mail).\n\nimport { z } from \"zod\";\n\n// ─────────────────────────────────────────────────────────────── escaping\n\nconst ESCAPES: Record<string, string> = { \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\", \"'\": \"'\" };\n\n/** Escape text for HTML. Covers quotes too, so the same function is safe in an attribute. */\nexport function escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) => ESCAPES[c] ?? c);\n}\n\n/** Escape an attribute value. Same job as escapeHtml here; named so a call site says which it means. */\nexport function escapeAttr(s: string): string {\n return escapeHtml(s);\n}\n\n// ─────────────────────────────────────────────────────────────── the shape\n\n/** A body BLOCK. The template supplies structure; the shell supplies markup.\n *\n * vn-leker, reviewing their own v1: a body field that takes `<p class style>`\n * works while one person writes both halves — a foreign filler either loses all\n * formatting (plain text) or breaks the layout (HTML). */\nexport const MailBlock = z.discriminatedUnion(\"type\", [\n z.object({ type: z.literal(\"paragraph\"), text: z.string() }),\n z.object({ type: z.literal(\"heading\"), text: z.string() }),\n /** The note box with a coloured left edge. */\n z.object({ type: z.literal(\"note\"), text: z.string() }),\n z.object({ type: z.literal(\"button\"), text: z.string(), url: z.string() }),\n z.object({ type: z.literal(\"divider\") }),\n /** A slot for PRE-RENDERED html (a letter body with headings and lists) — the\n * only block that does not escape. Deliberately narrow: a value filling it is\n * still escaped unless the caller lists its key in rawValueKeys, so a template\n * author cannot make a plain value raw by choosing this block. */\n z.object({ type: z.literal(\"html\"), text: z.string() }),\n]);\nexport type MailBlock = z.infer<typeof MailBlock>;\n\n/** A brand COLOUR — REFUSED, not escaped, when it is not one.\n *\n * Proved against the built shell: an accent colour of\n * `#fff;\"></td></tr></table><a href=\"https://phish.example\">Log ind her</a>…`\n * put that anchor into the mail. An escaped non-colour still leaves the building\n * as a broken mail nobody sees an error for; a refusal is an error the project\n * reads while it still has the file open. */\nexport function isBrandColor(v: string): boolean {\n const s = v.trim();\n if (!s || s.length > 32) return false;\n if (/^#[0-9a-fA-F]{3}$|^#[0-9a-fA-F]{6}$|^#[0-9a-fA-F]{8}$/.test(s)) return true;\n // digits, commas, dots, spaces, % only — nothing that can close an attribute\n if (/^(rgb|rgba|hsl|hsla)\\(\\s*[0-9.,%\\s/]+\\)$/.test(s)) return true;\n // a CSS colour name: letters only (`rebeccapurple` is real)\n return /^[a-zA-Z]{3,20}$/.test(s);\n}\n\n/** A font STACK may hold quotes and commas, never a tag delimiter or a quote that closes its attribute. */\nexport function isFontStack(v: string): boolean {\n return v.trim().length > 0 && v.length <= 200 && !/[<>\"`;{}]/.test(v);\n}\n\n/** Ceilings on what a stored override may hold — far above any real template, low\n * enough that a hostile body costs one issue, not one per element. */\nexport const MAX_BLOCKS = 100;\nexport const MAX_FOOTER_LINES = 20;\nexport const MAX_COPY_KEYS = 200;\n\nconst brandColor = z\n .string()\n .refine(isBrandColor, { message: \"not a colour: use #rrggbb, rgb()/hsl(), or a CSS colour name\" })\n .optional();\nconst brandFontStack = z\n .string()\n .refine(isFontStack, { message: \"not a font stack: use single quotes for family names, e.g. 'DM Sans',Arial,sans-serif\" })\n .optional();\n\n/** Brand values, ALL optional — and EMPTY is a value, not an absence (fd-sundhed\n * renders different, correct copy for an empty partner name). */\nexport const MailBrand = z\n .object({\n partner: z.string().optional(),\n senderName: z.string().optional(),\n senderTitle: z.string().optional(),\n senderEmail: z.string().optional(),\n /** Both delivery forms: a hosted URL works from any sender; a CID only if the\n * sender attaches the bytes with that Content-ID (sanne shipped a broken logo twice). */\n logoUrl: z.string().optional(),\n /** px. Outlook ignores CSS dimensions on an image; only the HTML attribute holds. */\n logoWidth: z.number().int().positive().max(600).optional(),\n logoCid: z.string().optional(),\n colorAccent: brandColor,\n colorInk: brandColor,\n fontSans: brandFontStack,\n fontSerif: brandFontStack,\n webfontHref: z.string().optional(),\n footerLines: z.array(z.string()).max(MAX_FOOTER_LINES).optional(),\n })\n .passthrough();\nexport type MailBrand = z.infer<typeof MailBrand>;\n\n/** Greeting, sign-off and other fixed phrases are DATA, per language (vn-leker runs da/no/sv). */\n// A record has no .max(): count the keys BEFORE zod walks them, or 200k bad keys\n// cost 200k issues (GHSA-mrhx-c7xc-m3w5 / buddy #4342).\nexport const MailCopy = z\n .unknown()\n .refine((v) => v === null || typeof v !== \"object\" || Object.keys(v).length <= MAX_COPY_KEYS, {\n message: `at most ${MAX_COPY_KEYS} phrases`,\n })\n .pipe(z.record(z.string(), z.string()));\nexport type MailCopy = z.infer<typeof MailCopy>;\n\n/** One template, keyed on its PURPOSE (fd-sport's model: a project has a set, not one file). */\nexport const MailTemplate = z.object({\n purpose: z.string().min(1),\n subject: z.string().min(1),\n blocks: z.array(MailBlock).min(1).max(MAX_BLOCKS),\n /** Required-or-derived, never silently empty — see derivePreheader. */\n preheader: z.string().optional(),\n brand: MailBrand.optional(),\n copy: MailCopy.optional(),\n});\nexport type MailTemplate = z.infer<typeof MailTemplate>;\n\n/** What a tenant stores to change a coded default. Every field optional; `purpose` is the key, not a field. */\nexport const MailTemplateOverride = z.object({\n subject: z.string().min(1).optional(),\n blocks: z.array(MailBlock).min(1).max(MAX_BLOCKS).optional(),\n preheader: z.string().optional(),\n brand: MailBrand.optional(),\n copy: MailCopy.optional(),\n});\nexport type MailTemplateOverride = z.infer<typeof MailTemplateOverride>;\n\n/** The class vocabulary the shell styles — SMALL AND CLOSED. fd-sundhed's dark-mode\n * rules target classes IN the content, so the hooks have to be agreed names. */\nexport const MAIL_CLASSES = {\n bg: \"cm-bg\",\n card: \"cm-card\",\n p: \"cm-p\",\n sm: \"cm-sm\",\n btn: \"cm-btn\",\n box: \"cm-box\",\n} as const;\n\n// ─────────────────────────────────────────────────────────────── small rules\n\n/** The preheader, DERIVED when not given: the first paragraph or heading, cut on a\n * word. Omitted, the inbox shows whatever words fall first (vn-leker #6). */\nexport function derivePreheader(blocks: MailBlock[], given?: string): string {\n const explicit = (given ?? \"\").trim();\n if (explicit) return explicit;\n const first = blocks.find((b) => b.type === \"paragraph\" || b.type === \"heading\");\n const text = first && \"text\" in first ? first.text.trim() : \"\";\n if (text.length <= 120) return text;\n return `${text.slice(0, 117).replace(/\\s+\\S*$/, \"\")}…`;\n}\n\nconst TOKEN = /\\{\\{\\s*([a-zA-Z0-9_.]+)\\s*\\}\\}/g;\n\n/** A link in a mail must be ABSOLUTE — `/da/shop/bog` has no page to resolve\n * against (sanne's customer). Returns the reason so a refusal can NAME the link.\n *\n * `filled: true` at render time: a url that is EXACTLY one `{{token}}` is accepted\n * when a template is authored (it must not have to know the host's routes) and\n * refused once filling has happened, because a surviving token is a dead button. */\nexport function checkUrl(url: string, opts?: { filled?: boolean }): { ok: true } | { ok: false; reason: string } {\n const u = url.trim();\n if (!u) return { ok: false, reason: \"empty url\" };\n if (opts?.filled && /\\{\\{\\s*[a-zA-Z0-9_.]+\\s*\\}\\}/.test(u))\n return { ok: false, reason: `link still contains an unfilled placeholder \"${u}\" — the button would be dead, so it is shown as plain text instead` };\n if (/^\\{\\{\\s*[a-zA-Z0-9_.]+\\s*\\}\\}$/.test(u)) return { ok: true };\n if (/^https?:\\/\\//i.test(u)) return { ok: true };\n if (/^mailto:/i.test(u)) return { ok: true };\n if (u.startsWith(\"/\"))\n return { ok: false, reason: `relative link \"${u}\" — a mail has no page to resolve it against. Use an absolute https:// URL.` };\n return { ok: false, reason: `unsupported link \"${u}\" — use https:// or mailto:` };\n}\n\n// ─────────────────────────────────────────────────────────────── rendering\n\n/** The @broberg/mail-core primitives the body needs, INJECTED.\n *\n * mail-core reads files at module scope (node:fs, for the logo attachment), so\n * importing it here would break every browser bundle that renders a preview —\n * cardmem measured exactly that on `vite build`. The host passes mail-core's own\n * functions; there is no fallback, because a fallback IS a second implementation. */\nexport interface MailPrimitives {\n heading: (text: string, opts: { accentColor?: string; fontSerif?: string }) => string;\n paragraph: (text: string) => string;\n noteBox: (html: string, opts: { accentColor: string }) => string;\n cta: (href: string, label: string, opts: { accentColor: string }) => string;\n}\n\nfunction withClass(html: string, cls: string): string {\n return html.replace(/^\\s*<([a-z0-9]+)/i, (_m, tag) => `<${tag} class=\"${cls}\"`);\n}\n\n/** Blocks → body html, styled inline by mail-core and carrying the class hooks. */\nexport function renderBlocks(\n blocks: MailBlock[],\n opts: { accentColor: string; primitives: MailPrimitives; fontSerif?: string },\n): { html: string; notes: string[] } {\n const notes: string[] = [];\n const out: string[] = [];\n for (const b of blocks) {\n switch (b.type) {\n case \"heading\":\n out.push(withClass(opts.primitives.heading(b.text, { accentColor: opts.accentColor, ...(opts.fontSerif ? { fontSerif: opts.fontSerif } : {}) }), MAIL_CLASSES.p));\n break;\n case \"paragraph\":\n out.push(withClass(opts.primitives.paragraph(b.text), MAIL_CLASSES.p));\n break;\n case \"note\":\n // A coloured left EDGE, not a filled panel: in Outlook iOS card and footer\n // both became #484848, so a zone defined by fill stops existing.\n out.push(withClass(opts.primitives.noteBox(escapeHtml(b.text), { accentColor: opts.accentColor }), MAIL_CLASSES.box));\n break;\n case \"button\": {\n const check = checkUrl(b.url, { filled: true });\n if (!check.ok) {\n // Plain TEXT, not dropped: a missing button is a silent change; a visible\n // non-link is a visible one, and the note says why.\n notes.push(check.reason);\n out.push(withClass(opts.primitives.paragraph(b.text), MAIL_CLASSES.p));\n break;\n }\n out.push(withClass(opts.primitives.cta(b.url, b.text, { accentColor: opts.accentColor }), MAIL_CLASSES.btn));\n break;\n }\n case \"divider\":\n out.push(\"<hr>\");\n break;\n case \"html\":\n out.push(b.text);\n break;\n }\n }\n return { html: out.join(\"\\n\"), notes };\n}\n\n/** Fill `{{name}}` placeholders, escaping each value.\n *\n * A MISSING value stays as the literal placeholder and is named in `missing` —\n * a visible `{{navn}}` is caught; a silently blank sentence is not. An EMPTY value\n * is honoured as empty (`in values`, not truthiness). `rawKeys` names values that\n * are already HTML — opt-in per call, so a caller must say which key it trusts. */\nexport function fillPlaceholders(\n text: string,\n values: Record<string, string>,\n rawKeys: readonly string[] = [],\n): { text: string; missing: string[] } {\n const raw = new Set(rawKeys);\n const missing: string[] = [];\n const filled = text.replace(TOKEN, (whole, key: string) => {\n if (!(key in values)) {\n missing.push(key);\n return whole;\n }\n const v = values[key] ?? \"\";\n return raw.has(key) ? v : escapeHtml(v);\n });\n return { text: filled, missing: [...new Set(missing)] };\n}\n\n/** Fill WITHOUT escaping, for a destination that escapes for itself (a subject line,\n * a button url). A separate function, not an `escape: false` flag someone flips on a text field. */\nexport function fillPlaceholdersRaw(text: string, values: Record<string, string>): { text: string; missing: string[] } {\n const missing: string[] = [];\n const filled = text.replace(TOKEN, (whole, key: string) => {\n if (!(key in values)) {\n missing.push(key);\n return whole;\n }\n return values[key] ?? \"\";\n });\n return { text: filled, missing: [...new Set(missing)] };\n}\n\nexport interface RenderedTemplate {\n /** The BODY html — wrap it in @broberg/mail-core's renderShell. */\n html: string;\n /** Plain text, filled without escaping: a mail client never renders a subject as HTML. */\n subject: string;\n /** Explicit or derived — never silently empty. */\n preheader: string;\n /** Placeholders with no value, from body, subject and button urls. */\n missing: string[];\n /** Findings that did not stop the render but a person should see. */\n notes: string[];\n}\n\n/** The WHOLE render, in the one order that is correct: render blocks, THEN fill.\n *\n * Both steps escape, so the other order escapes twice:\n * fill THEN render → \"Sørensen & Søn\" → \"Sørensen &amp; Søn\"\n * render THEN fill → \"Sørensen & Søn\" → \"Sørensen & Søn\" ✓\n * The wrong order looks right for every customer without an `&` in their name.\n *\n * ONE exception, for the same reason: a button URL is filled FIRST, unescaped, so\n * it goes through checkUrl (a value resolving to a relative or javascript: link\n * degrades to text) and then the cta primitive's own attribute escaping. */\nexport function renderTemplate(\n template: Pick<MailTemplate, \"subject\" | \"blocks\" | \"preheader\">,\n opts: {\n /** Resolved by the caller (the shell needs the same value — two places deciding a brand colour disagree). */\n accentColor: string;\n fontSerif?: string;\n primitives: MailPrimitives;\n /** Value keys that are already HTML. See fillPlaceholders. */\n rawValueKeys?: readonly string[];\n },\n values: Record<string, string> = {},\n): RenderedTemplate {\n const urlMissing: string[] = [];\n const blocks = template.blocks.map((b) => {\n if (b.type !== \"button\") return b;\n const f = fillPlaceholdersRaw(b.url, values);\n urlMissing.push(...f.missing);\n return { ...b, url: f.text };\n });\n const rendered = renderBlocks(blocks, { accentColor: opts.accentColor, primitives: opts.primitives, ...(opts.fontSerif ? { fontSerif: opts.fontSerif } : {}) });\n // Brace text that is NOT a token (\"{{brevets brødtekst indsættes her}}\") matches\n // neither `missing` nor anything else — fd-sundhed shipped one for two days.\n // Checked BEFORE filling, where a filled token is not yet gone.\n for (const m of rendered.html.matchAll(/\\{\\{([^}]*)\\}\\}/g)) {\n if (/^\\s*[a-zA-Z0-9_.]+\\s*$/.test(m[1] ?? \"\")) continue;\n rendered.notes.push(`\"${m[0]}\" looks like a placeholder but is not one — it will be delivered to the recipient verbatim. A placeholder is a single key, e.g. {{body}}.`);\n }\n const body = fillPlaceholders(rendered.html, values, opts.rawValueKeys ?? []);\n const subject = fillPlaceholdersRaw(template.subject, values);\n const preheader = fillPlaceholdersRaw(derivePreheader(template.blocks, template.preheader), values);\n return {\n html: body.text,\n subject: subject.text,\n preheader: preheader.text,\n missing: [...new Set([...body.missing, ...subject.missing, ...urlMissing, ...preheader.missing])],\n notes: rendered.notes,\n };\n}\n\n// ─────────────────────────────────────────────────────────────── per-tenant overrides\n\n/** The EFFECTIVE template: a stored override over the coded default.\n *\n * subject, blocks and preheader REPLACE; brand and copy MERGE key by key, so a\n * tenant that sets only its accent keeps the default's sender and footer. */\nexport function resolveTemplate(standard: MailTemplate, override?: MailTemplateOverride | null): MailTemplate {\n if (!override) return standard;\n const out: MailTemplate = { ...standard };\n if (override.subject !== undefined) out.subject = override.subject;\n if (override.blocks !== undefined) out.blocks = override.blocks;\n if (override.preheader !== undefined) out.preheader = override.preheader;\n if (override.brand !== undefined) out.brand = { ...(standard.brand ?? {}), ...override.brand };\n if (override.copy !== undefined) out.copy = { ...(standard.copy ?? {}), ...override.copy };\n return out;\n}\n\nexport interface EffectiveTemplate {\n purpose: string;\n effective: MailTemplate;\n /** The coded default — what \"reset\" goes back to. */\n default: MailTemplate;\n /** True when the tenant's override changes at least one field. */\n isCustom: boolean;\n}\n\n/** Every coded template with its tenant's override applied — what an editor lists.\n *\n * An override whose purpose has no coded default is NAMED in `unknownOverrides`,\n * not dropped: a template renamed in code would otherwise lose a tenant's edits\n * with nothing to show for it. */\nexport function listEffectiveTemplates(\n defaults: readonly MailTemplate[],\n overrides: Readonly<Record<string, MailTemplateOverride | null | undefined>> = {},\n): { templates: EffectiveTemplate[]; unknownOverrides: string[] } {\n const known = new Set(defaults.map((d) => d.purpose));\n const templates = defaults.map((d) => {\n const o = overrides[d.purpose];\n const isCustom = !!o && Object.values(o).some((v) => v !== undefined);\n return { purpose: d.purpose, effective: resolveTemplate(d, o), default: d, isCustom };\n });\n const unknownOverrides = Object.keys(overrides).filter((p) => !known.has(p) && overrides[p]);\n return { templates, unknownOverrides };\n}\n\n// ─────────────────────────────────────────────────────────────── versioning\n\nfunction stable(v: unknown): unknown {\n if (Array.isArray(v)) return v.map(stable);\n if (v && typeof v === \"object\") {\n return Object.fromEntries(\n Object.entries(v as Record<string, unknown>)\n .filter(([, val]) => val !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, val]) => [k, stable(val)]),\n );\n }\n return v;\n}\n\n/** The canonical content a version's hash is taken over: stable key ORDER, so a\n * moved JSON key is not reported as a change (a hash that cries wolf teaches\n * everyone to ignore it). Extra fields a host adds to its template are included. */\nexport function canonicalContent(t: MailTemplate & Record<string, unknown>): string {\n return JSON.stringify(stable({ ...t, preheader: t.preheader ?? null, brand: t.brand ?? null, copy: t.copy ?? null }));\n}\n\n/** SHA-256 hex over canonical content. Submission and sync check must call THIS — two hashers drift. */\nexport async function hashContent(canonical: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(canonical));\n return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n"]}
|