@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,18 +1,115 @@
|
|
|
1
1
|
import { n as isObject } from "./guards-eV1qLbf2.js";
|
|
2
2
|
import { n as inputSchema, r as jsonSchema } from "./input-schema-zjkgrXn2.js";
|
|
3
3
|
import { n as TEMPLATE_NAME_PATTERN, t as TEMPLATE_ID_PATTERN } from "./template-zLUjcwfF.js";
|
|
4
|
-
import {
|
|
4
|
+
import { a as kebab, g as BRAND_FOOTER_MARKER, n as declarationValue, r as escapeAttribute, s as serializeStyle, t as checkAttribute } from "./html-DJUK2YJF.js";
|
|
5
|
+
import { a as EMAIL_ELEMENTS, d as reservedBrandAttributes, f as samvaBrandPlugin, s as emailElement, u as parseBrandSpecifier } from "./render-template-DwPDm3Hq.js";
|
|
5
6
|
import { FORMATTER_NAMES } from "./fmt.js";
|
|
6
|
-
import {
|
|
7
|
-
import { a as Email, d as equalColumnWidth, i as Divider, l as Section, n as Column, o as Image, p as PRIMITIVE_CLASSES, r as Columns, s as Link, t as Button, u as Spacer } from "./components-
|
|
8
|
-
import {
|
|
9
|
-
import { i as lineColumn, t as
|
|
10
|
-
import {
|
|
11
|
-
import { a as
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
import { r as collectEmailDiagnostics, t as EmailNode } from "./jsx-runtime-qL19u3Fy.js";
|
|
8
|
+
import { a as Email, d as equalColumnWidth, i as Divider, l as Section, n as Column, o as Image, p as PRIMITIVE_CLASSES, r as Columns, s as Link, t as Button, u as Spacer } from "./components-C5850-Qg.js";
|
|
9
|
+
import { l as isAssetPath, n as brandComponents } from "./brand-CIiEqkU7.js";
|
|
10
|
+
import { i as buildLineMap, s as lineColumn, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-DZKb0Xcg.js";
|
|
11
|
+
import { i as withinDarkScheme, t as DARK_SCHEME_MEDIA } from "./dark-scheme-CbCwGDWH.js";
|
|
12
|
+
import { a as lacksGenericFallback, i as fontShorthandFamilies, n as matchesSelector, r as fontFallbackDiagnostic, t as classListOf } from "./selectors-CYWCoxZr.js";
|
|
13
|
+
import { parse } from "css-tree";
|
|
14
|
+
import { parse as parse$1 } from "@babel/parser";
|
|
15
|
+
//#region src/email/theme-imports.ts
|
|
16
|
+
/** The directory part of a project path, with no trailing slash (`""` at the root). */
|
|
17
|
+
const projectDirectory = (path) => path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
|
|
18
|
+
/** A relative or root-absolute stylesheet reference, resolved from a project directory. */
|
|
19
|
+
const resolveFromDirectory = (directory, reference) => {
|
|
20
|
+
const segments = reference.startsWith("/") || directory === "" ? [] : directory.split("/");
|
|
21
|
+
for (const segment of reference.split("/")) {
|
|
22
|
+
if (segment === "" || segment === ".") continue;
|
|
23
|
+
if (segment === "..") {
|
|
24
|
+
if (segments.length === 0) return void 0;
|
|
25
|
+
segments.pop();
|
|
26
|
+
} else segments.push(segment);
|
|
27
|
+
}
|
|
28
|
+
return segments.length === 0 ? void 0 : segments.join("/");
|
|
29
|
+
};
|
|
30
|
+
const importTarget = (node) => {
|
|
31
|
+
if (node.type !== "Atrule" || node.prelude === null || node.prelude.type !== "AtrulePrelude") return { extra: true };
|
|
32
|
+
const parts = [...node.prelude.children].filter((part) => part.type !== "WhiteSpace");
|
|
33
|
+
const first = parts[0];
|
|
34
|
+
const target = first?.type === "String" ? first.value : first?.type === "Url" ? first.value : void 0;
|
|
35
|
+
return target === void 0 ? { extra: true } : {
|
|
36
|
+
target,
|
|
37
|
+
extra: parts.length > 1
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Walk the theme's `@import` graph. Project `.css` files resolve from the
|
|
42
|
+
* importing file's directory; the plugin's brand specifier, bare or with a
|
|
43
|
+
* slug, is recorded for the brand the build supplies. Anything else is an
|
|
44
|
+
* error.
|
|
45
|
+
*/
|
|
46
|
+
const resolveThemeImports = (themePath, themeCss, files, brandPlugin) => {
|
|
47
|
+
const brand = JSON.stringify(brandPlugin.specifier);
|
|
48
|
+
const stylesheets = /* @__PURE__ */ new Map([[themePath, themeCss]]);
|
|
49
|
+
const brandSpecifiers = /* @__PURE__ */ new Set();
|
|
50
|
+
const diagnostics = [];
|
|
51
|
+
const visit = (path, source, chain) => {
|
|
52
|
+
const lineMap = buildLineMap(source);
|
|
53
|
+
const ast = parse(source, {
|
|
54
|
+
positions: true,
|
|
55
|
+
parseValue: false,
|
|
56
|
+
parseRulePrelude: false
|
|
57
|
+
});
|
|
58
|
+
if (ast.type !== "StyleSheet") return;
|
|
59
|
+
for (const node of ast.children) {
|
|
60
|
+
if (node.type !== "Atrule" || node.name.toLowerCase() !== "import") continue;
|
|
61
|
+
const position = lineColumn(lineMap, node.loc?.start.offset ?? 0);
|
|
62
|
+
const origins = [{
|
|
63
|
+
fileName: path,
|
|
64
|
+
lineNumber: position.line,
|
|
65
|
+
columnNumber: position.column
|
|
66
|
+
}];
|
|
67
|
+
const refuse = (message) => diagnostics.push({
|
|
68
|
+
code: "css-import-unresolved",
|
|
69
|
+
severity: "error",
|
|
70
|
+
message,
|
|
71
|
+
origins
|
|
72
|
+
});
|
|
73
|
+
const { target, extra } = importTarget(node);
|
|
74
|
+
if (target === void 0) {
|
|
75
|
+
refuse(`${path}: @import needs a quoted stylesheet path or ${brand}.`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (extra) {
|
|
79
|
+
refuse(`${path}: @import ${JSON.stringify(target)} takes no layer, supports or media conditions; the import order is the layering.`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (parseBrandSpecifier(target, { brandPlugin }) !== void 0) {
|
|
83
|
+
brandSpecifiers.add(target);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!/^\.{0,2}\//.test(target) || !/\.css$/i.test(target.replace(/[?#].*$/, ""))) {
|
|
87
|
+
refuse(`${path}: @import ${JSON.stringify(target)} is not a project stylesheet or ${brand}. The compiler supplies Tailwind itself; import project .css files by relative path.`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const resolved = resolveFromDirectory(projectDirectory(path), target.replace(/[?#].*$/, ""));
|
|
91
|
+
const content = resolved === void 0 ? void 0 : files[resolved];
|
|
92
|
+
if (resolved === void 0 || typeof content !== "string") {
|
|
93
|
+
refuse(`${path}: @import ${JSON.stringify(target)} names no stylesheet in the project.`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (chain.includes(resolved)) {
|
|
97
|
+
refuse(`${path}: @import ${JSON.stringify(target)} imports itself through ${chain.join(" → ")}.`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (stylesheets.has(resolved)) continue;
|
|
101
|
+
stylesheets.set(resolved, content);
|
|
102
|
+
visit(resolved, content, [...chain, resolved]);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
visit(themePath, themeCss, [themePath]);
|
|
106
|
+
return {
|
|
107
|
+
stylesheets,
|
|
108
|
+
brandSpecifiers: [...brandSpecifiers].sort(),
|
|
109
|
+
diagnostics
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
//#endregion
|
|
16
113
|
//#region src/sml/diagnostics.ts
|
|
17
114
|
/** Collects the findings of one compile. Every finding carries a code, a location and a fix. */
|
|
18
115
|
var Findings = class {
|
|
@@ -134,7 +231,7 @@ const parseSource = (path, source) => {
|
|
|
134
231
|
const position = (offset) => lineColumn(lines, offset);
|
|
135
232
|
let program;
|
|
136
233
|
try {
|
|
137
|
-
program = parse(source, {
|
|
234
|
+
program = parse$1(source, {
|
|
138
235
|
plugins: ["typescript", "jsx"],
|
|
139
236
|
sourceType: "module"
|
|
140
237
|
}).program;
|
|
@@ -1078,14 +1175,14 @@ const runBuiltinComponent = (call) => {
|
|
|
1078
1175
|
lowerer.error(ctx, node, "button-dynamic-label", "Classic Outlook draws the button from a fixed box, and a label read at render time cannot size it.", "Give the button a width: <Button width={200} …>.");
|
|
1079
1176
|
return [];
|
|
1080
1177
|
}
|
|
1081
|
-
drainEmailDiagnostics();
|
|
1082
1178
|
const output = fn(props);
|
|
1083
1179
|
const location = lowerer.findings.locate(ctx.module.parsed, node);
|
|
1084
|
-
|
|
1180
|
+
const isNode = isObject(output) && typeof output.type === "string";
|
|
1181
|
+
if (isNode) for (const diagnostic of collectEmailDiagnostics(output)) lowerer.findings.items.push({
|
|
1085
1182
|
...diagnostic,
|
|
1086
1183
|
origins: [location]
|
|
1087
1184
|
});
|
|
1088
|
-
if (output !== null &&
|
|
1185
|
+
if (output !== null && !isNode) return [];
|
|
1089
1186
|
return wrap([...emailNodeToIr(output, tokens, lowerer.src(ctx.module, node))]);
|
|
1090
1187
|
};
|
|
1091
1188
|
//#endregion
|
|
@@ -2888,6 +2985,552 @@ const channelBodies = (input) => {
|
|
|
2888
2985
|
};
|
|
2889
2986
|
};
|
|
2890
2987
|
//#endregion
|
|
2988
|
+
//#region src/email/css-values.ts
|
|
2989
|
+
const MAX_RESOLVED_VALUE_LENGTH = 65536;
|
|
2990
|
+
const MAX_RESOLUTION_WORK = 262144;
|
|
2991
|
+
const resolveVariablesWithin = (value, variables, depth, budget, work) => {
|
|
2992
|
+
if (value.length > work.remaining) return void 0;
|
|
2993
|
+
work.remaining -= value.length;
|
|
2994
|
+
if (value.length > budget) return void 0;
|
|
2995
|
+
if (depth > 8 || !value.includes("var(")) return value;
|
|
2996
|
+
const chunks = [];
|
|
2997
|
+
let length = 0;
|
|
2998
|
+
const append = (chunk) => {
|
|
2999
|
+
length += chunk.length;
|
|
3000
|
+
if (length > budget) return false;
|
|
3001
|
+
chunks.push(chunk);
|
|
3002
|
+
return true;
|
|
3003
|
+
};
|
|
3004
|
+
let cursor = 0;
|
|
3005
|
+
while (cursor < value.length) {
|
|
3006
|
+
const start = value.indexOf("var(", cursor);
|
|
3007
|
+
if (start < 0) {
|
|
3008
|
+
if (!append(value.slice(cursor))) return void 0;
|
|
3009
|
+
break;
|
|
3010
|
+
}
|
|
3011
|
+
if (!append(value.slice(cursor, start))) return void 0;
|
|
3012
|
+
let level = 0;
|
|
3013
|
+
let end = -1;
|
|
3014
|
+
for (let index = start + 3; index < value.length; index++) if (value[index] === "(") level += 1;
|
|
3015
|
+
else if (value[index] === ")") {
|
|
3016
|
+
level -= 1;
|
|
3017
|
+
if (level === 0) {
|
|
3018
|
+
end = index;
|
|
3019
|
+
break;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
if (end < 0) {
|
|
3023
|
+
if (!append(value.slice(start))) return void 0;
|
|
3024
|
+
break;
|
|
3025
|
+
}
|
|
3026
|
+
const body = value.slice(start + 4, end);
|
|
3027
|
+
const comma = body.indexOf(",");
|
|
3028
|
+
const name = (comma < 0 ? body : body.slice(0, comma)).trim();
|
|
3029
|
+
const fallback = comma < 0 ? void 0 : body.slice(comma + 1).trim();
|
|
3030
|
+
const replacement = variables[name] ?? fallback;
|
|
3031
|
+
const resolved = replacement === void 0 ? value.slice(start, end + 1) : resolveVariablesWithin(replacement, variables, depth + 1, budget - length, work);
|
|
3032
|
+
if (resolved === void 0 || !append(resolved)) return void 0;
|
|
3033
|
+
cursor = end + 1;
|
|
3034
|
+
}
|
|
3035
|
+
return chunks.join("");
|
|
3036
|
+
};
|
|
3037
|
+
/**
|
|
3038
|
+
* Expand `var(--x, fallback)` against the collected custom properties.
|
|
3039
|
+
* Values whose expansion exceeds the render-time budget are left as authored.
|
|
3040
|
+
*/
|
|
3041
|
+
const resolveVariables = (value, variables, depth = 0) => resolveVariablesWithin(value, variables, depth, MAX_RESOLVED_VALUE_LENGTH, { remaining: MAX_RESOLUTION_WORK }) ?? value;
|
|
3042
|
+
/**
|
|
3043
|
+
* Fold a constant `calc()` to a plain value. Tailwind writes every spacing
|
|
3044
|
+
* utility as `calc(var(--spacing) * n)`, and classic Outlook does not implement
|
|
3045
|
+
* `calc()` at all, so an unfolded value is a silently missing style. An
|
|
3046
|
+
* expression that cannot be reduced to one term is left exactly as authored.
|
|
3047
|
+
*/
|
|
3048
|
+
const foldCalc = (value) => {
|
|
3049
|
+
if (!value.includes("calc(")) return value;
|
|
3050
|
+
let result = "";
|
|
3051
|
+
let rest = value;
|
|
3052
|
+
while (rest.length > 0) {
|
|
3053
|
+
const start = rest.indexOf("calc(");
|
|
3054
|
+
if (start < 0) {
|
|
3055
|
+
result += rest;
|
|
3056
|
+
break;
|
|
3057
|
+
}
|
|
3058
|
+
result += rest.slice(0, start);
|
|
3059
|
+
let level = 0;
|
|
3060
|
+
let end = -1;
|
|
3061
|
+
for (let index = start + 4; index < rest.length; index++) if (rest[index] === "(") level += 1;
|
|
3062
|
+
else if (rest[index] === ")") {
|
|
3063
|
+
level -= 1;
|
|
3064
|
+
if (level === 0) {
|
|
3065
|
+
end = index;
|
|
3066
|
+
break;
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
if (end < 0) {
|
|
3070
|
+
result += rest.slice(start);
|
|
3071
|
+
break;
|
|
3072
|
+
}
|
|
3073
|
+
const folded = evaluate(rest.slice(start + 5, end));
|
|
3074
|
+
result += folded === void 0 ? rest.slice(start, end + 1) : format(folded);
|
|
3075
|
+
rest = rest.slice(end + 1);
|
|
3076
|
+
}
|
|
3077
|
+
return result;
|
|
3078
|
+
};
|
|
3079
|
+
const format = (value) => `${Math.round(value.n * 1e6) / 1e6}${value.n === 0 && value.unit !== "%" ? "" : value.unit}`;
|
|
3080
|
+
const evaluate = (expression) => {
|
|
3081
|
+
const tokens = expression.match(/\(|\)|[+-]\s|[*/]|[^\s()*/]+/g);
|
|
3082
|
+
if (tokens === null) return void 0;
|
|
3083
|
+
let position = 0;
|
|
3084
|
+
const peek = () => tokens[position]?.trim();
|
|
3085
|
+
const sum = () => {
|
|
3086
|
+
let left = product();
|
|
3087
|
+
while (left !== void 0 && (peek() === "+" || peek() === "-")) {
|
|
3088
|
+
const operator = peek();
|
|
3089
|
+
position += 1;
|
|
3090
|
+
const right = product();
|
|
3091
|
+
if (right === void 0 || left.unit !== right.unit) return void 0;
|
|
3092
|
+
left = {
|
|
3093
|
+
n: operator === "+" ? left.n + right.n : left.n - right.n,
|
|
3094
|
+
unit: left.unit
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3097
|
+
return left;
|
|
3098
|
+
};
|
|
3099
|
+
const product = () => {
|
|
3100
|
+
let left = term();
|
|
3101
|
+
while (left !== void 0 && (peek() === "*" || peek() === "/")) {
|
|
3102
|
+
const operator = peek();
|
|
3103
|
+
position += 1;
|
|
3104
|
+
const right = term();
|
|
3105
|
+
if (right === void 0) return void 0;
|
|
3106
|
+
if (operator === "*") {
|
|
3107
|
+
if (left.unit !== "" && right.unit !== "") return void 0;
|
|
3108
|
+
left = {
|
|
3109
|
+
n: left.n * right.n,
|
|
3110
|
+
unit: left.unit === "" ? right.unit : left.unit
|
|
3111
|
+
};
|
|
3112
|
+
} else {
|
|
3113
|
+
if (right.unit !== "" || right.n === 0) return void 0;
|
|
3114
|
+
left = {
|
|
3115
|
+
n: left.n / right.n,
|
|
3116
|
+
unit: left.unit
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
return left;
|
|
3121
|
+
};
|
|
3122
|
+
const term = () => {
|
|
3123
|
+
const token = tokens[position];
|
|
3124
|
+
if (token === void 0) return void 0;
|
|
3125
|
+
if (token === "(") {
|
|
3126
|
+
position += 1;
|
|
3127
|
+
const inner = sum();
|
|
3128
|
+
if (peek() !== ")") return void 0;
|
|
3129
|
+
position += 1;
|
|
3130
|
+
return inner;
|
|
3131
|
+
}
|
|
3132
|
+
const match = /^(-?(?:\d+\.?\d*|\.\d+))([a-z%]*)$/i.exec(token.trim());
|
|
3133
|
+
if (match === null) return void 0;
|
|
3134
|
+
position += 1;
|
|
3135
|
+
return {
|
|
3136
|
+
n: Number.parseFloat(match[1]),
|
|
3137
|
+
unit: (match[2] ?? "").toLowerCase()
|
|
3138
|
+
};
|
|
3139
|
+
};
|
|
3140
|
+
const value = sum();
|
|
3141
|
+
return position === tokens.length ? value : void 0;
|
|
3142
|
+
};
|
|
3143
|
+
/** Resolve custom properties, then fold what the arithmetic allows. */
|
|
3144
|
+
const finalizeValue = (value, variables) => foldCalc(resolveVariables(value, variables));
|
|
3145
|
+
const RANGE_FEATURE = /\(\s*([a-z-]+)\s*(<=|>=|<|>)\s*([^)]+?)\s*\)/gi;
|
|
3146
|
+
/** Media Queries Level 4 range syntax down to the `min-`/`max-` prefixes clients support. */
|
|
3147
|
+
const downlevelMediaQuery = (condition) => condition.replace(RANGE_FEATURE, (whole, feature, comparison, bound) => {
|
|
3148
|
+
return `(${comparison.startsWith(">") ? "min-" : "max-"}${feature}:${bound})`;
|
|
3149
|
+
});
|
|
3150
|
+
/** Render the rules that could not be inlined as one `<style>` body. */
|
|
3151
|
+
const renderHeadCss = (rules, headAtRules, variables, darkVariables = {}) => {
|
|
3152
|
+
const blocks = [...headAtRules];
|
|
3153
|
+
const darkScope = {
|
|
3154
|
+
...variables,
|
|
3155
|
+
...darkVariables
|
|
3156
|
+
};
|
|
3157
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3158
|
+
for (const rule of rules) {
|
|
3159
|
+
const conditions = rule.conditions.map(downlevelMediaQuery);
|
|
3160
|
+
const key = conditions.join("\0");
|
|
3161
|
+
const bucket = grouped.get(key);
|
|
3162
|
+
if (bucket === void 0) grouped.set(key, {
|
|
3163
|
+
conditions,
|
|
3164
|
+
rules: [rule]
|
|
3165
|
+
});
|
|
3166
|
+
else bucket.rules.push(rule);
|
|
3167
|
+
}
|
|
3168
|
+
for (const { conditions, rules: bucket } of grouped.values()) {
|
|
3169
|
+
const scope = withinDarkScheme(conditions) ? darkScope : variables;
|
|
3170
|
+
const body = bucket.map((rule) => {
|
|
3171
|
+
const declarations = rule.declarations.filter((declaration) => !declaration.property.startsWith("--")).map((declaration) => `${declaration.property}:${finalizeValue(declaration.value, scope)}!important`).join(";");
|
|
3172
|
+
return declarations === "" ? "" : `${rule.selector}{${declarations}}`;
|
|
3173
|
+
}).filter((block) => block !== "").join("");
|
|
3174
|
+
if (body === "") continue;
|
|
3175
|
+
blocks.push(conditions.reduceRight((inner, condition) => `${condition}{${inner}}`, body));
|
|
3176
|
+
}
|
|
3177
|
+
return blocks.join("");
|
|
3178
|
+
};
|
|
3179
|
+
//#endregion
|
|
3180
|
+
//#region src/email/cascade.ts
|
|
3181
|
+
const VARIABLE_REFERENCE = /var\(\s*--[\w-]+\s*(?:,[^()]*)?\)/g;
|
|
3182
|
+
/**
|
|
3183
|
+
* Compatibility markup a primitive emits (a Button's VML) and a presentational
|
|
3184
|
+
* `bgcolor` attribute are not CSS, so no client resolves `var()` in them. Their
|
|
3185
|
+
* theme variables resolve here, in the scope of the element they sit in, as a
|
|
3186
|
+
* declaration on that element would.
|
|
3187
|
+
*/
|
|
3188
|
+
const resolveMarkupVariables = (markup, variables) => markup.replace(VARIABLE_REFERENCE, (reference) => {
|
|
3189
|
+
const value = finalizeValue(reference, variables);
|
|
3190
|
+
return value === reference ? reference : escapeAttribute(value);
|
|
3191
|
+
});
|
|
3192
|
+
/**
|
|
3193
|
+
* Cascade rank as the tuple CSS actually sorts by: importance first, then
|
|
3194
|
+
* whether the declaration is attached to the element itself, then layer, then
|
|
3195
|
+
* specificity, then order of appearance down to the declaration. Normal
|
|
3196
|
+
* declarations sort by layer ascending (unlayered last, so it wins); important
|
|
3197
|
+
* declarations reverse the layer order, which is why the layer term is negated
|
|
3198
|
+
* for them. Element-attached declarations are compared before layers and
|
|
3199
|
+
* specificity, so an author's important inline value beats an important
|
|
3200
|
+
* stylesheet declaration exactly as it does in a browser. Two declarations never
|
|
3201
|
+
* tie: rule order is unique across every registered sheet, and the position
|
|
3202
|
+
* inside a rule separates the declarations that share it.
|
|
3203
|
+
*/
|
|
3204
|
+
const rank = (important, attached, layer, specificity, order, position) => [
|
|
3205
|
+
important ? 1 : 0,
|
|
3206
|
+
attached ? 1 : 0,
|
|
3207
|
+
important ? -layer : layer,
|
|
3208
|
+
specificity[0],
|
|
3209
|
+
specificity[1],
|
|
3210
|
+
specificity[2],
|
|
3211
|
+
order,
|
|
3212
|
+
position
|
|
3213
|
+
];
|
|
3214
|
+
const compareRank = (left, right) => {
|
|
3215
|
+
for (let index = 0; index < Math.max(left.length, right.length); index++) {
|
|
3216
|
+
const a = left[index] ?? 0;
|
|
3217
|
+
const b = right[index] ?? 0;
|
|
3218
|
+
if (a !== b) return a - b;
|
|
3219
|
+
}
|
|
3220
|
+
return 0;
|
|
3221
|
+
};
|
|
3222
|
+
const outranks = (left, right) => compareRank(left, right) >= 0;
|
|
3223
|
+
/** An authored `style` object becomes declarations with the same normalization the serializer uses. */
|
|
3224
|
+
const inlineDeclarations = (style) => {
|
|
3225
|
+
if (!isObject(style) || Array.isArray(style)) return [];
|
|
3226
|
+
const output = [];
|
|
3227
|
+
for (const [property, value] of Object.entries(style)) {
|
|
3228
|
+
if (value === void 0 || value === null) continue;
|
|
3229
|
+
if (typeof value !== "string" && typeof value !== "number") continue;
|
|
3230
|
+
const text = declarationValue(property, value);
|
|
3231
|
+
const important = /!important\s*$/i.test(text);
|
|
3232
|
+
output.push({
|
|
3233
|
+
property: kebab(property),
|
|
3234
|
+
value: important ? text.replace(/\s*!important\s*$/i, "") : text,
|
|
3235
|
+
important
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3238
|
+
return output;
|
|
3239
|
+
};
|
|
3240
|
+
const horizontalWriting = {
|
|
3241
|
+
direction: "ltr",
|
|
3242
|
+
writingMode: "horizontal-tb"
|
|
3243
|
+
};
|
|
3244
|
+
const paddingValues = (value) => {
|
|
3245
|
+
const values = [];
|
|
3246
|
+
let depth = 0;
|
|
3247
|
+
let start = 0;
|
|
3248
|
+
for (let index = 0; index < value.length; index++) {
|
|
3249
|
+
const character = value[index];
|
|
3250
|
+
if (character === "(") depth++;
|
|
3251
|
+
else if (character === ")") depth--;
|
|
3252
|
+
else if (/\s/.test(character) && depth === 0) {
|
|
3253
|
+
if (index > start) values.push(value.slice(start, index));
|
|
3254
|
+
start = index + 1;
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
if (start < value.length) values.push(value.slice(start));
|
|
3258
|
+
return values;
|
|
3259
|
+
};
|
|
3260
|
+
const physicalPadding = (property, value, context) => {
|
|
3261
|
+
const original = [[property, value]];
|
|
3262
|
+
const match = /^padding-(inline|block)(?:-(start|end))?$/.exec(property);
|
|
3263
|
+
if (match === null || context.writingMode !== "horizontal-tb" || !["ltr", "rtl"].includes(context.direction) || /^(inherit|revert|revert-layer)$/.test(value) || value.includes("var(")) return original;
|
|
3264
|
+
const values = paddingValues(value.trim());
|
|
3265
|
+
if (values.length < 1 || values.length > (match[2] === void 0 ? 2 : 1)) return original;
|
|
3266
|
+
const sides = match[1] === "block" ? ["top", "bottom"] : context.direction === "rtl" ? ["right", "left"] : ["left", "right"];
|
|
3267
|
+
if (match[2] !== void 0) return [[`padding-${sides[match[2] === "start" ? 0 : 1]}`, values[0]]];
|
|
3268
|
+
return [[`padding-${sides[0]}`, values[0]], [`padding-${sides[1]}`, values[1] ?? values[0]]];
|
|
3269
|
+
};
|
|
3270
|
+
/** Generated dark-mode classes share this prefix; it is reserved for the compiler. */
|
|
3271
|
+
const DARK_CLASS_PREFIX = "sml-dark-";
|
|
3272
|
+
/** A class named for its declarations, so equal sets share one rule and builds are stable. */
|
|
3273
|
+
const darkClassName = (body) => {
|
|
3274
|
+
let hash = 2166136261;
|
|
3275
|
+
for (let index = 0; index < body.length; index++) {
|
|
3276
|
+
hash ^= body.charCodeAt(index);
|
|
3277
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
3278
|
+
}
|
|
3279
|
+
return `${DARK_CLASS_PREFIX}${hash.toString(36)}`;
|
|
3280
|
+
};
|
|
3281
|
+
const SIDES = [
|
|
3282
|
+
"top",
|
|
3283
|
+
"right",
|
|
3284
|
+
"bottom",
|
|
3285
|
+
"left"
|
|
3286
|
+
];
|
|
3287
|
+
const BORDER_PARTS = [
|
|
3288
|
+
"width",
|
|
3289
|
+
"style",
|
|
3290
|
+
"color"
|
|
3291
|
+
];
|
|
3292
|
+
const CORNERS = [
|
|
3293
|
+
"top-left",
|
|
3294
|
+
"top-right",
|
|
3295
|
+
"bottom-right",
|
|
3296
|
+
"bottom-left"
|
|
3297
|
+
];
|
|
3298
|
+
const BORDER_IMAGE = [
|
|
3299
|
+
"source",
|
|
3300
|
+
"slice",
|
|
3301
|
+
"width",
|
|
3302
|
+
"outset",
|
|
3303
|
+
"repeat"
|
|
3304
|
+
].map((part) => `border-image-${part}`);
|
|
3305
|
+
/**
|
|
3306
|
+
* The longhands a border property writes: `border` writes the twelve per-side longhands and
|
|
3307
|
+
* resets the border image, `border-top` the top three, `border-color` the four colors,
|
|
3308
|
+
* `border-radius` the four corners.
|
|
3309
|
+
* `undefined` for a property outside the border box.
|
|
3310
|
+
*/
|
|
3311
|
+
const borderLonghands = (property) => {
|
|
3312
|
+
if (property === "border") return [...SIDES.flatMap((side) => BORDER_PARTS.map((part) => `border-${side}-${part}`)), ...BORDER_IMAGE];
|
|
3313
|
+
if (property === "border-image") return BORDER_IMAGE;
|
|
3314
|
+
if (property === "border-radius") return CORNERS.map((corner) => `border-${corner}-radius`);
|
|
3315
|
+
const side = /^border-(top|right|bottom|left)$/.exec(property)?.[1];
|
|
3316
|
+
if (side !== void 0) return BORDER_PARTS.map((part) => `border-${side}-${part}`);
|
|
3317
|
+
const part = /^border-(width|style|color)$/.exec(property)?.[1];
|
|
3318
|
+
if (part !== void 0) return SIDES.map((each) => `border-${each}-${part}`);
|
|
3319
|
+
return property.startsWith("border-") ? [property] : void 0;
|
|
3320
|
+
};
|
|
3321
|
+
/**
|
|
3322
|
+
* Whether two declarations can override one another because they write a property in common:
|
|
3323
|
+
* `background` and `background-color`, `border-color` and `border-top`, `font` and `line-height`.
|
|
3324
|
+
* `border-radius` shares nothing with `border-color`.
|
|
3325
|
+
*/
|
|
3326
|
+
const overlaps = (left, right) => {
|
|
3327
|
+
if (left === right) return true;
|
|
3328
|
+
const leftBorder = borderLonghands(left);
|
|
3329
|
+
const rightBorder = borderLonghands(right);
|
|
3330
|
+
if (leftBorder !== void 0 || rightBorder !== void 0) return leftBorder !== void 0 && rightBorder !== void 0 && leftBorder.some((longhand) => rightBorder.includes(longhand));
|
|
3331
|
+
const sets = (shorthand, longhand) => longhand.startsWith(`${shorthand}-`) || shorthand === "font" && longhand === "line-height";
|
|
3332
|
+
return sets(left, right) || sets(right, left);
|
|
3333
|
+
};
|
|
3334
|
+
/** Add a class to an element's props, whichever of `class` and `className` it uses. */
|
|
3335
|
+
const withClass = (props, name) => {
|
|
3336
|
+
const key = typeof props.class === "string" || props.className === void 0 ? "class" : "className";
|
|
3337
|
+
const current = props[key];
|
|
3338
|
+
return {
|
|
3339
|
+
...props,
|
|
3340
|
+
[key]: typeof current === "string" && current.trim() !== "" ? `${current} ${name}` : name
|
|
3341
|
+
};
|
|
3342
|
+
};
|
|
3343
|
+
/** The generated dark-mode classes as one `@media (prefers-color-scheme:dark)` block, written as the head CSS writes its media queries. */
|
|
3344
|
+
const renderDarkRules = (rules) => rules.size === 0 ? "" : `${DARK_SCHEME_MEDIA.replace(": ", ":")}{${[...rules].sort(([left], [right]) => left.localeCompare(right)).map(([name, body]) => `.${name}{${body}}`).join("")}}`;
|
|
3345
|
+
/**
|
|
3346
|
+
* Resolve every stylesheet against the tree: inline what a client will honor on
|
|
3347
|
+
* the element, and keep the rest for `<head>`. Where the sheet has a dark-mode
|
|
3348
|
+
* theme, an element whose inlined styles read a color it redefines also gets a
|
|
3349
|
+
* generated class whose dark-mode rule restates those styles at their dark
|
|
3350
|
+
* values.
|
|
3351
|
+
*/
|
|
3352
|
+
const applyStylesheet = (root, sheet, options = {}) => {
|
|
3353
|
+
const diagnostics = [...sheet.diagnostics];
|
|
3354
|
+
const hasDarkTheme = Object.keys(sheet.darkVariables).length > 0;
|
|
3355
|
+
const darkRules = /* @__PURE__ */ new Map();
|
|
3356
|
+
const darkClassFor = (declarations) => {
|
|
3357
|
+
const body = declarations.map(([property, value]) => `${property}:${value}!important`).join(";");
|
|
3358
|
+
const name = darkClassName(body);
|
|
3359
|
+
darkRules.set(name, body);
|
|
3360
|
+
return name;
|
|
3361
|
+
};
|
|
3362
|
+
const inlinable = [];
|
|
3363
|
+
const retained = [];
|
|
3364
|
+
const styledClasses = /* @__PURE__ */ new Set();
|
|
3365
|
+
for (const rule of sheet.rules) {
|
|
3366
|
+
(rule.inlinable ? inlinable : retained).push(rule);
|
|
3367
|
+
for (const name of rule.classes) styledClasses.add(name);
|
|
3368
|
+
}
|
|
3369
|
+
const conditionalWritingRules = retained.filter((rule) => rule.declarations.some((declaration) => declaration.property === "direction" || declaration.property === "writing-mode" || declaration.property === "all"));
|
|
3370
|
+
const unmatched = /* @__PURE__ */ new Map();
|
|
3371
|
+
const declaredFamilies = new Set(sheet.fontFaces.map((face) => face.family.toLowerCase()));
|
|
3372
|
+
const fallbackFindings = /* @__PURE__ */ new Map();
|
|
3373
|
+
const checkFallback = (property, value, origins) => {
|
|
3374
|
+
if (declaredFamilies.size === 0) return;
|
|
3375
|
+
const stack = property === "font-family" ? value : property === "font" ? fontShorthandFamilies(value) : void 0;
|
|
3376
|
+
if (stack === void 0 || fallbackFindings.has(stack)) return;
|
|
3377
|
+
if (lacksGenericFallback(stack, declaredFamilies)) fallbackFindings.set(stack, origins);
|
|
3378
|
+
};
|
|
3379
|
+
/**
|
|
3380
|
+
* Children are visited with their own preceding-sibling list so `+`/`~` work.
|
|
3381
|
+
* A fragment is not an element in the serialized document, so its children
|
|
3382
|
+
* join that same list rather than starting a new one.
|
|
3383
|
+
*/
|
|
3384
|
+
const visitChildren = (children, ancestry, variables, darkScope, writing) => {
|
|
3385
|
+
const previous = [];
|
|
3386
|
+
const visit = (nodes) => nodes.map((child) => {
|
|
3387
|
+
if (child.type === "Element") {
|
|
3388
|
+
const result = applyTo(child, {
|
|
3389
|
+
element: child,
|
|
3390
|
+
parent: ancestry,
|
|
3391
|
+
previous: [...previous]
|
|
3392
|
+
}, variables, darkScope, writing);
|
|
3393
|
+
previous.push(child);
|
|
3394
|
+
return result;
|
|
3395
|
+
}
|
|
3396
|
+
if (child.type === "Fragment") return EmailNode.Fragment({
|
|
3397
|
+
...child,
|
|
3398
|
+
children: visit(child.children)
|
|
3399
|
+
});
|
|
3400
|
+
if (child.type === "Raw" && child.html.includes("var(")) return EmailNode.Raw({
|
|
3401
|
+
...child,
|
|
3402
|
+
html: resolveMarkupVariables(child.html, variables)
|
|
3403
|
+
});
|
|
3404
|
+
return child;
|
|
3405
|
+
});
|
|
3406
|
+
return visit(children);
|
|
3407
|
+
};
|
|
3408
|
+
/**
|
|
3409
|
+
* `inherited` is the custom-property scope this element sees from its
|
|
3410
|
+
* ancestors, rooted in the sheet's unconditional properties. Custom properties
|
|
3411
|
+
* cascade like any other declaration and then inherit, so a `--brand` declared
|
|
3412
|
+
* on `.card` has to reach `var(--brand)` on `.card` and everything under it.
|
|
3413
|
+
*/
|
|
3414
|
+
const applyTo = (element, ancestry, inherited, inheritedDark, inheritedWriting) => {
|
|
3415
|
+
const conditionalWriting = conditionalWritingRules.some((rule) => {
|
|
3416
|
+
if (rule.matcher === void 0 || rule.matcher.parts.length === 0 || rule.matcher.parts.some((part) => part.compound.opaque)) return true;
|
|
3417
|
+
for (let context = ancestry; context !== void 0; context = context.parent) if (matchesSelector(rule.matcher, context)) return true;
|
|
3418
|
+
return false;
|
|
3419
|
+
});
|
|
3420
|
+
const winners = /* @__PURE__ */ new Map();
|
|
3421
|
+
const claim = (declaration, key) => {
|
|
3422
|
+
const current = winners.get(declaration.property);
|
|
3423
|
+
if (current === void 0 || outranks(key, current.key)) winners.set(declaration.property, {
|
|
3424
|
+
value: declaration.value,
|
|
3425
|
+
key
|
|
3426
|
+
});
|
|
3427
|
+
};
|
|
3428
|
+
for (const rule of inlinable) {
|
|
3429
|
+
if (!matchesSelector(rule.matcher, ancestry)) continue;
|
|
3430
|
+
for (const [position, declaration] of rule.declarations.entries()) claim(declaration, rank(declaration.important, false, rule.layer, rule.specificity, rule.order, position));
|
|
3431
|
+
}
|
|
3432
|
+
for (const [position, declaration] of inlineDeclarations(element.props.style).entries()) claim(declaration, rank(declaration.important, true, 0, [
|
|
3433
|
+
0,
|
|
3434
|
+
0,
|
|
3435
|
+
0
|
|
3436
|
+
], 0, position));
|
|
3437
|
+
for (const name of classListOf(element)) if (!styledClasses.has(name) && options.knownClasses?.has(name) !== true) unmatched.set(name, [...unmatched.get(name) ?? [], ...element.origins]);
|
|
3438
|
+
const declared = {};
|
|
3439
|
+
for (const [property, entry] of winners) if (property.startsWith("--")) declared[property] = entry.value;
|
|
3440
|
+
const variables = Object.keys(declared).length === 0 ? inherited : {
|
|
3441
|
+
...inherited,
|
|
3442
|
+
...declared
|
|
3443
|
+
};
|
|
3444
|
+
const darkScope = Object.keys(declared).length === 0 ? inheritedDark : {
|
|
3445
|
+
...inheritedDark,
|
|
3446
|
+
...declared
|
|
3447
|
+
};
|
|
3448
|
+
const inheritedValue = (property, parent, initial, fallback = parent) => {
|
|
3449
|
+
const value = winners.get(property)?.value;
|
|
3450
|
+
if (value === void 0) return fallback;
|
|
3451
|
+
const resolved = finalizeValue(value, variables);
|
|
3452
|
+
return resolved === "inherit" || resolved === "unset" ? parent : resolved === "initial" ? initial : resolved;
|
|
3453
|
+
};
|
|
3454
|
+
const writing = {
|
|
3455
|
+
direction: inheritedValue("direction", inheritedWriting.direction, "ltr", typeof element.props.dir === "string" ? element.props.dir : element.tag === "bdi" ? "auto" : inheritedWriting.direction),
|
|
3456
|
+
writingMode: winners.has("all") ? "unknown" : inheritedValue("writing-mode", inheritedWriting.writingMode, "horizontal-tb")
|
|
3457
|
+
};
|
|
3458
|
+
const emitted = [...winners].filter(([property]) => !property.startsWith("--")).sort(([, left], [, right]) => compareRank(left.key, right.key)).map(([property, entry]) => [property, finalizeValue(entry.value, variables)]);
|
|
3459
|
+
for (const [property, value] of emitted) checkFallback(property, value, element.origins.slice(0, 1));
|
|
3460
|
+
const physical = /* @__PURE__ */ new Map();
|
|
3461
|
+
for (const [property, value] of emitted) for (const [name, lowered] of conditionalWriting ? [[property, value]] : physicalPadding(property, value, writing)) {
|
|
3462
|
+
physical.delete(name);
|
|
3463
|
+
physical.set(name, lowered);
|
|
3464
|
+
}
|
|
3465
|
+
const bgcolor = element.props.bgcolor;
|
|
3466
|
+
const lightProps = typeof bgcolor === "string" && bgcolor.includes("var(") ? {
|
|
3467
|
+
...element.props,
|
|
3468
|
+
bgcolor: finalizeValue(bgcolor, variables)
|
|
3469
|
+
} : element.props;
|
|
3470
|
+
const dark = [];
|
|
3471
|
+
if (hasDarkTheme) {
|
|
3472
|
+
const changed = [];
|
|
3473
|
+
for (const [property, entry] of winners) {
|
|
3474
|
+
if (property.startsWith("--") || !entry.value.includes("var(")) continue;
|
|
3475
|
+
if (finalizeValue(entry.value, darkScope) !== finalizeValue(entry.value, variables)) changed.push(property);
|
|
3476
|
+
}
|
|
3477
|
+
const restated = new Set(changed);
|
|
3478
|
+
for (let grew = changed.length > 0; grew;) {
|
|
3479
|
+
grew = false;
|
|
3480
|
+
for (const property of winners.keys()) if (!property.startsWith("--") && !restated.has(property) && [...restated].some((candidate) => overlaps(candidate, property))) {
|
|
3481
|
+
restated.add(property);
|
|
3482
|
+
grew = true;
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
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)]);
|
|
3486
|
+
if (typeof bgcolor === "string" && bgcolor.includes("var(") && !winners.has("background-color") && !winners.has("background")) {
|
|
3487
|
+
const value = finalizeValue(bgcolor, darkScope);
|
|
3488
|
+
if (value !== finalizeValue(bgcolor, variables)) dark.push(["background-color", value]);
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
const darkClass = dark.length === 0 ? void 0 : darkClassFor(dark);
|
|
3492
|
+
const props = darkClass === void 0 ? lightProps : withClass(lightProps, darkClass);
|
|
3493
|
+
return EmailNode.Element({
|
|
3494
|
+
...element,
|
|
3495
|
+
props: emitted.length === 0 ? props : {
|
|
3496
|
+
...props,
|
|
3497
|
+
style: Object.fromEntries(physical)
|
|
3498
|
+
},
|
|
3499
|
+
children: visitChildren(element.children, ancestry, variables, darkScope, writing)
|
|
3500
|
+
});
|
|
3501
|
+
};
|
|
3502
|
+
const rootDark = {
|
|
3503
|
+
...sheet.variables,
|
|
3504
|
+
...sheet.darkVariables
|
|
3505
|
+
};
|
|
3506
|
+
const tree = root.type === "Element" ? applyTo(root, {
|
|
3507
|
+
element: root,
|
|
3508
|
+
parent: void 0,
|
|
3509
|
+
previous: []
|
|
3510
|
+
}, sheet.variables, rootDark, horizontalWriting) : root.type === "Fragment" ? EmailNode.Fragment({
|
|
3511
|
+
...root,
|
|
3512
|
+
children: visitChildren(root.children, void 0, sheet.variables, rootDark, horizontalWriting)
|
|
3513
|
+
}) : root;
|
|
3514
|
+
for (const rule of retained) for (const declaration of rule.declarations) checkFallback(declaration.property, finalizeValue(declaration.value, sheet.variables), []);
|
|
3515
|
+
for (const [value, origins] of fallbackFindings) diagnostics.push({
|
|
3516
|
+
...fontFallbackDiagnostic(value),
|
|
3517
|
+
origins
|
|
3518
|
+
});
|
|
3519
|
+
for (const [name, origins] of unmatched) diagnostics.push({
|
|
3520
|
+
code: "unmatched-class",
|
|
3521
|
+
severity: "warning",
|
|
3522
|
+
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.`,
|
|
3523
|
+
origins: origins.slice(0, 1)
|
|
3524
|
+
});
|
|
3525
|
+
return {
|
|
3526
|
+
tree,
|
|
3527
|
+
headCss: renderHeadCss(retained, sheet.headAtRules, sheet.variables, sheet.darkVariables),
|
|
3528
|
+
darkRules,
|
|
3529
|
+
fontFaceCss: sheet.fontFaces.map((face) => face.css).join(""),
|
|
3530
|
+
diagnostics
|
|
3531
|
+
};
|
|
3532
|
+
};
|
|
3533
|
+
//#endregion
|
|
2891
3534
|
//#region src/sml/styles.ts
|
|
2892
3535
|
const MAX_CLASS_VARIANTS = 8;
|
|
2893
3536
|
const product = (left, right) => {
|
|
@@ -3298,18 +3941,45 @@ const editFirst = (nodes, matches, edit) => {
|
|
|
3298
3941
|
};
|
|
3299
3942
|
const HEAD_COMMENT_OPEN = { raw: ["<!--[if !mso]><!-->"] };
|
|
3300
3943
|
const HEAD_COMMENT_CLOSE = { raw: ["<!--<![endif]-->"] };
|
|
3301
|
-
|
|
3302
|
-
|
|
3944
|
+
/** Literal absolute http(s) image sources outside the project's asset base. */
|
|
3945
|
+
const externalImages = (root, assetBase) => {
|
|
3946
|
+
const found = [];
|
|
3947
|
+
const base = assetBase === void 0 ? void 0 : assetBase.replace(/\/+$/, "") + "/";
|
|
3948
|
+
const visit = (node) => {
|
|
3949
|
+
if (typeof node !== "object" || node === null) return;
|
|
3950
|
+
if ("el" in node) {
|
|
3951
|
+
const url = node.el === "img" ? node.attrs?.src : void 0;
|
|
3952
|
+
if (typeof url === "string" && /^https?:\/\//i.test(url)) {
|
|
3953
|
+
const host = /^https?:\/\/([^/?#]+)/i.exec(url)?.[1] ?? url;
|
|
3954
|
+
if (base === void 0 || !url.toLowerCase().startsWith(base.toLowerCase())) found.push({
|
|
3955
|
+
url,
|
|
3956
|
+
host,
|
|
3957
|
+
src: node.src
|
|
3958
|
+
});
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
const nested = "el" in node || "each" in node ? node.children : void 0;
|
|
3962
|
+
for (const item of nested ?? []) visit(item);
|
|
3963
|
+
if ("if" in node && "then" in node && Array.isArray(node.then)) {
|
|
3964
|
+
for (const item of node.then) visit(item);
|
|
3965
|
+
if (Array.isArray(node.else)) for (const item of node.else) visit(item);
|
|
3966
|
+
}
|
|
3967
|
+
};
|
|
3968
|
+
visit(root);
|
|
3969
|
+
return found;
|
|
3970
|
+
};
|
|
3971
|
+
const completed = (findings, template, fixtures = {}, assets = []) => ({
|
|
3972
|
+
template: findings.blocking ? void 0 : template,
|
|
3303
3973
|
fixtures,
|
|
3304
3974
|
assets,
|
|
3305
3975
|
diagnostics: findings.items
|
|
3306
3976
|
});
|
|
3307
3977
|
/**
|
|
3308
|
-
* Lower a loaded project to
|
|
3309
|
-
* or brand. `
|
|
3978
|
+
* Lower a loaded project to a compiled template: the part of the compile that reads structure and
|
|
3979
|
+
* needs no Tailwind or brand. `checkTemplate` runs it with empty stylesheets.
|
|
3310
3980
|
*/
|
|
3311
3981
|
const lowerProject = (options, project, findings, inputs) => {
|
|
3312
|
-
const done = (
|
|
3982
|
+
const done = (template, fixtures = {}, assets = inputs.assets) => completed(findings, template, fixtures, assets);
|
|
3313
3983
|
const urlFor = inputs.urlFor;
|
|
3314
3984
|
const staticContext = {
|
|
3315
3985
|
project,
|
|
@@ -3419,7 +4089,7 @@ const lowerProject = (options, project, findings, inputs) => {
|
|
|
3419
4089
|
"locale",
|
|
3420
4090
|
...CHANNELS
|
|
3421
4091
|
].includes(name)) findings.add(module.parsed, at(name), "unknown-field", `\`${name}\` is not a defineTemplate field.`, "The fields are id, schema, fixtures, locale, email, sms and whatsapp.");
|
|
3422
|
-
let
|
|
4092
|
+
let template;
|
|
3423
4093
|
const checks = [];
|
|
3424
4094
|
const emailField = fields.get("email");
|
|
3425
4095
|
let emailIr;
|
|
@@ -3514,6 +4184,16 @@ const lowerProject = (options, project, findings, inputs) => {
|
|
|
3514
4184
|
else root = edited.nodes[0] ?? root;
|
|
3515
4185
|
}
|
|
3516
4186
|
checkStructure([root], (finding) => checks.push(finding));
|
|
4187
|
+
for (const external of externalImages(root, options.assetBase)) {
|
|
4188
|
+
const where = locate(external.src);
|
|
4189
|
+
findings.report({
|
|
4190
|
+
code: "email-external-asset",
|
|
4191
|
+
severity: "warning",
|
|
4192
|
+
message: `The image ${JSON.stringify(external.url)} is hosted on ${external.host}, outside the project. A hotlinked image can disappear, be blocked by the client, track recipients or slow the message.`,
|
|
4193
|
+
fix: "Add the image to the project's files and import it (import logo from './logo.png'), then use the import as the src.",
|
|
4194
|
+
origins: where === void 0 ? [] : [where]
|
|
4195
|
+
});
|
|
4196
|
+
}
|
|
3517
4197
|
if (schemaJson !== void 0) checks.push(...checkBindings(schemaJson, [root], [subject, ...preheader === void 0 ? [] : [preheader]]));
|
|
3518
4198
|
emailIr = {
|
|
3519
4199
|
subject,
|
|
@@ -3551,7 +4231,7 @@ const lowerProject = (options, project, findings, inputs) => {
|
|
|
3551
4231
|
}]
|
|
3552
4232
|
});
|
|
3553
4233
|
}
|
|
3554
|
-
if (schemaJson !== void 0 && templateId !== "" && (emailIr !== void 0 || channelIr.sms !== void 0 || channelIr.whatsapp !== void 0))
|
|
4234
|
+
if (schemaJson !== void 0 && templateId !== "" && (emailIr !== void 0 || channelIr.sms !== void 0 || channelIr.whatsapp !== void 0)) template = {
|
|
3555
4235
|
sml: 1,
|
|
3556
4236
|
template: templateId,
|
|
3557
4237
|
...locale === void 0 ? {} : { locale },
|
|
@@ -3562,7 +4242,7 @@ const lowerProject = (options, project, findings, inputs) => {
|
|
|
3562
4242
|
schema: schemaJson
|
|
3563
4243
|
};
|
|
3564
4244
|
else if (emailField === void 0 && !fields.has("sms") && !fields.has("whatsapp")) findings.add(module.parsed, definition.call, "no-channel", "The template declares no channel.", "Add `email`, `sms` or `whatsapp`.");
|
|
3565
|
-
return done(
|
|
4245
|
+
return done(template, fixtures);
|
|
3566
4246
|
};
|
|
3567
4247
|
//#endregion
|
|
3568
4248
|
//#region src/sml/project.ts
|
|
@@ -3667,7 +4347,7 @@ const loadProject = (files, entryPath, findings, loadOptions) => {
|
|
|
3667
4347
|
}
|
|
3668
4348
|
break;
|
|
3669
4349
|
}
|
|
3670
|
-
if (parseBrandSpecifier(specifier, loadOptions.brandPlugin) !== void 0) {
|
|
4350
|
+
if (parseBrandSpecifier(specifier, { brandPlugin: loadOptions.brandPlugin }) !== void 0) {
|
|
3671
4351
|
for (const item of specifiers) if (text(item, "importKind") !== "type") imports.set(local(item), {
|
|
3672
4352
|
kind: "brand",
|
|
3673
4353
|
specifier,
|
|
@@ -3792,6 +4472,8 @@ const loadProject = (files, entryPath, findings, loadOptions) => {
|
|
|
3792
4472
|
//#endregion
|
|
3793
4473
|
//#region src/sml/profile.ts
|
|
3794
4474
|
const DEFAULT_ENTRY = "template.tsx";
|
|
4475
|
+
/** A stand-in base: a structural check has no build, and an imported asset must read as a project asset. */
|
|
4476
|
+
const ASSET_BASE = "https://assets.invalid";
|
|
3795
4477
|
/** Findings that come from the inputs this check leaves out, not from the source. */
|
|
3796
4478
|
const MISSING_INPUT_CODES = /* @__PURE__ */ new Set(["brand-unavailable", "unmatched-class"]);
|
|
3797
4479
|
const asFiles = (files, entry) => typeof files === "string" ? { [entry]: files } : files;
|
|
@@ -3800,7 +4482,9 @@ const asFiles = (files, entry) => typeof files === "string" ? { [entry]: files }
|
|
|
3800
4482
|
* and the entry's path. Imports of files that are absent are trusted to exist, and the brand,
|
|
3801
4483
|
* Tailwind and theme are not resolved, so the result holds only findings the source itself owns.
|
|
3802
4484
|
*/
|
|
3803
|
-
const
|
|
4485
|
+
const checkTemplate = (files, options = {}) => {
|
|
4486
|
+
const entry = options.entry ?? DEFAULT_ENTRY;
|
|
4487
|
+
const brandPlugin = options.brandPlugin ?? samvaBrandPlugin;
|
|
3804
4488
|
const findings = new Findings();
|
|
3805
4489
|
const project = loadProject(asFiles(files, entry), entry, findings, {
|
|
3806
4490
|
brandPlugin,
|
|
@@ -3810,12 +4494,13 @@ const checkStaticProfile = (files, entry = DEFAULT_ENTRY, brandPlugin = samvaBra
|
|
|
3810
4494
|
lowerProject({
|
|
3811
4495
|
entry,
|
|
3812
4496
|
brand: void 0,
|
|
3813
|
-
brandPlugin
|
|
4497
|
+
brandPlugin,
|
|
4498
|
+
assetBase: ASSET_BASE
|
|
3814
4499
|
}, project, findings, {
|
|
3815
4500
|
assets: [],
|
|
3816
4501
|
stylesheets: /* @__PURE__ */ new Map(),
|
|
3817
4502
|
tailwindSheet: void 0,
|
|
3818
|
-
urlFor: (path) =>
|
|
4503
|
+
urlFor: (path) => `${ASSET_BASE}/${path}`
|
|
3819
4504
|
});
|
|
3820
4505
|
const unresolved = [...project.unresolved];
|
|
3821
4506
|
return findings.items.filter((item) => !MISSING_INPUT_CODES.has(item.code) && !item.code.startsWith("tailwind-") && !unresolved.some((name) => item.message.includes(`<${name}`) || item.message.includes(`\`${name}\``)));
|
|
@@ -3825,7 +4510,7 @@ const key = (item) => `${item.code}\0${item.message}`;
|
|
|
3825
4510
|
* The errors in `after` that `before` did not already have, compared by code and message so a
|
|
3826
4511
|
* finding that only moved does not count as new.
|
|
3827
4512
|
*/
|
|
3828
|
-
const
|
|
4513
|
+
const introducedErrors = (before, after) => {
|
|
3829
4514
|
const known = /* @__PURE__ */ new Map();
|
|
3830
4515
|
for (const item of before) if (item.severity === "error") known.set(key(item), (known.get(key(item)) ?? 0) + 1);
|
|
3831
4516
|
const added = [];
|
|
@@ -3838,4 +4523,4 @@ const introducedProfileErrors = (before, after) => {
|
|
|
3838
4523
|
return added;
|
|
3839
4524
|
};
|
|
3840
4525
|
//#endregion
|
|
3841
|
-
export { lowerProject as a,
|
|
4526
|
+
export { lowerProject as a, SMS_CATEGORIES as c, Findings as d, projectDirectory as f, completed as i, WHATSAPP_CATEGORIES as l, resolveThemeImports as m, introducedErrors as n, applyStylesheet as o, resolveFromDirectory as p, loadProject as r, renderDarkRules as s, checkTemplate as t, WHATSAPP_HEADER_TYPES as u };
|