@skhema/embed 0.1.9 → 0.1.10
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/README.md +55 -0
- package/dist/components/SkhemaComponent.d.ts +0 -1
- package/dist/components/SkhemaComponent.d.ts.map +1 -1
- package/dist/components/SkhemaElement.d.ts.map +1 -1
- package/dist/embed.min.js +1 -1
- package/dist/embed.min.js.map +1 -1
- package/dist/index-DBeyneZT.cjs +215 -0
- package/dist/index-DBeyneZT.cjs.map +1 -0
- package/dist/index-jwx5CXLB.js +216 -0
- package/dist/index-jwx5CXLB.js.map +1 -0
- package/dist/index.cjs +79 -440
- package/dist/index.cjs.map +1 -1
- package/dist/index.es.js +81 -442
- package/dist/index.es.js.map +1 -1
- package/dist/render/index.d.ts +55 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render.cjs +7 -0
- package/dist/render.cjs.map +1 -0
- package/dist/render.d.ts +2 -0
- package/dist/render.es.js +7 -0
- package/dist/render.es.js.map +1 -0
- package/dist/styles/design-tokens.d.ts +32 -4
- package/dist/styles/design-tokens.d.ts.map +1 -1
- package/dist/tokens.cjs +1 -1
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.es.js +1 -1
- package/dist/utils/color.d.ts +25 -0
- package/dist/utils/color.d.ts.map +1 -0
- package/dist/utils/sanitization.d.ts +7 -1
- package/dist/utils/sanitization.d.ts.map +1 -1
- package/dist/{validation-CNWaQfh_.cjs → validation-DCw9rQeH.cjs} +40 -95
- package/dist/validation-DCw9rQeH.cjs.map +1 -0
- package/dist/{validation-MSiRF5Wt.js → validation-Dt4L_df8.js} +42 -97
- package/dist/validation-Dt4L_df8.js.map +1 -0
- package/package.json +11 -4
- package/dist/utils/author-attribution.d.ts +0 -16
- package/dist/utils/author-attribution.d.ts.map +0 -1
- package/dist/validation-CNWaQfh_.cjs.map +0 -1
- package/dist/validation-MSiRF5Wt.js.map +0 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const validation = require("./validation-DCw9rQeH.cjs");
|
|
3
|
+
const BRAND_PINK = "#cd476a";
|
|
4
|
+
function hexToRgb(hex) {
|
|
5
|
+
const h = hex.replace("#", "");
|
|
6
|
+
return [
|
|
7
|
+
parseInt(h.slice(0, 2), 16),
|
|
8
|
+
parseInt(h.slice(2, 4), 16),
|
|
9
|
+
parseInt(h.slice(4, 6), 16)
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
function oklchToHex(token, overHex = "#ffffff") {
|
|
13
|
+
const m = token.match(
|
|
14
|
+
/oklch\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*(?:\/\s*([\d.]+)\s*)?\)/
|
|
15
|
+
);
|
|
16
|
+
if (!m) return BRAND_PINK;
|
|
17
|
+
const L = parseFloat(m[1]);
|
|
18
|
+
const C = parseFloat(m[2]);
|
|
19
|
+
const H = parseFloat(m[3]);
|
|
20
|
+
const alpha = m[4] !== void 0 ? parseFloat(m[4]) : 1;
|
|
21
|
+
const h = H * Math.PI / 180;
|
|
22
|
+
const a = C * Math.cos(h);
|
|
23
|
+
const b = C * Math.sin(h);
|
|
24
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
25
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
26
|
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b;
|
|
27
|
+
const lr = l_ ** 3;
|
|
28
|
+
const mr = m_ ** 3;
|
|
29
|
+
const sr = s_ ** 3;
|
|
30
|
+
const R = 4.0767416621 * lr - 3.3077115913 * mr + 0.2309699292 * sr;
|
|
31
|
+
const G = -1.2684380046 * lr + 2.6097574011 * mr - 0.3413193965 * sr;
|
|
32
|
+
const B = -0.0041960863 * lr - 0.7034186147 * mr + 1.707614701 * sr;
|
|
33
|
+
const gamma = (x) => {
|
|
34
|
+
const c = Math.max(0, Math.min(1, x));
|
|
35
|
+
return c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
36
|
+
};
|
|
37
|
+
let rgb = [gamma(R) * 255, gamma(G) * 255, gamma(B) * 255];
|
|
38
|
+
if (alpha < 1) {
|
|
39
|
+
const [br, bg, bb] = hexToRgb(overHex);
|
|
40
|
+
rgb = [
|
|
41
|
+
rgb[0] * alpha + br * (1 - alpha),
|
|
42
|
+
rgb[1] * alpha + bg * (1 - alpha),
|
|
43
|
+
rgb[2] * alpha + bb * (1 - alpha)
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
return "#" + rgb.map((c) => Math.round(c).toString(16).padStart(2, "0")).join("");
|
|
47
|
+
}
|
|
48
|
+
function htmlEncode(str) {
|
|
49
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
50
|
+
}
|
|
51
|
+
function sanitizeContent(content) {
|
|
52
|
+
let sanitized = stripUrls(content);
|
|
53
|
+
sanitized = htmlEncode(sanitized);
|
|
54
|
+
sanitized = sanitized.replace(/\n/g, "<br>");
|
|
55
|
+
sanitized = applyTextWrapping(sanitized);
|
|
56
|
+
return sanitized;
|
|
57
|
+
}
|
|
58
|
+
function stripUrls(text) {
|
|
59
|
+
const patterns = [
|
|
60
|
+
// Standard URLs with protocols
|
|
61
|
+
/https?:\/\/[^\s<>"{}|\\^`[\]]+/gi,
|
|
62
|
+
// FTP URLs
|
|
63
|
+
/ftp:\/\/[^\s<>"{}|\\^`[\]]+/gi,
|
|
64
|
+
// URLs without protocol but with www
|
|
65
|
+
/www\.[^\s<>"{}|\\^`[\]]+/gi,
|
|
66
|
+
// Email-like patterns
|
|
67
|
+
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/gi,
|
|
68
|
+
// Common domain patterns (anything.com, anything.org, etc.)
|
|
69
|
+
/(?:^|\s)([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/[^\s]*)?/gi
|
|
70
|
+
];
|
|
71
|
+
let stripped = text;
|
|
72
|
+
patterns.forEach((pattern) => {
|
|
73
|
+
stripped = stripped.replace(pattern, "");
|
|
74
|
+
});
|
|
75
|
+
stripped = stripped.replace(/\s+/g, " ").trim();
|
|
76
|
+
return stripped;
|
|
77
|
+
}
|
|
78
|
+
function applyTextWrapping(text) {
|
|
79
|
+
const words = text.split(/(\s+)/);
|
|
80
|
+
return words.map((word) => {
|
|
81
|
+
if (/^\s+$/.test(word) || word.includes("<")) {
|
|
82
|
+
return word;
|
|
83
|
+
}
|
|
84
|
+
if (word.length > 30) {
|
|
85
|
+
return word.replace(/(.{10})/g, "$1");
|
|
86
|
+
}
|
|
87
|
+
return word;
|
|
88
|
+
}).join("");
|
|
89
|
+
}
|
|
90
|
+
function validateContentSecurity(content) {
|
|
91
|
+
const issues = [];
|
|
92
|
+
if (/<script[\s>]/i.test(content)) {
|
|
93
|
+
issues.push("Script tags detected");
|
|
94
|
+
}
|
|
95
|
+
if (/on\w+\s*=/i.test(content)) {
|
|
96
|
+
issues.push("Event handlers detected");
|
|
97
|
+
}
|
|
98
|
+
if (/javascript:/i.test(content)) {
|
|
99
|
+
issues.push("JavaScript protocol detected");
|
|
100
|
+
}
|
|
101
|
+
if (/data:[^,]*script/i.test(content)) {
|
|
102
|
+
issues.push("Data URL with script detected");
|
|
103
|
+
}
|
|
104
|
+
if (/<iframe[\s>]/i.test(content)) {
|
|
105
|
+
issues.push("Iframe tags detected");
|
|
106
|
+
}
|
|
107
|
+
if (/https?:\/\//i.test(content) || /www\./i.test(content)) {
|
|
108
|
+
issues.push("URLs detected in content");
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
isSecure: issues.length === 0,
|
|
112
|
+
issues
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function escapeHtml(text) {
|
|
116
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
117
|
+
}
|
|
118
|
+
function escapeAttr(text) {
|
|
119
|
+
return escapeHtml(text).replace(/'/g, "'");
|
|
120
|
+
}
|
|
121
|
+
function humaniseContributorId(contributorId) {
|
|
122
|
+
return contributorId.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
|
|
123
|
+
}
|
|
124
|
+
function resolveBadgePalette(componentType, theme) {
|
|
125
|
+
const surface = validation.CARD_PALETTE[theme].cardBg;
|
|
126
|
+
const colors = validation.COMPONENT_COLORS[componentType];
|
|
127
|
+
if (!colors) {
|
|
128
|
+
return {
|
|
129
|
+
badgeBg: surface,
|
|
130
|
+
badgeText: validation.PRIMARY_HEX,
|
|
131
|
+
badgeBorder: validation.PRIMARY_HEX,
|
|
132
|
+
topBorder: validation.PRIMARY_HEX
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const text = oklchToHex(colors.text);
|
|
136
|
+
return {
|
|
137
|
+
badgeBg: oklchToHex(colors.bg, surface),
|
|
138
|
+
badgeText: text,
|
|
139
|
+
badgeBorder: oklchToHex(colors.border, surface),
|
|
140
|
+
topBorder: text
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function renderAuthorHtml(author, mutedColor) {
|
|
144
|
+
let label = "";
|
|
145
|
+
if (author.authorName && author.authorName.trim()) {
|
|
146
|
+
const name = escapeHtml(author.authorName.trim());
|
|
147
|
+
label = author.authorSlug ? `By <a href="https://skhema.com/contributors/${encodeURIComponent(
|
|
148
|
+
author.authorSlug
|
|
149
|
+
)}" style="color:${mutedColor};text-decoration:none;" target="_blank" rel="noopener noreferrer">${name}</a>` : `By ${name}`;
|
|
150
|
+
} else if (author.contributorId && author.contributorId.trim()) {
|
|
151
|
+
label = `By ${escapeHtml(humaniseContributorId(author.contributorId.trim()))}`;
|
|
152
|
+
}
|
|
153
|
+
if (!label) return " ";
|
|
154
|
+
return `<span style="display:inline-block;width:14px;height:14px;vertical-align:middle;color:${mutedColor};">${validation.USER_ICON_SVG}</span><span style="vertical-align:middle;padding-left:6px;">${label}</span>`;
|
|
155
|
+
}
|
|
156
|
+
function renderFooter(saveUrl, author, theme) {
|
|
157
|
+
const p = validation.CARD_PALETTE[theme];
|
|
158
|
+
return `<tr><td style="padding:12px 16px;border-top:1px solid ${p.border};"><table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;"><tr><td style="vertical-align:middle;"><span style="font-size:12px;color:${p.textMuted};">${renderAuthorHtml(
|
|
159
|
+
author,
|
|
160
|
+
p.textMuted
|
|
161
|
+
)}</span></td><td style="text-align:right;vertical-align:middle;white-space:nowrap;"><a href="${escapeAttr(saveUrl)}" class="skhema-save-btn" target="_blank" rel="noopener noreferrer" title="Save this to Skhema" style="display:inline-block;background:${validation.PRIMARY_HEX};color:#ffffff;font-size:12px;font-weight:500;text-decoration:none;padding:6px 14px;border-radius:${validation.CARD_RADIUS};white-space:nowrap;">Save to Skhema →</a></td></tr></table><div style="font-size:11px;line-height:1.4;color:${p.textMuted};margin-top:8px;">Strategy powered by <a href="https://skhema.com" style="color:${p.textMuted};text-decoration:underline;" target="_blank" rel="noopener noreferrer">Skhema</a></div></td></tr>`;
|
|
162
|
+
}
|
|
163
|
+
function renderHeader(badge, acronym, typeLabel, labelColor, theme, title) {
|
|
164
|
+
const p = validation.CARD_PALETTE[theme];
|
|
165
|
+
const titleHtml = title ? `<span style="color:${p.textMuted};"> — </span><span style="font-weight:600;color:${p.text};">${escapeHtml(
|
|
166
|
+
title
|
|
167
|
+
)}</span>` : "";
|
|
168
|
+
return `<tr><td style="padding:12px 16px;border-bottom:1px solid ${p.border};"><table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;"><tr><td style="width:1%;white-space:nowrap;padding-right:8px;vertical-align:middle;"><span class="skhema-acronym-badge" title="${escapeAttr(typeLabel)}" style="display:inline-block;font-family:${validation.MONO_STACK};font-size:10px;font-weight:600;letter-spacing:0.02em;padding:2px 6px;border-radius:2px;background:${badge.badgeBg};color:${badge.badgeText};border:1px solid ${badge.badgeBorder};">${escapeHtml(
|
|
169
|
+
acronym
|
|
170
|
+
)}</span></td><td style="vertical-align:middle;"><span style="font-size:13px;font-weight:500;color:${labelColor};">${escapeHtml(
|
|
171
|
+
typeLabel
|
|
172
|
+
)}${titleHtml}</span></td></tr></table></td></tr>`;
|
|
173
|
+
}
|
|
174
|
+
function cardOpen(theme, kind) {
|
|
175
|
+
const p = validation.CARD_PALETTE[theme];
|
|
176
|
+
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" class="skhema-card" data-skhema-kind="${kind}" data-theme="${theme}" style="width:100%;max-width:600px;border-collapse:separate;background:${p.cardBg};border:1px solid ${p.border};border-radius:${validation.CARD_RADIUS};box-shadow:${validation.CARD_SHADOW};overflow:hidden;margin:8px 0;font-family:${validation.FONT_STACK};line-height:1.5;color:${p.text};">`;
|
|
177
|
+
}
|
|
178
|
+
function renderElementCardHtml(data) {
|
|
179
|
+
const theme = data.theme ?? "light";
|
|
180
|
+
const p = validation.CARD_PALETTE[theme];
|
|
181
|
+
const label = validation.getElementTypeLabel(data.elementType);
|
|
182
|
+
const componentType = validation.resolveComponentType(data.elementType);
|
|
183
|
+
const acronym = validation.getComponentTypeAcronym(componentType);
|
|
184
|
+
const badge = resolveBadgePalette(componentType, theme);
|
|
185
|
+
return cardOpen(theme, "element") + renderHeader(badge, acronym, label, p.text, theme) + `<tr><td style="padding:16px;"><div style="font-size:15px;line-height:1.6;color:${p.text};word-wrap:break-word;overflow-wrap:break-word;">${sanitizeContent(
|
|
186
|
+
data.content
|
|
187
|
+
)}</div></td></tr>` + renderFooter(data.saveUrl, data, theme) + `</table>`;
|
|
188
|
+
}
|
|
189
|
+
function renderComponentCardHtml(data) {
|
|
190
|
+
const theme = data.theme ?? "light";
|
|
191
|
+
const p = validation.CARD_PALETTE[theme];
|
|
192
|
+
const label = validation.getComponentTypeLabel(data.componentType);
|
|
193
|
+
const acronym = validation.getComponentTypeAcronym(data.componentType);
|
|
194
|
+
const badge = resolveBadgePalette(data.componentType, theme);
|
|
195
|
+
const groups = /* @__PURE__ */ new Map();
|
|
196
|
+
for (const el of data.elements) {
|
|
197
|
+
const existing = groups.get(el.elementType);
|
|
198
|
+
if (existing) existing.push(el.content);
|
|
199
|
+
else groups.set(el.elementType, [el.content]);
|
|
200
|
+
}
|
|
201
|
+
const groupsHtml = Array.from(groups.entries()).map(
|
|
202
|
+
([elementType, contents]) => `<div style="margin-bottom:16px;"><div style="text-transform:uppercase;letter-spacing:0.05em;font-family:${validation.MONO_STACK};font-size:10px;font-weight:600;color:${p.textMuted};margin:0 0 4px;">${escapeHtml(
|
|
203
|
+
validation.getElementTypeLabel(elementType)
|
|
204
|
+
)}</div>` + contents.map(
|
|
205
|
+
(content) => `<div style="font-size:14px;line-height:1.6;color:${p.text};padding-left:8px;border-left:2px solid ${p.border};word-wrap:break-word;overflow-wrap:break-word;">${sanitizeContent(
|
|
206
|
+
content
|
|
207
|
+
)}</div>`
|
|
208
|
+
).join("") + `</div>`
|
|
209
|
+
).join("");
|
|
210
|
+
return cardOpen(theme, "component") + `<tr><td style="height:2px;line-height:2px;font-size:0;background:${badge.topBorder};"> </td></tr>` + renderHeader(badge, acronym, label, p.textMuted, theme, data.title) + `<tr><td style="padding:16px;">${groupsHtml}</td></tr>` + renderFooter(data.saveUrl, data, theme) + `</table>`;
|
|
211
|
+
}
|
|
212
|
+
exports.renderComponentCardHtml = renderComponentCardHtml;
|
|
213
|
+
exports.renderElementCardHtml = renderElementCardHtml;
|
|
214
|
+
exports.validateContentSecurity = validateContentSecurity;
|
|
215
|
+
//# sourceMappingURL=index-DBeyneZT.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-DBeyneZT.cjs","sources":["../src/utils/color.ts","../src/utils/sanitization.ts","../src/render/index.ts"],"sourcesContent":["/**\n * DOM-free colour utilities for the email-safe card renderer.\n *\n * The card palette (`COMPONENT_COLORS`) is authored in `oklch()` for the live\n * browser embed, but email clients can't parse `oklch()` and `<style>` is\n * stripped, so every colour must be a pre-computed sRGB hex with all styles\n * inlined. This converts a single `oklch(...)` token to hex, compositing any\n * alpha over a flat background (white for light cards, the dark card surface\n * for dark cards) so the result is opaque and email-safe.\n *\n * This is the single source of the conversion that `sk comms curated` and the\n * `CuratedElements` email template previously duplicated.\n */\n\n/** Brand pink (hsl(344 57% 54%)) — fallback when a component palette is missing. */\nexport const BRAND_PINK = '#cd476a'\n\nfunction hexToRgb(hex: string): [number, number, number] {\n const h = hex.replace('#', '')\n return [\n parseInt(h.slice(0, 2), 16),\n parseInt(h.slice(2, 4), 16),\n parseInt(h.slice(4, 6), 16),\n ]\n}\n\n/**\n * Convert a single `oklch(...)` token to an email-safe sRGB hex. When the token\n * carries an alpha (e.g. the badge `bg` @ 0.15 / `border` @ 0.3), the colour is\n * composited over `overHex` so the result is a flat opaque hex.\n *\n * @param token An `oklch(L C H)` or `oklch(L C H / A)` string.\n * @param overHex The flat background to composite alpha over (default white).\n */\nexport function oklchToHex(token: string, overHex = '#ffffff'): string {\n const m = token.match(\n /oklch\\(\\s*([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s*(?:\\/\\s*([\\d.]+)\\s*)?\\)/\n )\n if (!m) return BRAND_PINK\n const L = parseFloat(m[1])\n const C = parseFloat(m[2])\n const H = parseFloat(m[3])\n const alpha = m[4] !== undefined ? parseFloat(m[4]) : 1\n const h = (H * Math.PI) / 180\n const a = C * Math.cos(h)\n const b = C * Math.sin(h)\n const l_ = L + 0.3963377774 * a + 0.2158037573 * b\n const m_ = L - 0.1055613458 * a - 0.0638541728 * b\n const s_ = L - 0.0894841775 * a - 1.291485548 * b\n const lr = l_ ** 3\n const mr = m_ ** 3\n const sr = s_ ** 3\n const R = 4.0767416621 * lr - 3.3077115913 * mr + 0.2309699292 * sr\n const G = -1.2684380046 * lr + 2.6097574011 * mr - 0.3413193965 * sr\n const B = -0.0041960863 * lr - 0.7034186147 * mr + 1.707614701 * sr\n const gamma = (x: number): number => {\n const c = Math.max(0, Math.min(1, x))\n return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055\n }\n let rgb = [gamma(R) * 255, gamma(G) * 255, gamma(B) * 255]\n if (alpha < 1) {\n const [br, bg, bb] = hexToRgb(overHex)\n rgb = [\n rgb[0] * alpha + br * (1 - alpha),\n rgb[1] * alpha + bg * (1 - alpha),\n rgb[2] * alpha + bb * (1 - alpha),\n ]\n }\n return (\n '#' + rgb.map((c) => Math.round(c).toString(16).padStart(2, '0')).join('')\n )\n}\n","/**\n * Content sanitization utilities for Skhema cards.\n *\n * DOM-free by design: `sanitizeContent` is imported by the email-safe card\n * renderer (`@skhema/embed/render`), which must run in Node / email / CLI\n * runtimes with no `document`. Keep every export here free of DOM access at\n * call time (the legacy `stripHtml` below is the one exception and is not used\n * by the renderer).\n */\n\n/**\n * HTML entity encoding for basic XSS protection. Regex-based (no DOM) so it is\n * safe in non-browser runtimes. Escapes the text-context entities plus quotes.\n */\nfunction htmlEncode(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n}\n\n/**\n * Sanitizes content to prevent XSS attacks and removes URLs\n * @param content The raw content to sanitize\n * @returns Sanitized HTML-safe content with URLs removed\n */\nexport function sanitizeContent(content: string): string {\n // Strip URLs first\n let sanitized = stripUrls(content)\n\n // Encode all HTML entities to prevent script injection\n sanitized = htmlEncode(sanitized)\n\n // Preserve line breaks for readability\n sanitized = sanitized.replace(/\\n/g, '<br>')\n\n // Apply text wrapping rules for long text\n sanitized = applyTextWrapping(sanitized)\n\n return sanitized\n}\n\n/**\n * Strips all URLs from the content\n * @param text The text containing potential URLs\n * @returns Text with all URLs removed\n */\nfunction stripUrls(text: string): string {\n // Comprehensive URL patterns to remove\n const patterns = [\n // Standard URLs with protocols\n /https?:\\/\\/[^\\s<>\"{}|\\\\^`[\\]]+/gi,\n // FTP URLs\n /ftp:\\/\\/[^\\s<>\"{}|\\\\^`[\\]]+/gi,\n // URLs without protocol but with www\n /www\\.[^\\s<>\"{}|\\\\^`[\\]]+/gi,\n // Email-like patterns\n /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/gi,\n // Common domain patterns (anything.com, anything.org, etc.)\n /(?:^|\\s)([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}(?:\\/[^\\s]*)?/gi,\n ]\n\n let stripped = text\n patterns.forEach((pattern) => {\n stripped = stripped.replace(pattern, '')\n })\n\n // Clean up any multiple spaces left after URL removal\n stripped = stripped.replace(/\\s+/g, ' ').trim()\n\n return stripped\n}\n\n/**\n * Applies intelligent text wrapping to prevent layout breaking\n * @param text The text to apply wrapping rules to\n * @returns Text with appropriate wrapping hints\n */\nfunction applyTextWrapping(text: string): string {\n // Split text into words\n const words = text.split(/(\\s+)/)\n\n return words\n .map((word) => {\n // Skip if it's whitespace or already contains HTML\n if (/^\\s+$/.test(word) || word.includes('<')) {\n return word\n }\n\n // For very long words (>30 chars), add word-break opportunities\n if (word.length > 30) {\n // Insert zero-width spaces every 10 characters for breaking\n return word.replace(/(.{10})/g, '$1\\u200B')\n }\n\n return word\n })\n .join('')\n}\n\n/**\n * Validates if content contains potentially malicious patterns or URLs\n * @param content The content to validate\n * @returns Object with validation status and detected issues\n */\nexport function validateContentSecurity(content: string): {\n isSecure: boolean\n issues: string[]\n} {\n const issues: string[] = []\n\n // Check for script tags\n if (/<script[\\s>]/i.test(content)) {\n issues.push('Script tags detected')\n }\n\n // Check for event handlers\n if (/on\\w+\\s*=/i.test(content)) {\n issues.push('Event handlers detected')\n }\n\n // Check for javascript: protocol\n if (/javascript:/i.test(content)) {\n issues.push('JavaScript protocol detected')\n }\n\n // Check for data: URLs that could contain scripts\n if (/data:[^,]*script/i.test(content)) {\n issues.push('Data URL with script detected')\n }\n\n // Check for iframe tags\n if (/<iframe[\\s>]/i.test(content)) {\n issues.push('Iframe tags detected')\n }\n\n // Check for URLs (since we want to disallow them)\n if (/https?:\\/\\//i.test(content) || /www\\./i.test(content)) {\n issues.push('URLs detected in content')\n }\n\n return {\n isSecure: issues.length === 0,\n issues,\n }\n}\n\n/**\n * Strips all HTML tags from content\n * @param content The content to strip\n * @returns Plain text content\n */\nexport function stripHtml(content: string): string {\n const div = document.createElement('div')\n div.innerHTML = content\n return div.textContent || div.innerText || ''\n}\n\n/**\n * Checks if content contains any URLs\n * @param content The content to check\n * @returns True if URLs are found\n */\nexport function containsUrls(content: string): boolean {\n const urlPatterns = [\n /https?:\\/\\//i,\n /ftp:\\/\\//i,\n /www\\./i,\n /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/i,\n /(?:^|\\s)([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}(?:\\/[^\\s]*)?/i,\n ]\n\n return urlPatterns.some((pattern) => pattern.test(content))\n}\n","/**\n * `@skhema/embed/render` — the canonical, DOM-free source of truth for the\n * official Skhema element / component card HTML.\n *\n * `renderElementCardHtml(data)` / `renderComponentCardHtml(data)` return\n * EMAIL-SAFE HTML: a `role=\"presentation\"` table layout with every style\n * inlined as flat hex (no shadow DOM, no `<style>`, no `oklch()`, no CSS vars).\n * The same output is used three ways:\n *\n * 1. the live browser embed (`SkhemaElement` / `SkhemaComponent` inject it\n * into shadow DOM and layer hover/transition CSS on top);\n * 2. the `CuratedElements` email template (injected verbatim); and\n * 3. any third-party / contributor email generator.\n *\n * The module is pure — importing it never touches the DOM, so it is safe in\n * Node, edge, and email build runtimes. It builds NO URLs: callers pass the\n * fully-formed `saveUrl` (e.g. the `/save` handoff) so each surface controls\n * its own UTM tagging.\n */\nimport type { ElementValue } from '@skhema/types'\nimport {\n CARD_PALETTE,\n CARD_RADIUS,\n CARD_SHADOW,\n COMPONENT_COLORS,\n FONT_STACK,\n MONO_STACK,\n PRIMARY_HEX,\n USER_ICON_SVG,\n type CardTheme,\n} from '../styles/design-tokens.js'\nimport { oklchToHex } from '../utils/color.js'\nimport {\n getComponentTypeAcronym,\n getComponentTypeLabel,\n resolveComponentType,\n} from '../utils/component-validation.js'\nimport { sanitizeContent } from '../utils/sanitization.js'\nimport { getElementTypeLabel } from '../utils/validation.js'\n\n/* ------------------------------------------------------------------ *\n * Public data shapes (documented contract — see README \"render\") *\n * ------------------------------------------------------------------ */\n\n/** Card theme. Email is always `'light'`; the browser embed passes the\n * detected page theme. Defaults to `'light'` when omitted. */\nexport type { CardTheme }\n\n/** Author attribution shared by both card kinds. */\ninterface AuthorFields {\n /** Display name. When omitted, falls back to a humanised `contributorId`. */\n authorName?: string | null\n /** Public contributor slug — when present, the name links to the profile. */\n authorSlug?: string | null\n /** Contributor id, used only for the name fallback when `authorName` is unset. */\n contributorId?: string | null\n}\n\n/** Input for {@link renderElementCardHtml}. */\nexport interface ElementCardData extends AuthorFields {\n /** Skhema element type value, e.g. `\"key_challenge\"`. */\n elementType: string\n /** The element content / premise (plain text; sanitised + escaped here). */\n content: string\n /** Pre-built handoff URL for the \"Save to Skhema\" button. */\n saveUrl: string\n /** Card theme (default `'light'`). */\n theme?: CardTheme\n}\n\n/** A single element row inside a component card. */\nexport interface ComponentCardElement {\n elementType: string\n content: string\n}\n\n/** Input for {@link renderComponentCardHtml}. */\nexport interface ComponentCardData extends AuthorFields {\n /** Skhema component type value, e.g. `\"diagnosis\"`. */\n componentType: string\n /** Optional component title shown after a \"—\" separator in the header. */\n title?: string | null\n /** The component's elements, in display order. */\n elements: ComponentCardElement[]\n /** Pre-built handoff URL for the \"Save to Skhema\" button. */\n saveUrl: string\n /** Card theme (default `'light'`). */\n theme?: CardTheme\n}\n\n/* ------------------------------------------------------------------ *\n * Internal helpers *\n * ------------------------------------------------------------------ */\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n}\n\nfunction escapeAttr(text: string): string {\n return escapeHtml(text).replace(/'/g, ''')\n}\n\nfunction humaniseContributorId(contributorId: string): string {\n return contributorId\n .split(/[_-]/)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ')\n}\n\ninterface BadgePalette {\n badgeBg: string\n badgeText: string\n badgeBorder: string\n topBorder: string\n}\n\n/** Resolve the email-safe badge / top-border hex for a component type + theme. */\nfunction resolveBadgePalette(\n componentType: string,\n theme: CardTheme\n): BadgePalette {\n const surface = CARD_PALETTE[theme].cardBg\n const colors =\n COMPONENT_COLORS[componentType as keyof typeof COMPONENT_COLORS]\n if (!colors) {\n return {\n badgeBg: surface,\n badgeText: PRIMARY_HEX,\n badgeBorder: PRIMARY_HEX,\n topBorder: PRIMARY_HEX,\n }\n }\n const text = oklchToHex(colors.text)\n return {\n badgeBg: oklchToHex(colors.bg, surface),\n badgeText: text,\n badgeBorder: oklchToHex(colors.border, surface),\n topBorder: text,\n }\n}\n\n/** Contributor line inner HTML: person icon + \"By {author}\" (linked if slug). */\nfunction renderAuthorHtml(author: AuthorFields, mutedColor: string): string {\n let label = ''\n if (author.authorName && author.authorName.trim()) {\n const name = escapeHtml(author.authorName.trim())\n label = author.authorSlug\n ? `By <a href=\"https://skhema.com/contributors/${encodeURIComponent(\n author.authorSlug\n )}\" style=\"color:${mutedColor};text-decoration:none;\" target=\"_blank\" rel=\"noopener noreferrer\">${name}</a>`\n : `By ${name}`\n } else if (author.contributorId && author.contributorId.trim()) {\n label = `By ${escapeHtml(humaniseContributorId(author.contributorId.trim()))}`\n }\n\n if (!label) return ' '\n\n return (\n `<span style=\"display:inline-block;width:14px;height:14px;vertical-align:middle;color:${mutedColor};\">${USER_ICON_SVG}</span>` +\n `<span style=\"vertical-align:middle;padding-left:6px;\">${label}</span>`\n )\n}\n\n/** The shared footer (contributor line + save button + attribution). */\nfunction renderFooter(\n saveUrl: string,\n author: AuthorFields,\n theme: CardTheme\n): string {\n const p = CARD_PALETTE[theme]\n return (\n `<tr><td style=\"padding:12px 16px;border-top:1px solid ${p.border};\">` +\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width:100%;border-collapse:collapse;\"><tr>` +\n `<td style=\"vertical-align:middle;\"><span style=\"font-size:12px;color:${p.textMuted};\">${renderAuthorHtml(\n author,\n p.textMuted\n )}</span></td>` +\n `<td style=\"text-align:right;vertical-align:middle;white-space:nowrap;\">` +\n `<a href=\"${escapeAttr(saveUrl)}\" class=\"skhema-save-btn\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Save this to Skhema\" style=\"display:inline-block;background:${PRIMARY_HEX};color:#ffffff;font-size:12px;font-weight:500;text-decoration:none;padding:6px 14px;border-radius:${CARD_RADIUS};white-space:nowrap;\">Save to Skhema →</a>` +\n `</td></tr></table>` +\n `<div style=\"font-size:11px;line-height:1.4;color:${p.textMuted};margin-top:8px;\">Strategy powered by <a href=\"https://skhema.com\" style=\"color:${p.textMuted};text-decoration:underline;\" target=\"_blank\" rel=\"noopener noreferrer\">Skhema</a></div>` +\n `</td></tr>`\n )\n}\n\n/** Header row: acronym badge + type label (+ optional \"— title\" for components). */\nfunction renderHeader(\n badge: BadgePalette,\n acronym: string,\n typeLabel: string,\n labelColor: string,\n theme: CardTheme,\n title?: string | null\n): string {\n const p = CARD_PALETTE[theme]\n const titleHtml = title\n ? `<span style=\"color:${p.textMuted};\"> — </span><span style=\"font-weight:600;color:${p.text};\">${escapeHtml(\n title\n )}</span>`\n : ''\n return (\n `<tr><td style=\"padding:12px 16px;border-bottom:1px solid ${p.border};\">` +\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width:100%;border-collapse:collapse;\"><tr>` +\n `<td style=\"width:1%;white-space:nowrap;padding-right:8px;vertical-align:middle;\">` +\n `<span class=\"skhema-acronym-badge\" title=\"${escapeAttr(typeLabel)}\" style=\"display:inline-block;font-family:${MONO_STACK};font-size:10px;font-weight:600;letter-spacing:0.02em;padding:2px 6px;border-radius:2px;background:${badge.badgeBg};color:${badge.badgeText};border:1px solid ${badge.badgeBorder};\">${escapeHtml(\n acronym\n )}</span></td>` +\n `<td style=\"vertical-align:middle;\"><span style=\"font-size:13px;font-weight:500;color:${labelColor};\">${escapeHtml(\n typeLabel\n )}${titleHtml}</span></td>` +\n `</tr></table></td></tr>`\n )\n}\n\n/** Open the outer card table with inline card styling for the theme. */\nfunction cardOpen(theme: CardTheme, kind: 'element' | 'component'): string {\n const p = CARD_PALETTE[theme]\n return (\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" class=\"skhema-card\" data-skhema-kind=\"${kind}\" data-theme=\"${theme}\" ` +\n `style=\"width:100%;max-width:600px;border-collapse:separate;background:${p.cardBg};border:1px solid ${p.border};border-radius:${CARD_RADIUS};box-shadow:${CARD_SHADOW};overflow:hidden;margin:8px 0;font-family:${FONT_STACK};line-height:1.5;color:${p.text};\">`\n )\n}\n\n/* ------------------------------------------------------------------ *\n * Public renderers *\n * ------------------------------------------------------------------ */\n\n/**\n * Render the official Skhema **element** card as email-safe HTML.\n * The badge acronym + palette resolve from the element's owning component type,\n * exactly like the live `<skhema-element>` embed.\n */\nexport function renderElementCardHtml(data: ElementCardData): string {\n const theme = data.theme ?? 'light'\n const p = CARD_PALETTE[theme]\n const label = getElementTypeLabel(data.elementType as ElementValue)\n const componentType = resolveComponentType(data.elementType)\n const acronym = getComponentTypeAcronym(componentType)\n const badge = resolveBadgePalette(componentType, theme)\n\n return (\n cardOpen(theme, 'element') +\n renderHeader(badge, acronym, label, p.text, theme) +\n `<tr><td style=\"padding:16px;\">` +\n `<div style=\"font-size:15px;line-height:1.6;color:${p.text};word-wrap:break-word;overflow-wrap:break-word;\">${sanitizeContent(\n data.content\n )}</div>` +\n `</td></tr>` +\n renderFooter(data.saveUrl, data, theme) +\n `</table>`\n )\n}\n\n/**\n * Render the official Skhema **component** card as email-safe HTML — a coloured\n * top bar, header (badge + type label + optional title), per-element-type\n * groups, and the shared footer.\n */\nexport function renderComponentCardHtml(data: ComponentCardData): string {\n const theme = data.theme ?? 'light'\n const p = CARD_PALETTE[theme]\n const label = getComponentTypeLabel(data.componentType)\n const acronym = getComponentTypeAcronym(data.componentType)\n const badge = resolveBadgePalette(data.componentType, theme)\n\n // Group elements by type (preserving first-occurrence order), mirroring the\n // live `<skhema-component>` body: one small-caps label per type, with each\n // element's content as a left-ruled block beneath it.\n const groups = new Map<string, string[]>()\n for (const el of data.elements) {\n const existing = groups.get(el.elementType)\n if (existing) existing.push(el.content)\n else groups.set(el.elementType, [el.content])\n }\n\n const groupsHtml = Array.from(groups.entries())\n .map(\n ([elementType, contents]) =>\n `<div style=\"margin-bottom:16px;\">` +\n `<div style=\"text-transform:uppercase;letter-spacing:0.05em;font-family:${MONO_STACK};font-size:10px;font-weight:600;color:${p.textMuted};margin:0 0 4px;\">${escapeHtml(\n getElementTypeLabel(elementType as ElementValue)\n )}</div>` +\n contents\n .map(\n (content) =>\n `<div style=\"font-size:14px;line-height:1.6;color:${p.text};padding-left:8px;border-left:2px solid ${p.border};word-wrap:break-word;overflow-wrap:break-word;\">${sanitizeContent(\n content\n )}</div>`\n )\n .join('') +\n `</div>`\n )\n .join('')\n\n return (\n cardOpen(theme, 'component') +\n `<tr><td style=\"height:2px;line-height:2px;font-size:0;background:${badge.topBorder};\"> </td></tr>` +\n renderHeader(badge, acronym, label, p.textMuted, theme, data.title) +\n `<tr><td style=\"padding:16px;\">${groupsHtml}</td></tr>` +\n renderFooter(data.saveUrl, data, theme) +\n `</table>`\n )\n}\n"],"names":["CARD_PALETTE","COMPONENT_COLORS","PRIMARY_HEX","USER_ICON_SVG","CARD_RADIUS","MONO_STACK","CARD_SHADOW","FONT_STACK","getElementTypeLabel","resolveComponentType","getComponentTypeAcronym","getComponentTypeLabel"],"mappings":"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"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { d as getElementTypeLabel, r as resolveComponentType, g as getComponentTypeAcronym, h as CARD_PALETTE, b as getComponentTypeLabel, M as MONO_STACK, P as PRIMARY_HEX, a as COMPONENT_COLORS, j as CARD_RADIUS, k as CARD_SHADOW, F as FONT_STACK, U as USER_ICON_SVG } from "./validation-Dt4L_df8.js";
|
|
2
|
+
const BRAND_PINK = "#cd476a";
|
|
3
|
+
function hexToRgb(hex) {
|
|
4
|
+
const h = hex.replace("#", "");
|
|
5
|
+
return [
|
|
6
|
+
parseInt(h.slice(0, 2), 16),
|
|
7
|
+
parseInt(h.slice(2, 4), 16),
|
|
8
|
+
parseInt(h.slice(4, 6), 16)
|
|
9
|
+
];
|
|
10
|
+
}
|
|
11
|
+
function oklchToHex(token, overHex = "#ffffff") {
|
|
12
|
+
const m = token.match(
|
|
13
|
+
/oklch\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*(?:\/\s*([\d.]+)\s*)?\)/
|
|
14
|
+
);
|
|
15
|
+
if (!m) return BRAND_PINK;
|
|
16
|
+
const L = parseFloat(m[1]);
|
|
17
|
+
const C = parseFloat(m[2]);
|
|
18
|
+
const H = parseFloat(m[3]);
|
|
19
|
+
const alpha = m[4] !== void 0 ? parseFloat(m[4]) : 1;
|
|
20
|
+
const h = H * Math.PI / 180;
|
|
21
|
+
const a = C * Math.cos(h);
|
|
22
|
+
const b = C * Math.sin(h);
|
|
23
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
24
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
25
|
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b;
|
|
26
|
+
const lr = l_ ** 3;
|
|
27
|
+
const mr = m_ ** 3;
|
|
28
|
+
const sr = s_ ** 3;
|
|
29
|
+
const R = 4.0767416621 * lr - 3.3077115913 * mr + 0.2309699292 * sr;
|
|
30
|
+
const G = -1.2684380046 * lr + 2.6097574011 * mr - 0.3413193965 * sr;
|
|
31
|
+
const B = -0.0041960863 * lr - 0.7034186147 * mr + 1.707614701 * sr;
|
|
32
|
+
const gamma = (x) => {
|
|
33
|
+
const c = Math.max(0, Math.min(1, x));
|
|
34
|
+
return c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
35
|
+
};
|
|
36
|
+
let rgb = [gamma(R) * 255, gamma(G) * 255, gamma(B) * 255];
|
|
37
|
+
if (alpha < 1) {
|
|
38
|
+
const [br, bg, bb] = hexToRgb(overHex);
|
|
39
|
+
rgb = [
|
|
40
|
+
rgb[0] * alpha + br * (1 - alpha),
|
|
41
|
+
rgb[1] * alpha + bg * (1 - alpha),
|
|
42
|
+
rgb[2] * alpha + bb * (1 - alpha)
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
return "#" + rgb.map((c) => Math.round(c).toString(16).padStart(2, "0")).join("");
|
|
46
|
+
}
|
|
47
|
+
function htmlEncode(str) {
|
|
48
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
49
|
+
}
|
|
50
|
+
function sanitizeContent(content) {
|
|
51
|
+
let sanitized = stripUrls(content);
|
|
52
|
+
sanitized = htmlEncode(sanitized);
|
|
53
|
+
sanitized = sanitized.replace(/\n/g, "<br>");
|
|
54
|
+
sanitized = applyTextWrapping(sanitized);
|
|
55
|
+
return sanitized;
|
|
56
|
+
}
|
|
57
|
+
function stripUrls(text) {
|
|
58
|
+
const patterns = [
|
|
59
|
+
// Standard URLs with protocols
|
|
60
|
+
/https?:\/\/[^\s<>"{}|\\^`[\]]+/gi,
|
|
61
|
+
// FTP URLs
|
|
62
|
+
/ftp:\/\/[^\s<>"{}|\\^`[\]]+/gi,
|
|
63
|
+
// URLs without protocol but with www
|
|
64
|
+
/www\.[^\s<>"{}|\\^`[\]]+/gi,
|
|
65
|
+
// Email-like patterns
|
|
66
|
+
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/gi,
|
|
67
|
+
// Common domain patterns (anything.com, anything.org, etc.)
|
|
68
|
+
/(?:^|\s)([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/[^\s]*)?/gi
|
|
69
|
+
];
|
|
70
|
+
let stripped = text;
|
|
71
|
+
patterns.forEach((pattern) => {
|
|
72
|
+
stripped = stripped.replace(pattern, "");
|
|
73
|
+
});
|
|
74
|
+
stripped = stripped.replace(/\s+/g, " ").trim();
|
|
75
|
+
return stripped;
|
|
76
|
+
}
|
|
77
|
+
function applyTextWrapping(text) {
|
|
78
|
+
const words = text.split(/(\s+)/);
|
|
79
|
+
return words.map((word) => {
|
|
80
|
+
if (/^\s+$/.test(word) || word.includes("<")) {
|
|
81
|
+
return word;
|
|
82
|
+
}
|
|
83
|
+
if (word.length > 30) {
|
|
84
|
+
return word.replace(/(.{10})/g, "$1");
|
|
85
|
+
}
|
|
86
|
+
return word;
|
|
87
|
+
}).join("");
|
|
88
|
+
}
|
|
89
|
+
function validateContentSecurity(content) {
|
|
90
|
+
const issues = [];
|
|
91
|
+
if (/<script[\s>]/i.test(content)) {
|
|
92
|
+
issues.push("Script tags detected");
|
|
93
|
+
}
|
|
94
|
+
if (/on\w+\s*=/i.test(content)) {
|
|
95
|
+
issues.push("Event handlers detected");
|
|
96
|
+
}
|
|
97
|
+
if (/javascript:/i.test(content)) {
|
|
98
|
+
issues.push("JavaScript protocol detected");
|
|
99
|
+
}
|
|
100
|
+
if (/data:[^,]*script/i.test(content)) {
|
|
101
|
+
issues.push("Data URL with script detected");
|
|
102
|
+
}
|
|
103
|
+
if (/<iframe[\s>]/i.test(content)) {
|
|
104
|
+
issues.push("Iframe tags detected");
|
|
105
|
+
}
|
|
106
|
+
if (/https?:\/\//i.test(content) || /www\./i.test(content)) {
|
|
107
|
+
issues.push("URLs detected in content");
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
isSecure: issues.length === 0,
|
|
111
|
+
issues
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function escapeHtml(text) {
|
|
115
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
116
|
+
}
|
|
117
|
+
function escapeAttr(text) {
|
|
118
|
+
return escapeHtml(text).replace(/'/g, "'");
|
|
119
|
+
}
|
|
120
|
+
function humaniseContributorId(contributorId) {
|
|
121
|
+
return contributorId.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
|
|
122
|
+
}
|
|
123
|
+
function resolveBadgePalette(componentType, theme) {
|
|
124
|
+
const surface = CARD_PALETTE[theme].cardBg;
|
|
125
|
+
const colors = COMPONENT_COLORS[componentType];
|
|
126
|
+
if (!colors) {
|
|
127
|
+
return {
|
|
128
|
+
badgeBg: surface,
|
|
129
|
+
badgeText: PRIMARY_HEX,
|
|
130
|
+
badgeBorder: PRIMARY_HEX,
|
|
131
|
+
topBorder: PRIMARY_HEX
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const text = oklchToHex(colors.text);
|
|
135
|
+
return {
|
|
136
|
+
badgeBg: oklchToHex(colors.bg, surface),
|
|
137
|
+
badgeText: text,
|
|
138
|
+
badgeBorder: oklchToHex(colors.border, surface),
|
|
139
|
+
topBorder: text
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function renderAuthorHtml(author, mutedColor) {
|
|
143
|
+
let label = "";
|
|
144
|
+
if (author.authorName && author.authorName.trim()) {
|
|
145
|
+
const name = escapeHtml(author.authorName.trim());
|
|
146
|
+
label = author.authorSlug ? `By <a href="https://skhema.com/contributors/${encodeURIComponent(
|
|
147
|
+
author.authorSlug
|
|
148
|
+
)}" style="color:${mutedColor};text-decoration:none;" target="_blank" rel="noopener noreferrer">${name}</a>` : `By ${name}`;
|
|
149
|
+
} else if (author.contributorId && author.contributorId.trim()) {
|
|
150
|
+
label = `By ${escapeHtml(humaniseContributorId(author.contributorId.trim()))}`;
|
|
151
|
+
}
|
|
152
|
+
if (!label) return " ";
|
|
153
|
+
return `<span style="display:inline-block;width:14px;height:14px;vertical-align:middle;color:${mutedColor};">${USER_ICON_SVG}</span><span style="vertical-align:middle;padding-left:6px;">${label}</span>`;
|
|
154
|
+
}
|
|
155
|
+
function renderFooter(saveUrl, author, theme) {
|
|
156
|
+
const p = CARD_PALETTE[theme];
|
|
157
|
+
return `<tr><td style="padding:12px 16px;border-top:1px solid ${p.border};"><table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;"><tr><td style="vertical-align:middle;"><span style="font-size:12px;color:${p.textMuted};">${renderAuthorHtml(
|
|
158
|
+
author,
|
|
159
|
+
p.textMuted
|
|
160
|
+
)}</span></td><td style="text-align:right;vertical-align:middle;white-space:nowrap;"><a href="${escapeAttr(saveUrl)}" class="skhema-save-btn" target="_blank" rel="noopener noreferrer" title="Save this to Skhema" style="display:inline-block;background:${PRIMARY_HEX};color:#ffffff;font-size:12px;font-weight:500;text-decoration:none;padding:6px 14px;border-radius:${CARD_RADIUS};white-space:nowrap;">Save to Skhema →</a></td></tr></table><div style="font-size:11px;line-height:1.4;color:${p.textMuted};margin-top:8px;">Strategy powered by <a href="https://skhema.com" style="color:${p.textMuted};text-decoration:underline;" target="_blank" rel="noopener noreferrer">Skhema</a></div></td></tr>`;
|
|
161
|
+
}
|
|
162
|
+
function renderHeader(badge, acronym, typeLabel, labelColor, theme, title) {
|
|
163
|
+
const p = CARD_PALETTE[theme];
|
|
164
|
+
const titleHtml = title ? `<span style="color:${p.textMuted};"> — </span><span style="font-weight:600;color:${p.text};">${escapeHtml(
|
|
165
|
+
title
|
|
166
|
+
)}</span>` : "";
|
|
167
|
+
return `<tr><td style="padding:12px 16px;border-bottom:1px solid ${p.border};"><table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;"><tr><td style="width:1%;white-space:nowrap;padding-right:8px;vertical-align:middle;"><span class="skhema-acronym-badge" title="${escapeAttr(typeLabel)}" style="display:inline-block;font-family:${MONO_STACK};font-size:10px;font-weight:600;letter-spacing:0.02em;padding:2px 6px;border-radius:2px;background:${badge.badgeBg};color:${badge.badgeText};border:1px solid ${badge.badgeBorder};">${escapeHtml(
|
|
168
|
+
acronym
|
|
169
|
+
)}</span></td><td style="vertical-align:middle;"><span style="font-size:13px;font-weight:500;color:${labelColor};">${escapeHtml(
|
|
170
|
+
typeLabel
|
|
171
|
+
)}${titleHtml}</span></td></tr></table></td></tr>`;
|
|
172
|
+
}
|
|
173
|
+
function cardOpen(theme, kind) {
|
|
174
|
+
const p = CARD_PALETTE[theme];
|
|
175
|
+
return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" class="skhema-card" data-skhema-kind="${kind}" data-theme="${theme}" style="width:100%;max-width:600px;border-collapse:separate;background:${p.cardBg};border:1px solid ${p.border};border-radius:${CARD_RADIUS};box-shadow:${CARD_SHADOW};overflow:hidden;margin:8px 0;font-family:${FONT_STACK};line-height:1.5;color:${p.text};">`;
|
|
176
|
+
}
|
|
177
|
+
function renderElementCardHtml(data) {
|
|
178
|
+
const theme = data.theme ?? "light";
|
|
179
|
+
const p = CARD_PALETTE[theme];
|
|
180
|
+
const label = getElementTypeLabel(data.elementType);
|
|
181
|
+
const componentType = resolveComponentType(data.elementType);
|
|
182
|
+
const acronym = getComponentTypeAcronym(componentType);
|
|
183
|
+
const badge = resolveBadgePalette(componentType, theme);
|
|
184
|
+
return cardOpen(theme, "element") + renderHeader(badge, acronym, label, p.text, theme) + `<tr><td style="padding:16px;"><div style="font-size:15px;line-height:1.6;color:${p.text};word-wrap:break-word;overflow-wrap:break-word;">${sanitizeContent(
|
|
185
|
+
data.content
|
|
186
|
+
)}</div></td></tr>` + renderFooter(data.saveUrl, data, theme) + `</table>`;
|
|
187
|
+
}
|
|
188
|
+
function renderComponentCardHtml(data) {
|
|
189
|
+
const theme = data.theme ?? "light";
|
|
190
|
+
const p = CARD_PALETTE[theme];
|
|
191
|
+
const label = getComponentTypeLabel(data.componentType);
|
|
192
|
+
const acronym = getComponentTypeAcronym(data.componentType);
|
|
193
|
+
const badge = resolveBadgePalette(data.componentType, theme);
|
|
194
|
+
const groups = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const el of data.elements) {
|
|
196
|
+
const existing = groups.get(el.elementType);
|
|
197
|
+
if (existing) existing.push(el.content);
|
|
198
|
+
else groups.set(el.elementType, [el.content]);
|
|
199
|
+
}
|
|
200
|
+
const groupsHtml = Array.from(groups.entries()).map(
|
|
201
|
+
([elementType, contents]) => `<div style="margin-bottom:16px;"><div style="text-transform:uppercase;letter-spacing:0.05em;font-family:${MONO_STACK};font-size:10px;font-weight:600;color:${p.textMuted};margin:0 0 4px;">${escapeHtml(
|
|
202
|
+
getElementTypeLabel(elementType)
|
|
203
|
+
)}</div>` + contents.map(
|
|
204
|
+
(content) => `<div style="font-size:14px;line-height:1.6;color:${p.text};padding-left:8px;border-left:2px solid ${p.border};word-wrap:break-word;overflow-wrap:break-word;">${sanitizeContent(
|
|
205
|
+
content
|
|
206
|
+
)}</div>`
|
|
207
|
+
).join("") + `</div>`
|
|
208
|
+
).join("");
|
|
209
|
+
return cardOpen(theme, "component") + `<tr><td style="height:2px;line-height:2px;font-size:0;background:${badge.topBorder};"> </td></tr>` + renderHeader(badge, acronym, label, p.textMuted, theme, data.title) + `<tr><td style="padding:16px;">${groupsHtml}</td></tr>` + renderFooter(data.saveUrl, data, theme) + `</table>`;
|
|
210
|
+
}
|
|
211
|
+
export {
|
|
212
|
+
renderElementCardHtml as a,
|
|
213
|
+
renderComponentCardHtml as r,
|
|
214
|
+
validateContentSecurity as v
|
|
215
|
+
};
|
|
216
|
+
//# sourceMappingURL=index-jwx5CXLB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-jwx5CXLB.js","sources":["../src/utils/color.ts","../src/utils/sanitization.ts","../src/render/index.ts"],"sourcesContent":["/**\n * DOM-free colour utilities for the email-safe card renderer.\n *\n * The card palette (`COMPONENT_COLORS`) is authored in `oklch()` for the live\n * browser embed, but email clients can't parse `oklch()` and `<style>` is\n * stripped, so every colour must be a pre-computed sRGB hex with all styles\n * inlined. This converts a single `oklch(...)` token to hex, compositing any\n * alpha over a flat background (white for light cards, the dark card surface\n * for dark cards) so the result is opaque and email-safe.\n *\n * This is the single source of the conversion that `sk comms curated` and the\n * `CuratedElements` email template previously duplicated.\n */\n\n/** Brand pink (hsl(344 57% 54%)) — fallback when a component palette is missing. */\nexport const BRAND_PINK = '#cd476a'\n\nfunction hexToRgb(hex: string): [number, number, number] {\n const h = hex.replace('#', '')\n return [\n parseInt(h.slice(0, 2), 16),\n parseInt(h.slice(2, 4), 16),\n parseInt(h.slice(4, 6), 16),\n ]\n}\n\n/**\n * Convert a single `oklch(...)` token to an email-safe sRGB hex. When the token\n * carries an alpha (e.g. the badge `bg` @ 0.15 / `border` @ 0.3), the colour is\n * composited over `overHex` so the result is a flat opaque hex.\n *\n * @param token An `oklch(L C H)` or `oklch(L C H / A)` string.\n * @param overHex The flat background to composite alpha over (default white).\n */\nexport function oklchToHex(token: string, overHex = '#ffffff'): string {\n const m = token.match(\n /oklch\\(\\s*([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s*(?:\\/\\s*([\\d.]+)\\s*)?\\)/\n )\n if (!m) return BRAND_PINK\n const L = parseFloat(m[1])\n const C = parseFloat(m[2])\n const H = parseFloat(m[3])\n const alpha = m[4] !== undefined ? parseFloat(m[4]) : 1\n const h = (H * Math.PI) / 180\n const a = C * Math.cos(h)\n const b = C * Math.sin(h)\n const l_ = L + 0.3963377774 * a + 0.2158037573 * b\n const m_ = L - 0.1055613458 * a - 0.0638541728 * b\n const s_ = L - 0.0894841775 * a - 1.291485548 * b\n const lr = l_ ** 3\n const mr = m_ ** 3\n const sr = s_ ** 3\n const R = 4.0767416621 * lr - 3.3077115913 * mr + 0.2309699292 * sr\n const G = -1.2684380046 * lr + 2.6097574011 * mr - 0.3413193965 * sr\n const B = -0.0041960863 * lr - 0.7034186147 * mr + 1.707614701 * sr\n const gamma = (x: number): number => {\n const c = Math.max(0, Math.min(1, x))\n return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055\n }\n let rgb = [gamma(R) * 255, gamma(G) * 255, gamma(B) * 255]\n if (alpha < 1) {\n const [br, bg, bb] = hexToRgb(overHex)\n rgb = [\n rgb[0] * alpha + br * (1 - alpha),\n rgb[1] * alpha + bg * (1 - alpha),\n rgb[2] * alpha + bb * (1 - alpha),\n ]\n }\n return (\n '#' + rgb.map((c) => Math.round(c).toString(16).padStart(2, '0')).join('')\n )\n}\n","/**\n * Content sanitization utilities for Skhema cards.\n *\n * DOM-free by design: `sanitizeContent` is imported by the email-safe card\n * renderer (`@skhema/embed/render`), which must run in Node / email / CLI\n * runtimes with no `document`. Keep every export here free of DOM access at\n * call time (the legacy `stripHtml` below is the one exception and is not used\n * by the renderer).\n */\n\n/**\n * HTML entity encoding for basic XSS protection. Regex-based (no DOM) so it is\n * safe in non-browser runtimes. Escapes the text-context entities plus quotes.\n */\nfunction htmlEncode(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n}\n\n/**\n * Sanitizes content to prevent XSS attacks and removes URLs\n * @param content The raw content to sanitize\n * @returns Sanitized HTML-safe content with URLs removed\n */\nexport function sanitizeContent(content: string): string {\n // Strip URLs first\n let sanitized = stripUrls(content)\n\n // Encode all HTML entities to prevent script injection\n sanitized = htmlEncode(sanitized)\n\n // Preserve line breaks for readability\n sanitized = sanitized.replace(/\\n/g, '<br>')\n\n // Apply text wrapping rules for long text\n sanitized = applyTextWrapping(sanitized)\n\n return sanitized\n}\n\n/**\n * Strips all URLs from the content\n * @param text The text containing potential URLs\n * @returns Text with all URLs removed\n */\nfunction stripUrls(text: string): string {\n // Comprehensive URL patterns to remove\n const patterns = [\n // Standard URLs with protocols\n /https?:\\/\\/[^\\s<>\"{}|\\\\^`[\\]]+/gi,\n // FTP URLs\n /ftp:\\/\\/[^\\s<>\"{}|\\\\^`[\\]]+/gi,\n // URLs without protocol but with www\n /www\\.[^\\s<>\"{}|\\\\^`[\\]]+/gi,\n // Email-like patterns\n /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/gi,\n // Common domain patterns (anything.com, anything.org, etc.)\n /(?:^|\\s)([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}(?:\\/[^\\s]*)?/gi,\n ]\n\n let stripped = text\n patterns.forEach((pattern) => {\n stripped = stripped.replace(pattern, '')\n })\n\n // Clean up any multiple spaces left after URL removal\n stripped = stripped.replace(/\\s+/g, ' ').trim()\n\n return stripped\n}\n\n/**\n * Applies intelligent text wrapping to prevent layout breaking\n * @param text The text to apply wrapping rules to\n * @returns Text with appropriate wrapping hints\n */\nfunction applyTextWrapping(text: string): string {\n // Split text into words\n const words = text.split(/(\\s+)/)\n\n return words\n .map((word) => {\n // Skip if it's whitespace or already contains HTML\n if (/^\\s+$/.test(word) || word.includes('<')) {\n return word\n }\n\n // For very long words (>30 chars), add word-break opportunities\n if (word.length > 30) {\n // Insert zero-width spaces every 10 characters for breaking\n return word.replace(/(.{10})/g, '$1\\u200B')\n }\n\n return word\n })\n .join('')\n}\n\n/**\n * Validates if content contains potentially malicious patterns or URLs\n * @param content The content to validate\n * @returns Object with validation status and detected issues\n */\nexport function validateContentSecurity(content: string): {\n isSecure: boolean\n issues: string[]\n} {\n const issues: string[] = []\n\n // Check for script tags\n if (/<script[\\s>]/i.test(content)) {\n issues.push('Script tags detected')\n }\n\n // Check for event handlers\n if (/on\\w+\\s*=/i.test(content)) {\n issues.push('Event handlers detected')\n }\n\n // Check for javascript: protocol\n if (/javascript:/i.test(content)) {\n issues.push('JavaScript protocol detected')\n }\n\n // Check for data: URLs that could contain scripts\n if (/data:[^,]*script/i.test(content)) {\n issues.push('Data URL with script detected')\n }\n\n // Check for iframe tags\n if (/<iframe[\\s>]/i.test(content)) {\n issues.push('Iframe tags detected')\n }\n\n // Check for URLs (since we want to disallow them)\n if (/https?:\\/\\//i.test(content) || /www\\./i.test(content)) {\n issues.push('URLs detected in content')\n }\n\n return {\n isSecure: issues.length === 0,\n issues,\n }\n}\n\n/**\n * Strips all HTML tags from content\n * @param content The content to strip\n * @returns Plain text content\n */\nexport function stripHtml(content: string): string {\n const div = document.createElement('div')\n div.innerHTML = content\n return div.textContent || div.innerText || ''\n}\n\n/**\n * Checks if content contains any URLs\n * @param content The content to check\n * @returns True if URLs are found\n */\nexport function containsUrls(content: string): boolean {\n const urlPatterns = [\n /https?:\\/\\//i,\n /ftp:\\/\\//i,\n /www\\./i,\n /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/i,\n /(?:^|\\s)([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}(?:\\/[^\\s]*)?/i,\n ]\n\n return urlPatterns.some((pattern) => pattern.test(content))\n}\n","/**\n * `@skhema/embed/render` — the canonical, DOM-free source of truth for the\n * official Skhema element / component card HTML.\n *\n * `renderElementCardHtml(data)` / `renderComponentCardHtml(data)` return\n * EMAIL-SAFE HTML: a `role=\"presentation\"` table layout with every style\n * inlined as flat hex (no shadow DOM, no `<style>`, no `oklch()`, no CSS vars).\n * The same output is used three ways:\n *\n * 1. the live browser embed (`SkhemaElement` / `SkhemaComponent` inject it\n * into shadow DOM and layer hover/transition CSS on top);\n * 2. the `CuratedElements` email template (injected verbatim); and\n * 3. any third-party / contributor email generator.\n *\n * The module is pure — importing it never touches the DOM, so it is safe in\n * Node, edge, and email build runtimes. It builds NO URLs: callers pass the\n * fully-formed `saveUrl` (e.g. the `/save` handoff) so each surface controls\n * its own UTM tagging.\n */\nimport type { ElementValue } from '@skhema/types'\nimport {\n CARD_PALETTE,\n CARD_RADIUS,\n CARD_SHADOW,\n COMPONENT_COLORS,\n FONT_STACK,\n MONO_STACK,\n PRIMARY_HEX,\n USER_ICON_SVG,\n type CardTheme,\n} from '../styles/design-tokens.js'\nimport { oklchToHex } from '../utils/color.js'\nimport {\n getComponentTypeAcronym,\n getComponentTypeLabel,\n resolveComponentType,\n} from '../utils/component-validation.js'\nimport { sanitizeContent } from '../utils/sanitization.js'\nimport { getElementTypeLabel } from '../utils/validation.js'\n\n/* ------------------------------------------------------------------ *\n * Public data shapes (documented contract — see README \"render\") *\n * ------------------------------------------------------------------ */\n\n/** Card theme. Email is always `'light'`; the browser embed passes the\n * detected page theme. Defaults to `'light'` when omitted. */\nexport type { CardTheme }\n\n/** Author attribution shared by both card kinds. */\ninterface AuthorFields {\n /** Display name. When omitted, falls back to a humanised `contributorId`. */\n authorName?: string | null\n /** Public contributor slug — when present, the name links to the profile. */\n authorSlug?: string | null\n /** Contributor id, used only for the name fallback when `authorName` is unset. */\n contributorId?: string | null\n}\n\n/** Input for {@link renderElementCardHtml}. */\nexport interface ElementCardData extends AuthorFields {\n /** Skhema element type value, e.g. `\"key_challenge\"`. */\n elementType: string\n /** The element content / premise (plain text; sanitised + escaped here). */\n content: string\n /** Pre-built handoff URL for the \"Save to Skhema\" button. */\n saveUrl: string\n /** Card theme (default `'light'`). */\n theme?: CardTheme\n}\n\n/** A single element row inside a component card. */\nexport interface ComponentCardElement {\n elementType: string\n content: string\n}\n\n/** Input for {@link renderComponentCardHtml}. */\nexport interface ComponentCardData extends AuthorFields {\n /** Skhema component type value, e.g. `\"diagnosis\"`. */\n componentType: string\n /** Optional component title shown after a \"—\" separator in the header. */\n title?: string | null\n /** The component's elements, in display order. */\n elements: ComponentCardElement[]\n /** Pre-built handoff URL for the \"Save to Skhema\" button. */\n saveUrl: string\n /** Card theme (default `'light'`). */\n theme?: CardTheme\n}\n\n/* ------------------------------------------------------------------ *\n * Internal helpers *\n * ------------------------------------------------------------------ */\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n}\n\nfunction escapeAttr(text: string): string {\n return escapeHtml(text).replace(/'/g, ''')\n}\n\nfunction humaniseContributorId(contributorId: string): string {\n return contributorId\n .split(/[_-]/)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ')\n}\n\ninterface BadgePalette {\n badgeBg: string\n badgeText: string\n badgeBorder: string\n topBorder: string\n}\n\n/** Resolve the email-safe badge / top-border hex for a component type + theme. */\nfunction resolveBadgePalette(\n componentType: string,\n theme: CardTheme\n): BadgePalette {\n const surface = CARD_PALETTE[theme].cardBg\n const colors =\n COMPONENT_COLORS[componentType as keyof typeof COMPONENT_COLORS]\n if (!colors) {\n return {\n badgeBg: surface,\n badgeText: PRIMARY_HEX,\n badgeBorder: PRIMARY_HEX,\n topBorder: PRIMARY_HEX,\n }\n }\n const text = oklchToHex(colors.text)\n return {\n badgeBg: oklchToHex(colors.bg, surface),\n badgeText: text,\n badgeBorder: oklchToHex(colors.border, surface),\n topBorder: text,\n }\n}\n\n/** Contributor line inner HTML: person icon + \"By {author}\" (linked if slug). */\nfunction renderAuthorHtml(author: AuthorFields, mutedColor: string): string {\n let label = ''\n if (author.authorName && author.authorName.trim()) {\n const name = escapeHtml(author.authorName.trim())\n label = author.authorSlug\n ? `By <a href=\"https://skhema.com/contributors/${encodeURIComponent(\n author.authorSlug\n )}\" style=\"color:${mutedColor};text-decoration:none;\" target=\"_blank\" rel=\"noopener noreferrer\">${name}</a>`\n : `By ${name}`\n } else if (author.contributorId && author.contributorId.trim()) {\n label = `By ${escapeHtml(humaniseContributorId(author.contributorId.trim()))}`\n }\n\n if (!label) return ' '\n\n return (\n `<span style=\"display:inline-block;width:14px;height:14px;vertical-align:middle;color:${mutedColor};\">${USER_ICON_SVG}</span>` +\n `<span style=\"vertical-align:middle;padding-left:6px;\">${label}</span>`\n )\n}\n\n/** The shared footer (contributor line + save button + attribution). */\nfunction renderFooter(\n saveUrl: string,\n author: AuthorFields,\n theme: CardTheme\n): string {\n const p = CARD_PALETTE[theme]\n return (\n `<tr><td style=\"padding:12px 16px;border-top:1px solid ${p.border};\">` +\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width:100%;border-collapse:collapse;\"><tr>` +\n `<td style=\"vertical-align:middle;\"><span style=\"font-size:12px;color:${p.textMuted};\">${renderAuthorHtml(\n author,\n p.textMuted\n )}</span></td>` +\n `<td style=\"text-align:right;vertical-align:middle;white-space:nowrap;\">` +\n `<a href=\"${escapeAttr(saveUrl)}\" class=\"skhema-save-btn\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Save this to Skhema\" style=\"display:inline-block;background:${PRIMARY_HEX};color:#ffffff;font-size:12px;font-weight:500;text-decoration:none;padding:6px 14px;border-radius:${CARD_RADIUS};white-space:nowrap;\">Save to Skhema →</a>` +\n `</td></tr></table>` +\n `<div style=\"font-size:11px;line-height:1.4;color:${p.textMuted};margin-top:8px;\">Strategy powered by <a href=\"https://skhema.com\" style=\"color:${p.textMuted};text-decoration:underline;\" target=\"_blank\" rel=\"noopener noreferrer\">Skhema</a></div>` +\n `</td></tr>`\n )\n}\n\n/** Header row: acronym badge + type label (+ optional \"— title\" for components). */\nfunction renderHeader(\n badge: BadgePalette,\n acronym: string,\n typeLabel: string,\n labelColor: string,\n theme: CardTheme,\n title?: string | null\n): string {\n const p = CARD_PALETTE[theme]\n const titleHtml = title\n ? `<span style=\"color:${p.textMuted};\"> — </span><span style=\"font-weight:600;color:${p.text};\">${escapeHtml(\n title\n )}</span>`\n : ''\n return (\n `<tr><td style=\"padding:12px 16px;border-bottom:1px solid ${p.border};\">` +\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width:100%;border-collapse:collapse;\"><tr>` +\n `<td style=\"width:1%;white-space:nowrap;padding-right:8px;vertical-align:middle;\">` +\n `<span class=\"skhema-acronym-badge\" title=\"${escapeAttr(typeLabel)}\" style=\"display:inline-block;font-family:${MONO_STACK};font-size:10px;font-weight:600;letter-spacing:0.02em;padding:2px 6px;border-radius:2px;background:${badge.badgeBg};color:${badge.badgeText};border:1px solid ${badge.badgeBorder};\">${escapeHtml(\n acronym\n )}</span></td>` +\n `<td style=\"vertical-align:middle;\"><span style=\"font-size:13px;font-weight:500;color:${labelColor};\">${escapeHtml(\n typeLabel\n )}${titleHtml}</span></td>` +\n `</tr></table></td></tr>`\n )\n}\n\n/** Open the outer card table with inline card styling for the theme. */\nfunction cardOpen(theme: CardTheme, kind: 'element' | 'component'): string {\n const p = CARD_PALETTE[theme]\n return (\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" class=\"skhema-card\" data-skhema-kind=\"${kind}\" data-theme=\"${theme}\" ` +\n `style=\"width:100%;max-width:600px;border-collapse:separate;background:${p.cardBg};border:1px solid ${p.border};border-radius:${CARD_RADIUS};box-shadow:${CARD_SHADOW};overflow:hidden;margin:8px 0;font-family:${FONT_STACK};line-height:1.5;color:${p.text};\">`\n )\n}\n\n/* ------------------------------------------------------------------ *\n * Public renderers *\n * ------------------------------------------------------------------ */\n\n/**\n * Render the official Skhema **element** card as email-safe HTML.\n * The badge acronym + palette resolve from the element's owning component type,\n * exactly like the live `<skhema-element>` embed.\n */\nexport function renderElementCardHtml(data: ElementCardData): string {\n const theme = data.theme ?? 'light'\n const p = CARD_PALETTE[theme]\n const label = getElementTypeLabel(data.elementType as ElementValue)\n const componentType = resolveComponentType(data.elementType)\n const acronym = getComponentTypeAcronym(componentType)\n const badge = resolveBadgePalette(componentType, theme)\n\n return (\n cardOpen(theme, 'element') +\n renderHeader(badge, acronym, label, p.text, theme) +\n `<tr><td style=\"padding:16px;\">` +\n `<div style=\"font-size:15px;line-height:1.6;color:${p.text};word-wrap:break-word;overflow-wrap:break-word;\">${sanitizeContent(\n data.content\n )}</div>` +\n `</td></tr>` +\n renderFooter(data.saveUrl, data, theme) +\n `</table>`\n )\n}\n\n/**\n * Render the official Skhema **component** card as email-safe HTML — a coloured\n * top bar, header (badge + type label + optional title), per-element-type\n * groups, and the shared footer.\n */\nexport function renderComponentCardHtml(data: ComponentCardData): string {\n const theme = data.theme ?? 'light'\n const p = CARD_PALETTE[theme]\n const label = getComponentTypeLabel(data.componentType)\n const acronym = getComponentTypeAcronym(data.componentType)\n const badge = resolveBadgePalette(data.componentType, theme)\n\n // Group elements by type (preserving first-occurrence order), mirroring the\n // live `<skhema-component>` body: one small-caps label per type, with each\n // element's content as a left-ruled block beneath it.\n const groups = new Map<string, string[]>()\n for (const el of data.elements) {\n const existing = groups.get(el.elementType)\n if (existing) existing.push(el.content)\n else groups.set(el.elementType, [el.content])\n }\n\n const groupsHtml = Array.from(groups.entries())\n .map(\n ([elementType, contents]) =>\n `<div style=\"margin-bottom:16px;\">` +\n `<div style=\"text-transform:uppercase;letter-spacing:0.05em;font-family:${MONO_STACK};font-size:10px;font-weight:600;color:${p.textMuted};margin:0 0 4px;\">${escapeHtml(\n getElementTypeLabel(elementType as ElementValue)\n )}</div>` +\n contents\n .map(\n (content) =>\n `<div style=\"font-size:14px;line-height:1.6;color:${p.text};padding-left:8px;border-left:2px solid ${p.border};word-wrap:break-word;overflow-wrap:break-word;\">${sanitizeContent(\n content\n )}</div>`\n )\n .join('') +\n `</div>`\n )\n .join('')\n\n return (\n cardOpen(theme, 'component') +\n `<tr><td style=\"height:2px;line-height:2px;font-size:0;background:${badge.topBorder};\"> </td></tr>` +\n renderHeader(badge, acronym, label, p.textMuted, theme, data.title) +\n `<tr><td style=\"padding:16px;\">${groupsHtml}</td></tr>` +\n renderFooter(data.saveUrl, data, theme) +\n `</table>`\n )\n}\n"],"names":[],"mappings":"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"}
|