@samva/markup 0.15.1 → 0.17.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.
- package/AGENTS.md +42 -35
- package/CHANGELOG.md +92 -0
- package/README.md +10 -9
- package/dist/{assets-tISHaQNF.d.ts → assets-CS5Xa_mr.d.ts} +3 -3
- package/dist/{brand-nreeKWxF.js → brand-CIiEqkU7.js} +24 -31
- package/dist/{brand-plugin-CHhcei-j.d.ts → brand-plugin-Dj9SMpfS.d.ts} +3 -1
- package/dist/brand.d.ts +2 -2
- package/dist/brand.js +3 -3
- package/dist/compatibility.d.ts +76 -0
- package/dist/compatibility.js +807 -0
- package/dist/compiled-template-Bc4Dq9zy.d.ts +268 -0
- package/dist/compiler.d.ts +58 -225
- package/dist/compiler.js +261 -1297
- package/dist/{components-3XHjYD3i.js → components-C5850-Qg.js} +37 -29
- package/dist/css-B_aVKrDo.js +436 -0
- package/dist/css-color-DLM12Osu.js +315 -0
- package/dist/{diagnostic-codes-UXwOO3A-.js → diagnostic-codes-DZKb0Xcg.js} +4 -0
- package/dist/{diagnostics-CUYlQ_6x.d.ts → diagnostics-D4vSLQrO.d.ts} +1 -10
- package/dist/diagnostics.d.ts +6 -2
- package/dist/diagnostics.js +3 -3
- package/dist/edit.d.ts +10 -9
- package/dist/edit.js +12 -11
- package/dist/email/jsx-dev-runtime.d.ts +1 -1
- package/dist/email/jsx-dev-runtime.js +1 -1
- package/dist/email/jsx-runtime.d.ts +2 -2
- package/dist/email/jsx-runtime.js +2 -2
- package/dist/email.d.ts +1 -1
- package/dist/email.js +1 -1
- package/dist/html-DJUK2YJF.js +483 -0
- package/dist/index.d.ts +1 -1
- package/dist/{jsx-runtime-DC7xG0aA.d.ts → jsx-runtime-DL8NVF1f.d.ts} +12 -6
- package/dist/{jsx-runtime-CBlexoeR.js → jsx-runtime-qL19u3Fy.js} +17 -10
- package/dist/preview-rNoJBEIY.d.ts +104 -0
- package/dist/{profile-AatO_SvP.js → profile-JTvgCD6X.js} +614 -24
- package/dist/{html-BvRFepeO.js → render-template-DwPDm3Hq.js} +155 -572
- package/dist/render.d.ts +40 -28
- package/dist/render.js +3 -47
- package/dist/selectors-CYWCoxZr.js +157 -0
- package/dist/sms.d.ts +1 -1
- package/dist/sms.js +1 -1
- package/dist/{template-B-UDXK8a.d.ts → template-DmNcoH4Q.d.ts} +2 -9
- package/dist/{theme-css-s9QwD5Ko.js → theme-css-P9F5S7Ic.js} +3 -316
- package/dist/whatsapp.d.ts +1 -1
- package/dist/whatsapp.js +1 -1
- package/generated/contract.json +7 -2
- package/generated/reference.md +3 -2
- package/package.json +5 -1
- package/skills/sml/SKILL.md +2 -2
- package/dist/ir-DNL6H7jA.js +0 -705
- package/dist/render-BdvewoQt.d.ts +0 -564
- package/dist/render-DpYouYYG.js +0 -258
package/dist/ir-DNL6H7jA.js
DELETED
|
@@ -1,705 +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
|
-
//#region src/ir.ts
|
|
702
|
-
/** The IR version this package writes. A publication pins the version it was built with. */
|
|
703
|
-
const SML_IR_VERSION = 1;
|
|
704
|
-
//#endregion
|
|
705
|
-
export { downlevelMediaQuery as a, renderHeadCss as c, matchesSelector as d, unquote as f, renderDarkRules as i, resolveVariables as l, DARK_CLASS_PREFIX as n, finalizeValue as o, applyStylesheet as r, foldCalc as s, SML_IR_VERSION as t, classListOf as u };
|