@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.
Files changed (60) hide show
  1. package/AGENTS.md +42 -40
  2. package/CHANGELOG.md +72 -0
  3. package/README.md +10 -9
  4. package/dist/{brand-jFV6YtSV.d.ts → assets-CS5Xa_mr.d.ts} +41 -3
  5. package/dist/{brand-Ci05p37c.js → brand-CIiEqkU7.js} +64 -3
  6. package/dist/{brand-plugin-CHhcei-j.d.ts → brand-plugin-Dj9SMpfS.d.ts} +3 -1
  7. package/dist/brand.d.ts +2 -3
  8. package/dist/brand.js +3 -4
  9. package/dist/compatibility.d.ts +76 -0
  10. package/dist/compatibility.js +807 -0
  11. package/dist/compiled-template-Bc4Dq9zy.d.ts +268 -0
  12. package/dist/compiler.d.ts +59 -193
  13. package/dist/compiler.js +473 -775
  14. package/dist/{components-3XHjYD3i.js → components-C5850-Qg.js} +37 -29
  15. package/dist/css-B_aVKrDo.js +436 -0
  16. package/dist/css-color-DLM12Osu.js +315 -0
  17. package/dist/{diagnostic-codes-8XDLR9ef.js → diagnostic-codes-DZKb0Xcg.js} +40 -1
  18. package/dist/{diagnostics-CUYlQ_6x.d.ts → diagnostics-D4vSLQrO.d.ts} +1 -10
  19. package/dist/diagnostics.d.ts +6 -2
  20. package/dist/diagnostics.js +3 -4
  21. package/dist/edit.d.ts +10 -9
  22. package/dist/edit.js +12 -11
  23. package/dist/email/jsx-dev-runtime.d.ts +1 -1
  24. package/dist/email/jsx-dev-runtime.js +1 -1
  25. package/dist/email/jsx-runtime.d.ts +2 -2
  26. package/dist/email/jsx-runtime.js +2 -2
  27. package/dist/email.d.ts +1 -1
  28. package/dist/email.js +1 -1
  29. package/dist/html-DJUK2YJF.js +483 -0
  30. package/dist/index.d.ts +1 -1
  31. package/dist/{jsx-runtime-DC7xG0aA.d.ts → jsx-runtime-DL8NVF1f.d.ts} +12 -6
  32. package/dist/{jsx-runtime-CBlexoeR.js → jsx-runtime-qL19u3Fy.js} +17 -10
  33. package/dist/preview-rNoJBEIY.d.ts +104 -0
  34. package/dist/{profile-BsY4dkan.js → profile-JTvgCD6X.js} +714 -29
  35. package/dist/{html-BvRFepeO.js → render-template-DwPDm3Hq.js} +155 -572
  36. package/dist/render.d.ts +40 -28
  37. package/dist/render.js +3 -47
  38. package/dist/selectors-CYWCoxZr.js +157 -0
  39. package/dist/sms.d.ts +1 -1
  40. package/dist/sms.js +1 -1
  41. package/dist/{template-B-UDXK8a.d.ts → template-DmNcoH4Q.d.ts} +2 -9
  42. package/dist/{theme-css-s9QwD5Ko.js → theme-css-P9F5S7Ic.js} +3 -316
  43. package/dist/whatsapp.d.ts +1 -1
  44. package/dist/whatsapp.js +1 -1
  45. package/generated/contract.json +7 -2
  46. package/generated/reference.md +3 -2
  47. package/package.json +5 -5
  48. package/skills/sml/SKILL.md +2 -2
  49. package/dist/assets-BSpgZOSR.js +0 -61
  50. package/dist/assets-BSuNT9gr.d.ts +0 -39
  51. package/dist/brand-footer-ClRtWnIc.d.ts +0 -40
  52. package/dist/brand-footer-DXVGsKE5.js +0 -746
  53. package/dist/brand-sample.d.ts +0 -4
  54. package/dist/brand-sample.js +0 -2
  55. package/dist/cascade-DoLWZ66g.js +0 -701
  56. package/dist/diagnostic-model-DJtqbETd.js +0 -36
  57. package/dist/ir-KJLFW08u.js +0 -5
  58. package/dist/render-CVnXZMyK.js +0 -258
  59. package/dist/render-CuZej25A.d.ts +0 -564
  60. package/dist/theme-imports-u_P0ygvQ.js +0 -102
@@ -1,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 { 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 { n as applyStylesheet, r as renderDarkRules, t as DARK_CLASS_PREFIX } from "./cascade-DoLWZ66g.js";
9
- import { i as lineColumn, t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
10
- import { n as resolveFromDirectory, t as projectDirectory } from "./theme-imports-u_P0ygvQ.js";
11
- import { a as isAssetPath } from "./assets-BSpgZOSR.js";
12
- import { n as brandComponents } from "./brand-Ci05p37c.js";
13
- import { t as DIAGNOSTIC_CODES } from "./diagnostic-codes-8XDLR9ef.js";
14
- import "./ir-KJLFW08u.js";
15
- import { parse } from "@babel/parser";
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
- 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({
1085
1182
  ...diagnostic,
1086
1183
  origins: [location]
1087
1184
  });
1088
- if (output !== null && (!isObject(output) || typeof output.type !== "string")) return [];
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
- const completed = (findings, ir, fixtures = {}, assets = []) => ({
3302
- 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,
3303
3973
  fixtures,
3304
3974
  assets,
3305
3975
  diagnostics: findings.items
3306
3976
  });
3307
3977
  /**
3308
- * Lower a loaded project to IR: the part of the compile that reads structure and needs no Tailwind
3309
- * 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.
3310
3980
  */
3311
3981
  const lowerProject = (options, project, findings, inputs) => {
3312
- 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);
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 ir;
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)) ir = {
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(ir, fixtures);
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 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;
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) => `https://assets.invalid/${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 introducedProfileErrors = (before, after) => {
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, WHATSAPP_HEADER_TYPES as c, completed as i, Findings as l, introducedProfileErrors as n, SMS_CATEGORIES as o, loadProject as r, WHATSAPP_CATEGORIES as s, checkStaticProfile as t };
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 };