best-email-builder 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +119 -44
  3. package/dist/ai-server.js +68 -0
  4. package/dist/ai-server.js.map +1 -0
  5. package/dist/ai.js +852 -0
  6. package/dist/ai.js.map +1 -0
  7. package/dist/builder.js +16 -8
  8. package/dist/chunk-2RZHGAH5.js +64 -0
  9. package/dist/chunk-2RZHGAH5.js.map +1 -0
  10. package/dist/chunk-3LKF4QBL.js +595 -0
  11. package/dist/chunk-3LKF4QBL.js.map +1 -0
  12. package/dist/chunk-5SASKCUC.js +251 -0
  13. package/dist/chunk-5SASKCUC.js.map +1 -0
  14. package/dist/{chunk-NAEGU6PR.js → chunk-6D3SLT5W.js} +18 -29
  15. package/dist/chunk-6D3SLT5W.js.map +1 -0
  16. package/dist/chunk-7U6BAKM7.js +119 -0
  17. package/dist/chunk-7U6BAKM7.js.map +1 -0
  18. package/dist/{chunk-HHLOE6OZ.js → chunk-DFNHICTF.js} +10 -333
  19. package/dist/chunk-DFNHICTF.js.map +1 -0
  20. package/dist/chunk-ECHGJZ2W.js +327 -0
  21. package/dist/chunk-ECHGJZ2W.js.map +1 -0
  22. package/dist/chunk-KXJJSVN5.js +98 -0
  23. package/dist/chunk-KXJJSVN5.js.map +1 -0
  24. package/dist/{chunk-AT73Y7JA.js → chunk-LXR4QTGP.js} +49 -581
  25. package/dist/chunk-LXR4QTGP.js.map +1 -0
  26. package/dist/{chunk-TGDZ4NFI.js → chunk-LYMJ2TBX.js} +3 -44
  27. package/dist/chunk-LYMJ2TBX.js.map +1 -0
  28. package/dist/{chunk-JNHFZXDL.js → chunk-MPX5CWTZ.js} +560 -649
  29. package/dist/chunk-MPX5CWTZ.js.map +1 -0
  30. package/dist/{chunk-MY6WPDT4.js → chunk-PB7YZ3WP.js} +463 -938
  31. package/dist/chunk-PB7YZ3WP.js.map +1 -0
  32. package/dist/chunk-UFKCLU2D.js +255 -0
  33. package/dist/chunk-UFKCLU2D.js.map +1 -0
  34. package/dist/index.js +31 -37
  35. package/dist/index.js.map +1 -1
  36. package/dist/library.js +12 -7
  37. package/dist/server.js +33 -74
  38. package/dist/server.js.map +1 -1
  39. package/dist/styles.css +1 -1
  40. package/dist/types/ai/ConnectAi.d.ts +12 -0
  41. package/dist/types/ai/GenerateButton.d.ts +4 -0
  42. package/dist/types/ai/SubjectIdeas.d.ts +25 -0
  43. package/dist/types/ai/createAi.d.ts +66 -0
  44. package/dist/types/{features/generate → ai}/draft.d.ts +44 -3
  45. package/dist/types/{features/generate → ai}/generator.d.ts +7 -2
  46. package/dist/types/ai/i18n/fr.d.ts +2 -0
  47. package/dist/types/ai/i18n/index.d.ts +2 -0
  48. package/dist/types/ai/index.d.ts +9 -0
  49. package/dist/types/ai/keys.d.ts +46 -0
  50. package/dist/types/ai/providers.d.ts +67 -0
  51. package/dist/types/{features/generate → ai}/rewrite.d.ts +1 -1
  52. package/dist/types/ai/server.d.ts +9 -0
  53. package/dist/types/{features/generate → ai}/subjects.d.ts +18 -4
  54. package/dist/types/core/constants.d.ts +1 -1
  55. package/dist/types/engine/NativeAdapter.d.ts +1 -1
  56. package/dist/types/features/preview/darkMode.d.ts +33 -16
  57. package/dist/types/features/variables/VariablePicker.d.ts +3 -2
  58. package/dist/types/i18n/index.d.ts +8 -0
  59. package/dist/types/index.d.ts +1 -7
  60. package/dist/types/server/handler.d.ts +5 -4
  61. package/dist/types/server/index.d.ts +1 -8
  62. package/dist/types/storage/client.d.ts +22 -40
  63. package/dist/types/ui/EmailBuilder.d.ts +5 -1
  64. package/dist/types/ui/EmailStudio.d.ts +6 -1
  65. package/dist/types/ui/PropertiesPanel.d.ts +2 -1
  66. package/dist/types/ui/primitives/searchShortcut.d.ts +16 -0
  67. package/package.json +15 -1
  68. package/dist/chunk-AT73Y7JA.js.map +0 -1
  69. package/dist/chunk-HHLOE6OZ.js.map +0 -1
  70. package/dist/chunk-JNHFZXDL.js.map +0 -1
  71. package/dist/chunk-MY6WPDT4.js.map +0 -1
  72. package/dist/chunk-NAEGU6PR.js.map +0 -1
  73. package/dist/chunk-TGDZ4NFI.js.map +0 -1
  74. package/dist/types/features/generate/GenerateButton.d.ts +0 -3
  75. package/dist/types/features/generate/SubjectIdeas.d.ts +0 -16
  76. package/dist/types/features/generate/index.d.ts +0 -2
  77. /package/dist/types/{features/generate → ai}/RewriteText.d.ts +0 -0
  78. /package/dist/types/{features/generate → ai}/chip.d.ts +0 -0
  79. /package/dist/types/{features/generate → ai}/claude.d.ts +0 -0
  80. /package/dist/types/{features/generate → ai}/prompt.d.ts +0 -0
  81. /package/dist/types/{features/generate → ai}/purposes.d.ts +0 -0
@@ -0,0 +1,327 @@
1
+ import {
2
+ FONT_OPTIONS,
3
+ PALETTE,
4
+ contrastRatio,
5
+ parseColor,
6
+ toHex,
7
+ toHsl
8
+ } from "./chunk-DFNHICTF.js";
9
+
10
+ // src/features/brand/extract.js
11
+ function decodeEntities(text) {
12
+ return String(text ?? "").replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/&quot;/g, '"').replace(/&#39;|&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&");
13
+ }
14
+ function tagAttributes(tag) {
15
+ const attributes = {};
16
+ const inner = tag.replace(/^<[^\s>/]+/, "").replace(/\/?>$/, "");
17
+ const pattern = /([^\s"'=<>/]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
18
+ let match;
19
+ while (match = pattern.exec(inner)) {
20
+ const name = match[1].toLowerCase();
21
+ if (!(name in attributes)) attributes[name] = decodeEntities(match[2] ?? match[3] ?? match[4] ?? "");
22
+ }
23
+ return attributes;
24
+ }
25
+ var startTags = (html, name) => html.match(new RegExp(`<${name}\\b[^>]*>`, "gi")) || [];
26
+ function resolve(href, base) {
27
+ const value = String(href ?? "").trim();
28
+ if (!value || /^(data|javascript|blob):/i.test(value)) return "";
29
+ try {
30
+ const url = new URL(value, base);
31
+ return url.protocol === "https:" || url.protocol === "http:" ? url.href : "";
32
+ } catch {
33
+ return "";
34
+ }
35
+ }
36
+ function largestOf(srcset) {
37
+ let best = { url: "", size: 0 };
38
+ for (const part of srcset.split(/,\s+/)) {
39
+ const [url, descriptor = "1x"] = part.trim().split(/\s+/);
40
+ const size = parseFloat(descriptor) * (descriptor.endsWith("w") ? 1 : 1e3);
41
+ if (url && size > best.size) best = { url, size };
42
+ }
43
+ return best.url;
44
+ }
45
+ function jsonLdNodes(value) {
46
+ if (Array.isArray(value)) return value.flatMap(jsonLdNodes);
47
+ if (!value || typeof value !== "object") return [];
48
+ const node = (
49
+ /** @type {any} */
50
+ value
51
+ );
52
+ return [node, ...jsonLdNodes(node["@graph"]), ...jsonLdNodes(node.publisher), ...jsonLdNodes(node.brand)];
53
+ }
54
+ var ORGANIZATION = /organization|corporation|business|brand|store|restaurant|company|ngo|school|airline|bank|agency/i;
55
+ function hostWords(host) {
56
+ const labels = host.toLowerCase().replace(/^www\d?\./, "").split(".");
57
+ const main = labels.length > 2 && labels[labels.length - 2].length <= 3 ? labels[labels.length - 3] : labels[0];
58
+ return main.replace(/[-_]+/g, " ");
59
+ }
60
+ var squash = (text) => text.toLowerCase().replace(/[^a-z0-9À-￿]+/g, "");
61
+ var cleanName = (value) => decodeEntities(String(value ?? "")).replace(/\s+/g, " ").trim().slice(0, 80);
62
+ function readPage(html, pageUrl) {
63
+ const source = String(html ?? "");
64
+ const head = source.slice(0, 4e5);
65
+ const baseTag = startTags(head, "base").map(tagAttributes).find((a) => a.href);
66
+ const base = resolve(baseTag?.href, pageUrl) || pageUrl;
67
+ const host = (() => {
68
+ try {
69
+ return new URL(pageUrl).hostname;
70
+ } catch {
71
+ return "";
72
+ }
73
+ })();
74
+ const meta = {};
75
+ for (const attributes of startTags(head, "meta").map(tagAttributes)) {
76
+ const key = (attributes.property || attributes.name || "").toLowerCase();
77
+ if (key && attributes.content && !(key in meta)) meta[key] = attributes.content;
78
+ }
79
+ let schemaName = "";
80
+ let schemaLogo = "";
81
+ for (const [, body] of source.matchAll(/<script\b[^>]*type\s*=\s*["']?application\/ld\+json[^>]*>([\s\S]*?)<\/script>/gi)) {
82
+ let data;
83
+ try {
84
+ data = JSON.parse(body.trim());
85
+ } catch {
86
+ continue;
87
+ }
88
+ for (const node of jsonLdNodes(data)) {
89
+ const types = [].concat(node["@type"] || []).join(" ");
90
+ if (!ORGANIZATION.test(types)) continue;
91
+ if (!schemaName && typeof node.name === "string") schemaName = node.name;
92
+ const logo = typeof node.logo === "string" ? node.logo : node.logo?.url || node.logo?.contentUrl || "";
93
+ if (!schemaLogo && typeof logo === "string") schemaLogo = resolve(logo, base);
94
+ }
95
+ }
96
+ const title = cleanName(head.match(/<title\b[^>]*>([\s\S]*?)<\/title>/i)?.[1] || "");
97
+ const words = hostWords(host);
98
+ const fromTitle = title.split(/\s+[|–—:·•»-]\s+/).map((part) => part.trim()).find((part) => part && squash(part).length <= 40 && (squash(part).includes(squash(words)) || squash(words).includes(squash(part)))) || "";
99
+ const fromHost = words.replace(/\b\p{L}/gu, (letter) => letter.toUpperCase());
100
+ const name = cleanName(meta["og:site_name"] || schemaName || meta["application-name"] || fromTitle || fromHost);
101
+ const logos = [];
102
+ const add = (url, from) => {
103
+ if (url && !logos.some((logo) => logo.url === url)) logos.push({ url, from });
104
+ };
105
+ add(schemaLogo, "schema");
106
+ const images = [];
107
+ const headerEnd = (() => {
108
+ const at = source.search(/<\/header>/i);
109
+ return at > 0 ? at : Math.min(source.length, 2e4);
110
+ })();
111
+ for (const match of source.matchAll(/<img\b[^>]*>/gi)) {
112
+ const attributes = tagAttributes(match[0]);
113
+ const own = [attributes.class, attributes.id, attributes.alt, attributes.src, attributes["data-src"]].join(" ").toLowerCase();
114
+ const before = source.slice(Math.max(0, (match.index ?? 0) - 400), match.index).toLowerCase();
115
+ const parent = /<(a|div|span|figure|picture)\b[^>]*(class|id)\s*=\s*["'][^"']*logo[^"']*["'][^>]*>[^<]*(<source\b[^>]*>\s*)*$/i.test(before);
116
+ if (!own.includes("logo") && !parent) continue;
117
+ if (/partner|client|customer|sponsor|award|payment|badge|trust|press|app-?store|google-?play/.test(own)) continue;
118
+ const url = resolve(attributes.srcset ? largestOf(attributes.srcset) : attributes.src || attributes["data-src"] || "", base);
119
+ if (!url) continue;
120
+ let score = own.includes("logo") ? 3 : 2;
121
+ if ((match.index ?? 0) < headerEnd) score += 3;
122
+ if (name && squash(attributes.alt || "").includes(squash(name))) score += 2;
123
+ if (/footer/.test(own)) score -= 2;
124
+ images.push({ url, score: score - images.length * 0.01 });
125
+ }
126
+ images.sort((a, b) => b.score - a.score).forEach((image) => add(image.url, "img"));
127
+ const icons = [];
128
+ for (const attributes of startTags(head, "link").map(tagAttributes)) {
129
+ const rel = (attributes.rel || "").toLowerCase();
130
+ if (!/apple-touch-icon|icon/.test(rel)) continue;
131
+ const size = parseInt(attributes.sizes || (rel.includes("apple") ? "180" : "0"), 10) || 0;
132
+ if (size >= 96) icons.push({ url: resolve(attributes.href, base), size });
133
+ }
134
+ icons.sort((a, b) => b.size - a.size).forEach((icon) => add(icon.url, "icon"));
135
+ const stylesheets = startTags(head, "link").map(tagAttributes).filter((a) => /(^|\s)stylesheet(\s|$)/i.test(a.rel || "") && !/^print$/i.test((a.media || "").trim())).map((a) => resolve(a.href, base)).filter(Boolean).filter((href) => !/fonts\.googleapis|use\.typekit|fontawesome|cookie|consent|onetrust/i.test(href));
136
+ const css = [
137
+ ...[...source.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map((m) => m[1]),
138
+ // Inline styles of buttons and links count too: `<a class="btn" style="…">`.
139
+ ...[...source.matchAll(/<(a|button)\b[^>]*>/gi)].map((m) => tagAttributes(m[0])).filter((a) => a.style).map((a) => `${/btn|button|cta/i.test(a.class || "") ? ".btn" : "a"}{${a.style}}`)
140
+ ].join("\n");
141
+ return {
142
+ name,
143
+ logos,
144
+ stylesheets: [...new Set(stylesheets)],
145
+ css,
146
+ themeColor: meta["theme-color"] || meta["msapplication-tilecolor"] || ""
147
+ };
148
+ }
149
+ var PRIMARY_NAME = /primary|brand|accent|main|theme|highlight|cta/i;
150
+ var NOT_A_BRAND_NAME = /text|foreground|fg|border|muted|gr[ae]y|neutral|surface|shadow|overlay|success|error|danger|warning|info|disabled|placeholder/i;
151
+ var NOISE = /error|danger|success|warning|alert|invalid|valid\b|notice|toast|badge|disabled|facebook|twitter|linkedin|instagram|youtube|whatsapp|pinterest|tiktok|social|cookie|consent|onetrust|ot-|cmp|gdpr|wp-block|has-[\w-]+-(background-)?color|syntax|hljs|code/i;
152
+ var COLOR_IN_VALUE = /#[0-9a-f]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\)/gi;
153
+ var COLOR_PROPERTY = /^(color|background|background-color|border|border-color|border-(top|bottom|left|right)(-color)?|fill|stroke|outline(-color)?|text-decoration-color|accent-color|caret-color)$/;
154
+ var stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, "");
155
+ function rulesOf(css) {
156
+ const rules = [];
157
+ for (const [, selector, body] of stripComments(css).matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
158
+ const declarations = body.split(";").map((part) => {
159
+ const colon = part.indexOf(":");
160
+ return (
161
+ /** @type {[string, string]} */
162
+ [part.slice(0, colon).trim().toLowerCase(), part.slice(colon + 1).trim()]
163
+ );
164
+ }).filter(([property, value]) => property && value);
165
+ if (declarations.length) rules.push([selector.trim(), declarations]);
166
+ }
167
+ return rules;
168
+ }
169
+ function customColor(value) {
170
+ const direct = parseColor(value.replace(/\s*!important$/, ""));
171
+ if (direct) return direct;
172
+ if (/^\d{1,3}(\.\d+)?\s+\d{1,3}(\.\d+)?\s+\d{1,3}(\.\d+)?$/.test(value)) return parseColor(`rgb(${value})`);
173
+ if (/^\d{1,3}(\.\d+)?(deg)?\s+\d{1,3}(\.\d+)?%\s+\d{1,3}(\.\d+)?%$/.test(value)) return parseColor(`hsl(${value.replace("deg", "")})`);
174
+ return null;
175
+ }
176
+ function resolveVars(value, custom) {
177
+ let out = value;
178
+ for (let depth = 0; depth < 4 && out.includes("var("); depth += 1) {
179
+ out = out.replace(/var\(\s*(--[\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\))?[^()]*))?\)/g, (_, name, fallback) => custom.get(name) ?? fallback ?? "");
180
+ }
181
+ return out;
182
+ }
183
+ var distance = (a, b) => Math.hypot(a.r - b.r, a.g - b.g, a.b - b.b);
184
+ var colorsIn = (value) => (value.match(COLOR_IN_VALUE) || []).map((text) => parseColor(text)).filter((c) => c && c.a >= 0.6);
185
+ function nearestFont(family) {
186
+ const stack = String(family ?? "").toLowerCase().replace(/["']/g, "");
187
+ if (!stack.trim() || /^(inherit|initial|unset|revert)$/.test(stack.trim())) return void 0;
188
+ const first = stack.split(",")[0].trim();
189
+ const known = FONT_OPTIONS.find((option) => first.startsWith(option.label.toLowerCase()));
190
+ if (known) return known.value;
191
+ if (/^helvetica/.test(first)) return FONT_OPTIONS[0].value;
192
+ if (/^times/.test(first)) return FONT_OPTIONS.find((o) => o.label === "Times")?.value;
193
+ if (/monospace|mono\b|courier|consolas/.test(stack)) return FONT_OPTIONS.find((o) => o.label === "Courier")?.value;
194
+ if (/(^|,)\s*serif\s*(,|$)|garamond|playfair|merriweather|lora|baskerville|caslon|didot|bodoni|crimson|libre baskerville|source serif|noto serif|pt serif/.test(stack)) {
195
+ return FONT_OPTIONS.find((o) => o.label === "Georgia")?.value;
196
+ }
197
+ return FONT_OPTIONS[0].value;
198
+ }
199
+ function brandFromCss({ css, themeColor = "" }) {
200
+ const rules = rulesOf(String(css ?? ""));
201
+ const custom = /* @__PURE__ */ new Map();
202
+ for (const [selector, declarations] of rules) {
203
+ const root = /^(:root|html|body|\*)(\s*,|$)/i.test(selector);
204
+ for (const [property, value] of declarations) {
205
+ if (property.startsWith("--") && (root || !custom.has(property))) custom.set(property, value.replace(/\s*!important$/, ""));
206
+ }
207
+ }
208
+ const found = [];
209
+ const darkText = [];
210
+ const fonts = [];
211
+ const count = (color, weight) => found.push({ color, weight });
212
+ for (const [selector, declarations] of rules) {
213
+ if (selector.startsWith("@")) continue;
214
+ const noise = NOISE.test(selector);
215
+ const emphasis = /btn|button|cta|primary|brand|accent|hero|highlight/i.test(selector) ? 4 : /(^|[\s,>+~])a([:.[\s,]|$)|link/i.test(selector) ? 2 : 1;
216
+ const isHeading = /(^|[\s,>])h[1-3]\b|title|heading|headline/i.test(selector);
217
+ const isBody = /^(html|body|:root)(\s*,|$)|(^|,)\s*(html|body)\s*(,|$)/i.test(selector);
218
+ for (const [property, raw] of declarations) {
219
+ if (property.startsWith("--")) {
220
+ const color = customColor(resolveVars(raw, custom));
221
+ if (color && color.a >= 0.6 && PRIMARY_NAME.test(property) && !NOT_A_BRAND_NAME.test(property)) count(color, 10);
222
+ continue;
223
+ }
224
+ const value = resolveVars(raw, custom);
225
+ if (property === "font-family" || property === "font") {
226
+ const family = property === "font" ? value.replace(/^.*?\d[\w%.]*(\/[\w%.]+)?\s+/, "") : value;
227
+ if (isBody || isHeading) fonts.push({ font: family, weight: isBody ? 2 : 1 });
228
+ continue;
229
+ }
230
+ if (!COLOR_PROPERTY.test(property) || noise) continue;
231
+ for (const color of colorsIn(value)) {
232
+ if (!color) continue;
233
+ count(color, emphasis * (property.startsWith("background") ? 1.5 : 1));
234
+ if (property === "color" && (isHeading || isBody)) darkText.push({ color, weight: isHeading ? 2 : 1 });
235
+ }
236
+ }
237
+ }
238
+ const theme = parseColor(themeColor);
239
+ if (theme && theme.a >= 0.6) count(theme, 15);
240
+ const clusters = [];
241
+ for (const entry of found) {
242
+ const near = clusters.find((cluster) => distance(cluster.color, entry.color) < 24);
243
+ if (near) near.weight += entry.weight;
244
+ else clusters.push({ ...entry });
245
+ }
246
+ clusters.sort((a, b) => b.weight - a.weight);
247
+ const primary = clusters.find(({ color }) => {
248
+ const { s, l } = toHsl(color);
249
+ return s >= 0.25 && l >= 0.15 && l <= 0.85;
250
+ });
251
+ const result = {};
252
+ if (primary) result.primary = toHex(primary.color);
253
+ const dark = darkText.filter(({ color }) => toHsl(color).l < 0.3).reduce((map, { color, weight }) => map.set(toHex(color), (map.get(toHex(color)) || 0) + weight), /* @__PURE__ */ new Map());
254
+ const heading = [...dark.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
255
+ if (heading) result.heading = heading;
256
+ else if (result.primary && contrastRatio(result.primary, PALETTE.white) >= 7) result.heading = result.primary;
257
+ else result.heading = PALETTE.ink;
258
+ const font = fonts.sort((a, b) => b.weight - a.weight).map(({ font: font2 }) => nearestFont(font2)).find(Boolean);
259
+ if (font) result.font = font;
260
+ return result;
261
+ }
262
+ function imageSize(bytes) {
263
+ if (!bytes || bytes.length < 24) return null;
264
+ const at = (i) => bytes[i];
265
+ if (at(0) === 137 && at(1) === 80 && at(2) === 78 && at(3) === 71) {
266
+ const read32 = (i) => (at(i) << 24 | at(i + 1) << 16 | at(i + 2) << 8 | at(i + 3)) >>> 0;
267
+ return { width: read32(16), height: read32(20) };
268
+ }
269
+ if (at(0) === 71 && at(1) === 73 && at(2) === 70) {
270
+ return { width: at(6) | at(7) << 8, height: at(8) | at(9) << 8 };
271
+ }
272
+ if (at(0) === 255 && at(1) === 216) {
273
+ let i = 2;
274
+ while (i + 9 < bytes.length) {
275
+ if (at(i) !== 255) {
276
+ i += 1;
277
+ continue;
278
+ }
279
+ const marker = at(i + 1);
280
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215 || marker === 255) {
281
+ i += marker === 255 ? 1 : 2;
282
+ continue;
283
+ }
284
+ const length = at(i + 2) << 8 | at(i + 3);
285
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
286
+ return { height: at(i + 5) << 8 | at(i + 6), width: at(i + 7) << 8 | at(i + 8) };
287
+ }
288
+ i += 2 + length;
289
+ }
290
+ }
291
+ return null;
292
+ }
293
+ function logoWidthFor({ width, height }) {
294
+ if (!width || !height) return 140;
295
+ const ratio = width / height;
296
+ const shown = width >= 240 ? width / 2 : width;
297
+ const target = ratio >= 2.5 ? 180 : ratio >= 1.3 ? 140 : 72;
298
+ return Math.max(24, Math.round(Math.min(target, shown)));
299
+ }
300
+
301
+ // src/features/brand/messages.js
302
+ var BRAND_IMPORT_ERRORS = {
303
+ BAD_URL: [400, "Enter the address of a website, like acme.com"],
304
+ PRIVATE_ADDRESS: [400, "This address is not a public website"],
305
+ UNREACHABLE: [422, "The website could not be reached. Check the address."],
306
+ TIMEOUT: [422, "The website took too long to answer"],
307
+ HTTP_ERROR: [422, "The website does not let the builder read it. Fill in your brand below."],
308
+ NOT_HTML: [422, "This address is not a web page"],
309
+ TOO_MANY_REDIRECTS: [422, "The website redirects too many times"],
310
+ TOO_LARGE: [422, "The website sent too much data"]
311
+ };
312
+ var BRAND_NOTES = {
313
+ LOGO_NOT_FOUND: "No logo was found on the website. Upload it below.",
314
+ LOGO_UNSUPPORTED: "The logo on the website is an SVG or WebP image, which most email apps do not show. Upload a PNG or JPEG version.",
315
+ LOGO_ICON: "The website icon is used as the logo. Upload your full logo if you have one.",
316
+ COLORS_NOT_FOUND: "No main color was found on the website. Pick one below."
317
+ };
318
+
319
+ export {
320
+ readPage,
321
+ brandFromCss,
322
+ imageSize,
323
+ logoWidthFor,
324
+ BRAND_IMPORT_ERRORS,
325
+ BRAND_NOTES
326
+ };
327
+ //# sourceMappingURL=chunk-ECHGJZ2W.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/features/brand/extract.js","../src/features/brand/messages.js"],"sourcesContent":["// @ts-check\n/**\n * BRAND FROM A WEBSITE — what a company's home page says about its look:\n * its name, where its logo is, its main color, the color of its headings\n * and its font.\n *\n * Pure text work on what the server downloaded (src/server/brandImport.js\n * does the downloading, with its safety rules): no network, no DOM, so it\n * runs anywhere and is tested on fixtures.\n *\n * name og:site_name → JSON-LD Organization → application-name → the\n * part of <title> that matches the domain → the domain;\n * logo JSON-LD Organization logo → an <img> called \"logo\" (header\n * first) → apple-touch-icon; SVG and WebP are skipped later\n * (most email apps do not show them);\n * colors counted in the CSS, weighted by where they are used (buttons,\n * links, custom properties named primary/brand/accent, the\n * theme-color meta); grays, alert and social colors set aside;\n * font the body's, mapped to the nearest web-safe stack.\n */\n\nimport { FONT_OPTIONS, PALETTE } from '../../blocks/shared.js';\nimport { contrastRatio, parseColor, toHex, toHsl } from './brand.js';\n\n/**\n * @typedef {{\n * name: string,\n * logos: {url: string, from: 'schema'|'img'|'icon'}[],\n * stylesheets: string[],\n * css: string,\n * themeColor: string,\n * }} PageReading\n */\n\n/* ── HTML ───────────────────────────────────────────────────────────────── */\n\n/** @param {string} text */\nfunction decodeEntities(text) {\n return String(text ?? '')\n .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))\n .replace(/&#(\\d+);/g, (_, dec) => String.fromCodePoint(Number(dec)))\n .replace(/&quot;/g, '\"')\n .replace(/&#39;|&apos;/g, \"'\")\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&nbsp;/g, ' ')\n .replace(/&amp;/g, '&');\n}\n\n/**\n * The attributes of one start tag, names in lower case.\n * @param {string} tag `<img src=\"…\" class=logo>`\n * @returns {Record<string, string>}\n */\nexport function tagAttributes(tag) {\n /** @type {Record<string, string>} */\n const attributes = {};\n const inner = tag.replace(/^<[^\\s>/]+/, '').replace(/\\/?>$/, '');\n const pattern = /([^\\s\"'=<>/]+)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+)))?/g;\n let match;\n while ((match = pattern.exec(inner))) {\n const name = match[1].toLowerCase();\n if (!(name in attributes)) attributes[name] = decodeEntities(match[2] ?? match[3] ?? match[4] ?? '');\n }\n return attributes;\n}\n\n/** @param {string} html @param {string} name */\nconst startTags = (html, name) => html.match(new RegExp(`<${name}\\\\b[^>]*>`, 'gi')) || [];\n\n/** An http(s) address, resolved against the page; '' otherwise. @param {string|undefined} href @param {string} base */\nfunction resolve(href, base) {\n const value = String(href ?? '').trim();\n if (!value || /^(data|javascript|blob):/i.test(value)) return '';\n try {\n const url = new URL(value, base);\n return url.protocol === 'https:' || url.protocol === 'http:' ? url.href : '';\n } catch {\n return '';\n }\n}\n\n/** The largest image of a srcset (\"a.png 1x, a@2x.png 2x\"). @param {string} srcset */\nfunction largestOf(srcset) {\n let best = { url: '', size: 0 };\n for (const part of srcset.split(/,\\s+/)) {\n const [url, descriptor = '1x'] = part.trim().split(/\\s+/);\n const size = parseFloat(descriptor) * (descriptor.endsWith('w') ? 1 : 1000);\n if (url && size > best.size) best = { url, size };\n }\n return best.url;\n}\n\n/** Objects of a JSON-LD script, flattened (@graph, arrays). @param {unknown} value @returns {any[]} */\nfunction jsonLdNodes(value) {\n if (Array.isArray(value)) return value.flatMap(jsonLdNodes);\n if (!value || typeof value !== 'object') return [];\n const node = /** @type {any} */ (value);\n return [node, ...jsonLdNodes(node['@graph']), ...jsonLdNodes(node.publisher), ...jsonLdNodes(node.brand)];\n}\n\nconst ORGANIZATION = /organization|corporation|business|brand|store|restaurant|company|ngo|school|airline|bank|agency/i;\n\n/** The words of a host name that name the company: \"www.acme-corp.co.uk\" → \"acme corp\". @param {string} host */\nfunction hostWords(host) {\n const labels = host.toLowerCase().replace(/^www\\d?\\./, '').split('.');\n const main = labels.length > 2 && labels[labels.length - 2].length <= 3 ? labels[labels.length - 3] : labels[0];\n return main.replace(/[-_]+/g, ' ');\n}\n\n/** @param {string} text */\nconst squash = (text) => text.toLowerCase().replace(/[^a-z0-9À-￿]+/g, '');\n\n/** @param {unknown} value */\nconst cleanName = (value) =>\n decodeEntities(String(value ?? ''))\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, 80);\n\n/**\n * What a home page says about its brand.\n * @param {string} html\n * @param {string} pageUrl the address it was read from (after redirects)\n * @returns {PageReading}\n */\nexport function readPage(html, pageUrl) {\n const source = String(html ?? '');\n const head = source.slice(0, 400_000);\n const baseTag = startTags(head, 'base').map(tagAttributes).find((a) => a.href);\n const base = resolve(baseTag?.href, pageUrl) || pageUrl;\n const host = (() => {\n try {\n return new URL(pageUrl).hostname;\n } catch {\n return '';\n }\n })();\n\n /** @type {Record<string, string>} */\n const meta = {};\n for (const attributes of startTags(head, 'meta').map(tagAttributes)) {\n const key = (attributes.property || attributes.name || '').toLowerCase();\n if (key && attributes.content && !(key in meta)) meta[key] = attributes.content;\n }\n\n // JSON-LD: the organization's own name and logo, when the site states them.\n let schemaName = '';\n let schemaLogo = '';\n for (const [, body] of source.matchAll(/<script\\b[^>]*type\\s*=\\s*[\"']?application\\/ld\\+json[^>]*>([\\s\\S]*?)<\\/script>/gi)) {\n let data;\n try {\n data = JSON.parse(body.trim());\n } catch {\n continue;\n }\n for (const node of jsonLdNodes(data)) {\n const types = [].concat(node['@type'] || []).join(' ');\n if (!ORGANIZATION.test(types)) continue;\n if (!schemaName && typeof node.name === 'string') schemaName = node.name;\n const logo = typeof node.logo === 'string' ? node.logo : node.logo?.url || node.logo?.contentUrl || '';\n if (!schemaLogo && typeof logo === 'string') schemaLogo = resolve(logo, base);\n }\n }\n\n /* Name */\n const title = cleanName(head.match(/<title\\b[^>]*>([\\s\\S]*?)<\\/title>/i)?.[1] || '');\n const words = hostWords(host);\n const fromTitle =\n title\n .split(/\\s+[|–—:·•»-]\\s+/)\n .map((part) => part.trim())\n .find((part) => part && squash(part).length <= 40 && (squash(part).includes(squash(words)) || squash(words).includes(squash(part)))) || '';\n const fromHost = words.replace(/\\b\\p{L}/gu, (letter) => letter.toUpperCase());\n const name = cleanName(meta['og:site_name'] || schemaName || meta['application-name'] || fromTitle || fromHost);\n\n /* Logo candidates, best first */\n /** @type {PageReading['logos']} */\n const logos = [];\n const add = (/** @type {string} */ url, /** @type {PageReading['logos'][number]['from']} */ from) => {\n if (url && !logos.some((logo) => logo.url === url)) logos.push({ url, from });\n };\n add(schemaLogo, 'schema');\n /** @type {{url: string, score: number}[]} */\n const images = [];\n const headerEnd = (() => {\n const at = source.search(/<\\/header>/i);\n return at > 0 ? at : Math.min(source.length, 20_000);\n })();\n for (const match of source.matchAll(/<img\\b[^>]*>/gi)) {\n const attributes = tagAttributes(match[0]);\n const own = [attributes.class, attributes.id, attributes.alt, attributes.src, attributes['data-src']].join(' ').toLowerCase();\n const before = source.slice(Math.max(0, (match.index ?? 0) - 400), match.index).toLowerCase();\n const parent = /<(a|div|span|figure|picture)\\b[^>]*(class|id)\\s*=\\s*[\"'][^\"']*logo[^\"']*[\"'][^>]*>[^<]*(<source\\b[^>]*>\\s*)*$/i.test(before);\n if (!own.includes('logo') && !parent) continue;\n // Partner, client and payment logos are someone else's.\n if (/partner|client|customer|sponsor|award|payment|badge|trust|press|app-?store|google-?play/.test(own)) continue;\n const url = resolve(attributes.srcset ? largestOf(attributes.srcset) : attributes.src || attributes['data-src'] || '', base);\n if (!url) continue;\n let score = own.includes('logo') ? 3 : 2;\n if ((match.index ?? 0) < headerEnd) score += 3;\n if (name && squash(attributes.alt || '').includes(squash(name))) score += 2;\n if (/footer/.test(own)) score -= 2;\n images.push({ url, score: score - images.length * 0.01 });\n }\n images.sort((a, b) => b.score - a.score).forEach((image) => add(image.url, 'img'));\n /** @type {{url: string, size: number}[]} */\n const icons = [];\n for (const attributes of startTags(head, 'link').map(tagAttributes)) {\n const rel = (attributes.rel || '').toLowerCase();\n if (!/apple-touch-icon|icon/.test(rel)) continue;\n const size = parseInt(attributes.sizes || (rel.includes('apple') ? '180' : '0'), 10) || 0;\n if (size >= 96) icons.push({ url: resolve(attributes.href, base), size });\n }\n icons.sort((a, b) => b.size - a.size).forEach((icon) => add(icon.url, 'icon'));\n\n /* Styles */\n const stylesheets = startTags(head, 'link')\n .map(tagAttributes)\n .filter((a) => /(^|\\s)stylesheet(\\s|$)/i.test(a.rel || '') && !/^print$/i.test((a.media || '').trim()))\n .map((a) => resolve(a.href, base))\n .filter(Boolean)\n // Fonts and third-party widgets say nothing about the brand's colors.\n .filter((href) => !/fonts\\.googleapis|use\\.typekit|fontawesome|cookie|consent|onetrust/i.test(href));\n const css = [\n ...[...source.matchAll(/<style\\b[^>]*>([\\s\\S]*?)<\\/style>/gi)].map((m) => m[1]),\n // Inline styles of buttons and links count too: `<a class=\"btn\" style=\"…\">`.\n ...[...source.matchAll(/<(a|button)\\b[^>]*>/gi)]\n .map((m) => tagAttributes(m[0]))\n .filter((a) => a.style)\n .map((a) => `${/btn|button|cta/i.test(a.class || '') ? '.btn' : 'a'}{${a.style}}`),\n ].join('\\n');\n\n return {\n name,\n logos,\n stylesheets: [...new Set(stylesheets)],\n css,\n themeColor: meta['theme-color'] || meta['msapplication-tilecolor'] || '',\n };\n}\n\n/* ── CSS ────────────────────────────────────────────────────────────────── */\n\nconst PRIMARY_NAME = /primary|brand|accent|main|theme|highlight|cta/i;\nconst NOT_A_BRAND_NAME = /text|foreground|fg|border|muted|gr[ae]y|neutral|surface|shadow|overlay|success|error|danger|warning|info|disabled|placeholder/i;\nconst NOISE = /error|danger|success|warning|alert|invalid|valid\\b|notice|toast|badge|disabled|facebook|twitter|linkedin|instagram|youtube|whatsapp|pinterest|tiktok|social|cookie|consent|onetrust|ot-|cmp|gdpr|wp-block|has-[\\w-]+-(background-)?color|syntax|hljs|code/i;\nconst COLOR_IN_VALUE = /#[0-9a-f]{3,8}\\b|rgba?\\([^)]*\\)|hsla?\\([^)]*\\)/gi;\nconst COLOR_PROPERTY = /^(color|background|background-color|border|border-color|border-(top|bottom|left|right)(-color)?|fill|stroke|outline(-color)?|text-decoration-color|accent-color|caret-color)$/;\n\n/** @param {string} css */\nconst stripComments = (css) => css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n\n/** Rules as [selector, declarations], @media flattened. @param {string} css */\nfunction rulesOf(css) {\n /** @type {[string, [string, string][]][]} */\n const rules = [];\n for (const [, selector, body] of stripComments(css).matchAll(/([^{}]+)\\{([^{}]*)\\}/g)) {\n const declarations = body\n .split(';')\n .map((part) => {\n const colon = part.indexOf(':');\n return /** @type {[string, string]} */ ([part.slice(0, colon).trim().toLowerCase(), part.slice(colon + 1).trim()]);\n })\n .filter(([property, value]) => property && value);\n if (declarations.length) rules.push([selector.trim(), declarations]);\n }\n return rules;\n}\n\n/**\n * A custom property's value as a color: `#2977bc`, `rgb(…)`, and the bare\n * channels design systems keep there (`41 119 188`, `210 64% 45%`).\n * @param {string} value\n */\nfunction customColor(value) {\n const direct = parseColor(value.replace(/\\s*!important$/, ''));\n if (direct) return direct;\n if (/^\\d{1,3}(\\.\\d+)?\\s+\\d{1,3}(\\.\\d+)?\\s+\\d{1,3}(\\.\\d+)?$/.test(value)) return parseColor(`rgb(${value})`);\n if (/^\\d{1,3}(\\.\\d+)?(deg)?\\s+\\d{1,3}(\\.\\d+)?%\\s+\\d{1,3}(\\.\\d+)?%$/.test(value)) return parseColor(`hsl(${value.replace('deg', '')})`);\n return null;\n}\n\n/**\n * `var(--x, fallback)` replaced by what --x holds, a few levels deep.\n * @param {string} value @param {Map<string, string>} custom\n */\nfunction resolveVars(value, custom) {\n let out = value;\n for (let depth = 0; depth < 4 && out.includes('var('); depth += 1) {\n out = out.replace(/var\\(\\s*(--[\\w-]+)\\s*(?:,\\s*([^()]*(?:\\([^()]*\\))?[^()]*))?\\)/g, (_, name, fallback) => custom.get(name) ?? fallback ?? '');\n }\n return out;\n}\n\n/** @param {{r: number, g: number, b: number}} a @param {{r: number, g: number, b: number}} b */\nconst distance = (a, b) => Math.hypot(a.r - b.r, a.g - b.g, a.b - b.b);\n\n/** Colors of one value, as found (not resolved through var()). @param {string} value */\nconst colorsIn = (value) => (value.match(COLOR_IN_VALUE) || []).map((text) => parseColor(text)).filter((c) => c && c.a >= 0.6);\n\n/**\n * The web-safe stack nearest to a CSS font-family.\n * @param {string} family\n * @returns {string|undefined}\n */\nexport function nearestFont(family) {\n const stack = String(family ?? '')\n .toLowerCase()\n .replace(/[\"']/g, '');\n if (!stack.trim() || /^(inherit|initial|unset|revert)$/.test(stack.trim())) return undefined;\n const first = stack.split(',')[0].trim();\n const known = FONT_OPTIONS.find((option) => first.startsWith(option.label.toLowerCase()));\n if (known) return known.value;\n if (/^helvetica/.test(first)) return FONT_OPTIONS[0].value;\n if (/^times/.test(first)) return FONT_OPTIONS.find((o) => o.label === 'Times')?.value;\n if (/monospace|mono\\b|courier|consolas/.test(stack)) return FONT_OPTIONS.find((o) => o.label === 'Courier')?.value;\n // Web fonts: their family decides (serif, else sans-serif).\n if (/(^|,)\\s*serif\\s*(,|$)|garamond|playfair|merriweather|lora|baskerville|caslon|didot|bodoni|crimson|libre baskerville|source serif|noto serif|pt serif/.test(stack)) {\n return FONT_OPTIONS.find((o) => o.label === 'Georgia')?.value;\n }\n return FONT_OPTIONS[0].value;\n}\n\n/**\n * Main color, headings color and font of a site, from its CSS.\n * @param {{css: string, themeColor?: string}} input\n * @returns {{primary?: string, heading?: string, font?: string}}\n */\nexport function brandFromCss({ css, themeColor = '' }) {\n const rules = rulesOf(String(css ?? ''));\n\n /** @type {Map<string, string>} */\n const custom = new Map();\n for (const [selector, declarations] of rules) {\n // :root first: a theme's dark variant must not replace it.\n const root = /^(:root|html|body|\\*)(\\s*,|$)/i.test(selector);\n for (const [property, value] of declarations) {\n if (property.startsWith('--') && (root || !custom.has(property))) custom.set(property, value.replace(/\\s*!important$/, ''));\n }\n }\n\n /** @type {{color: {r: number, g: number, b: number}, weight: number}[]} */\n const found = [];\n /** @type {{color: {r: number, g: number, b: number}, weight: number}[]} */\n const darkText = [];\n /** @type {{font: string, weight: number}[]} */\n const fonts = [];\n const count = (/** @type {{r: number, g: number, b: number}} */ color, /** @type {number} */ weight) => found.push({ color, weight });\n\n for (const [selector, declarations] of rules) {\n if (selector.startsWith('@')) continue;\n const noise = NOISE.test(selector);\n const emphasis = /btn|button|cta|primary|brand|accent|hero|highlight/i.test(selector) ? 4 : /(^|[\\s,>+~])a([:.[\\s,]|$)|link/i.test(selector) ? 2 : 1;\n const isHeading = /(^|[\\s,>])h[1-3]\\b|title|heading|headline/i.test(selector);\n const isBody = /^(html|body|:root)(\\s*,|$)|(^|,)\\s*(html|body)\\s*(,|$)/i.test(selector);\n for (const [property, raw] of declarations) {\n if (property.startsWith('--')) {\n const color = customColor(resolveVars(raw, custom));\n if (color && color.a >= 0.6 && PRIMARY_NAME.test(property) && !NOT_A_BRAND_NAME.test(property)) count(color, 10);\n continue;\n }\n const value = resolveVars(raw, custom);\n if (property === 'font-family' || property === 'font') {\n const family = property === 'font' ? value.replace(/^.*?\\d[\\w%.]*(\\/[\\w%.]+)?\\s+/, '') : value;\n if (isBody || isHeading) fonts.push({ font: family, weight: isBody ? 2 : 1 });\n continue;\n }\n if (!COLOR_PROPERTY.test(property) || noise) continue;\n for (const color of colorsIn(value)) {\n if (!color) continue;\n count(color, emphasis * (property.startsWith('background') ? 1.5 : 1));\n if (property === 'color' && (isHeading || isBody)) darkText.push({ color, weight: isHeading ? 2 : 1 });\n }\n }\n }\n const theme = parseColor(themeColor);\n if (theme && theme.a >= 0.6) count(theme, 15);\n\n // Near colors are one color (hover shades, rounding).\n /** @type {{color: {r: number, g: number, b: number}, weight: number}[]} */\n const clusters = [];\n for (const entry of found) {\n const near = clusters.find((cluster) => distance(cluster.color, entry.color) < 24);\n if (near) near.weight += entry.weight;\n else clusters.push({ ...entry });\n }\n clusters.sort((a, b) => b.weight - a.weight);\n const primary = clusters.find(({ color }) => {\n const { s, l } = toHsl(color);\n return s >= 0.25 && l >= 0.15 && l <= 0.85;\n });\n\n /** @type {{primary?: string, heading?: string, font?: string}} */\n const result = {};\n if (primary) result.primary = toHex(primary.color);\n\n const dark = darkText\n .filter(({ color }) => toHsl(color).l < 0.3)\n .reduce((/** @type {Map<string, number>} */ map, { color, weight }) => map.set(toHex(color), (map.get(toHex(color)) || 0) + weight), new Map());\n const heading = [...dark.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];\n if (heading) result.heading = heading;\n else if (result.primary && contrastRatio(result.primary, PALETTE.white) >= 7) result.heading = result.primary;\n else result.heading = PALETTE.ink;\n\n const font = fonts.sort((a, b) => b.weight - a.weight).map(({ font }) => nearestFont(font)).find(Boolean);\n if (font) result.font = font;\n return result;\n}\n\n/* ── Images ─────────────────────────────────────────────────────────────── */\n\n/**\n * Width and height of a PNG, GIF or JPEG, from its header; null otherwise.\n * @param {Uint8Array} bytes\n * @returns {{width: number, height: number}|null}\n */\nexport function imageSize(bytes) {\n if (!bytes || bytes.length < 24) return null;\n const at = (/** @type {number} */ i) => bytes[i];\n // PNG: IHDR right after the signature.\n if (at(0) === 0x89 && at(1) === 0x50 && at(2) === 0x4e && at(3) === 0x47) {\n const read32 = (/** @type {number} */ i) => ((at(i) << 24) | (at(i + 1) << 16) | (at(i + 2) << 8) | at(i + 3)) >>> 0;\n return { width: read32(16), height: read32(20) };\n }\n // GIF: logical screen size.\n if (at(0) === 0x47 && at(1) === 0x49 && at(2) === 0x46) {\n return { width: at(6) | (at(7) << 8), height: at(8) | (at(9) << 8) };\n }\n // JPEG: the first start-of-frame marker.\n if (at(0) === 0xff && at(1) === 0xd8) {\n let i = 2;\n while (i + 9 < bytes.length) {\n if (at(i) !== 0xff) {\n i += 1;\n continue;\n }\n const marker = at(i + 1);\n if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7) || marker === 0xff) {\n i += marker === 0xff ? 1 : 2;\n continue;\n }\n const length = (at(i + 2) << 8) | at(i + 3);\n if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {\n return { height: (at(i + 5) << 8) | at(i + 6), width: (at(i + 7) << 8) | at(i + 8) };\n }\n i += 2 + length;\n }\n }\n return null;\n}\n\n/**\n * The width a logo is shown at in an email: a wordmark wider, a square mark\n * smaller; never wider than the image itself (a blurry logo is worse than a\n * small one). Retina images (twice the size shown) are common: halved first.\n * @param {{width: number, height: number}} size\n */\nexport function logoWidthFor({ width, height }) {\n if (!width || !height) return 140;\n const ratio = width / height;\n const shown = width >= 240 ? width / 2 : width;\n const target = ratio >= 2.5 ? 180 : ratio >= 1.3 ? 140 : 72;\n return Math.max(24, Math.round(Math.min(target, shown)));\n}\n","// @ts-check\n/**\n * What the user is told about reading a brand from a website: why it failed\n * (the server's answer) and what it could not find (notes under the field).\n * English source strings, translated where they are shown.\n */\n\n/**\n * Why a website could not be read: [HTTP status, message], by\n * BrandImportError code (src/server/brandImport.js).\n * @type {Record<string, [number, string]>}\n */\nexport const BRAND_IMPORT_ERRORS = {\n BAD_URL: [400, 'Enter the address of a website, like acme.com'],\n PRIVATE_ADDRESS: [400, 'This address is not a public website'],\n UNREACHABLE: [422, 'The website could not be reached. Check the address.'],\n TIMEOUT: [422, 'The website took too long to answer'],\n HTTP_ERROR: [422, 'The website does not let the builder read it. Fill in your brand below.'],\n NOT_HTML: [422, 'This address is not a web page'],\n TOO_MANY_REDIRECTS: [422, 'The website redirects too many times'],\n TOO_LARGE: [422, 'The website sent too much data'],\n};\n\n/** What could not be read, by note code. @type {Record<string, string>} */\nexport const BRAND_NOTES = {\n LOGO_NOT_FOUND: 'No logo was found on the website. Upload it below.',\n LOGO_UNSUPPORTED: 'The logo on the website is an SVG or WebP image, which most email apps do not show. Upload a PNG or JPEG version.',\n LOGO_ICON: 'The website icon is used as the logo. Upload your full logo if you have one.',\n COLORS_NOT_FOUND: 'No main color was found on the website. Pick one below.',\n};\n"],"mappings":"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","names":["font"]}
@@ -0,0 +1,98 @@
1
+ import {
2
+ INLINE_TAGS,
3
+ isSafeUrl
4
+ } from "./chunk-5SASKCUC.js";
5
+
6
+ // src/engine/richText.js
7
+ var DROPPED_WITH_CONTENT = /* @__PURE__ */ new Set(["script", "style", "iframe", "object", "embed", "template", "svg", "math", "head", "title", "meta", "link"]);
8
+ var KEPT_ATTRIBUTES = {
9
+ "*": /* @__PURE__ */ new Set(["style"]),
10
+ a: /* @__PURE__ */ new Set(["href", "target", "title", "rel"]),
11
+ font: /* @__PURE__ */ new Set(["color", "face", "size"])
12
+ };
13
+ var KEPT_STYLES = /* @__PURE__ */ new Set([
14
+ "color",
15
+ "background-color",
16
+ "font-weight",
17
+ "font-style",
18
+ "font-size",
19
+ "font-family",
20
+ "text-decoration",
21
+ "text-align",
22
+ "line-height",
23
+ "letter-spacing"
24
+ ]);
25
+ function cleanStyle(style) {
26
+ return style.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
27
+ const colon = part.indexOf(":");
28
+ if (colon === -1) return null;
29
+ const name = part.slice(0, colon).trim().toLowerCase();
30
+ const value = part.slice(colon + 1).trim();
31
+ if (!KEPT_STYLES.has(name) || /expression|url\s*\(|javascript:/i.test(value)) return null;
32
+ return `${name}:${value}`;
33
+ }).filter(Boolean).join(";");
34
+ }
35
+ function cleanAttributes(element) {
36
+ const tag = element.tagName.toLowerCase();
37
+ const own = (
38
+ /** @type {Record<string, Set<string>>} */
39
+ KEPT_ATTRIBUTES[tag]
40
+ );
41
+ for (const attribute of [...element.attributes]) {
42
+ const name = attribute.name.toLowerCase();
43
+ if (!KEPT_ATTRIBUTES["*"].has(name) && !(own && own.has(name))) {
44
+ element.removeAttribute(attribute.name);
45
+ continue;
46
+ }
47
+ if (name === "href" && !isSafeUrl(attribute.value, "link")) element.removeAttribute(attribute.name);
48
+ if (name === "style") {
49
+ const style = cleanStyle(attribute.value);
50
+ if (style) element.setAttribute("style", style);
51
+ else element.removeAttribute("style");
52
+ }
53
+ }
54
+ if (tag === "a" && element.getAttribute("target") === "_blank") element.setAttribute("rel", "noopener");
55
+ }
56
+ function cleanChildren(node) {
57
+ for (const child of [...node.childNodes]) {
58
+ if (child.nodeType === 8) {
59
+ child.remove();
60
+ continue;
61
+ }
62
+ if (child.nodeType !== 1) continue;
63
+ const element = (
64
+ /** @type {Element} */
65
+ child
66
+ );
67
+ const tag = element.tagName.toLowerCase();
68
+ if (DROPPED_WITH_CONTENT.has(tag)) {
69
+ element.remove();
70
+ continue;
71
+ }
72
+ cleanChildren(element);
73
+ if (!INLINE_TAGS.has(tag)) {
74
+ element.replaceWith(...element.childNodes);
75
+ continue;
76
+ }
77
+ cleanAttributes(element);
78
+ }
79
+ }
80
+ function cleanRichText(html, document) {
81
+ const template = document.createElement("template");
82
+ template.innerHTML = String(html || "");
83
+ cleanChildren(template.content);
84
+ return template.innerHTML.replace(/<br>/g, "<br />").replace(/(<br \/>)+$/, "").trim();
85
+ }
86
+ function escapeText(text) {
87
+ return String(text ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
88
+ }
89
+ function textOfHtml(html) {
90
+ return String(html || "").replace(/<br\s*\/?>/gi, " ").replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&").trim();
91
+ }
92
+
93
+ export {
94
+ cleanRichText,
95
+ escapeText,
96
+ textOfHtml
97
+ };
98
+ //# sourceMappingURL=chunk-KXJJSVN5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/engine/richText.js"],"sourcesContent":["// @ts-check\n/**\n * RICH TEXT — what a text typed in the canvas may keep.\n *\n * The canvas edits text with the browser's own editing (contenteditable):\n * it can produce, or receive by paste, anything. Before it enters the model\n * the HTML is reduced to what an email and the server's validator accept:\n * the formatting tags of `INLINE_TAGS`, a few harmless attributes, safe\n * links. Other tags are unwrapped (their text stays), scripts and styles are\n * dropped with their content.\n *\n * Needs a DOM (the canvas frame's document).\n */\n\nimport { INLINE_TAGS } from '../validation/mjmlValidator.js';\nimport { isSafeUrl } from '../validation/urls.js';\n\nconst DROPPED_WITH_CONTENT = new Set(['script', 'style', 'iframe', 'object', 'embed', 'template', 'svg', 'math', 'head', 'title', 'meta', 'link']);\n\n/** Attributes kept, per tag (`*`: every allowed tag). */\nconst KEPT_ATTRIBUTES = {\n '*': new Set(['style']),\n a: new Set(['href', 'target', 'title', 'rel']),\n font: new Set(['color', 'face', 'size']),\n};\n\n/** CSS properties kept in a `style` attribute (formatting only, no layout tricks). */\nconst KEPT_STYLES = new Set([\n 'color',\n 'background-color',\n 'font-weight',\n 'font-style',\n 'font-size',\n 'font-family',\n 'text-decoration',\n 'text-align',\n 'line-height',\n 'letter-spacing',\n]);\n\n/** @param {string} style */\nfunction cleanStyle(style) {\n return style\n .split(';')\n .map((part) => part.trim())\n .filter(Boolean)\n .map((part) => {\n const colon = part.indexOf(':');\n if (colon === -1) return null;\n const name = part.slice(0, colon).trim().toLowerCase();\n const value = part.slice(colon + 1).trim();\n if (!KEPT_STYLES.has(name) || /expression|url\\s*\\(|javascript:/i.test(value)) return null;\n return `${name}:${value}`;\n })\n .filter(Boolean)\n .join(';');\n}\n\n/**\n * @param {Element} element\n */\nfunction cleanAttributes(element) {\n const tag = element.tagName.toLowerCase();\n const own = /** @type {Record<string, Set<string>>} */ (KEPT_ATTRIBUTES)[tag];\n for (const attribute of [...element.attributes]) {\n const name = attribute.name.toLowerCase();\n if (!KEPT_ATTRIBUTES['*'].has(name) && !(own && own.has(name))) {\n element.removeAttribute(attribute.name);\n continue;\n }\n if (name === 'href' && !isSafeUrl(attribute.value, 'link')) element.removeAttribute(attribute.name);\n if (name === 'style') {\n const style = cleanStyle(attribute.value);\n if (style) element.setAttribute('style', style);\n else element.removeAttribute('style');\n }\n }\n if (tag === 'a' && element.getAttribute('target') === '_blank') element.setAttribute('rel', 'noopener');\n}\n\n/** @param {Node} node */\nfunction cleanChildren(node) {\n for (const child of [...node.childNodes]) {\n if (child.nodeType === 8) {\n child.remove(); // comment\n continue;\n }\n if (child.nodeType !== 1) continue;\n const element = /** @type {Element} */ (child);\n const tag = element.tagName.toLowerCase();\n if (DROPPED_WITH_CONTENT.has(tag)) {\n element.remove();\n continue;\n }\n cleanChildren(element);\n if (!INLINE_TAGS.has(tag)) {\n // Unknown tag: keep its text, drop the tag.\n element.replaceWith(...element.childNodes);\n continue;\n }\n cleanAttributes(element);\n }\n}\n\n/**\n * @param {string} html\n * @param {Document} document any document (to parse without running anything)\n */\nexport function cleanRichText(html, document) {\n const template = document.createElement('template');\n template.innerHTML = String(html || '');\n cleanChildren(template.content);\n return template.innerHTML\n .replace(/<br>/g, '<br />')\n .replace(/(<br \\/>)+$/, '')\n .trim();\n}\n\n/**\n * Plain text, escaped, for a label (button, menu link).\n * @param {string} text\n */\nexport function escapeText(text) {\n return String(text ?? '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;');\n}\n\n/**\n * The text of an HTML fragment (`<br>` as a space), entities decoded.\n * @param {string} html\n */\nexport function textOfHtml(html) {\n return String(html || '')\n .replace(/<br\\s*\\/?>/gi, ' ')\n .replace(/<[^>]*>/g, '')\n .replace(/&nbsp;/g, ' ')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&amp;/g, '&')\n .trim();\n}\n"],"mappings":"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","names":[]}