@samva/markup 0.16.0 → 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 -40
- package/CHANGELOG.md +72 -0
- package/README.md +10 -9
- package/dist/{brand-jFV6YtSV.d.ts → assets-CS5Xa_mr.d.ts} +41 -3
- package/dist/{brand-Ci05p37c.js → brand-CIiEqkU7.js} +64 -3
- package/dist/{brand-plugin-CHhcei-j.d.ts → brand-plugin-Dj9SMpfS.d.ts} +3 -1
- package/dist/brand.d.ts +2 -3
- package/dist/brand.js +3 -4
- 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 +59 -193
- package/dist/compiler.js +473 -775
- 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-8XDLR9ef.js → diagnostic-codes-DZKb0Xcg.js} +40 -1
- 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 -4
- 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-BsY4dkan.js → profile-JTvgCD6X.js} +714 -29
- 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 -5
- package/skills/sml/SKILL.md +2 -2
- package/dist/assets-BSpgZOSR.js +0 -61
- package/dist/assets-BSuNT9gr.d.ts +0 -39
- package/dist/brand-footer-ClRtWnIc.d.ts +0 -40
- package/dist/brand-footer-DXVGsKE5.js +0 -746
- package/dist/brand-sample.d.ts +0 -4
- package/dist/brand-sample.js +0 -2
- package/dist/cascade-DoLWZ66g.js +0 -701
- package/dist/diagnostic-model-DJtqbETd.js +0 -36
- package/dist/ir-KJLFW08u.js +0 -5
- package/dist/render-CVnXZMyK.js +0 -258
- package/dist/render-CuZej25A.d.ts +0 -564
- package/dist/theme-imports-u_P0ygvQ.js +0 -102
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as escapeText, r as escapeAttribute } from "./html-
|
|
2
|
-
import { a as jsx, c as rawMarkup, l as
|
|
1
|
+
import { i as escapeText, r as escapeAttribute } from "./html-DJUK2YJF.js";
|
|
2
|
+
import { a as jsx, c as rawMarkup, l as withEmailDiagnostics, n as Fragment, s as normalizeChildren, t as EmailNode } from "./jsx-runtime-qL19u3Fy.js";
|
|
3
3
|
//#region src/email/primitive-css.ts
|
|
4
4
|
/** The classes the primitives put on the elements they generate. */
|
|
5
5
|
const PRIMITIVE_CLASSES = {
|
|
@@ -58,10 +58,10 @@ const presentation = (table, row, rowProps = {}) => jsx("table", {
|
|
|
58
58
|
});
|
|
59
59
|
/** A primitive's own class, then the author's. */
|
|
60
60
|
const classNames = (own, authored) => authored === void 0 || authored.trim() === "" ? own : `${own} ${authored}`;
|
|
61
|
-
const optionalNumber = (component, name, value, bounds = {}) => {
|
|
61
|
+
const optionalNumber = (found, component, name, value, bounds = {}) => {
|
|
62
62
|
if (value === void 0) return void 0;
|
|
63
63
|
if (typeof value === "number" && Number.isFinite(value) && (bounds.min === void 0 || value >= bounds.min)) return value;
|
|
64
|
-
|
|
64
|
+
found.push({
|
|
65
65
|
code: "invalid-number-prop",
|
|
66
66
|
severity: "error",
|
|
67
67
|
message: `<${component} ${name}> must be a finite number${bounds.min === void 0 ? "" : ` of at least ${bounds.min}`}.`
|
|
@@ -90,7 +90,8 @@ const preheaderNode = (text) => jsx("div", {
|
|
|
90
90
|
* screen edge (`gutter`).
|
|
91
91
|
*/
|
|
92
92
|
const Email = ({ className, lang = "en", dir = "ltr", title, backgroundColor = "var(--color-background)", gutter, style, children }) => {
|
|
93
|
-
const
|
|
93
|
+
const found = [];
|
|
94
|
+
const safeGutter = optionalNumber(found, "Email", "gutter", gutter, { min: 0 });
|
|
94
95
|
const cellStyle = {
|
|
95
96
|
...safeGutter === void 0 ? {} : {
|
|
96
97
|
paddingLeft: `${safeGutter}px`,
|
|
@@ -98,7 +99,7 @@ const Email = ({ className, lang = "en", dir = "ltr", title, backgroundColor = "
|
|
|
98
99
|
},
|
|
99
100
|
...style
|
|
100
101
|
};
|
|
101
|
-
|
|
102
|
+
const document = jsx("html", {
|
|
102
103
|
lang,
|
|
103
104
|
dir,
|
|
104
105
|
xmlns: "http://www.w3.org/1999/xhtml",
|
|
@@ -146,6 +147,7 @@ const Email = ({ className, lang = "en", dir = "ltr", title, backgroundColor = "
|
|
|
146
147
|
}))]
|
|
147
148
|
})]
|
|
148
149
|
});
|
|
150
|
+
return withEmailDiagnostics(document, found);
|
|
149
151
|
};
|
|
150
152
|
/** A pixel width as `width: 100%` capped at it, so a fixed-width table still fits a phone. */
|
|
151
153
|
const fluidWidth = (width) => {
|
|
@@ -234,9 +236,10 @@ const Columns = ({ className, align = "center", width = "100%", style, each, per
|
|
|
234
236
|
width,
|
|
235
237
|
...style === void 0 ? {} : { style }
|
|
236
238
|
};
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
if (
|
|
239
|
+
const found = [];
|
|
240
|
+
const safeGap = optionalNumber(found, "Columns", "gap", gap, { min: 0 });
|
|
241
|
+
if (each === void 0 || typeof children !== "function") return withEmailDiagnostics(presentation(table, withGaps(normalizeChildren(children).map((cell) => withColumnLayout(cell, void 0, stack)), safeGap, stack), { style: { width: "100%" } }), found);
|
|
242
|
+
if (per !== void 0 && (!Number.isInteger(per) || per < 1)) found.push({
|
|
240
243
|
code: "invalid-columns-per",
|
|
241
244
|
severity: "error",
|
|
242
245
|
message: "<Columns per> must be a positive integer."
|
|
@@ -246,7 +249,8 @@ const Columns = ({ className, align = "center", width = "100%", style, each, per
|
|
|
246
249
|
if (size === void 0) chunks.push(each);
|
|
247
250
|
else for (let at = 0; at < each.length; at += size) chunks.push(each.slice(at, at + size));
|
|
248
251
|
const share = equalColumnWidth(size ?? Math.max(each.length, 1));
|
|
249
|
-
|
|
252
|
+
const rows = jsx(Fragment, { children: chunks.map((chunk) => presentation(table, withGaps(chunk.flatMap((item, index) => normalizeChildren(children(item, index)).map((cell) => withColumnLayout(cell, share, stack))), safeGap, stack), { style: { width: "100%" } })) });
|
|
253
|
+
return withEmailDiagnostics(rows, found);
|
|
250
254
|
};
|
|
251
255
|
/** One cell of a `Columns` row. */
|
|
252
256
|
const Column = ({ className, align, valign, width, style, children }) => jsx("td", {
|
|
@@ -259,25 +263,27 @@ const Column = ({ className, align, valign, width, style, children }) => jsx("td
|
|
|
259
263
|
});
|
|
260
264
|
/** Average advance of a bold sans-serif glyph, as a fraction of the font size. */
|
|
261
265
|
const BOLD_GLYPH_ADVANCE = .6;
|
|
262
|
-
const invalidButtonNumber = (name) =>
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
266
|
+
const invalidButtonNumber = (found, name) => {
|
|
267
|
+
found.push({
|
|
268
|
+
code: "invalid-button-number",
|
|
269
|
+
severity: "error",
|
|
270
|
+
message: `<Button ${name}> must be a finite number.`
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
const buttonNumber = (found, name, value, fallback) => {
|
|
268
274
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
269
|
-
invalidButtonNumber(name);
|
|
275
|
+
invalidButtonNumber(found, name);
|
|
270
276
|
return fallback;
|
|
271
277
|
};
|
|
272
|
-
const optionalButtonNumber = (name, value) => {
|
|
278
|
+
const optionalButtonNumber = (found, name, value) => {
|
|
273
279
|
if (value === void 0) return void 0;
|
|
274
280
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
275
|
-
invalidButtonNumber(name);
|
|
281
|
+
invalidButtonNumber(found, name);
|
|
276
282
|
};
|
|
277
|
-
const optionalPositiveButtonNumber = (name, value) => {
|
|
283
|
+
const optionalPositiveButtonNumber = (found, name, value) => {
|
|
278
284
|
if (value === void 0) return void 0;
|
|
279
285
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
280
|
-
|
|
286
|
+
found.push({
|
|
281
287
|
code: "invalid-button-number",
|
|
282
288
|
severity: "error",
|
|
283
289
|
message: `<Button ${name}> must be a positive finite number.`
|
|
@@ -291,15 +297,16 @@ const optionalPositiveButtonNumber = (name, value) => {
|
|
|
291
297
|
* does not use the theme's primary fill.
|
|
292
298
|
*/
|
|
293
299
|
const Button = ({ className, href, width, height, paddingX = 16, backgroundColor, color, borderRadius, borderColor, borderWidth = 0, fontSize = 16, fontFamily, align = "left", style, children }) => {
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
+
const found = [];
|
|
301
|
+
const safeWidth = optionalButtonNumber(found, "width", width);
|
|
302
|
+
const safeHeight = optionalPositiveButtonNumber(found, "height", height);
|
|
303
|
+
const safePaddingX = buttonNumber(found, "paddingX", paddingX, 16);
|
|
304
|
+
const safeBorderRadius = optionalButtonNumber(found, "borderRadius", borderRadius);
|
|
305
|
+
const safeBorderWidth = buttonNumber(found, "borderWidth", borderWidth, 0);
|
|
306
|
+
const safeFontSize = buttonNumber(found, "fontSize", fontSize, 16);
|
|
300
307
|
const label = typeof children === "string" ? children : void 0;
|
|
301
308
|
const fallback = safeHeight !== void 0 && label !== void 0;
|
|
302
|
-
if (!fallback)
|
|
309
|
+
if (!fallback) found.push({
|
|
303
310
|
code: "button-outlook-fallback",
|
|
304
311
|
severity: "warning",
|
|
305
312
|
message: "<Button> draws its classic Outlook shape from VML, which needs a height and a plain string label. Without both, classic Windows Outlook shows an unstyled link.",
|
|
@@ -310,7 +317,7 @@ const Button = ({ className, href, width, height, paddingX = 16, backgroundColor
|
|
|
310
317
|
const fill = backgroundColor ?? "var(--color-primary)";
|
|
311
318
|
const ink = color ?? "var(--color-primary-foreground)";
|
|
312
319
|
const family = fontFamily ?? "var(--font-sans)";
|
|
313
|
-
|
|
320
|
+
const button = presentation({
|
|
314
321
|
width: "100%",
|
|
315
322
|
style: { borderCollapse: "collapse" }
|
|
316
323
|
}, jsx("td", {
|
|
@@ -342,6 +349,7 @@ const Button = ({ className, href, width, height, paddingX = 16, backgroundColor
|
|
|
342
349
|
children
|
|
343
350
|
})]
|
|
344
351
|
}));
|
|
352
|
+
return withEmailDiagnostics(button, found);
|
|
345
353
|
};
|
|
346
354
|
/** Vertical space that survives clients which collapse margins. */
|
|
347
355
|
const Spacer = ({ height }) => presentation({ width: "100%" }, jsx("td", {
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { t as downlevelColors } from "./css-color-DLM12Osu.js";
|
|
2
|
+
import { n as isDarkSchemeCondition } from "./dark-scheme-CbCwGDWH.js";
|
|
3
|
+
import { o as unquote } from "./selectors-CYWCoxZr.js";
|
|
4
|
+
import { generate, ident, parse, walk } from "css-tree";
|
|
5
|
+
//#region src/email/fonts.ts
|
|
6
|
+
const FONT_FILE = /\.woff2$/i;
|
|
7
|
+
const WOFF2_FORMAT = /^woff2(?:-variations)?$/i;
|
|
8
|
+
const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
9
|
+
const declarationValue = (declaration) => declaration.type === "Declaration" ? generate(declaration.value).trim() : "";
|
|
10
|
+
/**
|
|
11
|
+
* Validate one `@font-face` and rewrite its project-relative `src` URLs.
|
|
12
|
+
* Accepts only WOFF2: a relative URL must name a `.woff2` file the project has,
|
|
13
|
+
* and an `https:` URL stays an external reference. `local()` entries pass.
|
|
14
|
+
*/
|
|
15
|
+
const compileFontFace = (node, origin, resolve) => {
|
|
16
|
+
const diagnostics = [];
|
|
17
|
+
const fail = (code, message) => diagnostics.push({
|
|
18
|
+
code,
|
|
19
|
+
severity: "error",
|
|
20
|
+
message: `${origin}: ${message}`,
|
|
21
|
+
origins: []
|
|
22
|
+
});
|
|
23
|
+
if (node.type !== "Atrule" || node.block === null || node.block.children === null) return {
|
|
24
|
+
rule: void 0,
|
|
25
|
+
diagnostics
|
|
26
|
+
};
|
|
27
|
+
let family;
|
|
28
|
+
let sources = 0;
|
|
29
|
+
for (const declaration of node.block.children) {
|
|
30
|
+
if (declaration.type !== "Declaration") continue;
|
|
31
|
+
const property = declaration.property.toLowerCase();
|
|
32
|
+
if (property === "font-family") family = unquote(declarationValue(declaration));
|
|
33
|
+
if (property !== "src") continue;
|
|
34
|
+
const authored = declarationValue(declaration);
|
|
35
|
+
let value;
|
|
36
|
+
try {
|
|
37
|
+
value = parse(authored, { context: "value" });
|
|
38
|
+
} catch {
|
|
39
|
+
fail("email-font-src", "@font-face src could not be parsed.");
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
walk(value, { enter: (part) => {
|
|
43
|
+
if (part.type === "Function" && part.name.toLowerCase() === "format") {
|
|
44
|
+
const format = unquote(generate(part).slice(7, -1));
|
|
45
|
+
if (!WOFF2_FORMAT.test(format)) fail("email-font-format", `@font-face format(${format}) is not supported. Email fonts are WOFF2 only; convert the file to .woff2.`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (part.type !== "Url") return;
|
|
49
|
+
sources++;
|
|
50
|
+
const reference = part.value;
|
|
51
|
+
const path = reference.replace(/[?#].*$/, "");
|
|
52
|
+
if (/^https:\/\//i.test(reference)) {
|
|
53
|
+
if (!FONT_FILE.test(path) && !/format\(\s*['"]?woff2/i.test(generate(value))) fail("email-font-format", `@font-face src ${JSON.stringify(reference)} is not a WOFF2 file. Email fonts are WOFF2 only.`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (SCHEME.test(reference) || reference.startsWith("//")) {
|
|
57
|
+
fail("email-font-src", `@font-face src ${JSON.stringify(reference)} must be a project .woff2 file or an https URL.`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!FONT_FILE.test(path)) {
|
|
61
|
+
fail("email-font-format", `@font-face src ${JSON.stringify(reference)} is not a .woff2 file. Email fonts are WOFF2 only; convert the file to .woff2.`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const url = resolve?.(reference);
|
|
65
|
+
if (url === void 0) {
|
|
66
|
+
fail("email-font-missing", `@font-face src ${JSON.stringify(reference)} names no file in the project. Paths resolve from the stylesheet's own directory.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
part.value = url;
|
|
70
|
+
} });
|
|
71
|
+
declaration.value = {
|
|
72
|
+
type: "Raw",
|
|
73
|
+
value: generate(value).replace(/\)(?=(?:format|tech)\()/gi, ") ")
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (family === void 0 || family === "") fail("email-font-family", "@font-face declares no font-family.");
|
|
77
|
+
if (sources === 0) fail("email-font-src", "@font-face declares no url() src.");
|
|
78
|
+
if (diagnostics.length > 0 || family === void 0) return {
|
|
79
|
+
rule: void 0,
|
|
80
|
+
diagnostics
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
rule: {
|
|
84
|
+
family,
|
|
85
|
+
css: generate(node)
|
|
86
|
+
},
|
|
87
|
+
diagnostics
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/email/css.ts
|
|
92
|
+
const UNLAYERED = Number.MAX_SAFE_INTEGER;
|
|
93
|
+
const CONDITIONAL_AT_RULES = /* @__PURE__ */ new Set([
|
|
94
|
+
"media",
|
|
95
|
+
"supports",
|
|
96
|
+
"container"
|
|
97
|
+
]);
|
|
98
|
+
const HEAD_AT_RULES = /* @__PURE__ */ new Set([
|
|
99
|
+
"page",
|
|
100
|
+
"counter-style",
|
|
101
|
+
"keyframes"
|
|
102
|
+
]);
|
|
103
|
+
const ROOT_SELECTORS = /* @__PURE__ */ new Set([
|
|
104
|
+
":root",
|
|
105
|
+
":host",
|
|
106
|
+
"html",
|
|
107
|
+
"*",
|
|
108
|
+
":where(:root)"
|
|
109
|
+
]);
|
|
110
|
+
const text = (node) => node === null || node === void 0 ? "" : generate(node);
|
|
111
|
+
/** The CSS specificity triple for one selector, ignoring the parts email clients drop. */
|
|
112
|
+
const specificityOf = (selector) => {
|
|
113
|
+
let id = 0;
|
|
114
|
+
let cls = 0;
|
|
115
|
+
let type = 0;
|
|
116
|
+
const visit = (node) => {
|
|
117
|
+
switch (node.type) {
|
|
118
|
+
case "IdSelector":
|
|
119
|
+
id += 1;
|
|
120
|
+
break;
|
|
121
|
+
case "ClassSelector":
|
|
122
|
+
case "AttributeSelector":
|
|
123
|
+
cls += 1;
|
|
124
|
+
break;
|
|
125
|
+
case "PseudoClassSelector":
|
|
126
|
+
if (node.name !== "where") cls += 1;
|
|
127
|
+
break;
|
|
128
|
+
case "TypeSelector":
|
|
129
|
+
if (node.name !== "*") type += 1;
|
|
130
|
+
break;
|
|
131
|
+
case "PseudoElementSelector": type += 1;
|
|
132
|
+
}
|
|
133
|
+
if ("children" in node && node.children !== null && node.children !== void 0) for (const child of node.children) visit(child);
|
|
134
|
+
};
|
|
135
|
+
visit(selector);
|
|
136
|
+
return [
|
|
137
|
+
id,
|
|
138
|
+
cls,
|
|
139
|
+
type
|
|
140
|
+
];
|
|
141
|
+
};
|
|
142
|
+
const hasPseudo = (selector) => {
|
|
143
|
+
let found = false;
|
|
144
|
+
const visit = (node) => {
|
|
145
|
+
if (node.type === "PseudoClassSelector" || node.type === "PseudoElementSelector") found = true;
|
|
146
|
+
if ("children" in node && node.children !== null && node.children !== void 0) for (const child of node.children) visit(child);
|
|
147
|
+
};
|
|
148
|
+
visit(selector);
|
|
149
|
+
return found;
|
|
150
|
+
};
|
|
151
|
+
const COMBINATORS = {
|
|
152
|
+
" ": "descendant",
|
|
153
|
+
">": "child",
|
|
154
|
+
"+": "adjacent",
|
|
155
|
+
"~": "sibling"
|
|
156
|
+
};
|
|
157
|
+
/** Compile a selector into the plain-data matcher the render lane uses. */
|
|
158
|
+
const compileSelector = (selector) => {
|
|
159
|
+
let ast;
|
|
160
|
+
try {
|
|
161
|
+
ast = parse(selector, {
|
|
162
|
+
context: "selector",
|
|
163
|
+
positions: false
|
|
164
|
+
});
|
|
165
|
+
} catch {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (ast.type !== "Selector" || ast.children === null) return void 0;
|
|
169
|
+
const parts = [];
|
|
170
|
+
let tag;
|
|
171
|
+
let id;
|
|
172
|
+
let classes = [];
|
|
173
|
+
let attributes = [];
|
|
174
|
+
let opaque = false;
|
|
175
|
+
const flush = (after) => {
|
|
176
|
+
parts.push({
|
|
177
|
+
compound: {
|
|
178
|
+
tag,
|
|
179
|
+
id,
|
|
180
|
+
classes,
|
|
181
|
+
attributes,
|
|
182
|
+
opaque
|
|
183
|
+
},
|
|
184
|
+
...after === void 0 ? {} : { after }
|
|
185
|
+
});
|
|
186
|
+
tag = void 0;
|
|
187
|
+
id = void 0;
|
|
188
|
+
classes = [];
|
|
189
|
+
attributes = [];
|
|
190
|
+
opaque = false;
|
|
191
|
+
};
|
|
192
|
+
for (const node of ast.children) switch (node.type) {
|
|
193
|
+
case "TypeSelector":
|
|
194
|
+
if (node.name !== "*") tag = node.name.toLowerCase();
|
|
195
|
+
break;
|
|
196
|
+
case "IdSelector":
|
|
197
|
+
id = node.name;
|
|
198
|
+
break;
|
|
199
|
+
case "ClassSelector":
|
|
200
|
+
classes.push(ident.decode(node.name));
|
|
201
|
+
break;
|
|
202
|
+
case "AttributeSelector":
|
|
203
|
+
attributes.push({
|
|
204
|
+
name: node.name.name,
|
|
205
|
+
operator: node.matcher ?? void 0,
|
|
206
|
+
value: node.value === null || node.value === void 0 ? void 0 : node.value.type === "String" ? node.value.value : node.value.name
|
|
207
|
+
});
|
|
208
|
+
break;
|
|
209
|
+
case "Combinator":
|
|
210
|
+
flush(COMBINATORS[node.name.trim() === "" ? " " : node.name] ?? "descendant");
|
|
211
|
+
break;
|
|
212
|
+
default: opaque = true;
|
|
213
|
+
}
|
|
214
|
+
flush();
|
|
215
|
+
return { parts };
|
|
216
|
+
};
|
|
217
|
+
/** Substitute a nested rule's `&` against its parent selector. */
|
|
218
|
+
const nestSelector = (parent, child) => child.includes("&") ? child.replaceAll("&", parent) : `${parent} ${child}`;
|
|
219
|
+
/** Parse one stylesheet into rules, keeping layer, order and conditions. */
|
|
220
|
+
const parseStylesheet = (source, options = {}) => {
|
|
221
|
+
const rules = [];
|
|
222
|
+
const headAtRules = [];
|
|
223
|
+
const fontFaces = [];
|
|
224
|
+
const variables = {};
|
|
225
|
+
const darkVariables = {};
|
|
226
|
+
const conditionalVariables = /* @__PURE__ */ new Set();
|
|
227
|
+
const diagnostics = [];
|
|
228
|
+
const layers = /* @__PURE__ */ new Map();
|
|
229
|
+
const selectorFacts = /* @__PURE__ */ new Map();
|
|
230
|
+
let order = options.startOrder ?? 0;
|
|
231
|
+
/** Everything a rule needs from its selector, computed once per distinct string. */
|
|
232
|
+
const factsFor = (selector) => {
|
|
233
|
+
const cached = selectorFacts.get(selector);
|
|
234
|
+
if (cached !== void 0) return cached;
|
|
235
|
+
let facts;
|
|
236
|
+
try {
|
|
237
|
+
const node = parse(selector, {
|
|
238
|
+
context: "selector",
|
|
239
|
+
positions: false
|
|
240
|
+
});
|
|
241
|
+
const classes = /* @__PURE__ */ new Set();
|
|
242
|
+
walk(node, {
|
|
243
|
+
visit: "ClassSelector",
|
|
244
|
+
enter: (part) => {
|
|
245
|
+
classes.add(ident.decode(part.name));
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
facts = {
|
|
249
|
+
classes: [...classes],
|
|
250
|
+
specificity: specificityOf(node),
|
|
251
|
+
pseudo: hasPseudo(node),
|
|
252
|
+
matcher: compileSelector(selector)
|
|
253
|
+
};
|
|
254
|
+
} catch {
|
|
255
|
+
facts = {
|
|
256
|
+
classes: [],
|
|
257
|
+
specificity: [
|
|
258
|
+
0,
|
|
259
|
+
0,
|
|
260
|
+
0
|
|
261
|
+
],
|
|
262
|
+
pseudo: true,
|
|
263
|
+
matcher: void 0
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
selectorFacts.set(selector, facts);
|
|
267
|
+
return facts;
|
|
268
|
+
};
|
|
269
|
+
let ast;
|
|
270
|
+
try {
|
|
271
|
+
ast = parse(source, {
|
|
272
|
+
parseValue: false,
|
|
273
|
+
parseCustomProperty: false,
|
|
274
|
+
positions: false
|
|
275
|
+
});
|
|
276
|
+
} catch (cause) {
|
|
277
|
+
return {
|
|
278
|
+
rules: [],
|
|
279
|
+
headAtRules: [],
|
|
280
|
+
fontFaces: [],
|
|
281
|
+
variables: {},
|
|
282
|
+
darkVariables: {},
|
|
283
|
+
diagnostics: [{
|
|
284
|
+
code: "css-parse-error",
|
|
285
|
+
severity: "error",
|
|
286
|
+
message: `${options.origin ?? "stylesheet"} could not be parsed: ${String(cause)}`,
|
|
287
|
+
origins: []
|
|
288
|
+
}]
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const declarationsOf = (block) => {
|
|
292
|
+
const output = [];
|
|
293
|
+
if (block === null || block.type !== "Block" || block.children === null) return output;
|
|
294
|
+
for (const child of block.children) {
|
|
295
|
+
if (child.type !== "Declaration") continue;
|
|
296
|
+
output.push({
|
|
297
|
+
property: child.property,
|
|
298
|
+
value: downlevelColors(text(child.value).trim().replace(/[\t\n\r\f]/g, " ")),
|
|
299
|
+
important: child.important === true
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return output;
|
|
303
|
+
};
|
|
304
|
+
const emit = (selector, context, declarations) => {
|
|
305
|
+
if (declarations.length === 0) return;
|
|
306
|
+
const facts = factsFor(selector);
|
|
307
|
+
if (ROOT_SELECTORS.has(selector.trim())) for (const declaration of declarations) {
|
|
308
|
+
if (!declaration.property.startsWith("--")) continue;
|
|
309
|
+
if (context.conditions.length === 0) variables[declaration.property] = declaration.value;
|
|
310
|
+
else if (isDarkSchemeCondition(context.conditions)) darkVariables[declaration.property] = declaration.value;
|
|
311
|
+
else conditionalVariables.add(declaration.property);
|
|
312
|
+
}
|
|
313
|
+
rules.push({
|
|
314
|
+
selector,
|
|
315
|
+
classes: facts.classes,
|
|
316
|
+
specificity: facts.specificity,
|
|
317
|
+
declarations,
|
|
318
|
+
order: order++,
|
|
319
|
+
layer: context.layer,
|
|
320
|
+
conditions: context.conditions,
|
|
321
|
+
inlinable: context.conditions.length === 0 && !facts.pseudo,
|
|
322
|
+
matcher: facts.matcher
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* Declarations directly in a block belong to the enclosing selector, whether
|
|
327
|
+
* that block is the rule's own or a `@media` nested inside it — which is
|
|
328
|
+
* exactly the shape Tailwind v4 emits for a responsive utility.
|
|
329
|
+
*/
|
|
330
|
+
const walkBlock = (block, context) => {
|
|
331
|
+
if (block === null || block.type !== "Block" || block.children === null) return;
|
|
332
|
+
const declarations = declarationsOf(block);
|
|
333
|
+
if (declarations.length > 0 && context.selector !== void 0) emit(context.selector, context, declarations);
|
|
334
|
+
for (const child of block.children) if (child.type !== "Declaration") visitNode(child, context);
|
|
335
|
+
};
|
|
336
|
+
const visitNode = (node, context) => {
|
|
337
|
+
if (node.type === "Rule") {
|
|
338
|
+
const prelude = node.prelude;
|
|
339
|
+
const selectors = prelude.type === "SelectorList" && prelude.children !== null ? [...prelude.children] : [prelude];
|
|
340
|
+
for (const selector of selectors) {
|
|
341
|
+
const own = text(selector).trim();
|
|
342
|
+
if (own === "") continue;
|
|
343
|
+
walkBlock(node.block, {
|
|
344
|
+
...context,
|
|
345
|
+
selector: context.selector === void 0 ? own : nestSelector(context.selector, own)
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (node.type !== "Atrule") return;
|
|
351
|
+
const name = node.name.toLowerCase();
|
|
352
|
+
if (name === "layer") {
|
|
353
|
+
const prelude = text(node.prelude).trim();
|
|
354
|
+
if (node.block === null) {
|
|
355
|
+
for (const declared of prelude.split(",").map((part) => part.trim())) if (declared !== "" && !layers.has(declared)) layers.set(declared, layers.size);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const named = prelude.split(",")[0]?.trim() ?? "";
|
|
359
|
+
if (named !== "" && !layers.has(named)) layers.set(named, layers.size);
|
|
360
|
+
walkBlock(node.block, {
|
|
361
|
+
...context,
|
|
362
|
+
layer: layers.get(named) ?? context.layer
|
|
363
|
+
});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (name === "theme" || name === "scope") {
|
|
367
|
+
const into = context.conditions.length === 0 ? variables : isDarkSchemeCondition(context.conditions) ? darkVariables : void 0;
|
|
368
|
+
for (const declaration of declarationsOf(node.block)) if (declaration.property.startsWith("--")) {
|
|
369
|
+
if (into === void 0) conditionalVariables.add(declaration.property);
|
|
370
|
+
else into[declaration.property] = declaration.value;
|
|
371
|
+
}
|
|
372
|
+
for (const child of node.block?.children ?? []) if (child.type !== "Declaration") visitNode(child, context);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (name === "property") {
|
|
376
|
+
const initial = declarationsOf(node.block).find((declaration) => declaration.property === "initial-value");
|
|
377
|
+
const property = text(node.prelude).trim();
|
|
378
|
+
if (initial !== void 0 && property.startsWith("--") && !(property in variables)) variables[property] = initial.value;
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (CONDITIONAL_AT_RULES.has(name)) {
|
|
382
|
+
walkBlock(node.block, {
|
|
383
|
+
...context,
|
|
384
|
+
conditions: [...context.conditions, `@${name} ${text(node.prelude).trim()}`]
|
|
385
|
+
});
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (name === "font-face") {
|
|
389
|
+
const face = compileFontFace(node, options.origin ?? "stylesheet", options.resolveFontFile);
|
|
390
|
+
if (face.rule !== void 0) fontFaces.push(face.rule);
|
|
391
|
+
diagnostics.push(...face.diagnostics);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (HEAD_AT_RULES.has(name)) {
|
|
395
|
+
headAtRules.push(generate(node));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (name === "import") {
|
|
399
|
+
if (options.themeImports === true) return;
|
|
400
|
+
diagnostics.push({
|
|
401
|
+
code: "css-import-unresolved",
|
|
402
|
+
severity: "error",
|
|
403
|
+
message: `${options.origin ?? "stylesheet"} uses @import ${text(node.prelude).trim()}; import the stylesheet from the template module instead so the compiler can resolve it.`,
|
|
404
|
+
origins: []
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (name === "charset" || name === "namespace") return;
|
|
409
|
+
diagnostics.push({
|
|
410
|
+
code: "css-unsupported-at-rule",
|
|
411
|
+
severity: "warning",
|
|
412
|
+
message: `@${name} is not applied to email output; remove it or move the declarations into a class rule.`,
|
|
413
|
+
origins: []
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
if (ast.type === "StyleSheet" && ast.children !== null) for (const child of ast.children) visitNode(child, {
|
|
417
|
+
conditions: [],
|
|
418
|
+
layer: UNLAYERED
|
|
419
|
+
});
|
|
420
|
+
for (const property of conditionalVariables) if (property in variables) diagnostics.push({
|
|
421
|
+
code: "css-conditional-variable",
|
|
422
|
+
severity: "warning",
|
|
423
|
+
message: `${property} is redefined inside a conditional at-rule. Inlined declarations take the unconditional value; keep the override on a class rule so the retained head rule can apply it.`,
|
|
424
|
+
origins: []
|
|
425
|
+
});
|
|
426
|
+
return {
|
|
427
|
+
rules,
|
|
428
|
+
headAtRules,
|
|
429
|
+
fontFaces,
|
|
430
|
+
variables,
|
|
431
|
+
darkVariables,
|
|
432
|
+
diagnostics
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
//#endregion
|
|
436
|
+
export { parseStylesheet as t };
|