@samva/markup 0.16.0 → 0.17.1

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 (60) hide show
  1. package/AGENTS.md +42 -40
  2. package/CHANGELOG.md +72 -0
  3. package/README.md +10 -9
  4. package/dist/{brand-jFV6YtSV.d.ts → assets-CS5Xa_mr.d.ts} +41 -3
  5. package/dist/{brand-Ci05p37c.js → brand-CIiEqkU7.js} +64 -3
  6. package/dist/{brand-plugin-CHhcei-j.d.ts → brand-plugin-Dj9SMpfS.d.ts} +3 -1
  7. package/dist/brand.d.ts +2 -3
  8. package/dist/brand.js +3 -4
  9. package/dist/compatibility.d.ts +76 -0
  10. package/dist/compatibility.js +807 -0
  11. package/dist/compiled-template-Bc4Dq9zy.d.ts +268 -0
  12. package/dist/compiler.d.ts +59 -193
  13. package/dist/compiler.js +473 -775
  14. package/dist/{components-3XHjYD3i.js → components-C5850-Qg.js} +37 -29
  15. package/dist/css-B_aVKrDo.js +436 -0
  16. package/dist/css-color-DLM12Osu.js +315 -0
  17. package/dist/{diagnostic-codes-8XDLR9ef.js → diagnostic-codes-DZKb0Xcg.js} +40 -1
  18. package/dist/{diagnostics-CUYlQ_6x.d.ts → diagnostics-D4vSLQrO.d.ts} +1 -10
  19. package/dist/diagnostics.d.ts +6 -2
  20. package/dist/diagnostics.js +3 -4
  21. package/dist/edit.d.ts +10 -9
  22. package/dist/edit.js +12 -11
  23. package/dist/email/jsx-dev-runtime.d.ts +1 -1
  24. package/dist/email/jsx-dev-runtime.js +1 -1
  25. package/dist/email/jsx-runtime.d.ts +2 -2
  26. package/dist/email/jsx-runtime.js +2 -2
  27. package/dist/email.d.ts +1 -1
  28. package/dist/email.js +1 -1
  29. package/dist/html-DJUK2YJF.js +483 -0
  30. package/dist/index.d.ts +1 -1
  31. package/dist/{jsx-runtime-DC7xG0aA.d.ts → jsx-runtime-DL8NVF1f.d.ts} +12 -6
  32. package/dist/{jsx-runtime-CBlexoeR.js → jsx-runtime-qL19u3Fy.js} +17 -10
  33. package/dist/preview-rNoJBEIY.d.ts +104 -0
  34. package/dist/{profile-BsY4dkan.js → profile-JTvgCD6X.js} +714 -29
  35. package/dist/{html-BvRFepeO.js → render-template-DwPDm3Hq.js} +155 -572
  36. package/dist/render.d.ts +40 -28
  37. package/dist/render.js +3 -47
  38. package/dist/selectors-CYWCoxZr.js +157 -0
  39. package/dist/sms.d.ts +1 -1
  40. package/dist/sms.js +1 -1
  41. package/dist/{template-B-UDXK8a.d.ts → template-DmNcoH4Q.d.ts} +2 -9
  42. package/dist/{theme-css-s9QwD5Ko.js → theme-css-P9F5S7Ic.js} +3 -316
  43. package/dist/whatsapp.d.ts +1 -1
  44. package/dist/whatsapp.js +1 -1
  45. package/generated/contract.json +7 -2
  46. package/generated/reference.md +3 -2
  47. package/package.json +5 -5
  48. package/skills/sml/SKILL.md +2 -2
  49. package/dist/assets-BSpgZOSR.js +0 -61
  50. package/dist/assets-BSuNT9gr.d.ts +0 -39
  51. package/dist/brand-footer-ClRtWnIc.d.ts +0 -40
  52. package/dist/brand-footer-DXVGsKE5.js +0 -746
  53. package/dist/brand-sample.d.ts +0 -4
  54. package/dist/brand-sample.js +0 -2
  55. package/dist/cascade-DoLWZ66g.js +0 -701
  56. package/dist/diagnostic-model-DJtqbETd.js +0 -36
  57. package/dist/ir-KJLFW08u.js +0 -5
  58. package/dist/render-CVnXZMyK.js +0 -258
  59. package/dist/render-CuZej25A.d.ts +0 -564
  60. package/dist/theme-imports-u_P0ygvQ.js +0 -102
@@ -1,701 +0,0 @@
1
- import { n as isObject } from "./guards-eV1qLbf2.js";
2
- import { a as kebab, n as declarationValue, r as escapeAttribute } from "./html-BvRFepeO.js";
3
- import { t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
4
- import { i as withinDarkScheme, t as DARK_SCHEME_MEDIA } from "./dark-scheme-CbCwGDWH.js";
5
- //#region src/email/font-stacks.ts
6
- /** Generic families every email client understands as the last resort in a stack. */
7
- const GENERIC_FAMILIES = /* @__PURE__ */ new Set([
8
- "serif",
9
- "sans-serif",
10
- "monospace",
11
- "cursive",
12
- "fantasy"
13
- ]);
14
- /** One family name as written, without its quotes and with runs of spaces collapsed. */
15
- const unquote = (value) => {
16
- const trimmed = value.trim();
17
- const quote = trimmed[0];
18
- return (quote === "\"" || quote === "'") && trimmed.endsWith(quote) && trimmed.length >= 2 ? trimmed.slice(1, -1) : trimmed.replace(/\s+/g, " ");
19
- };
20
- /** The families of a `font-family` value, unquoted, in stack order. */
21
- const fontStackFamilies = (value) => {
22
- const families = [];
23
- let start = 0;
24
- let quote;
25
- for (let index = 0; index < value.length; index++) {
26
- const character = value[index];
27
- if (quote !== void 0) {
28
- if (character === quote) quote = void 0;
29
- } else if (character === "'" || character === "\"") quote = character;
30
- else if (character === ",") {
31
- families.push(unquote(value.slice(start, index)));
32
- start = index + 1;
33
- }
34
- }
35
- families.push(unquote(value.slice(start)));
36
- return families.filter((family) => family !== "");
37
- };
38
- const FONT_SIZE = /^(?:xx-small|x-small|small|medium|large|x-large|xx-large|xxx-large|larger|smaller|[+-]?(?:\d+\.?\d*|\.\d+)(?:[a-z]+|%)|(?:calc|clamp|min|max|var)\(.*\))$/i;
39
- /** Whitespace-separated tokens at the top level, keeping parentheses and quotes whole. */
40
- const shorthandTokens = (value) => {
41
- const tokens = [];
42
- let current = "";
43
- let depth = 0;
44
- let quote;
45
- for (const character of value.trim()) {
46
- if (quote !== void 0) {
47
- if (character === quote) quote = void 0;
48
- } else if (character === "'" || character === "\"") quote = character;
49
- else if (character === "(") depth++;
50
- else if (character === ")") depth--;
51
- else if (/\s/.test(character) && depth === 0) {
52
- if (current !== "") tokens.push(current);
53
- current = "";
54
- continue;
55
- }
56
- current += character;
57
- }
58
- if (current !== "") tokens.push(current);
59
- return tokens;
60
- };
61
- /**
62
- * The family list of a `font` shorthand: everything after the size (and its
63
- * optional line height), or `undefined` for a system-font keyword that names
64
- * no family.
65
- */
66
- const fontShorthandFamilies = (value) => {
67
- const tokens = shorthandTokens(value);
68
- const size = tokens.findIndex((token) => FONT_SIZE.test(token.replace(/\/.*$/, "")));
69
- if (size < 0) return void 0;
70
- let next = size + 1;
71
- const sizeToken = tokens[size];
72
- if (sizeToken.endsWith("/")) next += 1;
73
- else if (!sizeToken.includes("/") && tokens[next]?.startsWith("/")) next += tokens[next] === "/" ? 2 : 1;
74
- const family = tokens.slice(next).join(" ");
75
- return family === "" ? void 0 : family;
76
- };
77
- /**
78
- * True when a stack names a declared project family without ending in a
79
- * generic. `declared` holds lowercased family names. A quoted `"sans-serif"`
80
- * names a font called sans-serif, not the generic, so it does not count.
81
- */
82
- const lacksGenericFallback = (value, declared) => {
83
- const stack = value.replace(/\s*!important\s*$/i, "");
84
- if (!fontStackFamilies(stack).some((family) => declared.has(family.toLowerCase()))) return false;
85
- const last = stack.slice(stack.lastIndexOf(",") + 1).trim();
86
- return !GENERIC_FAMILIES.has(last.toLowerCase());
87
- };
88
- const fontFallbackDiagnostic = (value) => ({
89
- code: "email-font-fallback",
90
- severity: "error",
91
- message: `font-family ${JSON.stringify(value)} uses a project font without a generic fallback. Clients that drop webfonts (Gmail, classic Outlook) render this stack as written, so end it with sans-serif, serif or monospace.`
92
- });
93
- //#endregion
94
- //#region src/email/selectors.ts
95
- const classListOf = (element) => {
96
- const value = element.props.class ?? element.props.className;
97
- return typeof value === "string" ? value.split(/\s+/).filter((name) => name !== "") : [];
98
- };
99
- const attributeOf = (element, name) => {
100
- const value = element.props[name];
101
- if (value === void 0 || value === null || value === false) return void 0;
102
- if (value === true) return "";
103
- return typeof value === "string" || typeof value === "number" ? String(value) : void 0;
104
- };
105
- const matchesAttribute = (element, test) => {
106
- const actual = test.name === "class" ? classListOf(element).join(" ") : attributeOf(element, test.name);
107
- if (actual === void 0) return false;
108
- if (test.operator === void 0 || test.value === void 0) return true;
109
- switch (test.operator) {
110
- case "=": return actual === test.value;
111
- case "~=": return actual.split(/\s+/).includes(test.value);
112
- case "^=": return actual.startsWith(test.value);
113
- case "$=": return actual.endsWith(test.value);
114
- case "*=": return actual.includes(test.value);
115
- case "|=": return actual === test.value || actual.startsWith(`${test.value}-`);
116
- default: return false;
117
- }
118
- };
119
- const matchesCompound = (element, compound) => {
120
- if (compound.opaque) return false;
121
- if (compound.tag !== void 0 && compound.tag !== element.tag) return false;
122
- if (compound.id !== void 0 && attributeOf(element, "id") !== compound.id) return false;
123
- if (compound.classes.length > 0) {
124
- const classes = classListOf(element);
125
- if (!compound.classes.every((name) => classes.includes(name))) return false;
126
- }
127
- return compound.attributes.every((test) => matchesAttribute(element, test));
128
- };
129
- const matchesFrom = (selector, index, ancestry) => {
130
- if (ancestry === void 0) return false;
131
- const part = selector.parts[index];
132
- if (part === void 0) return false;
133
- if (!matchesCompound(ancestry.element, part.compound)) return false;
134
- const left = selector.parts[index - 1];
135
- if (left === void 0) return true;
136
- switch (left.after ?? "descendant") {
137
- case "child": return matchesFrom(selector, index - 1, ancestry.parent);
138
- case "descendant":
139
- for (let parent = ancestry.parent; parent !== void 0; parent = parent.parent) if (matchesFrom(selector, index - 1, parent)) return true;
140
- return false;
141
- case "adjacent": {
142
- const sibling = ancestry.previous.at(-1);
143
- return sibling === void 0 ? false : matchesFrom(selector, index - 1, {
144
- element: sibling,
145
- parent: ancestry.parent,
146
- previous: ancestry.previous.slice(0, -1)
147
- });
148
- }
149
- case "sibling":
150
- for (let position = ancestry.previous.length - 1; position >= 0; position--) if (matchesFrom(selector, index - 1, {
151
- element: ancestry.previous[position],
152
- parent: ancestry.parent,
153
- previous: ancestry.previous.slice(0, position)
154
- })) return true;
155
- return false;
156
- }
157
- };
158
- /** Match a compiled selector against one element in its tree position. */
159
- const matchesSelector = (selector, ancestry) => selector === void 0 || selector.parts.length === 0 ? false : matchesFrom(selector, selector.parts.length - 1, ancestry);
160
- //#endregion
161
- //#region src/email/css-values.ts
162
- const MAX_RESOLVED_VALUE_LENGTH = 65536;
163
- const MAX_RESOLUTION_WORK = 262144;
164
- const resolveVariablesWithin = (value, variables, depth, budget, work) => {
165
- if (value.length > work.remaining) return void 0;
166
- work.remaining -= value.length;
167
- if (value.length > budget) return void 0;
168
- if (depth > 8 || !value.includes("var(")) return value;
169
- const chunks = [];
170
- let length = 0;
171
- const append = (chunk) => {
172
- length += chunk.length;
173
- if (length > budget) return false;
174
- chunks.push(chunk);
175
- return true;
176
- };
177
- let cursor = 0;
178
- while (cursor < value.length) {
179
- const start = value.indexOf("var(", cursor);
180
- if (start < 0) {
181
- if (!append(value.slice(cursor))) return void 0;
182
- break;
183
- }
184
- if (!append(value.slice(cursor, start))) return void 0;
185
- let level = 0;
186
- let end = -1;
187
- for (let index = start + 3; index < value.length; index++) if (value[index] === "(") level += 1;
188
- else if (value[index] === ")") {
189
- level -= 1;
190
- if (level === 0) {
191
- end = index;
192
- break;
193
- }
194
- }
195
- if (end < 0) {
196
- if (!append(value.slice(start))) return void 0;
197
- break;
198
- }
199
- const body = value.slice(start + 4, end);
200
- const comma = body.indexOf(",");
201
- const name = (comma < 0 ? body : body.slice(0, comma)).trim();
202
- const fallback = comma < 0 ? void 0 : body.slice(comma + 1).trim();
203
- const replacement = variables[name] ?? fallback;
204
- const resolved = replacement === void 0 ? value.slice(start, end + 1) : resolveVariablesWithin(replacement, variables, depth + 1, budget - length, work);
205
- if (resolved === void 0 || !append(resolved)) return void 0;
206
- cursor = end + 1;
207
- }
208
- return chunks.join("");
209
- };
210
- /**
211
- * Expand `var(--x, fallback)` against the collected custom properties.
212
- * Values whose expansion exceeds the render-time budget are left as authored.
213
- */
214
- const resolveVariables = (value, variables, depth = 0) => resolveVariablesWithin(value, variables, depth, MAX_RESOLVED_VALUE_LENGTH, { remaining: MAX_RESOLUTION_WORK }) ?? value;
215
- /**
216
- * Fold a constant `calc()` to a plain value. Tailwind writes every spacing
217
- * utility as `calc(var(--spacing) * n)`, and classic Outlook does not implement
218
- * `calc()` at all, so an unfolded value is a silently missing style. An
219
- * expression that cannot be reduced to one term is left exactly as authored.
220
- */
221
- const foldCalc = (value) => {
222
- if (!value.includes("calc(")) return value;
223
- let result = "";
224
- let rest = value;
225
- while (rest.length > 0) {
226
- const start = rest.indexOf("calc(");
227
- if (start < 0) {
228
- result += rest;
229
- break;
230
- }
231
- result += rest.slice(0, start);
232
- let level = 0;
233
- let end = -1;
234
- for (let index = start + 4; index < rest.length; index++) if (rest[index] === "(") level += 1;
235
- else if (rest[index] === ")") {
236
- level -= 1;
237
- if (level === 0) {
238
- end = index;
239
- break;
240
- }
241
- }
242
- if (end < 0) {
243
- result += rest.slice(start);
244
- break;
245
- }
246
- const folded = evaluate(rest.slice(start + 5, end));
247
- result += folded === void 0 ? rest.slice(start, end + 1) : format(folded);
248
- rest = rest.slice(end + 1);
249
- }
250
- return result;
251
- };
252
- const format = (value) => `${Math.round(value.n * 1e6) / 1e6}${value.n === 0 && value.unit !== "%" ? "" : value.unit}`;
253
- const evaluate = (expression) => {
254
- const tokens = expression.match(/\(|\)|[+-]\s|[*/]|[^\s()*/]+/g);
255
- if (tokens === null) return void 0;
256
- let position = 0;
257
- const peek = () => tokens[position]?.trim();
258
- const sum = () => {
259
- let left = product();
260
- while (left !== void 0 && (peek() === "+" || peek() === "-")) {
261
- const operator = peek();
262
- position += 1;
263
- const right = product();
264
- if (right === void 0 || left.unit !== right.unit) return void 0;
265
- left = {
266
- n: operator === "+" ? left.n + right.n : left.n - right.n,
267
- unit: left.unit
268
- };
269
- }
270
- return left;
271
- };
272
- const product = () => {
273
- let left = term();
274
- while (left !== void 0 && (peek() === "*" || peek() === "/")) {
275
- const operator = peek();
276
- position += 1;
277
- const right = term();
278
- if (right === void 0) return void 0;
279
- if (operator === "*") {
280
- if (left.unit !== "" && right.unit !== "") return void 0;
281
- left = {
282
- n: left.n * right.n,
283
- unit: left.unit === "" ? right.unit : left.unit
284
- };
285
- } else {
286
- if (right.unit !== "" || right.n === 0) return void 0;
287
- left = {
288
- n: left.n / right.n,
289
- unit: left.unit
290
- };
291
- }
292
- }
293
- return left;
294
- };
295
- const term = () => {
296
- const token = tokens[position];
297
- if (token === void 0) return void 0;
298
- if (token === "(") {
299
- position += 1;
300
- const inner = sum();
301
- if (peek() !== ")") return void 0;
302
- position += 1;
303
- return inner;
304
- }
305
- const match = /^(-?(?:\d+\.?\d*|\.\d+))([a-z%]*)$/i.exec(token.trim());
306
- if (match === null) return void 0;
307
- position += 1;
308
- return {
309
- n: Number.parseFloat(match[1]),
310
- unit: (match[2] ?? "").toLowerCase()
311
- };
312
- };
313
- const value = sum();
314
- return position === tokens.length ? value : void 0;
315
- };
316
- /** Resolve custom properties, then fold what the arithmetic allows. */
317
- const finalizeValue = (value, variables) => foldCalc(resolveVariables(value, variables));
318
- const RANGE_FEATURE = /\(\s*([a-z-]+)\s*(<=|>=|<|>)\s*([^)]+?)\s*\)/gi;
319
- /** Media Queries Level 4 range syntax down to the `min-`/`max-` prefixes clients support. */
320
- const downlevelMediaQuery = (condition) => condition.replace(RANGE_FEATURE, (whole, feature, comparison, bound) => {
321
- return `(${comparison.startsWith(">") ? "min-" : "max-"}${feature}:${bound})`;
322
- });
323
- /** Render the rules that could not be inlined as one `<style>` body. */
324
- const renderHeadCss = (rules, headAtRules, variables, darkVariables = {}) => {
325
- const blocks = [...headAtRules];
326
- const darkScope = {
327
- ...variables,
328
- ...darkVariables
329
- };
330
- const grouped = /* @__PURE__ */ new Map();
331
- for (const rule of rules) {
332
- const conditions = rule.conditions.map(downlevelMediaQuery);
333
- const key = conditions.join("\0");
334
- const bucket = grouped.get(key);
335
- if (bucket === void 0) grouped.set(key, {
336
- conditions,
337
- rules: [rule]
338
- });
339
- else bucket.rules.push(rule);
340
- }
341
- for (const { conditions, rules: bucket } of grouped.values()) {
342
- const scope = withinDarkScheme(conditions) ? darkScope : variables;
343
- const body = bucket.map((rule) => {
344
- const declarations = rule.declarations.filter((declaration) => !declaration.property.startsWith("--")).map((declaration) => `${declaration.property}:${finalizeValue(declaration.value, scope)}!important`).join(";");
345
- return declarations === "" ? "" : `${rule.selector}{${declarations}}`;
346
- }).filter((block) => block !== "").join("");
347
- if (body === "") continue;
348
- blocks.push(conditions.reduceRight((inner, condition) => `${condition}{${inner}}`, body));
349
- }
350
- return blocks.join("");
351
- };
352
- //#endregion
353
- //#region src/email/cascade.ts
354
- const VARIABLE_REFERENCE = /var\(\s*--[\w-]+\s*(?:,[^()]*)?\)/g;
355
- /**
356
- * Compatibility markup a primitive emits (a Button's VML) and a presentational
357
- * `bgcolor` attribute are not CSS, so no client resolves `var()` in them. Their
358
- * theme variables resolve here, in the scope of the element they sit in, as a
359
- * declaration on that element would.
360
- */
361
- const resolveMarkupVariables = (markup, variables) => markup.replace(VARIABLE_REFERENCE, (reference) => {
362
- const value = finalizeValue(reference, variables);
363
- return value === reference ? reference : escapeAttribute(value);
364
- });
365
- /**
366
- * Cascade rank as the tuple CSS actually sorts by: importance first, then
367
- * whether the declaration is attached to the element itself, then layer, then
368
- * specificity, then order of appearance down to the declaration. Normal
369
- * declarations sort by layer ascending (unlayered last, so it wins); important
370
- * declarations reverse the layer order, which is why the layer term is negated
371
- * for them. Element-attached declarations are compared before layers and
372
- * specificity, so an author's important inline value beats an important
373
- * stylesheet declaration exactly as it does in a browser. Two declarations never
374
- * tie: rule order is unique across every registered sheet, and the position
375
- * inside a rule separates the declarations that share it.
376
- */
377
- const rank = (important, attached, layer, specificity, order, position) => [
378
- important ? 1 : 0,
379
- attached ? 1 : 0,
380
- important ? -layer : layer,
381
- specificity[0],
382
- specificity[1],
383
- specificity[2],
384
- order,
385
- position
386
- ];
387
- const compareRank = (left, right) => {
388
- for (let index = 0; index < Math.max(left.length, right.length); index++) {
389
- const a = left[index] ?? 0;
390
- const b = right[index] ?? 0;
391
- if (a !== b) return a - b;
392
- }
393
- return 0;
394
- };
395
- const outranks = (left, right) => compareRank(left, right) >= 0;
396
- /** An authored `style` object becomes declarations with the same normalization the serializer uses. */
397
- const inlineDeclarations = (style) => {
398
- if (!isObject(style) || Array.isArray(style)) return [];
399
- const output = [];
400
- for (const [property, value] of Object.entries(style)) {
401
- if (value === void 0 || value === null) continue;
402
- if (typeof value !== "string" && typeof value !== "number") continue;
403
- const text = declarationValue(property, value);
404
- const important = /!important\s*$/i.test(text);
405
- output.push({
406
- property: kebab(property),
407
- value: important ? text.replace(/\s*!important\s*$/i, "") : text,
408
- important
409
- });
410
- }
411
- return output;
412
- };
413
- const horizontalWriting = {
414
- direction: "ltr",
415
- writingMode: "horizontal-tb"
416
- };
417
- const paddingValues = (value) => {
418
- const values = [];
419
- let depth = 0;
420
- let start = 0;
421
- for (let index = 0; index < value.length; index++) {
422
- const character = value[index];
423
- if (character === "(") depth++;
424
- else if (character === ")") depth--;
425
- else if (/\s/.test(character) && depth === 0) {
426
- if (index > start) values.push(value.slice(start, index));
427
- start = index + 1;
428
- }
429
- }
430
- if (start < value.length) values.push(value.slice(start));
431
- return values;
432
- };
433
- const physicalPadding = (property, value, context) => {
434
- const original = [[property, value]];
435
- const match = /^padding-(inline|block)(?:-(start|end))?$/.exec(property);
436
- if (match === null || context.writingMode !== "horizontal-tb" || !["ltr", "rtl"].includes(context.direction) || /^(inherit|revert|revert-layer)$/.test(value) || value.includes("var(")) return original;
437
- const values = paddingValues(value.trim());
438
- if (values.length < 1 || values.length > (match[2] === void 0 ? 2 : 1)) return original;
439
- const sides = match[1] === "block" ? ["top", "bottom"] : context.direction === "rtl" ? ["right", "left"] : ["left", "right"];
440
- if (match[2] !== void 0) return [[`padding-${sides[match[2] === "start" ? 0 : 1]}`, values[0]]];
441
- return [[`padding-${sides[0]}`, values[0]], [`padding-${sides[1]}`, values[1] ?? values[0]]];
442
- };
443
- /** Generated dark-mode classes share this prefix; it is reserved for the compiler. */
444
- const DARK_CLASS_PREFIX = "sml-dark-";
445
- /** A class named for its declarations, so equal sets share one rule and builds are stable. */
446
- const darkClassName = (body) => {
447
- let hash = 2166136261;
448
- for (let index = 0; index < body.length; index++) {
449
- hash ^= body.charCodeAt(index);
450
- hash = Math.imul(hash, 16777619) >>> 0;
451
- }
452
- return `${DARK_CLASS_PREFIX}${hash.toString(36)}`;
453
- };
454
- const SIDES = [
455
- "top",
456
- "right",
457
- "bottom",
458
- "left"
459
- ];
460
- const BORDER_PARTS = [
461
- "width",
462
- "style",
463
- "color"
464
- ];
465
- const CORNERS = [
466
- "top-left",
467
- "top-right",
468
- "bottom-right",
469
- "bottom-left"
470
- ];
471
- const BORDER_IMAGE = [
472
- "source",
473
- "slice",
474
- "width",
475
- "outset",
476
- "repeat"
477
- ].map((part) => `border-image-${part}`);
478
- /**
479
- * The longhands a border property writes: `border` writes the twelve per-side longhands and
480
- * resets the border image, `border-top` the top three, `border-color` the four colors,
481
- * `border-radius` the four corners.
482
- * `undefined` for a property outside the border box.
483
- */
484
- const borderLonghands = (property) => {
485
- if (property === "border") return [...SIDES.flatMap((side) => BORDER_PARTS.map((part) => `border-${side}-${part}`)), ...BORDER_IMAGE];
486
- if (property === "border-image") return BORDER_IMAGE;
487
- if (property === "border-radius") return CORNERS.map((corner) => `border-${corner}-radius`);
488
- const side = /^border-(top|right|bottom|left)$/.exec(property)?.[1];
489
- if (side !== void 0) return BORDER_PARTS.map((part) => `border-${side}-${part}`);
490
- const part = /^border-(width|style|color)$/.exec(property)?.[1];
491
- if (part !== void 0) return SIDES.map((each) => `border-${each}-${part}`);
492
- return property.startsWith("border-") ? [property] : void 0;
493
- };
494
- /**
495
- * Whether two declarations can override one another because they write a property in common:
496
- * `background` and `background-color`, `border-color` and `border-top`, `font` and `line-height`.
497
- * `border-radius` shares nothing with `border-color`.
498
- */
499
- const overlaps = (left, right) => {
500
- if (left === right) return true;
501
- const leftBorder = borderLonghands(left);
502
- const rightBorder = borderLonghands(right);
503
- if (leftBorder !== void 0 || rightBorder !== void 0) return leftBorder !== void 0 && rightBorder !== void 0 && leftBorder.some((longhand) => rightBorder.includes(longhand));
504
- const sets = (shorthand, longhand) => longhand.startsWith(`${shorthand}-`) || shorthand === "font" && longhand === "line-height";
505
- return sets(left, right) || sets(right, left);
506
- };
507
- /** Add a class to an element's props, whichever of `class` and `className` it uses. */
508
- const withClass = (props, name) => {
509
- const key = typeof props.class === "string" || props.className === void 0 ? "class" : "className";
510
- const current = props[key];
511
- return {
512
- ...props,
513
- [key]: typeof current === "string" && current.trim() !== "" ? `${current} ${name}` : name
514
- };
515
- };
516
- /** The generated dark-mode classes as one `@media (prefers-color-scheme:dark)` block, written as the head CSS writes its media queries. */
517
- const renderDarkRules = (rules) => rules.size === 0 ? "" : `${DARK_SCHEME_MEDIA.replace(": ", ":")}{${[...rules].sort(([left], [right]) => left.localeCompare(right)).map(([name, body]) => `.${name}{${body}}`).join("")}}`;
518
- /**
519
- * Resolve every stylesheet against the tree: inline what a client will honor on
520
- * the element, and keep the rest for `<head>`. Where the sheet has a dark-mode
521
- * theme, an element whose inlined styles read a color it redefines also gets a
522
- * generated class whose dark-mode rule restates those styles at their dark
523
- * values.
524
- */
525
- const applyStylesheet = (root, sheet, options = {}) => {
526
- const diagnostics = [...sheet.diagnostics];
527
- const hasDarkTheme = Object.keys(sheet.darkVariables).length > 0;
528
- const darkRules = /* @__PURE__ */ new Map();
529
- const darkClassFor = (declarations) => {
530
- const body = declarations.map(([property, value]) => `${property}:${value}!important`).join(";");
531
- const name = darkClassName(body);
532
- darkRules.set(name, body);
533
- return name;
534
- };
535
- const inlinable = [];
536
- const retained = [];
537
- const styledClasses = /* @__PURE__ */ new Set();
538
- for (const rule of sheet.rules) {
539
- (rule.inlinable ? inlinable : retained).push(rule);
540
- for (const name of rule.classes) styledClasses.add(name);
541
- }
542
- const conditionalWritingRules = retained.filter((rule) => rule.declarations.some((declaration) => declaration.property === "direction" || declaration.property === "writing-mode" || declaration.property === "all"));
543
- const unmatched = /* @__PURE__ */ new Map();
544
- const declaredFamilies = new Set(sheet.fontFaces.map((face) => face.family.toLowerCase()));
545
- const fallbackFindings = /* @__PURE__ */ new Map();
546
- const checkFallback = (property, value, origins) => {
547
- if (declaredFamilies.size === 0) return;
548
- const stack = property === "font-family" ? value : property === "font" ? fontShorthandFamilies(value) : void 0;
549
- if (stack === void 0 || fallbackFindings.has(stack)) return;
550
- if (lacksGenericFallback(stack, declaredFamilies)) fallbackFindings.set(stack, origins);
551
- };
552
- /**
553
- * Children are visited with their own preceding-sibling list so `+`/`~` work.
554
- * A fragment is not an element in the serialized document, so its children
555
- * join that same list rather than starting a new one.
556
- */
557
- const visitChildren = (children, ancestry, variables, darkScope, writing) => {
558
- const previous = [];
559
- const visit = (nodes) => nodes.map((child) => {
560
- if (child.type === "Element") {
561
- const result = applyTo(child, {
562
- element: child,
563
- parent: ancestry,
564
- previous: [...previous]
565
- }, variables, darkScope, writing);
566
- previous.push(child);
567
- return result;
568
- }
569
- if (child.type === "Fragment") return EmailNode.Fragment({ children: visit(child.children) });
570
- if (child.type === "Raw" && child.html.includes("var(")) return EmailNode.Raw({
571
- ...child,
572
- html: resolveMarkupVariables(child.html, variables)
573
- });
574
- return child;
575
- });
576
- return visit(children);
577
- };
578
- /**
579
- * `inherited` is the custom-property scope this element sees from its
580
- * ancestors, rooted in the sheet's unconditional properties. Custom properties
581
- * cascade like any other declaration and then inherit, so a `--brand` declared
582
- * on `.card` has to reach `var(--brand)` on `.card` and everything under it.
583
- */
584
- const applyTo = (element, ancestry, inherited, inheritedDark, inheritedWriting) => {
585
- const conditionalWriting = conditionalWritingRules.some((rule) => {
586
- if (rule.matcher === void 0 || rule.matcher.parts.length === 0 || rule.matcher.parts.some((part) => part.compound.opaque)) return true;
587
- for (let context = ancestry; context !== void 0; context = context.parent) if (matchesSelector(rule.matcher, context)) return true;
588
- return false;
589
- });
590
- const winners = /* @__PURE__ */ new Map();
591
- const claim = (declaration, key) => {
592
- const current = winners.get(declaration.property);
593
- if (current === void 0 || outranks(key, current.key)) winners.set(declaration.property, {
594
- value: declaration.value,
595
- key
596
- });
597
- };
598
- for (const rule of inlinable) {
599
- if (!matchesSelector(rule.matcher, ancestry)) continue;
600
- for (const [position, declaration] of rule.declarations.entries()) claim(declaration, rank(declaration.important, false, rule.layer, rule.specificity, rule.order, position));
601
- }
602
- for (const [position, declaration] of inlineDeclarations(element.props.style).entries()) claim(declaration, rank(declaration.important, true, 0, [
603
- 0,
604
- 0,
605
- 0
606
- ], 0, position));
607
- for (const name of classListOf(element)) if (!styledClasses.has(name) && options.knownClasses?.has(name) !== true) unmatched.set(name, [...unmatched.get(name) ?? [], ...element.origins]);
608
- const declared = {};
609
- for (const [property, entry] of winners) if (property.startsWith("--")) declared[property] = entry.value;
610
- const variables = Object.keys(declared).length === 0 ? inherited : {
611
- ...inherited,
612
- ...declared
613
- };
614
- const darkScope = Object.keys(declared).length === 0 ? inheritedDark : {
615
- ...inheritedDark,
616
- ...declared
617
- };
618
- const inheritedValue = (property, parent, initial, fallback = parent) => {
619
- const value = winners.get(property)?.value;
620
- if (value === void 0) return fallback;
621
- const resolved = finalizeValue(value, variables);
622
- return resolved === "inherit" || resolved === "unset" ? parent : resolved === "initial" ? initial : resolved;
623
- };
624
- const writing = {
625
- direction: inheritedValue("direction", inheritedWriting.direction, "ltr", typeof element.props.dir === "string" ? element.props.dir : element.tag === "bdi" ? "auto" : inheritedWriting.direction),
626
- writingMode: winners.has("all") ? "unknown" : inheritedValue("writing-mode", inheritedWriting.writingMode, "horizontal-tb")
627
- };
628
- const emitted = [...winners].filter(([property]) => !property.startsWith("--")).sort(([, left], [, right]) => compareRank(left.key, right.key)).map(([property, entry]) => [property, finalizeValue(entry.value, variables)]);
629
- for (const [property, value] of emitted) checkFallback(property, value, element.origins.slice(0, 1));
630
- const physical = /* @__PURE__ */ new Map();
631
- for (const [property, value] of emitted) for (const [name, lowered] of conditionalWriting ? [[property, value]] : physicalPadding(property, value, writing)) {
632
- physical.delete(name);
633
- physical.set(name, lowered);
634
- }
635
- const bgcolor = element.props.bgcolor;
636
- const lightProps = typeof bgcolor === "string" && bgcolor.includes("var(") ? {
637
- ...element.props,
638
- bgcolor: finalizeValue(bgcolor, variables)
639
- } : element.props;
640
- const dark = [];
641
- if (hasDarkTheme) {
642
- const changed = [];
643
- for (const [property, entry] of winners) {
644
- if (property.startsWith("--") || !entry.value.includes("var(")) continue;
645
- if (finalizeValue(entry.value, darkScope) !== finalizeValue(entry.value, variables)) changed.push(property);
646
- }
647
- const restated = new Set(changed);
648
- for (let grew = changed.length > 0; grew;) {
649
- grew = false;
650
- for (const property of winners.keys()) if (!property.startsWith("--") && !restated.has(property) && [...restated].some((candidate) => overlaps(candidate, property))) {
651
- restated.add(property);
652
- grew = true;
653
- }
654
- }
655
- for (const [property, entry] of [...winners].sort(([, left], [, right]) => compareRank(left.key, right.key))) if (restated.has(property)) dark.push([property, finalizeValue(entry.value, darkScope)]);
656
- if (typeof bgcolor === "string" && bgcolor.includes("var(") && !winners.has("background-color") && !winners.has("background")) {
657
- const value = finalizeValue(bgcolor, darkScope);
658
- if (value !== finalizeValue(bgcolor, variables)) dark.push(["background-color", value]);
659
- }
660
- }
661
- const darkClass = dark.length === 0 ? void 0 : darkClassFor(dark);
662
- const props = darkClass === void 0 ? lightProps : withClass(lightProps, darkClass);
663
- return EmailNode.Element({
664
- ...element,
665
- props: emitted.length === 0 ? props : {
666
- ...props,
667
- style: Object.fromEntries(physical)
668
- },
669
- children: visitChildren(element.children, ancestry, variables, darkScope, writing)
670
- });
671
- };
672
- const rootDark = {
673
- ...sheet.variables,
674
- ...sheet.darkVariables
675
- };
676
- const tree = root.type === "Element" ? applyTo(root, {
677
- element: root,
678
- parent: void 0,
679
- previous: []
680
- }, sheet.variables, rootDark, horizontalWriting) : root.type === "Fragment" ? EmailNode.Fragment({ children: visitChildren(root.children, void 0, sheet.variables, rootDark, horizontalWriting) }) : root;
681
- for (const rule of retained) for (const declaration of rule.declarations) checkFallback(declaration.property, finalizeValue(declaration.value, sheet.variables), []);
682
- for (const [value, origins] of fallbackFindings) diagnostics.push({
683
- ...fontFallbackDiagnostic(value),
684
- origins
685
- });
686
- for (const [name, origins] of unmatched) diagnostics.push({
687
- code: "unmatched-class",
688
- severity: "warning",
689
- message: `Class ${JSON.stringify(name)} produced no styles. If it is composed at run time, the compiler cannot discover it; list it in the template's declared classes or write the style inline.`,
690
- origins: origins.slice(0, 1)
691
- });
692
- return {
693
- tree,
694
- headCss: renderHeadCss(retained, sheet.headAtRules, sheet.variables, sheet.darkVariables),
695
- darkRules,
696
- fontFaceCss: sheet.fontFaces.map((face) => face.css).join(""),
697
- diagnostics
698
- };
699
- };
700
- //#endregion
701
- export { finalizeValue as a, resolveVariables as c, unquote as d, downlevelMediaQuery as i, classListOf as l, applyStylesheet as n, foldCalc as o, renderDarkRules as r, renderHeadCss as s, DARK_CLASS_PREFIX as t, matchesSelector as u };