@samva/markup 0.18.0 → 0.19.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 +11 -4
- package/CHANGELOG.md +36 -0
- package/README.md +89 -5
- package/dist/assets-BSpgZOSR.js +61 -0
- package/dist/assets-k7_v3IOJ.d.ts +39 -0
- package/dist/{assets-CS5Xa_mr.d.ts → brand-Bxb6Sm7y.d.ts} +34 -41
- package/dist/brand.d.ts +2 -2
- package/dist/brand.js +3 -3
- package/dist/compatibility.d.ts +15 -5
- package/dist/compatibility.js +48 -5
- package/dist/compile-C1GWLQ5C.js +490 -0
- package/dist/compile-CIeuGmP-.d.ts +98 -0
- package/dist/{compiled-template-Bc4Dq9zy.d.ts → compiled-template-CPDHvFlt.d.ts} +157 -6
- package/dist/compiler.d.ts +4 -89
- package/dist/compiler.js +8 -486
- package/dist/components-DNBCz20_.js +3267 -0
- package/dist/{css-color-DLM12Osu.js → dark-scheme-BGDCCaUq.js} +12 -1
- package/dist/{diagnostic-codes-DZKb0Xcg.js → diagnostic-codes-BCwfUgnF.js} +33 -36
- package/dist/diagnostic-model-DJtqbETd.js +36 -0
- package/dist/{diagnostics-D4vSLQrO.d.ts → diagnostics-C3TSx6SU.d.ts} +22 -2
- package/dist/diagnostics.d.ts +33 -1
- package/dist/diagnostics.js +2 -1
- package/dist/edit.d.ts +15 -4
- package/dist/edit.js +67 -4
- package/dist/email/jsx-dev-runtime.d.ts +2 -2
- package/dist/email/jsx-runtime.d.ts +1 -1
- package/dist/email.d.ts +6 -2
- package/dist/email.js +6 -2
- package/dist/index.d.ts +1 -1
- package/dist/{jsx-runtime-DL8NVF1f.d.ts → jsx-runtime-HHfrVZEf.d.ts} +1 -2
- package/dist/markdown.d.ts +69 -0
- package/dist/markdown.js +95 -0
- package/dist/parse-Bex7QFqj.js +921 -0
- package/dist/{preview-rNoJBEIY.d.ts → preview-DRLWgmfg.d.ts} +3 -3
- package/dist/profile-DFk4gs7o.js +56 -0
- package/dist/{profile-JTvgCD6X.js → project-B6D64SZS.js} +497 -234
- package/dist/render.d.ts +8 -8
- package/dist/render.js +1 -2
- package/dist/{css-B_aVKrDo.js → selectors-CKGuc9MO.js} +158 -4
- package/dist/sms.d.ts +1 -1
- package/dist/{template-DmNcoH4Q.d.ts → template-4Dse7_OC.d.ts} +1 -1
- package/dist/{theme-css-P9F5S7Ic.js → theme-css-BujDuuEA.js} +1 -2
- package/dist/whatsapp.d.ts +1 -1
- package/generated/contract.json +41 -1
- package/generated/reference.md +97 -89
- package/package.json +15 -4
- package/dist/brand-CIiEqkU7.js +0 -193
- package/dist/brand-plugin-Dj9SMpfS.d.ts +0 -33
- package/dist/components-C5850-Qg.js +0 -409
- package/dist/dark-scheme-CbCwGDWH.js +0 -12
- package/dist/html-DJUK2YJF.js +0 -483
- package/dist/render-template-DwPDm3Hq.js +0 -1226
- package/dist/selectors-CYWCoxZr.js +0 -157
- package/dist/source-locations-C8wCsiAy.d.ts +0 -22
|
@@ -1,16 +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 {
|
|
5
|
-
import { a as EMAIL_ELEMENTS, d as reservedBrandAttributes, f as samvaBrandPlugin, s as emailElement, u as parseBrandSpecifier } from "./render-template-DwPDm3Hq.js";
|
|
4
|
+
import { $ as reservedBrandAttributes, B as brandComponents, F as propFor, G as emailPresentationOverrides, I as rawParts, K as isValueNode, L as PRIMITIVE_CLASSES, M as lowerEmailNodes, N as Tokens, P as emailNodeToIr, Q as parseBrandSpecifier, R as PRIMITIVE_CSS, Y as emailElement, _ as kebab, a as Email, d as equalColumnWidth, et as samvaBrandPlugin, h as escapeAttribute, i as Divider, l as Section, m as declarationValue, n as Column, o as Image, p as checkAttribute, q as EMAIL_ELEMENTS, r as Columns, s as Link, t as Button, u as Spacer, y as serializeStyle } from "./components-DNBCz20_.js";
|
|
6
5
|
import { FORMATTER_NAMES } from "./fmt.js";
|
|
7
6
|
import { r as collectEmailDiagnostics, t as EmailNode } from "./jsx-runtime-qL19u3Fy.js";
|
|
8
|
-
import { a as
|
|
9
|
-
import {
|
|
10
|
-
import { i as
|
|
11
|
-
import { i as
|
|
12
|
-
import {
|
|
13
|
-
import { parse } from "css-tree";
|
|
7
|
+
import { a as isAssetPath } from "./assets-BSpgZOSR.js";
|
|
8
|
+
import { i as lineColumn, t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
|
|
9
|
+
import { i as withinDarkScheme, t as DARK_SCHEME_MEDIA } from "./dark-scheme-BGDCCaUq.js";
|
|
10
|
+
import { a as fontShorthandFamilies, i as fontFallbackDiagnostic, n as matchesSelector, o as lacksGenericFallback, r as parseStylesheet, t as classListOf } from "./selectors-CKGuc9MO.js";
|
|
11
|
+
import { t as DIAGNOSTIC_CODES } from "./diagnostic-codes-BCwfUgnF.js";
|
|
12
|
+
import { generate, parse } from "css-tree";
|
|
14
13
|
import { parse as parse$1 } from "@babel/parser";
|
|
15
14
|
//#region src/email/theme-imports.ts
|
|
16
15
|
/** The directory part of a project path, with no trailing slash (`""` at the root). */
|
|
@@ -145,6 +144,69 @@ var Findings = class {
|
|
|
145
144
|
}
|
|
146
145
|
};
|
|
147
146
|
//#endregion
|
|
147
|
+
//#region src/sml/issue-content.ts
|
|
148
|
+
/** Copy parser nodes into the layout's source table and resolve its host-owned brand. */
|
|
149
|
+
const issueNodes = (nodes, sourceIndex, brand) => nodes.map((node) => {
|
|
150
|
+
if (isValueNode(node) || !("type" in node)) return node;
|
|
151
|
+
const kids = node.children === void 0 ? {} : { children: issueNodes(node.children, sourceIndex, brand) };
|
|
152
|
+
const src = node.src === void 0 ? {} : { src: [
|
|
153
|
+
node.src[0],
|
|
154
|
+
node.src[1],
|
|
155
|
+
sourceIndex
|
|
156
|
+
] };
|
|
157
|
+
if (node.type === "component" && (node.name === "BrandLogo" || node.name === "BrandFooter") && brand !== void 0) {
|
|
158
|
+
const { css: _css, ...resolved } = brand;
|
|
159
|
+
return {
|
|
160
|
+
...node,
|
|
161
|
+
...kids,
|
|
162
|
+
...src,
|
|
163
|
+
brand: resolved
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
...node,
|
|
168
|
+
...kids,
|
|
169
|
+
...src
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
/** Literal classes on parsed issue nodes join Tailwind's candidate set before the cascade. */
|
|
173
|
+
const issueClasses = (nodes) => nodes.flatMap((node) => {
|
|
174
|
+
if (isValueNode(node) || !("type" in node)) return [];
|
|
175
|
+
const value = node.type === "component" ? node.props?.className : node.attrs?.className ?? node.attrs?.class;
|
|
176
|
+
return [...typeof value === "string" ? value.split(/\s+/).filter(Boolean) : [], ...issueClasses(node.children ?? [])];
|
|
177
|
+
});
|
|
178
|
+
/** Internal marker: authored intrinsic tags cannot use this compiler-owned name. */
|
|
179
|
+
const ISSUE_SLOT_TAG = "sml-issue-content";
|
|
180
|
+
/** Resolve the final expanded layout, where partials have already used or discarded children. */
|
|
181
|
+
const replaceIssueSlots = (nodes, content, report) => {
|
|
182
|
+
let count = 0;
|
|
183
|
+
const walk = (items, conditional) => items.flatMap((node) => {
|
|
184
|
+
if (isValueNode(node)) return [node];
|
|
185
|
+
if ("type" in node && node.type !== "component" && node.tag === "sml-issue-content") {
|
|
186
|
+
count++;
|
|
187
|
+
if (conditional) report(node.src);
|
|
188
|
+
return [...content];
|
|
189
|
+
}
|
|
190
|
+
if ("if" in node) return [{
|
|
191
|
+
...node,
|
|
192
|
+
then: walk(node.then, true),
|
|
193
|
+
...node.else === void 0 ? {} : { else: walk(node.else, true) }
|
|
194
|
+
}];
|
|
195
|
+
if ("each" in node) return [{
|
|
196
|
+
...node,
|
|
197
|
+
children: walk(node.children, true)
|
|
198
|
+
}];
|
|
199
|
+
return [{
|
|
200
|
+
...node,
|
|
201
|
+
..."children" in node && node.children !== void 0 ? { children: walk(node.children, conditional) } : {}
|
|
202
|
+
}];
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
nodes: walk(nodes, false),
|
|
206
|
+
count
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
//#endregion
|
|
148
210
|
//#region src/email/stylesheets.ts
|
|
149
211
|
/** Sheets flattened into one, with rule order preserved across sheets. */
|
|
150
212
|
const mergeStylesheets = (sheets) => {
|
|
@@ -178,6 +240,112 @@ const mergeStylesheets = (sheets) => {
|
|
|
178
240
|
};
|
|
179
241
|
};
|
|
180
242
|
//#endregion
|
|
243
|
+
//#region src/ir/web-presentation.ts
|
|
244
|
+
/** Parse compiler-owned font and keyframe rules once; send time only serializes checked data. */
|
|
245
|
+
const cssNodes = (node, tokens) => {
|
|
246
|
+
if (node.type === "StyleSheet") return [...node.children].flatMap((child) => cssNodes(child, tokens));
|
|
247
|
+
if (node.type !== "Rule" && node.type !== "Atrule") return [];
|
|
248
|
+
const style = [];
|
|
249
|
+
const children = [];
|
|
250
|
+
for (const child of node.block?.children ?? []) if (child.type === "Declaration") style.push([child.property, tokens?.toValue(`${generate(child.value)}${child.important ? "!important" : ""}`) ?? `${generate(child.value)}${child.important ? "!important" : ""}`]);
|
|
251
|
+
else children.push(...cssNodes(child, tokens));
|
|
252
|
+
return [{
|
|
253
|
+
...node.type === "Rule" ? { selector: generate(node.prelude) } : { at: `${node.name}${node.prelude === null ? "" : ` ${generate(node.prelude)}`}` },
|
|
254
|
+
...style.length === 0 ? {} : { style },
|
|
255
|
+
...children.length === 0 ? {} : { children }
|
|
256
|
+
}];
|
|
257
|
+
};
|
|
258
|
+
/** Retain project selectors, conditions and fonts independently of the email cascade. */
|
|
259
|
+
const webPresentation = (sheets, tailwind, body) => {
|
|
260
|
+
const rules = [];
|
|
261
|
+
const layers = [...new Set(sheets.flatMap((sheet) => sheet.rules.map((rule) => rule.layer)).filter((layer) => layer !== Number.MAX_SAFE_INTEGER))].sort((a, b) => a - b);
|
|
262
|
+
rules.push({ at: `layer sml-web-components${layers.map((layer) => `,sml-web-${layer}`).join("")}` });
|
|
263
|
+
const signature = (rule) => JSON.stringify([
|
|
264
|
+
rule.selector,
|
|
265
|
+
rule.declarations,
|
|
266
|
+
rule.conditions
|
|
267
|
+
]);
|
|
268
|
+
const defaults = new Set(parseStylesheet(PRIMITIVE_CSS).rules.map(signature));
|
|
269
|
+
const fonts = /* @__PURE__ */ new Set();
|
|
270
|
+
for (const sheet of sheets) {
|
|
271
|
+
if (Object.keys(sheet.variables).length > 0) rules.push({
|
|
272
|
+
selector: ":root",
|
|
273
|
+
style: Object.entries(sheet.variables)
|
|
274
|
+
});
|
|
275
|
+
if (Object.keys(sheet.darkVariables).length > 0) rules.push({
|
|
276
|
+
at: "media (prefers-color-scheme:dark)",
|
|
277
|
+
children: [{
|
|
278
|
+
selector: ":root",
|
|
279
|
+
style: Object.entries(sheet.darkVariables)
|
|
280
|
+
}]
|
|
281
|
+
});
|
|
282
|
+
for (const rule of sheet.rules) {
|
|
283
|
+
if (sheet === tailwind && defaults.has(signature(rule))) continue;
|
|
284
|
+
let out = {
|
|
285
|
+
selector: rule.selector,
|
|
286
|
+
style: rule.declarations.filter(({ property }) => !/^mso-/i.test(property)).map(({ property, value, important }) => [property, `${value}${important ? "!important" : ""}`])
|
|
287
|
+
};
|
|
288
|
+
for (const condition of rule.conditions.toReversed()) out = {
|
|
289
|
+
at: condition.replace(/^@/, ""),
|
|
290
|
+
children: [out]
|
|
291
|
+
};
|
|
292
|
+
if (rule.layer !== Number.MAX_SAFE_INTEGER) out = {
|
|
293
|
+
at: `layer sml-web-${rule.layer}`,
|
|
294
|
+
children: [out]
|
|
295
|
+
};
|
|
296
|
+
rules.push(out);
|
|
297
|
+
}
|
|
298
|
+
for (const face of sheet.fontFaces) if (!fonts.has(face.css)) {
|
|
299
|
+
fonts.add(face.css);
|
|
300
|
+
rules.push(...cssNodes(parse(face.css)));
|
|
301
|
+
}
|
|
302
|
+
for (const css of sheet.headAtRules) rules.push(...cssNodes(parse(css)));
|
|
303
|
+
}
|
|
304
|
+
const componentLayer = tailwind?.rules.find((rule) => rule.selector === ".sml-email")?.layer;
|
|
305
|
+
let inlineSpecificity = sheets.reduce((max, sheet) => sheet.rules.reduce((highest, rule) => Math.max(highest, rule.specificity[0]), max), 0) + 1;
|
|
306
|
+
const stylesheets = [];
|
|
307
|
+
const authored = (node) => {
|
|
308
|
+
if (isValueNode(node)) return;
|
|
309
|
+
if ("type" in node && node.type !== "component" && node.tag === "style" || "el" in node && node.el === "style") {
|
|
310
|
+
const tokens = new Tokens();
|
|
311
|
+
const cssValue = (part) => {
|
|
312
|
+
if (typeof part === "string") return part;
|
|
313
|
+
if (part === null || typeof part === "boolean") return "";
|
|
314
|
+
if (typeof part === "number") return String(part);
|
|
315
|
+
if ("concat" in part) return part.concat.map(cssValue).join("");
|
|
316
|
+
return tokens.value(part);
|
|
317
|
+
};
|
|
318
|
+
const css = (node.children ?? []).map((part) => isValueNode(part) ? cssValue(part) : "unsupported").join("");
|
|
319
|
+
let parsed;
|
|
320
|
+
try {
|
|
321
|
+
parsed = cssNodes(parse(css), tokens);
|
|
322
|
+
} catch {
|
|
323
|
+
parsed = [];
|
|
324
|
+
}
|
|
325
|
+
stylesheets.push(parsed.length === 0 && css.trim() !== "" ? [{ at: "unsupported-dynamic-stylesheet" }] : parsed);
|
|
326
|
+
const selectorIds = (list) => {
|
|
327
|
+
for (const rule of list) {
|
|
328
|
+
inlineSpecificity = Math.max(inlineSpecificity, (rule.selector?.match(/#/g)?.length ?? 0) + 1);
|
|
329
|
+
selectorIds(rule.children ?? []);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
selectorIds(parsed);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if ("if" in node) {
|
|
336
|
+
for (const part of node.then) authored(part);
|
|
337
|
+
for (const part of node.else ?? []) authored(part);
|
|
338
|
+
} else if ("children" in node) for (const part of node.children ?? []) authored(part);
|
|
339
|
+
};
|
|
340
|
+
authored(body);
|
|
341
|
+
return {
|
|
342
|
+
rules,
|
|
343
|
+
...componentLayer === void 0 ? {} : { componentLayer },
|
|
344
|
+
inlineSpecificity,
|
|
345
|
+
...stylesheets.length === 0 ? {} : { stylesheets }
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
//#endregion
|
|
181
349
|
//#region src/sml/ast.ts
|
|
182
350
|
const isNode = (value) => isObject(value) && typeof value.type === "string" && typeof value.start === "number" && typeof value.end === "number";
|
|
183
351
|
/** The child node under `key`, or `undefined`. */
|
|
@@ -260,14 +428,6 @@ const bindingValue = (binding) => {
|
|
|
260
428
|
default: return;
|
|
261
429
|
}
|
|
262
430
|
};
|
|
263
|
-
const isStaticScalar = (value) => value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
264
|
-
/** True when a node in child position is a value (rendered as text) rather than structure. */
|
|
265
|
-
const isValueNode = (node) => {
|
|
266
|
-
if (typeof node !== "object" || node === null) return true;
|
|
267
|
-
if ("el" in node || "each" in node || "raw" in node || "preheader" in node) return false;
|
|
268
|
-
if ("if" in node && Array.isArray(node.then)) return false;
|
|
269
|
-
return true;
|
|
270
|
-
};
|
|
271
431
|
//#endregion
|
|
272
432
|
//#region src/channel-specs.ts
|
|
273
433
|
const SMS_CATEGORIES = [
|
|
@@ -847,136 +1007,6 @@ const checkStructure = (roots, report) => {
|
|
|
847
1007
|
visit(roots);
|
|
848
1008
|
};
|
|
849
1009
|
//#endregion
|
|
850
|
-
//#region src/sml/tokens.ts
|
|
851
|
-
const OPEN = "";
|
|
852
|
-
const CLOSE = "";
|
|
853
|
-
const TOKEN = /(\d+)/g;
|
|
854
|
-
/** Values and node lists a component call reads at render time, addressable from inside a string. */
|
|
855
|
-
var Tokens = class {
|
|
856
|
-
entries = [];
|
|
857
|
-
value(value) {
|
|
858
|
-
if (typeof value === "string") return value;
|
|
859
|
-
this.entries.push({ value });
|
|
860
|
-
return `${OPEN}${this.entries.length - 1}${CLOSE}`;
|
|
861
|
-
}
|
|
862
|
-
nodes(nodes) {
|
|
863
|
-
this.entries.push({ nodes });
|
|
864
|
-
return `${OPEN}${this.entries.length - 1}${CLOSE}`;
|
|
865
|
-
}
|
|
866
|
-
/** A string with tokens in it, as static text and the entries it names. */
|
|
867
|
-
parse(input) {
|
|
868
|
-
const parts = [];
|
|
869
|
-
let last = 0;
|
|
870
|
-
for (const match of input.matchAll(TOKEN)) {
|
|
871
|
-
if (match.index > last) parts.push(input.slice(last, match.index));
|
|
872
|
-
const entry = this.entries[Number(match[1])];
|
|
873
|
-
if (entry !== void 0) parts.push(entry);
|
|
874
|
-
last = match.index + match[0].length;
|
|
875
|
-
}
|
|
876
|
-
if (last < input.length) parts.push(input.slice(last));
|
|
877
|
-
return parts;
|
|
878
|
-
}
|
|
879
|
-
/** The value a string with tokens stands for: itself, one entry's value, or their concatenation. */
|
|
880
|
-
toValue(input) {
|
|
881
|
-
const parts = this.parse(input);
|
|
882
|
-
if (parts.length === 0) return "";
|
|
883
|
-
const values = [];
|
|
884
|
-
for (const part of parts) if (typeof part === "string") values.push(part);
|
|
885
|
-
else if ("value" in part) values.push(part.value);
|
|
886
|
-
else return void 0;
|
|
887
|
-
return values.length === 1 ? values[0] : { concat: values };
|
|
888
|
-
}
|
|
889
|
-
};
|
|
890
|
-
/** A prop value for a primitive: a literal stays itself and anything else becomes a token. */
|
|
891
|
-
const propFor = (tokens, value) => isStaticScalar(value) ? value : tokens.value(value);
|
|
892
|
-
const SOURCE = (source) => source === void 0 ? {} : { src: source };
|
|
893
|
-
/** Split compatibility markup into parts, deciding by position whether a token is in a tag. */
|
|
894
|
-
const rawParts = (tokens, html) => {
|
|
895
|
-
const parts = [];
|
|
896
|
-
let inTag = false;
|
|
897
|
-
let quote;
|
|
898
|
-
let buffer = "";
|
|
899
|
-
const flush = () => {
|
|
900
|
-
if (buffer !== "") parts.push(buffer);
|
|
901
|
-
buffer = "";
|
|
902
|
-
};
|
|
903
|
-
for (let index = 0; index < html.length; index++) {
|
|
904
|
-
const character = html[index];
|
|
905
|
-
if (character === OPEN) {
|
|
906
|
-
const end = html.indexOf(CLOSE, index);
|
|
907
|
-
const entry = tokens.parse(html.slice(index, end + 1))[0];
|
|
908
|
-
if (end > index && entry !== void 0 && typeof entry !== "string" && "value" in entry) {
|
|
909
|
-
flush();
|
|
910
|
-
parts.push(inTag ? { attr: entry.value } : { text: entry.value });
|
|
911
|
-
index = end;
|
|
912
|
-
continue;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
if (!inTag && character === "<") inTag = true;
|
|
916
|
-
else if (inTag && quote === void 0 && (character === "\"" || character === "'")) quote = character;
|
|
917
|
-
else if (inTag && quote === character) quote = void 0;
|
|
918
|
-
else if (inTag && quote === void 0 && character === ">") inTag = false;
|
|
919
|
-
buffer += character;
|
|
920
|
-
}
|
|
921
|
-
flush();
|
|
922
|
-
return parts;
|
|
923
|
-
};
|
|
924
|
-
const styleList = (tokens, style) => {
|
|
925
|
-
if (!isObject(style)) return void 0;
|
|
926
|
-
const entries = [];
|
|
927
|
-
for (const [property, value] of Object.entries(style)) {
|
|
928
|
-
if (value === void 0 || value === null) continue;
|
|
929
|
-
if (typeof value === "number") entries.push([property, value]);
|
|
930
|
-
else if (typeof value === "string") {
|
|
931
|
-
const resolved = tokens.toValue(value);
|
|
932
|
-
if (resolved !== void 0) entries.push([property, resolved]);
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
return entries;
|
|
936
|
-
};
|
|
937
|
-
/** Turn the tree a primitive returned back into IR, restoring each token to what it stood for. */
|
|
938
|
-
const emailNodeToIr = (node, tokens, source) => {
|
|
939
|
-
if (node === null) return [];
|
|
940
|
-
switch (node.type) {
|
|
941
|
-
case "Text": {
|
|
942
|
-
const output = [];
|
|
943
|
-
for (const part of tokens.parse(node.value)) if (typeof part === "string") output.push(part);
|
|
944
|
-
else if ("value" in part) output.push(part.value);
|
|
945
|
-
else output.push(...part.nodes);
|
|
946
|
-
return output;
|
|
947
|
-
}
|
|
948
|
-
case "Fragment": return node.children.flatMap((childNode) => emailNodeToIr(childNode, tokens, source));
|
|
949
|
-
case "Raw": return [{
|
|
950
|
-
raw: rawParts(tokens, node.html),
|
|
951
|
-
...node.text === "" ? {} : { text: node.text },
|
|
952
|
-
...SOURCE(source)
|
|
953
|
-
}];
|
|
954
|
-
case "Element": {
|
|
955
|
-
const attrs = {};
|
|
956
|
-
let style;
|
|
957
|
-
for (const [key, value] of Object.entries(node.props)) {
|
|
958
|
-
if (key === "key" || value === void 0 || value === null || value === false) continue;
|
|
959
|
-
if (key === "style") {
|
|
960
|
-
style = styleList(tokens, value);
|
|
961
|
-
continue;
|
|
962
|
-
}
|
|
963
|
-
const name = key === "className" ? "class" : key;
|
|
964
|
-
if (typeof value === "string") attrs[name] = tokens.toValue(value) ?? value;
|
|
965
|
-
else if (typeof value === "number" || value === true) attrs[name] = value;
|
|
966
|
-
}
|
|
967
|
-
const reserved = node.props[BRAND_FOOTER_MARKER] === true;
|
|
968
|
-
return [{
|
|
969
|
-
el: node.tag,
|
|
970
|
-
...Object.keys(attrs).length === 0 ? {} : { attrs },
|
|
971
|
-
...style === void 0 || Array.isArray(style) && style.length === 0 ? {} : { style },
|
|
972
|
-
...node.children.length === 0 ? {} : { children: node.children.flatMap((childNode) => emailNodeToIr(childNode, tokens, source)) },
|
|
973
|
-
...reserved ? { reserved: true } : {},
|
|
974
|
-
...SOURCE(source)
|
|
975
|
-
}];
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
};
|
|
979
|
-
//#endregion
|
|
980
1010
|
//#region src/sml/components.ts
|
|
981
1011
|
const NUMERIC = {
|
|
982
1012
|
Email: ["gutter"],
|
|
@@ -1154,7 +1184,7 @@ const runBuiltinComponent = (call) => {
|
|
|
1154
1184
|
kids = [chunked.cells];
|
|
1155
1185
|
wrap = chunked.wrap;
|
|
1156
1186
|
} else kids = lowerer.jsxChildren(childCtx, node);
|
|
1157
|
-
if (exported === "Columns" && declared.has("gap") && kids.some((kid) => !isValueNode(kid) && !("el" in kid))) {
|
|
1187
|
+
if (exported === "Columns" && declared.has("gap") && kids.some((kid) => !isValueNode(kid) && !("el" in kid) && !("type" in kid))) {
|
|
1158
1188
|
lowerer.error(ctx, node, "invalid-columns-gap", "<Columns gap> spaces Column children that always render; a conditional or repeated Column would leave a stray gap.", "Pad the cells instead (className=\"pr-6\"), or write each Column unconditionally.");
|
|
1159
1189
|
return [];
|
|
1160
1190
|
}
|
|
@@ -1183,7 +1213,40 @@ const runBuiltinComponent = (call) => {
|
|
|
1183
1213
|
origins: [location]
|
|
1184
1214
|
});
|
|
1185
1215
|
if (output !== null && !isNode) return [];
|
|
1186
|
-
return wrap([...emailNodeToIr(output, tokens, lowerer.src(ctx.module, node))]);
|
|
1216
|
+
if (ctx.semantic !== true) return wrap([...emailNodeToIr(output, tokens, lowerer.src(ctx.module, node))]);
|
|
1217
|
+
const semanticProp = (bound) => {
|
|
1218
|
+
if (bound.k === "object") {
|
|
1219
|
+
const object = {};
|
|
1220
|
+
for (const [name, field] of bound.fields) {
|
|
1221
|
+
const value = semanticProp(field);
|
|
1222
|
+
if (value !== void 0) object[name] = value;
|
|
1223
|
+
}
|
|
1224
|
+
return { object };
|
|
1225
|
+
}
|
|
1226
|
+
return bindingValue(bound);
|
|
1227
|
+
};
|
|
1228
|
+
const authored = {};
|
|
1229
|
+
for (const [name, bound] of declared) {
|
|
1230
|
+
const value = semanticProp(bound);
|
|
1231
|
+
if (value !== void 0) authored[name] = value;
|
|
1232
|
+
}
|
|
1233
|
+
const brand = isBrand && call.brand !== void 0 ? {
|
|
1234
|
+
slug: call.brand.slug,
|
|
1235
|
+
...call.brand.assets === void 0 ? {} : { assets: call.brand.assets },
|
|
1236
|
+
...call.brand.footer === void 0 ? {} : { footer: call.brand.footer }
|
|
1237
|
+
} : void 0;
|
|
1238
|
+
return wrap([{
|
|
1239
|
+
type: "component",
|
|
1240
|
+
name: exported,
|
|
1241
|
+
...Object.keys(authored).length === 0 ? {} : { props: authored },
|
|
1242
|
+
...kids.length === 0 ? {} : { children: kids },
|
|
1243
|
+
...brand === void 0 ? {} : { brand },
|
|
1244
|
+
...exported === "Column" ? { emailColumn: {
|
|
1245
|
+
...ctx.columnWidth === void 0 ? {} : { width: ctx.columnWidth },
|
|
1246
|
+
stack: ctx.columnStack === true
|
|
1247
|
+
} } : {},
|
|
1248
|
+
src: lowerer.src(ctx.module, node)
|
|
1249
|
+
}]);
|
|
1187
1250
|
};
|
|
1188
1251
|
//#endregion
|
|
1189
1252
|
//#region src/sml/values.ts
|
|
@@ -2202,7 +2265,15 @@ var Lowerer = class {
|
|
|
2202
2265
|
}
|
|
2203
2266
|
const kids = spec.content === "void" ? [] : this.jsxChildren(ctx, node);
|
|
2204
2267
|
return [{
|
|
2205
|
-
|
|
2268
|
+
...ctx.semantic === true ? {
|
|
2269
|
+
type: [
|
|
2270
|
+
"inline",
|
|
2271
|
+
"link",
|
|
2272
|
+
"image",
|
|
2273
|
+
"break"
|
|
2274
|
+
].includes(spec.layout) ? "inline" : "block",
|
|
2275
|
+
tag
|
|
2276
|
+
} : { el: tag },
|
|
2206
2277
|
...Object.keys(attrs).length === 0 ? {} : { attrs },
|
|
2207
2278
|
...style === void 0 ? {} : { style },
|
|
2208
2279
|
...kids.length === 0 ? {} : { children: kids },
|
|
@@ -2317,6 +2388,23 @@ var Lowerer = class {
|
|
|
2317
2388
|
const imported = ctx.module.imports.get(head);
|
|
2318
2389
|
const local = ctx.module.declarations.get(head);
|
|
2319
2390
|
const attributes = children(opening, "attributes");
|
|
2391
|
+
if (imported?.kind === "markup" && imported.specifier === "@samva/markup/email" && imported.imported === "IssueContent") {
|
|
2392
|
+
const issue = this.options.issueContent;
|
|
2393
|
+
if (issue === void 0 || ctx.semantic !== true || attributes.length > 0 || children(node, "children").some((item) => item.type !== "JSXText" || (text(item, "value") ?? "").trim() !== "")) {
|
|
2394
|
+
this.error(ctx, node, "issue-slot", "IssueContent needs parsed issue content and takes no props or children; its layout position must be unconditional.", "Place one <IssueContent /> outside loops and conditions in the email layout.");
|
|
2395
|
+
return [];
|
|
2396
|
+
}
|
|
2397
|
+
const needsBrand = (nodes) => nodes.some((item) => typeof item === "object" && item !== null && "type" in item && (item.type === "component" && (item.name === "BrandLogo" || item.name === "BrandFooter") || needsBrand(item.children ?? [])));
|
|
2398
|
+
if (this.options.brand === void 0 && needsBrand(issue.nodes)) {
|
|
2399
|
+
this.error(ctx, node, "brand-unavailable", "This issue uses brand components but no brand was resolved.", "Supply the host-resolved brand when compiling the issue.");
|
|
2400
|
+
return [];
|
|
2401
|
+
}
|
|
2402
|
+
return [{
|
|
2403
|
+
type: "block",
|
|
2404
|
+
tag: ISSUE_SLOT_TAG,
|
|
2405
|
+
src: this.src(ctx.module, node)
|
|
2406
|
+
}];
|
|
2407
|
+
}
|
|
2320
2408
|
if (imported?.kind === "markup" || imported?.kind === "brand") return runBuiltinComponent({
|
|
2321
2409
|
lowerer: this,
|
|
2322
2410
|
ctx,
|
|
@@ -2464,7 +2552,8 @@ var Lowerer = class {
|
|
|
2464
2552
|
env: /* @__PURE__ */ new Map(),
|
|
2465
2553
|
stack: [...ctx.stack, id],
|
|
2466
2554
|
loops: ctx.loops,
|
|
2467
|
-
depth: ctx.depth + 1
|
|
2555
|
+
depth: ctx.depth + 1,
|
|
2556
|
+
semantic: ctx.semantic
|
|
2468
2557
|
};
|
|
2469
2558
|
const env = /* @__PURE__ */ new Map();
|
|
2470
2559
|
const param = children(fn, "params")[0];
|
|
@@ -3269,15 +3358,25 @@ const physicalPadding = (property, value, context) => {
|
|
|
3269
3358
|
};
|
|
3270
3359
|
/** Generated dark-mode classes share this prefix; it is reserved for the compiler. */
|
|
3271
3360
|
const DARK_CLASS_PREFIX = "sml-dark-";
|
|
3272
|
-
/**
|
|
3273
|
-
|
|
3361
|
+
/**
|
|
3362
|
+
* Generated lock classes share this prefix; it is reserved for the compiler.
|
|
3363
|
+
* An important color the dark-mode class restates is inlined without
|
|
3364
|
+
* `!important`, so a weaker retained rule (also marked `!important`) would
|
|
3365
|
+
* otherwise take it. The lock class carries that color on a more specific copy
|
|
3366
|
+
* of the retained selector.
|
|
3367
|
+
*/
|
|
3368
|
+
const LOCK_CLASS_PREFIX = "sml-lock-";
|
|
3369
|
+
/** FNV-1a, 32-bit, so equal declaration sets share one class and builds are stable. */
|
|
3370
|
+
const classSuffix = (body) => {
|
|
3274
3371
|
let hash = 2166136261;
|
|
3275
3372
|
for (let index = 0; index < body.length; index++) {
|
|
3276
3373
|
hash ^= body.charCodeAt(index);
|
|
3277
3374
|
hash = Math.imul(hash, 16777619) >>> 0;
|
|
3278
3375
|
}
|
|
3279
|
-
return
|
|
3376
|
+
return hash.toString(36);
|
|
3280
3377
|
};
|
|
3378
|
+
/** A class named for its declarations, so equal sets share one rule and builds are stable. */
|
|
3379
|
+
const darkClassName = (body) => `${DARK_CLASS_PREFIX}${classSuffix(body)}`;
|
|
3281
3380
|
const SIDES = [
|
|
3282
3381
|
"top",
|
|
3283
3382
|
"right",
|
|
@@ -3340,6 +3439,154 @@ const withClass = (props, name) => {
|
|
|
3340
3439
|
[key]: typeof current === "string" && current.trim() !== "" ? `${current} ${name}` : name
|
|
3341
3440
|
};
|
|
3342
3441
|
};
|
|
3442
|
+
/** Index where the subject compound starts, after the last combinator. */
|
|
3443
|
+
const subjectSplit = (selector) => {
|
|
3444
|
+
let depth = 0;
|
|
3445
|
+
let split = 0;
|
|
3446
|
+
for (let index = 0; index < selector.length; index++) {
|
|
3447
|
+
const character = selector[index];
|
|
3448
|
+
if (character === "\\") {
|
|
3449
|
+
index += 1;
|
|
3450
|
+
continue;
|
|
3451
|
+
}
|
|
3452
|
+
if (character === "(" || character === "[") depth++;
|
|
3453
|
+
else if (character === ")" || character === "]") depth = Math.max(0, depth - 1);
|
|
3454
|
+
else if (depth === 0 && (character === ">" || character === "+" || character === "~")) split = index + 1;
|
|
3455
|
+
else if (depth === 0 && character === " ") split = index + 1;
|
|
3456
|
+
}
|
|
3457
|
+
return selector.slice(split).trim() === "" ? void 0 : split;
|
|
3458
|
+
};
|
|
3459
|
+
/** Single-colon names that still select a pseudo-element, not a pseudo-class. */
|
|
3460
|
+
const LEGACY_PSEUDO_ELEMENTS = /* @__PURE__ */ new Set([
|
|
3461
|
+
"before",
|
|
3462
|
+
"after",
|
|
3463
|
+
"first-line",
|
|
3464
|
+
"first-letter"
|
|
3465
|
+
]);
|
|
3466
|
+
/**
|
|
3467
|
+
* A pseudo-element is a different box. An important color on the element does
|
|
3468
|
+
* not override a color the pseudo-element sets for itself, so a lock guard must
|
|
3469
|
+
* not be copied onto `::before`.
|
|
3470
|
+
*/
|
|
3471
|
+
const targetsPseudoElement = (selector) => {
|
|
3472
|
+
const split = subjectSplit(selector);
|
|
3473
|
+
if (split === void 0) return false;
|
|
3474
|
+
const subject = selector.slice(split);
|
|
3475
|
+
let depth = 0;
|
|
3476
|
+
for (let index = 0; index < subject.length; index++) {
|
|
3477
|
+
const character = subject[index];
|
|
3478
|
+
if (character === "\\") {
|
|
3479
|
+
index += 1;
|
|
3480
|
+
continue;
|
|
3481
|
+
}
|
|
3482
|
+
if (character === "(" || character === "[") depth++;
|
|
3483
|
+
else if (character === ")" || character === "]") depth = Math.max(0, depth - 1);
|
|
3484
|
+
else if (depth !== 0 || character !== ":") continue;
|
|
3485
|
+
if (subject[index + 1] === ":") return true;
|
|
3486
|
+
const name = /^:([A-Za-z-]+)/.exec(subject.slice(index))?.[1]?.toLowerCase();
|
|
3487
|
+
if (name !== void 0 && LEGACY_PSEUDO_ELEMENTS.has(name)) return true;
|
|
3488
|
+
}
|
|
3489
|
+
return false;
|
|
3490
|
+
};
|
|
3491
|
+
/** Insert `className` into the subject compound, before its first pseudo. */
|
|
3492
|
+
const strengthenSelector = (selector, className) => {
|
|
3493
|
+
const split = subjectSplit(selector);
|
|
3494
|
+
if (split === void 0) return void 0;
|
|
3495
|
+
const subject = selector.slice(split);
|
|
3496
|
+
const token = `.${className}`;
|
|
3497
|
+
let depth = 0;
|
|
3498
|
+
let pseudo = -1;
|
|
3499
|
+
for (let index = 0; index < subject.length; index++) {
|
|
3500
|
+
const character = subject[index];
|
|
3501
|
+
if (character === "\\") {
|
|
3502
|
+
index += 1;
|
|
3503
|
+
continue;
|
|
3504
|
+
}
|
|
3505
|
+
if (character === "(" || character === "[") depth++;
|
|
3506
|
+
else if (character === ")" || character === "]") depth = Math.max(0, depth - 1);
|
|
3507
|
+
else if (depth === 0 && character === ":") {
|
|
3508
|
+
pseudo = index;
|
|
3509
|
+
break;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
const strengthened = pseudo < 0 ? `${subject}${token}` : `${subject.slice(0, pseudo)}${token}${subject.slice(pseudo)}`;
|
|
3513
|
+
return `${selector.slice(0, split)}${strengthened}`;
|
|
3514
|
+
};
|
|
3515
|
+
const retainedKey = (rule, declaration, position) => rank(declaration.important, false, rule.layer, rule.specificity, rule.order, position);
|
|
3516
|
+
/** Whether `item` wins the real cascade over this rule's overlapping declaration. */
|
|
3517
|
+
const threatens = (item, rule) => rule.declarations.some((declaration, position) => overlaps(declaration.property, item.property) && compareRank(item.key, retainedKey(rule, declaration, position)) > 0);
|
|
3518
|
+
const nestConditions = (conditions, body) => conditions.reduceRight((inner, condition) => `${condition}{${inner}}`, body);
|
|
3519
|
+
/**
|
|
3520
|
+
* Restate important colors that were inlined without `!important` on a copy of
|
|
3521
|
+
* each weaker retained selector. The copy carries one more class, so it beats
|
|
3522
|
+
* the retained rule, and the dark-scheme copy of it keeps the theme switch.
|
|
3523
|
+
* A rule that outranks the themed color is copied again afterwards, on that
|
|
3524
|
+
* same class, so the guard does not bury it. A shorthand in that copy also
|
|
3525
|
+
* restates every locked declaration that still outranks it, in cascade order,
|
|
3526
|
+
* so the shorthand does not reset a stronger inline longhand. When either
|
|
3527
|
+
* value changes in dark mode, the copy is repeated inside the dark-mode query
|
|
3528
|
+
* at the dark values. Pseudo-elements are left alone: their color is not the
|
|
3529
|
+
* element's.
|
|
3530
|
+
*/
|
|
3531
|
+
const renderLockGuards = (locks, rules, variables, darkVariables) => {
|
|
3532
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3533
|
+
const blocks = [];
|
|
3534
|
+
const guarded = /* @__PURE__ */ new Set();
|
|
3535
|
+
const darkQuery = DARK_SCHEME_MEDIA.replace(": ", ":");
|
|
3536
|
+
const emit = (wrapping, selector, body) => {
|
|
3537
|
+
const block = nestConditions(wrapping, `${selector}{${body}}`);
|
|
3538
|
+
if (seen.has(block)) return;
|
|
3539
|
+
seen.add(block);
|
|
3540
|
+
blocks.push(block);
|
|
3541
|
+
};
|
|
3542
|
+
for (const rule of rules) {
|
|
3543
|
+
if (targetsPseudoElement(rule.selector)) continue;
|
|
3544
|
+
const conditions = rule.conditions.map(downlevelMediaQuery);
|
|
3545
|
+
for (const [className, declarations] of locks) {
|
|
3546
|
+
const beaten = declarations.filter((item) => threatens(item, rule));
|
|
3547
|
+
if (beaten.length === 0) continue;
|
|
3548
|
+
const selector = strengthenSelector(rule.selector, className);
|
|
3549
|
+
if (selector === void 0) continue;
|
|
3550
|
+
const inDark = withinDarkScheme(conditions);
|
|
3551
|
+
const lightBody = beaten.map((item) => `${item.property}:${item.light}!important`).join(";");
|
|
3552
|
+
const darkBody = beaten.map((item) => `${item.property}:${item.dark}!important`).join(";");
|
|
3553
|
+
if (!inDark) emit(conditions, selector, lightBody);
|
|
3554
|
+
emit(inDark ? conditions : [...conditions, darkQuery], selector, darkBody);
|
|
3555
|
+
guarded.add(className);
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
for (const [className, declarations] of locks) {
|
|
3559
|
+
if (!guarded.has(className)) continue;
|
|
3560
|
+
for (const rule of rules) {
|
|
3561
|
+
if (targetsPseudoElement(rule.selector)) continue;
|
|
3562
|
+
const selector = strengthenSelector(rule.selector, className);
|
|
3563
|
+
if (selector === void 0) continue;
|
|
3564
|
+
const conditions = rule.conditions.map(downlevelMediaQuery);
|
|
3565
|
+
const inDark = withinDarkScheme(conditions);
|
|
3566
|
+
const replay = (mode) => {
|
|
3567
|
+
const scope = mode === "dark" ? {
|
|
3568
|
+
...variables,
|
|
3569
|
+
...darkVariables
|
|
3570
|
+
} : variables;
|
|
3571
|
+
const parts = [];
|
|
3572
|
+
for (const [position, declaration] of rule.declarations.entries()) {
|
|
3573
|
+
const key = retainedKey(rule, declaration, position);
|
|
3574
|
+
if (!declarations.some((item) => overlaps(declaration.property, item.property) && compareRank(key, item.key) > 0)) continue;
|
|
3575
|
+
parts.push(`${declaration.property}:${finalizeValue(declaration.value, scope)}!important`);
|
|
3576
|
+
for (const item of declarations) if (overlaps(declaration.property, item.property) && compareRank(item.key, key) > 0) parts.push(`${item.property}:${mode === "dark" ? item.dark : item.light}!important`);
|
|
3577
|
+
}
|
|
3578
|
+
return parts.join(";");
|
|
3579
|
+
};
|
|
3580
|
+
const lightBody = replay(inDark ? "dark" : "light");
|
|
3581
|
+
if (lightBody === "") continue;
|
|
3582
|
+
emit(conditions, selector, lightBody);
|
|
3583
|
+
if (inDark) continue;
|
|
3584
|
+
const darkBody = replay("dark");
|
|
3585
|
+
if (darkBody !== lightBody) emit([...conditions, darkQuery], selector, darkBody);
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
return blocks.join("");
|
|
3589
|
+
};
|
|
3343
3590
|
/** The generated dark-mode classes as one `@media (prefers-color-scheme:dark)` block, written as the head CSS writes its media queries. */
|
|
3344
3591
|
const renderDarkRules = (rules) => rules.size === 0 ? "" : `${DARK_SCHEME_MEDIA.replace(": ", ":")}{${[...rules].sort(([left], [right]) => left.localeCompare(right)).map(([name, body]) => `.${name}{${body}}`).join("")}}`;
|
|
3345
3592
|
/**
|
|
@@ -3366,6 +3613,14 @@ const applyStylesheet = (root, sheet, options = {}) => {
|
|
|
3366
3613
|
(rule.inlinable ? inlinable : retained).push(rule);
|
|
3367
3614
|
for (const name of rule.classes) styledClasses.add(name);
|
|
3368
3615
|
}
|
|
3616
|
+
const locks = /* @__PURE__ */ new Map();
|
|
3617
|
+
const lockClassFor = (declarations) => {
|
|
3618
|
+
if (!declarations.some((item) => retained.some((rule) => !targetsPseudoElement(rule.selector) && threatens(item, rule)))) return void 0;
|
|
3619
|
+
const body = declarations.map((item) => `${item.property}:${item.light}=${item.dark}@${item.key.join(".")}`).join(";");
|
|
3620
|
+
const name = `${LOCK_CLASS_PREFIX}${classSuffix(body)}`;
|
|
3621
|
+
locks.set(name, declarations);
|
|
3622
|
+
return name;
|
|
3623
|
+
};
|
|
3369
3624
|
const conditionalWritingRules = retained.filter((rule) => rule.declarations.some((declaration) => declaration.property === "direction" || declaration.property === "writing-mode" || declaration.property === "all"));
|
|
3370
3625
|
const unmatched = /* @__PURE__ */ new Map();
|
|
3371
3626
|
const declaredFamilies = new Set(sheet.fontFaces.map((face) => face.family.toLowerCase()));
|
|
@@ -3455,12 +3710,32 @@ const applyStylesheet = (root, sheet, options = {}) => {
|
|
|
3455
3710
|
direction: inheritedValue("direction", inheritedWriting.direction, "ltr", typeof element.props.dir === "string" ? element.props.dir : element.tag === "bdi" ? "auto" : inheritedWriting.direction),
|
|
3456
3711
|
writingMode: winners.has("all") ? "unknown" : inheritedValue("writing-mode", inheritedWriting.writingMode, "horizontal-tb")
|
|
3457
3712
|
};
|
|
3458
|
-
const
|
|
3713
|
+
const restated = /* @__PURE__ */ new Set();
|
|
3714
|
+
if (hasDarkTheme) {
|
|
3715
|
+
for (const [property, entry] of winners) {
|
|
3716
|
+
if (property.startsWith("--") || !entry.value.includes("var(")) continue;
|
|
3717
|
+
if (finalizeValue(entry.value, darkScope) !== finalizeValue(entry.value, variables)) restated.add(property);
|
|
3718
|
+
}
|
|
3719
|
+
for (let grew = restated.size > 0; grew;) {
|
|
3720
|
+
grew = false;
|
|
3721
|
+
for (const property of winners.keys()) if (!property.startsWith("--") && !restated.has(property) && [...restated].some((candidate) => overlaps(candidate, property))) {
|
|
3722
|
+
restated.add(property);
|
|
3723
|
+
grew = true;
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
const emitted = [...winners].filter(([property]) => !property.startsWith("--")).sort(([, left], [, right]) => compareRank(left.key, right.key)).map(([property, entry]) => {
|
|
3728
|
+
return [
|
|
3729
|
+
property,
|
|
3730
|
+
finalizeValue(entry.value, variables),
|
|
3731
|
+
entry.key[0] === 1 && !restated.has(property)
|
|
3732
|
+
];
|
|
3733
|
+
});
|
|
3459
3734
|
for (const [property, value] of emitted) checkFallback(property, value, element.origins.slice(0, 1));
|
|
3460
3735
|
const physical = /* @__PURE__ */ new Map();
|
|
3461
|
-
for (const [property, value] of emitted) for (const [name, lowered] of conditionalWriting ? [[property, value]] : physicalPadding(property, value, writing)) {
|
|
3736
|
+
for (const [property, value, important] of emitted) for (const [name, lowered] of conditionalWriting ? [[property, value]] : physicalPadding(property, value, writing)) {
|
|
3462
3737
|
physical.delete(name);
|
|
3463
|
-
physical.set(name, lowered);
|
|
3738
|
+
physical.set(name, important ? `${lowered}!important` : lowered);
|
|
3464
3739
|
}
|
|
3465
3740
|
const bgcolor = element.props.bgcolor;
|
|
3466
3741
|
const lightProps = typeof bgcolor === "string" && bgcolor.includes("var(") ? {
|
|
@@ -3468,28 +3743,28 @@ const applyStylesheet = (root, sheet, options = {}) => {
|
|
|
3468
3743
|
bgcolor: finalizeValue(bgcolor, variables)
|
|
3469
3744
|
} : element.props;
|
|
3470
3745
|
const dark = [];
|
|
3746
|
+
const locked = [];
|
|
3471
3747
|
if (hasDarkTheme) {
|
|
3472
|
-
const
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
grew = true;
|
|
3483
|
-
}
|
|
3748
|
+
for (const [property, entry] of [...winners].sort(([, left], [, right]) => compareRank(left.key, right.key))) {
|
|
3749
|
+
if (!restated.has(property)) continue;
|
|
3750
|
+
const darkValue = finalizeValue(entry.value, darkScope);
|
|
3751
|
+
dark.push([property, darkValue]);
|
|
3752
|
+
if (entry.key[0] === 1) locked.push({
|
|
3753
|
+
property,
|
|
3754
|
+
light: finalizeValue(entry.value, variables),
|
|
3755
|
+
dark: darkValue,
|
|
3756
|
+
key: entry.key
|
|
3757
|
+
});
|
|
3484
3758
|
}
|
|
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
3759
|
if (typeof bgcolor === "string" && bgcolor.includes("var(") && !winners.has("background-color") && !winners.has("background")) {
|
|
3487
3760
|
const value = finalizeValue(bgcolor, darkScope);
|
|
3488
3761
|
if (value !== finalizeValue(bgcolor, variables)) dark.push(["background-color", value]);
|
|
3489
3762
|
}
|
|
3490
3763
|
}
|
|
3491
3764
|
const darkClass = dark.length === 0 ? void 0 : darkClassFor(dark);
|
|
3492
|
-
const
|
|
3765
|
+
const lockClass = lockClassFor(locked);
|
|
3766
|
+
const themed = darkClass === void 0 ? lightProps : withClass(lightProps, darkClass);
|
|
3767
|
+
const props = lockClass === void 0 ? themed : withClass(themed, lockClass);
|
|
3493
3768
|
return EmailNode.Element({
|
|
3494
3769
|
...element,
|
|
3495
3770
|
props: emitted.length === 0 ? props : {
|
|
@@ -3524,7 +3799,7 @@ const applyStylesheet = (root, sheet, options = {}) => {
|
|
|
3524
3799
|
});
|
|
3525
3800
|
return {
|
|
3526
3801
|
tree,
|
|
3527
|
-
headCss: renderHeadCss(retained, sheet.headAtRules, sheet.variables, sheet.darkVariables),
|
|
3802
|
+
headCss: `${renderHeadCss(retained, sheet.headAtRules, sheet.variables, sheet.darkVariables)}${renderLockGuards(locks, retained, sheet.variables, sheet.darkVariables)}`,
|
|
3528
3803
|
darkRules,
|
|
3529
3804
|
fontFaceCss: sheet.fontFaces.map((face) => face.css).join(""),
|
|
3530
3805
|
diagnostics
|
|
@@ -4042,13 +4317,17 @@ const lowerProject = (options, project, findings, inputs) => {
|
|
|
4042
4317
|
const localeRead = fields.has("locale") ? readStatic(staticContext, module, fields.get("locale")) : void 0;
|
|
4043
4318
|
const locale = localeRead?.ok === true && typeof localeRead.value === "string" ? localeRead.value : void 0;
|
|
4044
4319
|
if (fields.has("locale") && locale === void 0) findings.add(module.parsed, at("locale"), "invalid-locale", "The locale is a string literal.", "Write locale: \"en-US\".");
|
|
4045
|
-
const
|
|
4320
|
+
const staticPart = {
|
|
4046
4321
|
project,
|
|
4047
4322
|
findings,
|
|
4323
|
+
issueContent: options.issueContent,
|
|
4048
4324
|
staticContext,
|
|
4049
4325
|
assetUrl: urlFor,
|
|
4050
4326
|
brand: options.brand,
|
|
4051
|
-
brandPlugin: options.brandPlugin ?? samvaBrandPlugin
|
|
4327
|
+
brandPlugin: options.brandPlugin ?? samvaBrandPlugin
|
|
4328
|
+
};
|
|
4329
|
+
const lowerer = new Lowerer({
|
|
4330
|
+
...staticPart,
|
|
4052
4331
|
channel: channelLowering
|
|
4053
4332
|
});
|
|
4054
4333
|
const rootContext = (target) => ({
|
|
@@ -4117,17 +4396,47 @@ const lowerProject = (options, project, findings, inputs) => {
|
|
|
4117
4396
|
if (!parts.has("subject") && object.type === "ObjectExpression") findings.add(module.parsed, emailField, "missing-subject", "The email channel needs a subject.", "Add `subject: (input) => …`.");
|
|
4118
4397
|
const preheader = value("preheader");
|
|
4119
4398
|
let body;
|
|
4399
|
+
let expanded;
|
|
4120
4400
|
const bodyNode = parts.get("body");
|
|
4121
4401
|
if (bodyNode === void 0 && object.type === "ObjectExpression") findings.add(module.parsed, emailField, "missing-body", "The email channel needs a body.", "Add `body: (input) => (<Email>…</Email>)`.");
|
|
4122
4402
|
else if (bodyNode !== void 0) {
|
|
4123
4403
|
const opened = openFunction(bodyNode);
|
|
4124
4404
|
if (opened !== void 0) {
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4405
|
+
let nodes = lowerer.content({
|
|
4406
|
+
...opened.ctx,
|
|
4407
|
+
semantic: true
|
|
4408
|
+
}, opened.body);
|
|
4409
|
+
if (options.issueContent !== void 0) {
|
|
4410
|
+
let index = lowerer.sources.indexOf(options.issueContent.file);
|
|
4411
|
+
if (index < 0) {
|
|
4412
|
+
index = lowerer.sources.length;
|
|
4413
|
+
lowerer.sources.push(options.issueContent.file);
|
|
4414
|
+
}
|
|
4415
|
+
const content = issueNodes(options.issueContent.nodes, index + 1, options.brand);
|
|
4416
|
+
const resolved = replaceIssueSlots(nodes, content, (source) => {
|
|
4417
|
+
const file = source?.[2] === void 0 ? options.entry : lowerer.sources[source[2] - 1] ?? options.entry;
|
|
4418
|
+
findings.report({
|
|
4419
|
+
code: "issue-slot",
|
|
4420
|
+
severity: "error",
|
|
4421
|
+
message: "IssueContent must render unconditionally, outside loops and conditions.",
|
|
4422
|
+
origins: [{
|
|
4423
|
+
fileName: file,
|
|
4424
|
+
lineNumber: source?.[0] ?? 1,
|
|
4425
|
+
columnNumber: source?.[1] ?? 1
|
|
4426
|
+
}]
|
|
4427
|
+
});
|
|
4428
|
+
});
|
|
4429
|
+
nodes = resolved.nodes;
|
|
4430
|
+
if (resolved.count !== 1) findings.add(opened.ctx.module.parsed, opened.body, "issue-slot", "An issue layout must render exactly one IssueContent slot after expanding partials.", "Render one <IssueContent /> outside loops and conditions; do not discard or duplicate its children.");
|
|
4431
|
+
}
|
|
4432
|
+
const roots = lowerEmailNodes(nodes, staticPart.brandPlugin);
|
|
4433
|
+
if (nodes.length === 1 && roots.length === 1) {
|
|
4434
|
+
body = nodes[0];
|
|
4435
|
+
expanded = roots[0];
|
|
4436
|
+
} else if (!findings.blocking) findings.add(opened.ctx.module.parsed, opened.body, "multiple-roots", roots.length === 0 ? "The email body renders nothing." : "The email body renders more than one root.", "Wrap the body in a single element such as <Email>.");
|
|
4128
4437
|
}
|
|
4129
4438
|
}
|
|
4130
|
-
if (subject !== void 0 && body !== void 0) {
|
|
4439
|
+
if (subject !== void 0 && body !== void 0 && expanded !== void 0) {
|
|
4131
4440
|
const sheets = [];
|
|
4132
4441
|
if (inputs.tailwindSheet !== void 0) sheets.push(inputs.tailwindSheet);
|
|
4133
4442
|
for (const path of stylesheetOrder(project)) {
|
|
@@ -4143,7 +4452,7 @@ const lowerProject = (options, project, findings, inputs) => {
|
|
|
4143
4452
|
columnNumber: source[1]
|
|
4144
4453
|
};
|
|
4145
4454
|
};
|
|
4146
|
-
const styled = applyStyles(
|
|
4455
|
+
const styled = applyStyles(expanded, mergeStylesheets(sheets), {
|
|
4147
4456
|
locate,
|
|
4148
4457
|
report: (diagnostic) => {
|
|
4149
4458
|
findings.items.push(diagnostic);
|
|
@@ -4198,7 +4507,14 @@ const lowerProject = (options, project, findings, inputs) => {
|
|
|
4198
4507
|
emailIr = {
|
|
4199
4508
|
subject,
|
|
4200
4509
|
...preheader === void 0 ? {} : { preheader },
|
|
4201
|
-
body
|
|
4510
|
+
body,
|
|
4511
|
+
web: webPresentation(sheets, inputs.tailwindSheet, body),
|
|
4512
|
+
presentation: {
|
|
4513
|
+
nodes: emailPresentationOverrides(expanded, styled.root),
|
|
4514
|
+
headCss: styled.headCss,
|
|
4515
|
+
fontFaceCss: styled.fontFaceCss,
|
|
4516
|
+
brandPlugin: staticPart.brandPlugin
|
|
4517
|
+
}
|
|
4202
4518
|
};
|
|
4203
4519
|
}
|
|
4204
4520
|
}
|
|
@@ -4232,7 +4548,7 @@ const lowerProject = (options, project, findings, inputs) => {
|
|
|
4232
4548
|
});
|
|
4233
4549
|
}
|
|
4234
4550
|
if (schemaJson !== void 0 && templateId !== "" && (emailIr !== void 0 || channelIr.sms !== void 0 || channelIr.whatsapp !== void 0)) template = {
|
|
4235
|
-
sml:
|
|
4551
|
+
sml: 2,
|
|
4236
4552
|
template: templateId,
|
|
4237
4553
|
...locale === void 0 ? {} : { locale },
|
|
4238
4554
|
sources,
|
|
@@ -4470,57 +4786,4 @@ const loadProject = (files, entryPath, findings, loadOptions) => {
|
|
|
4470
4786
|
};
|
|
4471
4787
|
};
|
|
4472
4788
|
//#endregion
|
|
4473
|
-
|
|
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";
|
|
4477
|
-
/** Findings that come from the inputs this check leaves out, not from the source. */
|
|
4478
|
-
const MISSING_INPUT_CODES = /* @__PURE__ */ new Set(["brand-unavailable", "unmatched-class"]);
|
|
4479
|
-
const asFiles = (files, entry) => typeof files === "string" ? { [entry]: files } : files;
|
|
4480
|
-
/**
|
|
4481
|
-
* Check an entry against the static profile. Pass the entry's source alone, or the project's files
|
|
4482
|
-
* and the entry's path. Imports of files that are absent are trusted to exist, and the brand,
|
|
4483
|
-
* Tailwind and theme are not resolved, so the result holds only findings the source itself owns.
|
|
4484
|
-
*/
|
|
4485
|
-
const checkTemplate = (files, options = {}) => {
|
|
4486
|
-
const entry = options.entry ?? DEFAULT_ENTRY;
|
|
4487
|
-
const brandPlugin = options.brandPlugin ?? samvaBrandPlugin;
|
|
4488
|
-
const findings = new Findings();
|
|
4489
|
-
const project = loadProject(asFiles(files, entry), entry, findings, {
|
|
4490
|
-
brandPlugin,
|
|
4491
|
-
lenient: true
|
|
4492
|
-
});
|
|
4493
|
-
if (project === void 0) return findings.items;
|
|
4494
|
-
lowerProject({
|
|
4495
|
-
entry,
|
|
4496
|
-
brand: void 0,
|
|
4497
|
-
brandPlugin,
|
|
4498
|
-
assetBase: ASSET_BASE
|
|
4499
|
-
}, project, findings, {
|
|
4500
|
-
assets: [],
|
|
4501
|
-
stylesheets: /* @__PURE__ */ new Map(),
|
|
4502
|
-
tailwindSheet: void 0,
|
|
4503
|
-
urlFor: (path) => `${ASSET_BASE}/${path}`
|
|
4504
|
-
});
|
|
4505
|
-
const unresolved = [...project.unresolved];
|
|
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}\``)));
|
|
4507
|
-
};
|
|
4508
|
-
const key = (item) => `${item.code}\0${item.message}`;
|
|
4509
|
-
/**
|
|
4510
|
-
* The errors in `after` that `before` did not already have, compared by code and message so a
|
|
4511
|
-
* finding that only moved does not count as new.
|
|
4512
|
-
*/
|
|
4513
|
-
const introducedErrors = (before, after) => {
|
|
4514
|
-
const known = /* @__PURE__ */ new Map();
|
|
4515
|
-
for (const item of before) if (item.severity === "error") known.set(key(item), (known.get(key(item)) ?? 0) + 1);
|
|
4516
|
-
const added = [];
|
|
4517
|
-
for (const item of after) {
|
|
4518
|
-
if (item.severity !== "error") continue;
|
|
4519
|
-
const remaining = known.get(key(item)) ?? 0;
|
|
4520
|
-
if (remaining > 0) known.set(key(item), remaining - 1);
|
|
4521
|
-
else added.push(item);
|
|
4522
|
-
}
|
|
4523
|
-
return added;
|
|
4524
|
-
};
|
|
4525
|
-
//#endregion
|
|
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 };
|
|
4789
|
+
export { renderDarkRules as a, WHATSAPP_HEADER_TYPES as c, projectDirectory as d, resolveFromDirectory as f, applyStylesheet as i, issueClasses as l, completed as n, SMS_CATEGORIES as o, resolveThemeImports as p, lowerProject as r, WHATSAPP_CATEGORIES as s, loadProject as t, Findings as u };
|