@menapo/email 0.0.0-stage → 0.1.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.
@@ -0,0 +1,707 @@
1
+ //#region src/style.ts
2
+ const UNITLESS = /* @__PURE__ */ new Set([
3
+ "flex",
4
+ "flex-grow",
5
+ "flex-shrink",
6
+ "font-weight",
7
+ "line-height",
8
+ "opacity",
9
+ "order",
10
+ "z-index"
11
+ ]);
12
+ const kebab = (name) => name.startsWith("--") ? name : name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
13
+ /** Space-separated parts at the top level: `1px solid rgb(0 0 0)` → three. */
14
+ function parts(value) {
15
+ const out = [];
16
+ let depth = 0;
17
+ let start = 0;
18
+ for (let i = 0; i <= value.length; i++) {
19
+ const c = value[i];
20
+ if (c === "(") depth++;
21
+ else if (c === ")") depth--;
22
+ else if ((c === void 0 || c === " ") && depth === 0) {
23
+ if (i > start) out.push(value.slice(start, i));
24
+ start = i + 1;
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+ const SIDES = [
30
+ "top",
31
+ "right",
32
+ "bottom",
33
+ "left"
34
+ ];
35
+ /** `8px 16px` → top, right, bottom, left. */
36
+ function box(value) {
37
+ const [t = "0", r = t, b = t, l = r] = parts(value);
38
+ return [
39
+ t,
40
+ r,
41
+ b,
42
+ l
43
+ ];
44
+ }
45
+ /**
46
+ * Physical longhands for one declaration. Gmail and Outlook ignore logical
47
+ * properties, and box shorthands are expanded so a later longhand or
48
+ * shorthand overrides exactly what CSS would.
49
+ */
50
+ function physical(property, value, dir) {
51
+ const start = dir === "rtl" ? "right" : "left";
52
+ const end = dir === "rtl" ? "left" : "right";
53
+ const m = /^(padding|margin|inset|border)(?:-(inline|block))?(?:-(start|end))?(-width|-style|-color)?$/.exec(property);
54
+ if (m && (m[1] !== "border" || m[2])) {
55
+ const [, kind, axis, edge, suffix = ""] = m;
56
+ const side = (s) => kind === "inset" ? s : `${kind}-${s}${suffix}`;
57
+ if (!axis) {
58
+ if (kind === "inset") {
59
+ const v = box(value);
60
+ return SIDES.map((s, i) => [s, v[i]]);
61
+ }
62
+ if (kind === "border") return [[property, value]];
63
+ const v = box(value);
64
+ return SIDES.map((s, i) => [side(s), v[i]]);
65
+ }
66
+ const [a, b = a] = kind === "border" ? [value] : parts(value);
67
+ const pair = axis === "inline" ? [start, end] : ["top", "bottom"];
68
+ if (edge) return [[side(edge === "start" ? pair[0] : pair[1]), value]];
69
+ return [[side(pair[0]), a], [side(pair[1]), b]];
70
+ }
71
+ switch (property) {
72
+ case "text-align": return [[property, value === "start" ? start : value === "end" ? end : value]];
73
+ case "inline-size":
74
+ case "block-size":
75
+ case "min-inline-size":
76
+ case "max-inline-size":
77
+ case "min-block-size":
78
+ case "max-block-size": return [[property.replace("inline-size", "width").replace("block-size", "height"), value]];
79
+ case "background": return parts(value).length === 1 && !/url\(|gradient\(/.test(value) ? [["background-color", value]] : [[property, value]];
80
+ }
81
+ return [[property, value]];
82
+ }
83
+ const VAR = /var\((--[\w-]+)(?:,\s*([^()]*))?\)/g;
84
+ /** Sets a declaration as CSS would apply it after the ones already set. */
85
+ function apply(style, property, value, dir) {
86
+ for (const [p, v] of physical(property, value, dir)) {
87
+ style.delete(p);
88
+ style.set(p, v);
89
+ }
90
+ }
91
+ /**
92
+ * The element's style. Conditional rules of its classes are added to `rules`
93
+ * as CSS text.
94
+ */
95
+ function compute(node, reg, dir, rules) {
96
+ const style = /* @__PURE__ */ new Map();
97
+ const kept = [];
98
+ const cls = node.props.class;
99
+ if (cls) {
100
+ const inline = [];
101
+ for (const name of cls.split(" ")) {
102
+ if (!name) continue;
103
+ const entry = reg.css[name];
104
+ if (!entry) {
105
+ kept.push(name);
106
+ continue;
107
+ }
108
+ if (entry.i) inline.push(...entry.i);
109
+ if (entry.r) {
110
+ kept.push(name);
111
+ for (const rule of entry.r) rules.add(ruleCss(name, rule, dir));
112
+ }
113
+ }
114
+ inline.sort((a, b) => a[0] - b[0]);
115
+ for (const [, p, v] of inline) apply(style, p, v, dir);
116
+ }
117
+ const own = node.props.style;
118
+ if (own) for (const key in own) {
119
+ const value = own[key];
120
+ if (value == null) continue;
121
+ const p = kebab(key);
122
+ apply(style, p, typeof value === "number" && !UNITLESS.has(p) && value !== 0 ? `${value}px` : String(value), dir);
123
+ }
124
+ let custom = false;
125
+ for (const p of style.keys()) if (p.startsWith("--")) custom = true;
126
+ if (custom) {
127
+ for (const [p, v] of style) if (v.includes("var(")) style.set(p, v.replace(VAR, (match, name, fallback) => style.get(name) ?? fallback ?? match));
128
+ for (const p of style.keys()) if (p.startsWith("--")) style.delete(p);
129
+ }
130
+ return {
131
+ style,
132
+ classes: kept
133
+ };
134
+ }
135
+ /** CSS identifier escaping for a class in a selector (readable vicss names hold `:` and `.`). */
136
+ const escapeClass = (name) => name.replace(/^(\d)|[^\w-]/g, (c, digit) => digit ? `\\3${digit} ` : `\\${c}`);
137
+ function ruleCss(name, [selector, atRules, property, value], dir) {
138
+ const body = physical(property, value, dir).map(([p, v]) => `${p}:${v}!important`).join(";");
139
+ const block = `${selector.replaceAll("&", `.${escapeClass(name)}`)}{${body}}`;
140
+ return atRules.reduceRight((inner, at) => `${at}{${inner}}`, block);
141
+ }
142
+ /** The `style` attribute: padding and margin written as shorthands when all four sides are set. */
143
+ function styleText(style) {
144
+ let out = "";
145
+ const done = /* @__PURE__ */ new Set();
146
+ for (const [p, v] of style) {
147
+ if (done.has(p)) continue;
148
+ const m = /^(padding|margin)-(top|right|bottom|left)$/.exec(p);
149
+ if (m) {
150
+ const all = SIDES.map((s) => style.get(`${m[1]}-${s}`));
151
+ if (all.every((x) => x !== void 0)) {
152
+ for (const s of SIDES) done.add(`${m[1]}-${s}`);
153
+ const [t, r, b, l] = all;
154
+ const short = t === b && r === l ? t === r ? t : `${t} ${r}` : r === l ? `${t} ${r} ${b}` : `${t} ${r} ${b} ${l}`;
155
+ out += `${m[1]}:${short};`;
156
+ continue;
157
+ }
158
+ }
159
+ out += `${p}:${v};`;
160
+ }
161
+ return out.slice(0, -1);
162
+ }
163
+ //#endregion
164
+ //#region src/html.ts
165
+ const VOID = /* @__PURE__ */ new Set([
166
+ "area",
167
+ "br",
168
+ "col",
169
+ "hr",
170
+ "img",
171
+ "meta",
172
+ "source",
173
+ "wbr"
174
+ ]);
175
+ const FORBIDDEN = /* @__PURE__ */ new Set([
176
+ "script",
177
+ "iframe",
178
+ "object",
179
+ "embed",
180
+ "form",
181
+ "input",
182
+ "style",
183
+ "link",
184
+ "base",
185
+ "meta"
186
+ ]);
187
+ const SAFE_URL = /^(?:https?:|mailto:|tel:|cid:|#)/i;
188
+ const BOXES = /* @__PURE__ */ new Set([
189
+ "div",
190
+ "section",
191
+ "header",
192
+ "footer",
193
+ "main",
194
+ "article",
195
+ "aside",
196
+ "center"
197
+ ]);
198
+ const TABLE = "role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"";
199
+ const escapeText = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
200
+ const escapeAttr = (s) => escapeText(s).replace(/"/g, "&quot;");
201
+ function warn(ctx, message) {
202
+ const text = `email "${ctx.name}": ${message}`;
203
+ if (ctx.warned.has(text)) return;
204
+ ctx.warned.add(text);
205
+ (ctx.reg.warn ?? console.warn)(text);
206
+ }
207
+ function computedOf(node, ctx) {
208
+ let c = ctx.computed.get(node);
209
+ if (!c) ctx.computed.set(node, c = compute(node, ctx.reg, ctx.dir, ctx.rules));
210
+ return c;
211
+ }
212
+ function attributes(node, classes, style, extra = "") {
213
+ let out = "";
214
+ for (const name in node.props) {
215
+ if (name === "class" || name === "style") continue;
216
+ const value = node.props[name];
217
+ if (/^on/i.test(name)) throw new Error(`email: <${node.type} ${name}> is an event handler; mail clients run no script. Remove it.`);
218
+ if (value === true) {
219
+ out += ` ${name}`;
220
+ continue;
221
+ }
222
+ if (typeof value === "function" || typeof value === "object" && value !== null) throw new TypeError(`email: <${node.type} ${name}=…> is an object; pass a string or a number.`);
223
+ const text = String(value);
224
+ if ((name === "href" || name === "src") && !SAFE_URL.test(text.trim())) throw new Error(`email: <${node.type} ${name}=${JSON.stringify(text)}> needs an absolute https, http, mailto, tel or cid URL; mail has no base URL and clients block other schemes.`);
225
+ out += ` ${name}="${escapeAttr(text)}"`;
226
+ }
227
+ if (classes.length) out += ` class="${escapeAttr(classes.join(" "))}"`;
228
+ const css = styleText(style);
229
+ if (css) out += ` style="${escapeAttr(css)}"`;
230
+ return out + extra;
231
+ }
232
+ const px = (value) => value && /^\d+(\.\d+)?px$/.test(value) ? parseFloat(value) : void 0;
233
+ const take = (style, ...names) => {
234
+ for (const name of names) style.delete(name);
235
+ };
236
+ const elementsOf = (node) => node.children.filter((c) => typeof c !== "string" || c.trim());
237
+ function children$1(node, ctx, align) {
238
+ let out = "";
239
+ for (const child of node.children) out += write(child, ctx, align);
240
+ return out;
241
+ }
242
+ function features(style) {
243
+ const out = [];
244
+ for (const [p, v] of style) {
245
+ const d = `${p}:${v}`;
246
+ if (p === "display" && /flex/.test(v)) out.push(["display:flex", d]);
247
+ else if (p === "display" && /grid/.test(v)) out.push(["display:grid", d]);
248
+ else if (p === "position" && v !== "static") out.push(["position", d]);
249
+ else if (/^(flex-wrap|justify-content|transform|aspect-ratio|object-fit|background-image)$/.test(p)) out.push([p, d]);
250
+ else if (/^(row-|column-)?gap$/.test(p)) out.push(["gap", d]);
251
+ if (v.includes("var(")) out.push(["var()", d]);
252
+ }
253
+ return out;
254
+ }
255
+ function check(node, style, ctx) {
256
+ const checks = ctx.reg.checks;
257
+ if (!checks) return;
258
+ for (const [key, declaration] of features(style)) {
259
+ const c = checks[key];
260
+ if (c) warn(ctx, `<${node.type}> has ${declaration}, which ${c.clients} drop. ${c.fix}`);
261
+ }
262
+ if (node.type === "img" && node.props.alt == null) warn(ctx, `<img src="${String(node.props.src)}"> has no alt. Add alt="…" (alt="" for a decorative image).`);
263
+ const cls = node.props.class;
264
+ if (cls && /(?:^|\s)mi-/.test(cls)) warn(ctx, "an icon() is SVG, which Gmail and Outlook do not show. Use <img src=\"https://…/icon.png\" alt=\"…\" width=\"16\" height=\"16\"> with a PNG at an absolute URL.");
265
+ }
266
+ /** An `<a>` with a background and padding: a one-cell table Outlook pads (D8). */
267
+ function button(node, c, ctx, align) {
268
+ const style = new Map(c.style);
269
+ const bg = style.get("background-color");
270
+ const pad = [
271
+ "top",
272
+ "right",
273
+ "bottom",
274
+ "left"
275
+ ].map((s) => style.get(`padding-${s}`) ?? "0");
276
+ const outer = /* @__PURE__ */ new Map();
277
+ for (const s of [
278
+ "top",
279
+ "right",
280
+ "bottom",
281
+ "left"
282
+ ]) {
283
+ const m = style.get(`margin-${s}`);
284
+ if (m !== void 0) outer.set(`margin-${s}`, m);
285
+ style.delete(`margin-${s}`);
286
+ }
287
+ const full = style.get("display") === "block" || style.get("width") === "100%";
288
+ style.set("display", full ? "block" : "inline-block");
289
+ if (!style.has("text-decoration")) style.set("text-decoration", "none");
290
+ const cell = /* @__PURE__ */ new Map([["background-color", bg]]);
291
+ const radius = style.get("border-radius");
292
+ if (radius) cell.set("border-radius", radius);
293
+ cell.set("mso-padding-alt", pad.join(" "));
294
+ const center = align === "center";
295
+ if (center) outer.set("margin-left", "auto").set("margin-right", "auto");
296
+ check(node, style, ctx);
297
+ return `${`<table ${TABLE}${center ? " align=\"center\"" : ""}${full ? " width=\"100%\"" : ""}${outer.size ? ` style="${escapeAttr(styleText(outer))}"` : ""}>`}<tr><td align="center" bgcolor="${escapeAttr(bg)}" style="${escapeAttr(styleText(cell))}"><a${attributes(node, c.classes, style)}>${children$1(node, ctx, void 0)}</a></td></tr></table>`;
298
+ }
299
+ /** A `max-width` (or fixed-width, auto-margin) box: a table Outlook gives a width (D9). */
300
+ function boxTable(node, c, ctx, width, max) {
301
+ const style = new Map(c.style);
302
+ const centered = style.get("margin-left") === "auto" && style.get("margin-right") === "auto";
303
+ const outer = /* @__PURE__ */ new Map();
304
+ if (max) outer.set("max-width", `${width}px`);
305
+ outer.set("width", "100%");
306
+ for (const s of ["top", "bottom"]) {
307
+ const m = style.get(`margin-${s}`);
308
+ if (m !== void 0) outer.set(`margin-${s}`, m);
309
+ }
310
+ if (centered) outer.set("margin-left", "auto").set("margin-right", "auto");
311
+ take(style, "max-width", "width", "margin-top", "margin-right", "margin-bottom", "margin-left");
312
+ const align = centered ? "center" : ctx.dir === "rtl" ? "right" : "left";
313
+ const bg = style.get("background-color");
314
+ check(node, style, ctx);
315
+ return `<!--[if mso]><table ${TABLE} align="${align}" width="${width}"><tr><td><![endif]--><table ${TABLE} align="${align}" width="100%" style="${escapeAttr(styleText(outer))}"><tr><td${attributes(node, c.classes, style, bg ? ` bgcolor="${escapeAttr(bg)}"` : "")}>${children$1(node, ctx, style.get("text-align"))}</td></tr></table><!--[if mso]></td></tr></table><![endif]-->`;
316
+ }
317
+ const VALIGN = {
318
+ center: "middle",
319
+ "flex-end": "bottom",
320
+ end: "bottom"
321
+ };
322
+ const SIMPLE_JUSTIFY = /* @__PURE__ */ new Set([
323
+ "flex-start",
324
+ "start",
325
+ "normal",
326
+ "left"
327
+ ]);
328
+ /** A flex row or column of elements: table cells, `gap` as their padding (D9). */
329
+ function flexTable(node, c, ctx, align) {
330
+ const style = c.style;
331
+ const direction = style.get("flex-direction") ?? "row";
332
+ const wrap = style.get("flex-wrap");
333
+ const justify = style.get("justify-content");
334
+ const items = elementsOf(node);
335
+ if (style.get("display") !== "flex" || direction !== "row" && direction !== "column" || wrap && wrap !== "nowrap" || justify && !SIMPLE_JUSTIFY.has(justify) || items.some((i) => typeof i === "string")) return void 0;
336
+ const rest = new Map(style);
337
+ const [rowGap, columnGap = rowGap] = parts(style.get("gap") ?? "");
338
+ const gap = direction === "row" ? style.get("column-gap") ?? columnGap : style.get("row-gap") ?? rowGap;
339
+ const alignItems = style.get("align-items") ?? "";
340
+ take(rest, "display", "flex-direction", "flex-wrap", "gap", "row-gap", "column-gap", "align-items", "justify-content");
341
+ const inner = rest.get("text-align") ?? align;
342
+ const start = ctx.dir === "rtl" ? "right" : "left";
343
+ let cells = "";
344
+ items.forEach((item, i) => {
345
+ const cell = /* @__PURE__ */ new Map();
346
+ if (i > 0 && gap && gap !== "0" && gap !== "0px") cell.set(direction === "row" ? `padding-${start}` : "padding-top", gap);
347
+ const own = computedOf(item, ctx).style;
348
+ const w = own.get("width") ?? own.get("flex-basis");
349
+ const td = `<td${(w && /^\d+(\.\d+)?(px|%)$/.test(w) ? ` width="${w.endsWith("%") ? w : parseFloat(w)}"` : "") + (direction === "row" ? ` valign="${VALIGN[alignItems] ?? "top"}"` : alignItems === "center" ? " align=\"center\"" : /end/.test(alignItems) ? ` align="${ctx.dir === "rtl" ? "left" : "right"}"` : "") + (cell.size ? ` style="${escapeAttr(styleText(cell))}"` : "")}>${write(item, ctx, inner)}</td>`;
350
+ cells += direction === "row" ? td : `<tr>${td}</tr>`;
351
+ });
352
+ const grid = `<table ${TABLE} width="100%">${direction === "row" ? `<tr>${cells}</tr>` : cells}</table>`;
353
+ check(node, rest, ctx);
354
+ const outer = /* @__PURE__ */ new Map();
355
+ for (const s of [
356
+ "top",
357
+ "right",
358
+ "bottom",
359
+ "left"
360
+ ]) {
361
+ const m = rest.get(`margin-${s}`);
362
+ if (m !== void 0) outer.set(`margin-${s}`, m);
363
+ rest.delete(`margin-${s}`);
364
+ }
365
+ const bg = rest.get("background-color");
366
+ const attrs = attributes(node, c.classes, rest, bg ? ` bgcolor="${escapeAttr(bg)}"` : "");
367
+ const margins = outer.size ? ` style="${escapeAttr(styleText(outer))}"` : "";
368
+ if (attrs) return `<table ${TABLE} width="100%"${margins}><tr><td${attrs}>${grid}</td></tr></table>`;
369
+ return margins ? grid.replace("width=\"100%\">", `width="100%"${margins}>`) : grid;
370
+ }
371
+ /** Whether `node` is written as a table: a button, a flex row or column, a box. */
372
+ function becomesTable(node, ctx) {
373
+ const { style } = computedOf(node, ctx);
374
+ const tag = node.type.toLowerCase();
375
+ if (tag === "a") return style.has("background-color") && [...style.keys()].some((p) => p.startsWith("padding"));
376
+ if (style.get("display") === "flex") return true;
377
+ return BOXES.has(tag) && (px(style.get("max-width")) !== void 0 || px(style.get("width")) !== void 0);
378
+ }
379
+ /** One child as HTML; `align` is the text-align it inherits, which a button table follows. */
380
+ function write(child, ctx, align) {
381
+ if (typeof child === "string") return escapeText(child);
382
+ const node = child;
383
+ const tag = node.type.toLowerCase();
384
+ if (FORBIDDEN.has(tag)) throw new Error(`email: <${tag}> is not allowed in a template; mail clients strip it. The <head> and its <style> are render()'s own.`);
385
+ const c = computedOf(node, ctx);
386
+ const style = c.style;
387
+ if (tag === "a" && style.has("background-color") && [...style.keys()].some((p) => p.startsWith("padding"))) return button(node, c, ctx, align);
388
+ if (style.get("display") === "flex") {
389
+ const table = flexTable(node, c, ctx, align);
390
+ if (table) return table;
391
+ }
392
+ if (BOXES.has(tag)) {
393
+ const max = px(style.get("max-width"));
394
+ if (max) return boxTable(node, c, ctx, max, true);
395
+ const width = px(style.get("width"));
396
+ if (width && style.get("margin-left") === "auto" && style.get("margin-right") === "auto") return boxTable(node, c, ctx, width, false);
397
+ }
398
+ let extra = "";
399
+ let wrap;
400
+ let out = style;
401
+ let name = tag;
402
+ if (/^(p|h[1-6])$/.test(tag) && node.children.some((k) => typeof k !== "string" && becomesTable(k, ctx))) {
403
+ name = "div";
404
+ if (tag === "p") {
405
+ out = new Map(style);
406
+ if (!out.has("margin-top")) out.set("margin-top", "1em");
407
+ if (!out.has("margin-bottom")) out.set("margin-bottom", "1em");
408
+ }
409
+ }
410
+ if (tag === "img") {
411
+ out = new Map(style);
412
+ if (!out.has("display")) out.set("display", "block");
413
+ for (const side of ["width", "height"]) {
414
+ const n = px(out.get(side));
415
+ if (n !== void 0 && node.props[side] == null) extra += ` ${side}="${n}"`;
416
+ }
417
+ if (out.get("margin-left") === "auto" && out.get("margin-right") === "auto") {
418
+ take(out, "margin-left", "margin-right");
419
+ wrap = [`<table ${TABLE} align="center"><tr><td>`, "</td></tr></table>"];
420
+ }
421
+ } else if ((tag === "td" || tag === "table") && style.get("background-color")) extra += ` bgcolor="${escapeAttr(style.get("background-color"))}"`;
422
+ check(node, out, ctx);
423
+ const open = `<${name}${attributes(node, c.classes, out, extra)}>`;
424
+ const html = VOID.has(tag) ? open : `${open}${children$1(node, ctx, style.get("text-align") ?? align)}</${name}>`;
425
+ return wrap ? wrap[0] + html + wrap[1] : html;
426
+ }
427
+ //#endregion
428
+ //#region src/registry.ts
429
+ const KEY = Symbol.for("@menapo/email");
430
+ function registry() {
431
+ const g = globalThis;
432
+ return g[KEY] ??= { css: {} };
433
+ }
434
+ //#endregion
435
+ //#region src/text.ts
436
+ const BLOCKS = /* @__PURE__ */ new Set([
437
+ "address",
438
+ "article",
439
+ "aside",
440
+ "blockquote",
441
+ "center",
442
+ "dd",
443
+ "div",
444
+ "dl",
445
+ "dt",
446
+ "footer",
447
+ "h1",
448
+ "h2",
449
+ "h3",
450
+ "h4",
451
+ "h5",
452
+ "h6",
453
+ "header",
454
+ "main",
455
+ "ol",
456
+ "p",
457
+ "pre",
458
+ "section",
459
+ "table",
460
+ "tr",
461
+ "ul"
462
+ ]);
463
+ const BREAK = "\0";
464
+ function textOf(root, styles) {
465
+ let out = "";
466
+ const walk = (child, list) => {
467
+ if (typeof child === "string") {
468
+ out += child.replace(/\s+/g, " ");
469
+ return;
470
+ }
471
+ const { type, props } = child;
472
+ const style = styles.get(child)?.style;
473
+ if (props.hidden != null || props["aria-hidden"] === "true" || style?.get("display") === "none") return;
474
+ switch (type) {
475
+ case "br":
476
+ out += "\n";
477
+ return;
478
+ case "hr":
479
+ out += `${BREAK}---${BREAK}`;
480
+ return;
481
+ case "img":
482
+ if (typeof props.alt === "string" && props.alt) out += props.alt;
483
+ return;
484
+ case "a": {
485
+ const start = out.length;
486
+ for (const c of child.children) walk(c, list);
487
+ const label = out.slice(start).trim();
488
+ const href = typeof props.href === "string" ? props.href : "";
489
+ const shown = href.replace(/^mailto:|^tel:/, "");
490
+ if (href && shown !== label) out = `${out.slice(0, start)}${label ? `${label}: ` : ""}${href}`;
491
+ return;
492
+ }
493
+ case "li":
494
+ out += `\n${list?.ordered ? `${++list.n}. ` : "- "}`;
495
+ for (const c of child.children) walk(c);
496
+ out += "\n";
497
+ return;
498
+ case "td":
499
+ case "th":
500
+ for (const c of child.children) walk(c, list);
501
+ out += " ";
502
+ return;
503
+ }
504
+ const block = BLOCKS.has(type);
505
+ if (block) out += BREAK;
506
+ const inner = type === "ol" || type === "ul" ? {
507
+ ordered: type === "ol",
508
+ n: 0
509
+ } : list;
510
+ for (const c of child.children) walk(c, inner);
511
+ if (block) out += BREAK;
512
+ };
513
+ for (const child of root) walk(child);
514
+ return out.split(BREAK).map((block) => block.split("\n").map((line) => line.replace(/[ \t]+/g, " ").trim()).filter(Boolean).join("\n")).filter(Boolean).join("\n\n");
515
+ }
516
+ //#endregion
517
+ //#region ../../node_modules/.pnpm/htm@3.1.1/node_modules/htm/dist/htm.mjs
518
+ var n = function(t, s, r, e) {
519
+ var u;
520
+ s[0] = 0;
521
+ for (var h = 1; h < s.length; h++) {
522
+ var p = s[h++], a = s[h] ? (s[0] |= p ? 1 : 2, r[s[h++]]) : s[++h];
523
+ 3 === p ? e[0] = a : 4 === p ? e[1] = Object.assign(e[1] || {}, a) : 5 === p ? (e[1] = e[1] || {})[s[++h]] = a : 6 === p ? e[1][s[++h]] += a + "" : p ? (u = t.apply(a, n(t, a, r, ["", null])), e.push(u), a[0] ? s[0] |= 2 : (s[h - 2] = 0, s[h] = u)) : e.push(a);
524
+ }
525
+ return e;
526
+ };
527
+ var t = /* @__PURE__ */ new Map();
528
+ function htm_default(s) {
529
+ var r = t.get(this);
530
+ return r || (r = /* @__PURE__ */ new Map(), t.set(this, r)), (r = n(this, r.get(s) || (r.set(s, r = function(n) {
531
+ for (var t, s, r = 1, e = "", u = "", h = [0], p = function(n) {
532
+ 1 === r && (n || (e = e.replace(/^\s*\n\s*|\s*\n\s*$/g, ""))) ? h.push(0, n, e) : 3 === r && (n || e) ? (h.push(3, n, e), r = 2) : 2 === r && "..." === e && n ? h.push(4, n, 0) : 2 === r && e && !n ? h.push(5, 0, !0, e) : r >= 5 && ((e || !n && 5 === r) && (h.push(r, 0, e, s), r = 6), n && (h.push(r, n, 0, s), r = 6)), e = "";
533
+ }, a = 0; a < n.length; a++) {
534
+ a && (1 === r && p(), p(a));
535
+ for (var l = 0; l < n[a].length; l++) t = n[a][l], 1 === r ? "<" === t ? (p(), h = [h], r = 3) : e += t : 4 === r ? "--" === e && ">" === t ? (r = 1, e = "") : e = t + e[0] : u ? t === u ? u = "" : e += t : "\"" === t || "'" === t ? u = t : ">" === t ? (p(), r = 1) : r && ("=" === t ? (r = 5, s = e, e = "") : "/" === t && (r < 5 || ">" === n[a][l + 1]) ? (p(), 3 === r && (h = h[0]), r = h, (h = h[0]).push(2, 0, r), r = 0) : " " === t || " " === t || "\n" === t || "\r" === t ? (p(), r = 2) : e += t), 3 === r && "!--" === e && (r = 4, h = h[0]);
536
+ }
537
+ return p(), h;
538
+ }(s)), r), arguments, [])).length > 1 ? r : r[0];
539
+ }
540
+ //#endregion
541
+ //#region ../markup/src/index.ts
542
+ /**
543
+ * `@menapo/markup`: kit's templates (og PLAN §5). `html` is htm's parser
544
+ * bound to a node builder of ours, so a template becomes a plain tree that
545
+ * any renderer can walk (og's images now, e-mail's HTML later) with no
546
+ * framework. Holes are values: text is never parsed as markup, so a prop
547
+ * cannot inject an element.
548
+ */
549
+ /**
550
+ * vicss's `sx()` merge (packages/vicss/src/runtime/merge.ts), restated so
551
+ * templates need no `sx()` and markup no vicss: class names are kept in
552
+ * order, and a style's merge ids replace the same id and drop the ids below
553
+ * them in their chain (`p` drops `p-b-s`, `md:p` does not). A test checks
554
+ * the two agree, so they cannot drift.
555
+ */
556
+ function classOf(value, style) {
557
+ const names = /* @__PURE__ */ new Set();
558
+ const kept = /* @__PURE__ */ new Map();
559
+ const add = (list) => {
560
+ for (const name of list.split(/\s+/)) if (name) names.add(name);
561
+ };
562
+ for (let arg of [value].flat(Infinity)) {
563
+ if (!arg) continue;
564
+ if (typeof arg === "string") {
565
+ add(arg);
566
+ continue;
567
+ }
568
+ const dynamic = arg;
569
+ if (dynamic.$c) {
570
+ Object.assign(style, dynamic.$s);
571
+ arg = dynamic.$c;
572
+ }
573
+ const record = arg;
574
+ for (const id of kept.keys()) for (const key in record) if (id[key.length] === "-" && id.startsWith(key) && !id.includes(":", key.length)) {
575
+ kept.delete(id);
576
+ break;
577
+ }
578
+ for (const id in record) if (id === "$n") add(record[id]);
579
+ else kept.set(id, record[id]);
580
+ }
581
+ return [...names, ...[...kept.keys()].sort().map((id) => kept.get(id))].join(" ");
582
+ }
583
+ function children(list, into = []) {
584
+ for (const child of list) {
585
+ if (child == null || typeof child === "boolean") continue;
586
+ if (Array.isArray(child)) children(child, into);
587
+ else if (typeof child === "object") into.push(child);
588
+ else into.push(String(child));
589
+ }
590
+ return into;
591
+ }
592
+ function h(type, attributes, ...kids) {
593
+ if (typeof type === "function") return type({
594
+ ...attributes,
595
+ children: children(kids)
596
+ });
597
+ const props = {};
598
+ const style = {};
599
+ let cls;
600
+ for (const name in attributes) {
601
+ const value = attributes[name];
602
+ if (name === "class" || name === "className") (cls ??= []).push(value);
603
+ else if (name === "style") {
604
+ if (typeof value === "string") throw new TypeError(`markup: <${type} style="…"> is a string; pass an object: style=\${{ color: "red" }}.`);
605
+ Object.assign(style, value);
606
+ } else if (value != null && value !== false) props[name] = value;
607
+ }
608
+ if (cls) {
609
+ const merged = classOf(cls, style);
610
+ if (merged) props.class = merged;
611
+ }
612
+ for (const _ in style) {
613
+ props.style = style;
614
+ break;
615
+ }
616
+ return {
617
+ type,
618
+ props,
619
+ children: children(kids)
620
+ };
621
+ }
622
+ const bound = htm_default.bind(h);
623
+ /**
624
+ * A template: `` html`<p class=${card} ...${attrs}>${text}</p>` `` → a node
625
+ * tree. Several roots give an array.
626
+ */
627
+ const html = (strings, ...values) => bound(strings, ...values);
628
+ //#endregion
629
+ //#region src/email.ts
630
+ const KIND = Symbol.for("@menapo/email:email");
631
+ /** A defined email: pass it to `render()`. */
632
+ var Email = class {
633
+ [KIND] = true;
634
+ template;
635
+ samples;
636
+ constructor(template, samples = {}) {
637
+ this.template = template;
638
+ this.samples = samples;
639
+ }
640
+ };
641
+ /** Whether `value` is an `email()`, whichever copy of this package made it. */
642
+ const isEmail = (value) => typeof value === "object" && value !== null && value[KIND] === true;
643
+ /** Defines an email. The props type is the parameter's. */
644
+ function email(template, options = {}) {
645
+ return new Email(template, options.samples);
646
+ }
647
+ //#endregion
648
+ //#region src/index.ts
649
+ function direction(locale) {
650
+ try {
651
+ const l = new Intl.Locale(locale);
652
+ return (l.getTextInfo?.() ?? l.textInfo)?.direction === "rtl" ? "rtl" : "ltr";
653
+ } catch {
654
+ return "ltr";
655
+ }
656
+ }
657
+ const escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
658
+ const RESET = "body{margin:0;padding:0;width:100%!important;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}table,td{border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt}img{border:0;outline:none;text-decoration:none;-ms-interpolation-mode:bicubic;height:auto;line-height:100%}a[x-apple-data-detectors]{color:inherit!important;text-decoration:none!important;font-size:inherit!important;font-family:inherit!important;font-weight:inherit!important;line-height:inherit!important}";
659
+ const MSO = "<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><style>td,th,div,p,a,h1,h2,h3,h4,h5,h6{mso-line-height-rule:exactly}</style><![endif]-->";
660
+ const FILLER = "&#847;&zwnj;&nbsp;".repeat(60);
661
+ function shell(subject, preheader, body, rules, lang, dir) {
662
+ const dark = [...rules].some((r) => r.includes("prefers-color-scheme") || r.includes("[data-ogsc]") || r.includes("[data-ogsb]"));
663
+ const langAttrs = `${lang ? ` lang="${escape(lang)}"` : ""}${dir ? ` dir="${dir}"` : ""}`;
664
+ return `<!doctype html><html${langAttrs} xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="x-apple-disable-message-reformatting"><meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">` + (dark ? "<meta name=\"color-scheme\" content=\"light dark\"><meta name=\"supported-color-schemes\" content=\"light dark\">" : "") + `<title>${escape(subject)}</title>${MSO}<style>${RESET}${[...rules].join("")}</style></head><body style="margin:0;padding:0;word-spacing:normal"><div role="article" aria-roledescription="email" aria-label="${escape(subject)}"${langAttrs}>` + (preheader ? `<div style="display:none;max-height:0;overflow:hidden;mso-hide:all">${escape(preheader)}${FILLER}</div>` : "") + `<table role="presentation" width="100%" border="0" cellpadding="0" cellspacing="0" align="center"><tr><td>${body}</td></tr></table></div></body></html>`;
665
+ }
666
+ const list = (markup) => Array.isArray(markup) ? markup : [markup];
667
+ /** Renders an email for one recipient: its subject, HTML and plain text. */
668
+ async function render(mail, props, options = {}) {
669
+ const reg = registry();
670
+ const { locale, timeZone } = options;
671
+ const run = async () => {
672
+ const message = await mail.template(props);
673
+ const { lang, dir } = reg.i18n ? reg.i18n.htmlAttributes() : {
674
+ lang: locale,
675
+ dir: locale ? direction(locale) : void 0
676
+ };
677
+ const ctx = {
678
+ reg,
679
+ dir: dir ?? "ltr",
680
+ rules: /* @__PURE__ */ new Set(),
681
+ name: options.name ?? "email",
682
+ computed: /* @__PURE__ */ new WeakMap(),
683
+ warned: /* @__PURE__ */ new Set()
684
+ };
685
+ const roots = list(message.body);
686
+ let body = "";
687
+ for (const child of roots) body += write(child, ctx);
688
+ const text = textOf(roots, ctx.computed);
689
+ if (reg.checks && body.length > 1e5) warn(ctx, `the HTML is ${Math.round(body.length / 1e3)} KB; Gmail clips mail over about 102 KB. Shorten it.`);
690
+ return {
691
+ subject: message.subject,
692
+ html: shell(message.subject, message.preheader, body, ctx.rules, lang, dir),
693
+ text
694
+ };
695
+ };
696
+ if (reg.i18n) {
697
+ if (locale !== void 0) return reg.i18n.withLocale(locale, run, timeZone === void 0 ? {} : { timeZone });
698
+ try {
699
+ reg.i18n.htmlAttributes();
700
+ } catch (cause) {
701
+ throw new Error(`email: render(${options.name ?? "…"}) has no locale and runs outside a request, so t() has none. Pass the recipient's: render(email, props, { locale: user.locale }).`, { cause });
702
+ }
703
+ }
704
+ return run();
705
+ }
706
+ //#endregion
707
+ export { registry as a, html as i, email as n, isEmail as r, render as t };