@samva/markup 0.15.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +42 -35
- package/CHANGELOG.md +92 -0
- package/README.md +10 -9
- package/dist/{assets-tISHaQNF.d.ts → assets-CS5Xa_mr.d.ts} +3 -3
- package/dist/{brand-nreeKWxF.js → brand-CIiEqkU7.js} +24 -31
- package/dist/{brand-plugin-CHhcei-j.d.ts → brand-plugin-Dj9SMpfS.d.ts} +3 -1
- package/dist/brand.d.ts +2 -2
- package/dist/brand.js +3 -3
- package/dist/compatibility.d.ts +76 -0
- package/dist/compatibility.js +807 -0
- package/dist/compiled-template-Bc4Dq9zy.d.ts +268 -0
- package/dist/compiler.d.ts +58 -225
- package/dist/compiler.js +261 -1297
- package/dist/{components-3XHjYD3i.js → components-C5850-Qg.js} +37 -29
- package/dist/css-B_aVKrDo.js +436 -0
- package/dist/css-color-DLM12Osu.js +315 -0
- package/dist/{diagnostic-codes-UXwOO3A-.js → diagnostic-codes-DZKb0Xcg.js} +4 -0
- package/dist/{diagnostics-CUYlQ_6x.d.ts → diagnostics-D4vSLQrO.d.ts} +1 -10
- package/dist/diagnostics.d.ts +6 -2
- package/dist/diagnostics.js +3 -3
- package/dist/edit.d.ts +10 -9
- package/dist/edit.js +12 -11
- package/dist/email/jsx-dev-runtime.d.ts +1 -1
- package/dist/email/jsx-dev-runtime.js +1 -1
- package/dist/email/jsx-runtime.d.ts +2 -2
- package/dist/email/jsx-runtime.js +2 -2
- package/dist/email.d.ts +1 -1
- package/dist/email.js +1 -1
- package/dist/html-DJUK2YJF.js +483 -0
- package/dist/index.d.ts +1 -1
- package/dist/{jsx-runtime-DC7xG0aA.d.ts → jsx-runtime-DL8NVF1f.d.ts} +12 -6
- package/dist/{jsx-runtime-CBlexoeR.js → jsx-runtime-qL19u3Fy.js} +17 -10
- package/dist/preview-rNoJBEIY.d.ts +104 -0
- package/dist/{profile-AatO_SvP.js → profile-JTvgCD6X.js} +614 -24
- package/dist/{html-BvRFepeO.js → render-template-DwPDm3Hq.js} +155 -572
- package/dist/render.d.ts +40 -28
- package/dist/render.js +3 -47
- package/dist/selectors-CYWCoxZr.js +157 -0
- package/dist/sms.d.ts +1 -1
- package/dist/sms.js +1 -1
- package/dist/{template-B-UDXK8a.d.ts → template-DmNcoH4Q.d.ts} +2 -9
- package/dist/{theme-css-s9QwD5Ko.js → theme-css-P9F5S7Ic.js} +3 -316
- package/dist/whatsapp.d.ts +1 -1
- package/dist/whatsapp.js +1 -1
- package/generated/contract.json +7 -2
- package/generated/reference.md +3 -2
- package/package.json +5 -1
- package/skills/sml/SKILL.md +2 -2
- package/dist/ir-DNL6H7jA.js +0 -705
- package/dist/render-BdvewoQt.d.ts +0 -564
- package/dist/render-DpYouYYG.js +0 -258
|
@@ -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 {
|
|
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 buildLineMap, s as lineColumn, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-
|
|
10
|
-
import {
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
3397
|
-
|
|
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
|
|
3404
|
-
* 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.
|
|
3405
3980
|
*/
|
|
3406
3981
|
const lowerProject = (options, project, findings, inputs) => {
|
|
3407
|
-
const done = (
|
|
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
|
|
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))
|
|
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(
|
|
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
|
|
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) =>
|
|
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
|
|
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,
|
|
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 };
|