@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.
Files changed (51) hide show
  1. package/AGENTS.md +42 -35
  2. package/CHANGELOG.md +92 -0
  3. package/README.md +10 -9
  4. package/dist/{assets-tISHaQNF.d.ts → assets-CS5Xa_mr.d.ts} +3 -3
  5. package/dist/{brand-nreeKWxF.js → brand-CIiEqkU7.js} +24 -31
  6. package/dist/{brand-plugin-CHhcei-j.d.ts → brand-plugin-Dj9SMpfS.d.ts} +3 -1
  7. package/dist/brand.d.ts +2 -2
  8. package/dist/brand.js +3 -3
  9. package/dist/compatibility.d.ts +76 -0
  10. package/dist/compatibility.js +807 -0
  11. package/dist/compiled-template-Bc4Dq9zy.d.ts +268 -0
  12. package/dist/compiler.d.ts +58 -225
  13. package/dist/compiler.js +261 -1297
  14. package/dist/{components-3XHjYD3i.js → components-C5850-Qg.js} +37 -29
  15. package/dist/css-B_aVKrDo.js +436 -0
  16. package/dist/css-color-DLM12Osu.js +315 -0
  17. package/dist/{diagnostic-codes-UXwOO3A-.js → diagnostic-codes-DZKb0Xcg.js} +4 -0
  18. package/dist/{diagnostics-CUYlQ_6x.d.ts → diagnostics-D4vSLQrO.d.ts} +1 -10
  19. package/dist/diagnostics.d.ts +6 -2
  20. package/dist/diagnostics.js +3 -3
  21. package/dist/edit.d.ts +10 -9
  22. package/dist/edit.js +12 -11
  23. package/dist/email/jsx-dev-runtime.d.ts +1 -1
  24. package/dist/email/jsx-dev-runtime.js +1 -1
  25. package/dist/email/jsx-runtime.d.ts +2 -2
  26. package/dist/email/jsx-runtime.js +2 -2
  27. package/dist/email.d.ts +1 -1
  28. package/dist/email.js +1 -1
  29. package/dist/html-DJUK2YJF.js +483 -0
  30. package/dist/index.d.ts +1 -1
  31. package/dist/{jsx-runtime-DC7xG0aA.d.ts → jsx-runtime-DL8NVF1f.d.ts} +12 -6
  32. package/dist/{jsx-runtime-CBlexoeR.js → jsx-runtime-qL19u3Fy.js} +17 -10
  33. package/dist/preview-rNoJBEIY.d.ts +104 -0
  34. package/dist/{profile-AatO_SvP.js → profile-JTvgCD6X.js} +614 -24
  35. package/dist/{html-BvRFepeO.js → render-template-DwPDm3Hq.js} +155 -572
  36. package/dist/render.d.ts +40 -28
  37. package/dist/render.js +3 -47
  38. package/dist/selectors-CYWCoxZr.js +157 -0
  39. package/dist/sms.d.ts +1 -1
  40. package/dist/sms.js +1 -1
  41. package/dist/{template-B-UDXK8a.d.ts → template-DmNcoH4Q.d.ts} +2 -9
  42. package/dist/{theme-css-s9QwD5Ko.js → theme-css-P9F5S7Ic.js} +3 -316
  43. package/dist/whatsapp.d.ts +1 -1
  44. package/dist/whatsapp.js +1 -1
  45. package/generated/contract.json +7 -2
  46. package/generated/reference.md +3 -2
  47. package/package.json +5 -1
  48. package/skills/sml/SKILL.md +2 -2
  49. package/dist/ir-DNL6H7jA.js +0 -705
  50. package/dist/render-BdvewoQt.d.ts +0 -564
  51. package/dist/render-DpYouYYG.js +0 -258
@@ -1,13 +1,15 @@
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 { D as samvaBrandPlugin, E as reservedBrandAttributes, O as BRAND_FOOTER_MARKER, S as emailElement, T as parseBrandSpecifier, b as EMAIL_ELEMENTS, s as serializeStyle, t as checkAttribute } from "./html-BvRFepeO.js";
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 { i as drainEmailDiagnostics, t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
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-3XHjYD3i.js";
8
- import { i as renderDarkRules, n as DARK_CLASS_PREFIX, r as applyStylesheet } from "./ir-DNL6H7jA.js";
9
- import { i as buildLineMap, s as lineColumn, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-UXwOO3A-.js";
10
- import { l as isAssetPath, n as brandComponents } from "./brand-nreeKWxF.js";
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";
11
13
  import { parse } from "css-tree";
12
14
  import { parse as parse$1 } from "@babel/parser";
13
15
  //#region src/email/theme-imports.ts
@@ -77,7 +79,7 @@ const resolveThemeImports = (themePath, themeCss, files, brandPlugin) => {
77
79
  refuse(`${path}: @import ${JSON.stringify(target)} takes no layer, supports or media conditions; the import order is the layering.`);
78
80
  continue;
79
81
  }
80
- if (parseBrandSpecifier(target, brandPlugin) !== void 0) {
82
+ if (parseBrandSpecifier(target, { brandPlugin }) !== void 0) {
81
83
  brandSpecifiers.add(target);
82
84
  continue;
83
85
  }
@@ -1173,14 +1175,14 @@ const runBuiltinComponent = (call) => {
1173
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} …>.");
1174
1176
  return [];
1175
1177
  }
1176
- drainEmailDiagnostics();
1177
1178
  const output = fn(props);
1178
1179
  const location = lowerer.findings.locate(ctx.module.parsed, node);
1179
- for (const diagnostic of drainEmailDiagnostics()) lowerer.findings.items.push({
1180
+ const isNode = isObject(output) && typeof output.type === "string";
1181
+ if (isNode) for (const diagnostic of collectEmailDiagnostics(output)) lowerer.findings.items.push({
1180
1182
  ...diagnostic,
1181
1183
  origins: [location]
1182
1184
  });
1183
- if (output !== null && (!isObject(output) || typeof output.type !== "string")) return [];
1185
+ if (output !== null && !isNode) return [];
1184
1186
  return wrap([...emailNodeToIr(output, tokens, lowerer.src(ctx.module, node))]);
1185
1187
  };
1186
1188
  //#endregion
@@ -2983,6 +2985,552 @@ const channelBodies = (input) => {
2983
2985
  };
2984
2986
  };
2985
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
2986
3534
  //#region src/sml/styles.ts
2987
3535
  const MAX_CLASS_VARIANTS = 8;
2988
3536
  const product = (left, right) => {
@@ -3393,18 +3941,45 @@ const editFirst = (nodes, matches, edit) => {
3393
3941
  };
3394
3942
  const HEAD_COMMENT_OPEN = { raw: ["<!--[if !mso]><!-->"] };
3395
3943
  const HEAD_COMMENT_CLOSE = { raw: ["<!--<![endif]-->"] };
3396
- const completed = (findings, ir, fixtures = {}, assets = []) => ({
3397
- ir: findings.blocking ? void 0 : ir,
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,
3398
3973
  fixtures,
3399
3974
  assets,
3400
3975
  diagnostics: findings.items
3401
3976
  });
3402
3977
  /**
3403
- * Lower a loaded project to IR: the part of the compile that reads structure and needs no Tailwind
3404
- * or brand. `checkStaticProfile` runs it with empty stylesheets.
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.
3405
3980
  */
3406
3981
  const lowerProject = (options, project, findings, inputs) => {
3407
- const done = (ir, fixtures = {}, assets = inputs.assets) => completed(findings, ir, fixtures, assets);
3982
+ const done = (template, fixtures = {}, assets = inputs.assets) => completed(findings, template, fixtures, assets);
3408
3983
  const urlFor = inputs.urlFor;
3409
3984
  const staticContext = {
3410
3985
  project,
@@ -3514,7 +4089,7 @@ const lowerProject = (options, project, findings, inputs) => {
3514
4089
  "locale",
3515
4090
  ...CHANNELS
3516
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.");
3517
- let ir;
4092
+ let template;
3518
4093
  const checks = [];
3519
4094
  const emailField = fields.get("email");
3520
4095
  let emailIr;
@@ -3609,6 +4184,16 @@ const lowerProject = (options, project, findings, inputs) => {
3609
4184
  else root = edited.nodes[0] ?? root;
3610
4185
  }
3611
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
+ }
3612
4197
  if (schemaJson !== void 0) checks.push(...checkBindings(schemaJson, [root], [subject, ...preheader === void 0 ? [] : [preheader]]));
3613
4198
  emailIr = {
3614
4199
  subject,
@@ -3646,7 +4231,7 @@ const lowerProject = (options, project, findings, inputs) => {
3646
4231
  }]
3647
4232
  });
3648
4233
  }
3649
- if (schemaJson !== void 0 && templateId !== "" && (emailIr !== void 0 || channelIr.sms !== void 0 || channelIr.whatsapp !== void 0)) ir = {
4234
+ if (schemaJson !== void 0 && templateId !== "" && (emailIr !== void 0 || channelIr.sms !== void 0 || channelIr.whatsapp !== void 0)) template = {
3650
4235
  sml: 1,
3651
4236
  template: templateId,
3652
4237
  ...locale === void 0 ? {} : { locale },
@@ -3657,7 +4242,7 @@ const lowerProject = (options, project, findings, inputs) => {
3657
4242
  schema: schemaJson
3658
4243
  };
3659
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`.");
3660
- return done(ir, fixtures);
4245
+ return done(template, fixtures);
3661
4246
  };
3662
4247
  //#endregion
3663
4248
  //#region src/sml/project.ts
@@ -3762,7 +4347,7 @@ const loadProject = (files, entryPath, findings, loadOptions) => {
3762
4347
  }
3763
4348
  break;
3764
4349
  }
3765
- if (parseBrandSpecifier(specifier, loadOptions.brandPlugin) !== void 0) {
4350
+ if (parseBrandSpecifier(specifier, { brandPlugin: loadOptions.brandPlugin }) !== void 0) {
3766
4351
  for (const item of specifiers) if (text(item, "importKind") !== "type") imports.set(local(item), {
3767
4352
  kind: "brand",
3768
4353
  specifier,
@@ -3887,6 +4472,8 @@ const loadProject = (files, entryPath, findings, loadOptions) => {
3887
4472
  //#endregion
3888
4473
  //#region src/sml/profile.ts
3889
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";
3890
4477
  /** Findings that come from the inputs this check leaves out, not from the source. */
3891
4478
  const MISSING_INPUT_CODES = /* @__PURE__ */ new Set(["brand-unavailable", "unmatched-class"]);
3892
4479
  const asFiles = (files, entry) => typeof files === "string" ? { [entry]: files } : files;
@@ -3895,7 +4482,9 @@ const asFiles = (files, entry) => typeof files === "string" ? { [entry]: files }
3895
4482
  * and the entry's path. Imports of files that are absent are trusted to exist, and the brand,
3896
4483
  * Tailwind and theme are not resolved, so the result holds only findings the source itself owns.
3897
4484
  */
3898
- const checkStaticProfile = (files, entry = DEFAULT_ENTRY, brandPlugin = samvaBrandPlugin) => {
4485
+ const checkTemplate = (files, options = {}) => {
4486
+ const entry = options.entry ?? DEFAULT_ENTRY;
4487
+ const brandPlugin = options.brandPlugin ?? samvaBrandPlugin;
3899
4488
  const findings = new Findings();
3900
4489
  const project = loadProject(asFiles(files, entry), entry, findings, {
3901
4490
  brandPlugin,
@@ -3905,12 +4494,13 @@ const checkStaticProfile = (files, entry = DEFAULT_ENTRY, brandPlugin = samvaBra
3905
4494
  lowerProject({
3906
4495
  entry,
3907
4496
  brand: void 0,
3908
- brandPlugin
4497
+ brandPlugin,
4498
+ assetBase: ASSET_BASE
3909
4499
  }, project, findings, {
3910
4500
  assets: [],
3911
4501
  stylesheets: /* @__PURE__ */ new Map(),
3912
4502
  tailwindSheet: void 0,
3913
- urlFor: (path) => `https://assets.invalid/${path}`
4503
+ urlFor: (path) => `${ASSET_BASE}/${path}`
3914
4504
  });
3915
4505
  const unresolved = [...project.unresolved];
3916
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}\``)));
@@ -3920,7 +4510,7 @@ const key = (item) => `${item.code}\0${item.message}`;
3920
4510
  * The errors in `after` that `before` did not already have, compared by code and message so a
3921
4511
  * finding that only moved does not count as new.
3922
4512
  */
3923
- const introducedProfileErrors = (before, after) => {
4513
+ const introducedErrors = (before, after) => {
3924
4514
  const known = /* @__PURE__ */ new Map();
3925
4515
  for (const item of before) if (item.severity === "error") known.set(key(item), (known.get(key(item)) ?? 0) + 1);
3926
4516
  const added = [];
@@ -3933,4 +4523,4 @@ const introducedProfileErrors = (before, after) => {
3933
4523
  return added;
3934
4524
  };
3935
4525
  //#endregion
3936
- export { lowerProject as a, WHATSAPP_HEADER_TYPES as c, resolveFromDirectory as d, resolveThemeImports as f, completed as i, Findings as l, introducedProfileErrors as n, SMS_CATEGORIES as o, loadProject as r, WHATSAPP_CATEGORIES as s, checkStaticProfile as t, projectDirectory as u };
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 };