@samva/markup 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/AGENTS.md +107 -46
  2. package/CHANGELOG.md +345 -0
  3. package/README.md +188 -52
  4. package/dist/{brand-DcoVkh_S.d.ts → assets-BIsyMQ92.d.ts} +41 -11
  5. package/dist/{email/brand.js → brand-C0MKTwmX.js} +64 -4
  6. package/dist/{theme-css.d.ts → brand.d.ts} +12 -3
  7. package/dist/brand.js +4 -0
  8. package/dist/channel-specs-C2feJwuP.d.ts +13 -0
  9. package/dist/compiler.d.ts +88 -115
  10. package/dist/compiler.js +750 -171
  11. package/dist/components-BZCqegS8.js +1864 -0
  12. package/dist/diagnostic-model-DJtqbETd.js +38 -0
  13. package/dist/{diagnostics-OcQKQYSY.d.ts → diagnostic-model-DxthUZBT.d.ts} +3 -21
  14. package/dist/{diagnostics-BMYtBCQk.d.ts → diagnostics-B-Gym34R.d.ts} +8 -4
  15. package/dist/{email/diagnostics.js → diagnostics-CnKFefKz.js} +4 -3
  16. package/dist/diagnostics.d.ts +410 -2
  17. package/dist/diagnostics.js +402 -52
  18. package/dist/edit.d.ts +234 -0
  19. package/dist/edit.js +947 -0
  20. package/dist/email/jsx-dev-runtime.d.ts +2 -2
  21. package/dist/email/jsx-dev-runtime.js +1 -2
  22. package/dist/email/jsx-runtime.d.ts +1 -1
  23. package/dist/email/jsx-runtime.js +1 -85
  24. package/dist/{email/components.d.ts → email.d.ts} +22 -11
  25. package/dist/email.js +2 -0
  26. package/dist/fmt.d.ts +32 -0
  27. package/dist/fmt.js +174 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +2 -0
  30. package/dist/{render-vAv_4r9v.js → ir-CLhlklKV.js} +96 -279
  31. package/dist/jsx-runtime-CBlexoeR.js +98 -0
  32. package/dist/{elements-BzjjnoCS.d.ts → jsx-runtime-ylIOnaSf.d.ts} +76 -76
  33. package/dist/profile-pWGtWZXF.js +3803 -0
  34. package/dist/render-BPonDpCl.d.ts +539 -0
  35. package/dist/render-BQmfh94k.js +257 -0
  36. package/dist/render.d.ts +35 -0
  37. package/dist/render.js +49 -0
  38. package/dist/sms.d.ts +16 -0
  39. package/dist/sms.js +12 -0
  40. package/dist/source-locations-C8wCsiAy.d.ts +23 -0
  41. package/dist/template-DMPwxMSq.d.ts +65 -0
  42. package/dist/template-zLUjcwfF.js +42 -0
  43. package/dist/{theme-css-hFqcPSar.js → theme-css-JtRCm1fb.js} +2 -2
  44. package/dist/whatsapp.d.ts +57 -0
  45. package/dist/whatsapp.js +22 -0
  46. package/generated/contract.json +1788 -0
  47. package/generated/reference.md +360 -0
  48. package/package.json +46 -239
  49. package/samva-brand.d.ts +4 -4
  50. package/skills/sml/SKILL.md +123 -0
  51. package/dist/ast-BrDtApwS.d.ts +0 -107
  52. package/dist/ast.d.ts +0 -2
  53. package/dist/ast.js +0 -124
  54. package/dist/candidate.d.ts +0 -29
  55. package/dist/candidate.js +0 -88
  56. package/dist/catalog-CKuOooB8.d.ts +0 -354
  57. package/dist/checks-D73Xizbw.js +0 -59366
  58. package/dist/checks.d.ts +0 -33
  59. package/dist/checks.js +0 -2
  60. package/dist/classes.d.ts +0 -26
  61. package/dist/classes.js +0 -184
  62. package/dist/compile-sms.d.ts +0 -28
  63. package/dist/compile-sms.js +0 -44
  64. package/dist/compile-whatsapp.d.ts +0 -56
  65. package/dist/compile-whatsapp.js +0 -186
  66. package/dist/compile.d.ts +0 -18
  67. package/dist/compile.js +0 -29
  68. package/dist/components.d.ts +0 -289
  69. package/dist/components.js +0 -106
  70. package/dist/conditionals.d.ts +0 -10
  71. package/dist/conditionals.js +0 -74
  72. package/dist/corpus-CPPfgAHN.d.ts +0 -106
  73. package/dist/css-CS2jLID6.d.ts +0 -109
  74. package/dist/email/assets.d.ts +0 -40
  75. package/dist/email/assets.js +0 -63
  76. package/dist/email/brand-footer.d.ts +0 -34
  77. package/dist/email/brand-footer.js +0 -62
  78. package/dist/email/brand.d.ts +0 -2
  79. package/dist/email/components.js +0 -240
  80. package/dist/email/diagnostics.d.ts +0 -2
  81. package/dist/email/elements.d.ts +0 -2
  82. package/dist/email/elements.js +0 -299
  83. package/dist/email/html.d.ts +0 -2
  84. package/dist/email/html.js +0 -278
  85. package/dist/email/render.d.ts +0 -50
  86. package/dist/email/render.js +0 -3
  87. package/dist/email/selection.d.ts +0 -2
  88. package/dist/email/selection.js +0 -91
  89. package/dist/email/source-edit.d.ts +0 -67
  90. package/dist/email/source-edit.js +0 -185
  91. package/dist/email/stylesheets.d.ts +0 -15
  92. package/dist/email/stylesheets.js +0 -48
  93. package/dist/email/text.d.ts +0 -7
  94. package/dist/email/text.js +0 -100
  95. package/dist/emit.d.ts +0 -51
  96. package/dist/emit.js +0 -151
  97. package/dist/html-Bl9BAdqc.d.ts +0 -42
  98. package/dist/interpolate.d.ts +0 -24
  99. package/dist/interpolate.js +0 -43
  100. package/dist/jsx-dev-runtime.d.ts +0 -11
  101. package/dist/jsx-dev-runtime.js +0 -15
  102. package/dist/jsx-runtime-I-1nCtl2.d.ts +0 -57
  103. package/dist/jsx-runtime.d.ts +0 -2
  104. package/dist/jsx-runtime.js +0 -121
  105. package/dist/lexer.d.ts +0 -60
  106. package/dist/lexer.js +0 -281
  107. package/dist/model.d.ts +0 -24
  108. package/dist/model.js +0 -77
  109. package/dist/ops-BvwqHl7V.js +0 -630
  110. package/dist/ops.d.ts +0 -114
  111. package/dist/ops.js +0 -2
  112. package/dist/parser-t29cHLVL.d.ts +0 -396
  113. package/dist/parser.d.ts +0 -2
  114. package/dist/parser.js +0 -859
  115. package/dist/predicate-BI6uRRM-.js +0 -549
  116. package/dist/predicate-M-05Nd7R.d.ts +0 -56
  117. package/dist/predicate.d.ts +0 -2
  118. package/dist/predicate.js +0 -2
  119. package/dist/printer.d.ts +0 -6
  120. package/dist/printer.js +0 -122
  121. package/dist/registry-Dcp6QEK_.d.ts +0 -73
  122. package/dist/registry.d.ts +0 -2
  123. package/dist/registry.js +0 -646
  124. package/dist/render-template.d.ts +0 -45
  125. package/dist/render-template.js +0 -76
  126. package/dist/selection-CCArx9lU.d.ts +0 -91
  127. package/dist/showcase-egkusQBf.d.ts +0 -415
  128. package/dist/sms-segments-y4FpPilJ.d.ts +0 -13
  129. package/dist/sms-segments.d.ts +0 -2
  130. package/dist/sms-segments.js +0 -46
  131. package/dist/source-locations-DrCM82Ym.d.ts +0 -67
  132. package/dist/source-locations.d.ts +0 -2
  133. package/dist/source-locations.js +0 -102
  134. package/dist/starters/category-mapping.d.ts +0 -13
  135. package/dist/starters/category-mapping.js +0 -49
  136. package/dist/starters/corpus.d.ts +0 -2
  137. package/dist/starters/corpus.js +0 -194
  138. package/dist/starters/library.d.ts +0 -18
  139. package/dist/starters/library.js +0 -692
  140. package/dist/starters/public-gallery.d.ts +0 -266
  141. package/dist/starters/public-gallery.js +0 -916
  142. package/dist/starters/public-handoff.d.ts +0 -164
  143. package/dist/starters/public-handoff.js +0 -361
  144. package/dist/starters/public-preview-artifacts.d.ts +0 -43
  145. package/dist/starters/public-preview-artifacts.js +0 -104
  146. package/dist/starters/public-preview.d.ts +0 -61
  147. package/dist/starters/public-preview.js +0 -83
  148. package/dist/starters/showcase.d.ts +0 -2
  149. package/dist/starters/showcase.js +0 -335
  150. package/dist/starters/sources.d.ts +0 -45
  151. package/dist/starters/sources.js +0 -7762
  152. package/dist/starters/theme.d.ts +0 -45
  153. package/dist/starters/theme.js +0 -77
  154. package/dist/starters.d.ts +0 -2
  155. package/dist/starters.js +0 -874
  156. package/dist/tailwind-BYq7Lo6u.js +0 -727
  157. package/dist/template-DWftqh4x.d.ts +0 -60
  158. package/dist/template.d.ts +0 -2
  159. package/dist/template.js +0 -37
  160. package/dist/theme-css.js +0 -2
  161. package/dist/tsx-source-instrumentation.d.ts +0 -30
  162. package/dist/tsx-source-instrumentation.js +0 -115
  163. package/dist/tsx-tracer.d.ts +0 -112
  164. package/dist/tsx-tracer.js +0 -985
  165. package/dist/variables-J_L3qxRa.d.ts +0 -77
  166. package/dist/variables.d.ts +0 -2
  167. package/dist/variables.js +0 -141
  168. package/dist/version-BITr58kk.js +0 -96
  169. package/generated/sml-agent-contract.json +0 -709
  170. package/generated/sml-email-agent-contract.json +0 -615
  171. package/generated/sml-email-language-reference.md +0 -94
  172. package/generated/sml-language-reference.md +0 -195
@@ -0,0 +1,3803 @@
1
+ import { n as isObject } from "./guards-eV1qLbf2.js";
2
+ import { n as inputSchema, r as jsonSchema } from "./input-schema-zjkgrXn2.js";
3
+ import { n as TEMPLATE_NAME_PATTERN, t as TEMPLATE_ID_PATTERN } from "./template-zLUjcwfF.js";
4
+ import { n as RESERVED_FOOTER_ATTRIBUTES, t as BRAND_FOOTER_MARKER } from "./unsubscribe-link-CoesnQDU.js";
5
+ import { M as EMAIL_ELEMENTS, P as emailElement, a as Email, d as equalColumnWidth, i as Divider, l as Section, n as Column, o as Image, p as checkAttribute, r as Columns, s as Link, t as Button, u as Spacer, y as serializeStyle } from "./components-BZCqegS8.js";
6
+ import { FORMATTER_NAMES } from "./fmt.js";
7
+ import { i as drainEmailDiagnostics, t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
8
+ import { n as applyStylesheet } from "./ir-CLhlklKV.js";
9
+ import { i as lineColumn, t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
10
+ import { d as isAssetPath, i as brandComponents } from "./brand-C0MKTwmX.js";
11
+ import { parse } from "css-tree";
12
+ import { parse as parse$1 } from "@babel/parser";
13
+ //#region src/email/theme-imports.ts
14
+ /** The CSS and TSX specifier a template uses to reach its organization's brand. */
15
+ const BRAND_SPECIFIER = "samva:brand";
16
+ const BRAND_IMPORT = /^samva:brand(?:\/([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?))?$/;
17
+ /** The brand a `samva:brand` specifier names: `undefined` slug means the default brand. */
18
+ const parseBrandSpecifier = (specifier) => {
19
+ const match = BRAND_IMPORT.exec(specifier);
20
+ return match === null ? void 0 : { slug: match[1] };
21
+ };
22
+ /** The directory part of a project path, with no trailing slash (`""` at the root). */
23
+ const projectDirectory = (path) => path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
24
+ /** A relative or root-absolute stylesheet reference, resolved from a project directory. */
25
+ const resolveFromDirectory = (directory, reference) => {
26
+ const segments = reference.startsWith("/") || directory === "" ? [] : directory.split("/");
27
+ for (const segment of reference.split("/")) {
28
+ if (segment === "" || segment === ".") continue;
29
+ if (segment === "..") {
30
+ if (segments.length === 0) return void 0;
31
+ segments.pop();
32
+ } else segments.push(segment);
33
+ }
34
+ return segments.length === 0 ? void 0 : segments.join("/");
35
+ };
36
+ const importTarget = (node) => {
37
+ if (node.type !== "Atrule" || node.prelude === null || node.prelude.type !== "AtrulePrelude") return { extra: true };
38
+ const parts = [...node.prelude.children].filter((part) => part.type !== "WhiteSpace");
39
+ const first = parts[0];
40
+ const target = first?.type === "String" ? first.value : first?.type === "Url" ? first.value : void 0;
41
+ return target === void 0 ? { extra: true } : {
42
+ target,
43
+ extra: parts.length > 1
44
+ };
45
+ };
46
+ /**
47
+ * Walk the theme's `@import` graph. Project `.css` files resolve from the
48
+ * importing file's directory; `samva:brand` and `samva:brand/<slug>` are
49
+ * recorded for the brand the build supplies. Anything else is an error.
50
+ */
51
+ const resolveThemeImports = (themePath, themeCss, files) => {
52
+ const stylesheets = /* @__PURE__ */ new Map([[themePath, themeCss]]);
53
+ const brandSpecifiers = /* @__PURE__ */ new Set();
54
+ const diagnostics = [];
55
+ const visit = (path, source, chain) => {
56
+ const lineMap = buildLineMap(source);
57
+ const ast = parse(source, {
58
+ positions: true,
59
+ parseValue: false,
60
+ parseRulePrelude: false
61
+ });
62
+ if (ast.type !== "StyleSheet") return;
63
+ for (const node of ast.children) {
64
+ if (node.type !== "Atrule" || node.name.toLowerCase() !== "import") continue;
65
+ const position = lineColumn(lineMap, node.loc?.start.offset ?? 0);
66
+ const origins = [{
67
+ fileName: path,
68
+ lineNumber: position.line,
69
+ columnNumber: position.column
70
+ }];
71
+ const refuse = (message) => diagnostics.push({
72
+ code: "css-import-unresolved",
73
+ severity: "error",
74
+ message,
75
+ origins
76
+ });
77
+ const { target, extra } = importTarget(node);
78
+ if (target === void 0) {
79
+ refuse(`${path}: @import needs a quoted stylesheet path or "samva:brand".`);
80
+ continue;
81
+ }
82
+ if (extra) {
83
+ refuse(`${path}: @import ${JSON.stringify(target)} takes no layer, supports or media conditions; the import order is the layering.`);
84
+ continue;
85
+ }
86
+ if (parseBrandSpecifier(target) !== void 0) {
87
+ brandSpecifiers.add(target);
88
+ continue;
89
+ }
90
+ if (!/^\.{0,2}\//.test(target) || !/\.css$/i.test(target.replace(/[?#].*$/, ""))) {
91
+ refuse(`${path}: @import ${JSON.stringify(target)} is not a project stylesheet or "samva:brand". The compiler supplies Tailwind itself; import project .css files by relative path.`);
92
+ continue;
93
+ }
94
+ const resolved = resolveFromDirectory(projectDirectory(path), target.replace(/[?#].*$/, ""));
95
+ const content = resolved === void 0 ? void 0 : files[resolved];
96
+ if (resolved === void 0 || typeof content !== "string") {
97
+ refuse(`${path}: @import ${JSON.stringify(target)} names no stylesheet in the project.`);
98
+ continue;
99
+ }
100
+ if (chain.includes(resolved)) {
101
+ refuse(`${path}: @import ${JSON.stringify(target)} imports itself through ${chain.join(" → ")}.`);
102
+ continue;
103
+ }
104
+ if (stylesheets.has(resolved)) continue;
105
+ stylesheets.set(resolved, content);
106
+ visit(resolved, content, [...chain, resolved]);
107
+ }
108
+ };
109
+ visit(themePath, themeCss, [themePath]);
110
+ return {
111
+ stylesheets,
112
+ brandSpecifiers: [...brandSpecifiers].sort(),
113
+ diagnostics
114
+ };
115
+ };
116
+ //#endregion
117
+ //#region src/sml/diagnostics.ts
118
+ /** Collects the findings of one compile. Every finding carries a code, a location and a fix. */
119
+ var Findings = class {
120
+ items = [];
121
+ /** The `file:line:col` of a node. */
122
+ locate(source, node) {
123
+ const at = source.position(node.start);
124
+ return {
125
+ fileName: source.path,
126
+ lineNumber: at.line,
127
+ columnNumber: at.column
128
+ };
129
+ }
130
+ add(source, node, code, message, fix, severity = "error") {
131
+ this.items.push({
132
+ code,
133
+ severity,
134
+ message,
135
+ ...fix === void 0 ? {} : { fix },
136
+ origins: [this.locate(source, node)]
137
+ });
138
+ }
139
+ get blocking() {
140
+ return this.items.some((item) => item.severity === "error");
141
+ }
142
+ };
143
+ //#endregion
144
+ //#region src/email/stylesheets.ts
145
+ /** Sheets flattened into one, with rule order preserved across sheets. */
146
+ const mergeStylesheets = (sheets) => {
147
+ let base = 0;
148
+ const rules = sheets.flatMap((sheet) => {
149
+ const offset = base;
150
+ base += sheet.rules.length + 1;
151
+ return sheet.rules.map((rule) => ({
152
+ ...rule,
153
+ order: offset + rule.order
154
+ }));
155
+ });
156
+ const variables = {};
157
+ const headAtRules = [];
158
+ const fontFaces = /* @__PURE__ */ new Map();
159
+ const diagnostics = sheets.flatMap((sheet) => sheet.diagnostics);
160
+ for (const sheet of sheets) {
161
+ Object.assign(variables, sheet.variables);
162
+ headAtRules.push(...sheet.headAtRules);
163
+ for (const face of sheet.fontFaces) fontFaces.set(face.css, face);
164
+ }
165
+ return {
166
+ rules,
167
+ headAtRules,
168
+ fontFaces: [...fontFaces.values()],
169
+ variables,
170
+ diagnostics
171
+ };
172
+ };
173
+ //#endregion
174
+ //#region src/sml/ast.ts
175
+ const isNode = (value) => isObject(value) && typeof value.type === "string" && typeof value.start === "number" && typeof value.end === "number";
176
+ /** The child node under `key`, or `undefined`. */
177
+ const child = (node, key) => {
178
+ const value = node[key];
179
+ return isNode(value) ? value : void 0;
180
+ };
181
+ /** The child nodes under `key`; array holes and non-nodes are dropped. */
182
+ const children = (node, key) => {
183
+ const value = node[key];
184
+ return Array.isArray(value) ? value.filter(isNode) : [];
185
+ };
186
+ const text = (node, key) => {
187
+ const value = node[key];
188
+ return typeof value === "string" ? value : void 0;
189
+ };
190
+ /** The name a property key spells: an identifier or a string literal. */
191
+ const keyName = (key) => {
192
+ if (key === void 0) return void 0;
193
+ if (key.type === "Identifier") return text(key, "name");
194
+ if (key.type === "StringLiteral") return text(key, "value");
195
+ };
196
+ /** Strip wrappers that carry no runtime meaning: parentheses and TypeScript assertions. */
197
+ const unwrap = (node) => {
198
+ let current = node;
199
+ for (;;) {
200
+ if (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression" || current.type === "TSTypeAssertion" || current.type === "TypeCastExpression") {
201
+ const inner = child(current, "expression");
202
+ if (inner === void 0) return current;
203
+ current = inner;
204
+ continue;
205
+ }
206
+ return current;
207
+ }
208
+ };
209
+ var SourceSyntaxError = class extends Error {
210
+ path;
211
+ line;
212
+ column;
213
+ constructor(path, message, line, column) {
214
+ super(message);
215
+ this.name = "SourceSyntaxError";
216
+ this.path = path;
217
+ this.line = line;
218
+ this.column = column;
219
+ }
220
+ };
221
+ /** Parse a TSX/TS project file. Throws `SourceSyntaxError` on a syntax error. */
222
+ const parseSource = (path, source) => {
223
+ const lines = buildLineMap(source);
224
+ const position = (offset) => lineColumn(lines, offset);
225
+ let program;
226
+ try {
227
+ program = parse$1(source, {
228
+ plugins: ["typescript", "jsx"],
229
+ sourceType: "module"
230
+ }).program;
231
+ } catch (error) {
232
+ const at = isObject(error) && isObject(error.loc) ? error.loc : void 0;
233
+ const line = typeof at?.line === "number" ? at.line : 1;
234
+ const column = typeof at?.column === "number" ? at.column + 1 : 1;
235
+ throw new SourceSyntaxError(path, error instanceof Error ? error.message.replace(/\s*\(\d+:\d+\)\s*$/, "") : "", line, column);
236
+ }
237
+ if (!isNode(program)) throw new SourceSyntaxError(path, "The file did not parse to a program.", 1, 1);
238
+ return {
239
+ path,
240
+ source,
241
+ program,
242
+ position
243
+ };
244
+ };
245
+ //#endregion
246
+ //#region src/sml/bindings.ts
247
+ const joinPath = (base, segment) => base === "" ? segment : `${base}.${segment}`;
248
+ /** The IR value a binding reads, or `undefined` for bindings that are not values. */
249
+ const bindingValue = (binding) => {
250
+ switch (binding.k) {
251
+ case "path": return binding.path === "" ? void 0 : { bind: binding.path };
252
+ case "value": return binding.value;
253
+ default: return;
254
+ }
255
+ };
256
+ const isStaticScalar = (value) => value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
257
+ /** True when a node in child position is a value (rendered as text) rather than structure. */
258
+ const isValueNode = (node) => {
259
+ if (typeof node !== "object" || node === null) return true;
260
+ if ("el" in node || "each" in node || "raw" in node || "preheader" in node) return false;
261
+ if ("if" in node && Array.isArray(node.then)) return false;
262
+ return true;
263
+ };
264
+ //#endregion
265
+ //#region src/channel-specs.ts
266
+ const SMS_CATEGORIES = [
267
+ "transactional",
268
+ "promotional",
269
+ "service_implicit"
270
+ ];
271
+ const WHATSAPP_CATEGORIES = [
272
+ "marketing",
273
+ "utility",
274
+ "authentication"
275
+ ];
276
+ const WHATSAPP_HEADER_TYPES = [
277
+ "text",
278
+ "image",
279
+ "video",
280
+ "document"
281
+ ];
282
+ /** The attributes `<Sms>` accepts. */
283
+ const SMS_ATTRIBUTES = ["category"];
284
+ /** The attributes `<WhatsApp>` accepts. */
285
+ const WHATSAPP_ATTRIBUTES = [
286
+ "name",
287
+ "language",
288
+ "category"
289
+ ];
290
+ //#endregion
291
+ //#region src/whatsapp-template.ts
292
+ const isConcat = (value) => typeof value === "object" && value !== null && "concat" in value;
293
+ /**
294
+ * Number the non-literal values of a header, body or footer in order of first appearance. A value
295
+ * that appears twice (by JSON identity) shares its placeholder. Text pieces stay literal.
296
+ */
297
+ const whatsappTemplate = (part) => {
298
+ const params = [];
299
+ const keys = [];
300
+ let template = "";
301
+ const visit = (node) => {
302
+ if (node === null || typeof node === "boolean") return;
303
+ if (typeof node === "string") {
304
+ template += node;
305
+ return;
306
+ }
307
+ if (typeof node === "number") {
308
+ template += String(node);
309
+ return;
310
+ }
311
+ if (typeof node === "object" && isConcat(node)) {
312
+ for (const inner of node.concat) visit(inner);
313
+ return;
314
+ }
315
+ if (typeof node === "object" && ("el" in node || "each" in node || "raw" in node)) throw new TypeError("A WhatsApp text part holds text and values only.");
316
+ if (typeof node === "object" && "if" in node && Array.isArray(node.then)) throw new TypeError("A WhatsApp text part cannot branch.");
317
+ const value = node;
318
+ const key = JSON.stringify(value);
319
+ let index = keys.indexOf(key);
320
+ if (index < 0) {
321
+ index = keys.length;
322
+ keys.push(key);
323
+ params.push(value);
324
+ }
325
+ template += `{{${index + 1}}}`;
326
+ };
327
+ for (const node of part) visit(node);
328
+ return {
329
+ template,
330
+ params
331
+ };
332
+ };
333
+ //#endregion
334
+ //#region src/sml/suggest.ts
335
+ /** Levenshtein distance, capped so a long non-match is cheap. */
336
+ const distance = (a, b, cap) => {
337
+ if (Math.abs(a.length - b.length) > cap) return cap + 1;
338
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
339
+ for (let i = 1; i <= a.length; i++) {
340
+ const row = [i];
341
+ for (let j = 1; j <= b.length; j++) row[j] = Math.min(row[j - 1] + 1, previous[j] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
342
+ previous = row;
343
+ }
344
+ return previous[b.length];
345
+ };
346
+ /** The candidate closest to `name`, when one is close enough to be a typo. */
347
+ const didYouMean = (name, candidates) => {
348
+ const lower = name.toLowerCase();
349
+ const exact = candidates.find((candidate) => candidate.toLowerCase() === lower);
350
+ if (exact !== void 0 && exact !== name) return exact;
351
+ const cap = Math.max(1, Math.min(3, Math.floor(name.length / 3)));
352
+ let best;
353
+ let bestDistance = cap + 1;
354
+ for (const candidate of candidates) {
355
+ const d = distance(lower, candidate.toLowerCase(), cap);
356
+ if (d < bestDistance) {
357
+ best = candidate;
358
+ bestDistance = d;
359
+ }
360
+ }
361
+ return best;
362
+ };
363
+ //#endregion
364
+ //#region src/sml/check.ts
365
+ const isSchema = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
366
+ /** Follow local `$ref`s to the schema they name. */
367
+ const resolve = (root, schema, depth = 0) => {
368
+ const ref = schema.$ref;
369
+ if (typeof ref !== "string" || depth > 16) return schema;
370
+ if (ref === "#") return root;
371
+ let current = root;
372
+ for (const segment of ref.replace(/^#\//, "").split("/")) {
373
+ if (!isSchema(current)) return schema;
374
+ current = current[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
375
+ }
376
+ return isSchema(current) ? resolve(root, current, depth + 1) : schema;
377
+ };
378
+ const branches = (root, schema) => {
379
+ const resolved = resolve(root, schema);
380
+ return [resolved, ...[
381
+ resolved.anyOf,
382
+ resolved.oneOf,
383
+ resolved.allOf
384
+ ].flatMap((list) => Array.isArray(list) ? list.filter(isSchema).flatMap((item) => branches(root, item)) : [])];
385
+ };
386
+ const typeOf = (root, schema) => {
387
+ if (schema === void 0) return "unknown";
388
+ const types = /* @__PURE__ */ new Set();
389
+ for (const branch of branches(root, schema)) {
390
+ const declared = branch.type;
391
+ if (typeof declared === "string") types.add(declared);
392
+ else if (Array.isArray(declared)) {
393
+ for (const item of declared) if (typeof item === "string") types.add(item);
394
+ }
395
+ if (branch.const !== void 0) types.add(typeof branch.const);
396
+ if (Array.isArray(branch.enum)) for (const item of branch.enum) types.add(typeof item);
397
+ }
398
+ types.delete("null");
399
+ if (types.size !== 1) return "unknown";
400
+ const [only] = types;
401
+ switch (only) {
402
+ case "string": return "string";
403
+ case "number":
404
+ case "integer": return "number";
405
+ case "boolean": return "boolean";
406
+ case "array": return "array";
407
+ case "object": return "object";
408
+ default: return "unknown";
409
+ }
410
+ };
411
+ const step = (root, schema, segment) => {
412
+ const names = [];
413
+ let open = true;
414
+ let found;
415
+ for (const branch of branches(root, schema)) {
416
+ if (isSchema(branch.properties)) {
417
+ open = false;
418
+ names.push(...Object.keys(branch.properties));
419
+ if (Object.hasOwn(branch.properties, segment)) {
420
+ const property = branch.properties[segment];
421
+ const required = Array.isArray(branch.required) && branch.required.includes(segment);
422
+ found = {
423
+ schema: isSchema(property) ? property : void 0,
424
+ required: found?.required === true || required
425
+ };
426
+ }
427
+ }
428
+ if (branch.type === "object" && !isSchema(branch.properties) && branch.additionalProperties === false) open = false;
429
+ if (Array.isArray(branch.items) || isSchema(branch.items)) {
430
+ if (/^\d+$/.test(segment)) {
431
+ open = false;
432
+ return {
433
+ found: true,
434
+ schema: isSchema(branch.items) ? branch.items : void 0,
435
+ required: true,
436
+ names,
437
+ open: false
438
+ };
439
+ }
440
+ }
441
+ if (isSchema(branch.additionalProperties) && found === void 0) {
442
+ found = {
443
+ schema: branch.additionalProperties,
444
+ required: false
445
+ };
446
+ open = false;
447
+ }
448
+ }
449
+ if (found !== void 0) return {
450
+ found: true,
451
+ schema: found.schema,
452
+ required: found.required,
453
+ names,
454
+ open: false
455
+ };
456
+ return {
457
+ found: false,
458
+ schema: void 0,
459
+ required: false,
460
+ names,
461
+ open
462
+ };
463
+ };
464
+ const withGuards = (scope, extra) => extra.size === 0 ? scope : {
465
+ ...scope,
466
+ guards: /* @__PURE__ */ new Set([...scope.guards, ...extra])
467
+ };
468
+ const prefixes = (path) => {
469
+ const segments = path.split(".");
470
+ return segments.map((_, index) => segments.slice(0, index + 1).join("."));
471
+ };
472
+ const bindPath = (value) => typeof value === "object" && value !== null && "bind" in value ? value.bind : void 0;
473
+ const guardsOf = (predicate, positive) => {
474
+ const own = (path) => path === void 0 ? /* @__PURE__ */ new Set() : new Set(prefixes(path));
475
+ if ("present" in predicate) return positive ? own(predicate.present) : /* @__PURE__ */ new Set();
476
+ if ("truthy" in predicate) {
477
+ const value = predicate.truthy;
478
+ const path = bindPath(value) ?? (typeof value === "object" && value !== null && "length" in value ? bindPath(value.length) : void 0);
479
+ return positive ? own(path) : /* @__PURE__ */ new Set();
480
+ }
481
+ if ("not" in predicate) return guardsOf(predicate.not, !positive);
482
+ if ("and" in predicate || "or" in predicate) {
483
+ const list = "and" in predicate ? predicate.and : predicate.or;
484
+ const union = "and" in predicate ? positive : !positive;
485
+ const sets = list.map((item) => guardsOf(item, positive));
486
+ if (union) return new Set(sets.flatMap((set) => [...set]));
487
+ const [first, ...rest] = sets;
488
+ return new Set([...first ?? []].filter((path) => rest.every((set) => set.has(path))));
489
+ }
490
+ const [left, right] = predicate.args;
491
+ const path = bindPath(left) ?? bindPath(right);
492
+ const other = bindPath(left) === void 0 ? left : right;
493
+ if (path === void 0) return /* @__PURE__ */ new Set();
494
+ const isNull = other === null;
495
+ const literal = other === null || typeof other !== "object" || "concat" in other || "format" in other || "op" in other || "length" in other || "count" in other;
496
+ if (predicate.cmp === "eq") return !literal || positive === isNull ? /* @__PURE__ */ new Set() : own(path);
497
+ if (predicate.cmp === "ne") return !literal || positive !== isNull ? /* @__PURE__ */ new Set() : own(path);
498
+ return positive ? own(path) : /* @__PURE__ */ new Set();
499
+ };
500
+ /** Check every binding in a lowered email or channel body against the input schema. */
501
+ const checkBindings = (schema, roots, values) => {
502
+ const findings = [];
503
+ const reported = /* @__PURE__ */ new Set();
504
+ const report = (finding) => {
505
+ const key = `${finding.code}\0${finding.message}\0${finding.src?.join(",") ?? ""}`;
506
+ if (reported.has(key)) return;
507
+ reported.add(key);
508
+ findings.push(finding);
509
+ };
510
+ const display = (path) => path.startsWith("$") ? path.slice(1) : `input.${path}`;
511
+ /** Resolve a bound path, reporting unknown and unguarded fields. Returns its schema when known. */
512
+ const walk = (path, scope, src) => {
513
+ const segments = path.split(".");
514
+ const head = segments[0];
515
+ let current;
516
+ if (head.startsWith("$")) {
517
+ if (!scope.variables.has(head.slice(1))) return void 0;
518
+ current = scope.variables.get(head.slice(1));
519
+ } else {
520
+ const first = step(schema, schema, head);
521
+ if (!first.found) return unknownField(head, first, "", src, path);
522
+ if (!first.required && !scope.guards.has(head)) unguarded(head, src);
523
+ current = first.schema;
524
+ }
525
+ for (let index = 1; index < segments.length; index++) {
526
+ if (current === void 0) return void 0;
527
+ const segment = segments[index];
528
+ const next = step(schema, current, segment);
529
+ const prefix = segments.slice(0, index).join(".");
530
+ if (!next.found) {
531
+ if (next.open) return void 0;
532
+ return unknownField(segment, next, prefix, src, path);
533
+ }
534
+ const here = segments.slice(0, index + 1).join(".");
535
+ if (!next.required && !scope.guards.has(here)) unguarded(here, src);
536
+ current = next.schema;
537
+ }
538
+ return current;
539
+ };
540
+ const unknownField = (segment, stepResult, prefix, src, full) => {
541
+ if (stepResult.open) return void 0;
542
+ const near = didYouMean(segment, stepResult.names);
543
+ const owner = prefix === "" ? "input" : display(prefix);
544
+ report({
545
+ code: "unknown-field",
546
+ message: `\`${prefix === "" ? "input" : display(prefix)}.${segment}\` is not in the schema${stepResult.names.length === 0 ? "" : ` (${owner} has ${stepResult.names.map((name) => `\`${name}\``).join(", ")})`}.`,
547
+ ...near === void 0 ? { fix: `Add \`${segment}\` to the schema, or bind a field it declares.` } : { fix: `Did you mean \`${prefix === "" ? "input" : display(prefix)}.${near}\`?` },
548
+ src
549
+ });
550
+ };
551
+ const unguarded = (path, src) => {
552
+ report({
553
+ code: "unguarded-optional",
554
+ message: `\`${display(path)}\` is optional and is read outside a guard, so a send without it would render a blank.`,
555
+ fix: `Guard it: {${display(path)} && …}, or make the field required in the schema.`,
556
+ src
557
+ });
558
+ };
559
+ const valueType = (value, scope, src) => {
560
+ if (typeof value === "string") return "string";
561
+ if (typeof value === "number") return "number";
562
+ if (typeof value === "boolean") return "boolean";
563
+ if (value === null) return "unknown";
564
+ if ("bind" in value) return typeOf(schema, walk(value.bind, scope, src));
565
+ if ("lit" in value) return "unknown";
566
+ if ("concat" in value) {
567
+ for (const part of value.concat) valueType(part, scope, src);
568
+ return "string";
569
+ }
570
+ if ("format" in value) {
571
+ checkFormat(value.format, value.args, scope, src);
572
+ return "string";
573
+ }
574
+ if ("if" in value) {
575
+ visitPredicate(value.if, scope, src);
576
+ const then = valueType(value.then, withGuards(scope, guardsOf(value.if, true)), src);
577
+ valueType(value.else, withGuards(scope, guardsOf(value.if, false)), src);
578
+ return then;
579
+ }
580
+ if ("op" in value) {
581
+ for (const operand of value.args) requireType(operand, "number", scope, src, "arithmetic");
582
+ return "number";
583
+ }
584
+ if ("length" in value) {
585
+ valueType(value.length, scope, src);
586
+ return "number";
587
+ }
588
+ if ("count" in value) {
589
+ const path = bindPath(value.count);
590
+ let item;
591
+ if (path === void 0) valueType(value.count, scope, src);
592
+ else {
593
+ const list = walk(path, scope, src);
594
+ if (list !== void 0) {
595
+ for (const branch of branches(schema, list)) if (isSchema(branch.items)) item = resolve(schema, branch.items);
596
+ }
597
+ }
598
+ const variables = new Map(scope.variables).set(value.as.replace(/^\$/, ""), item);
599
+ visitPredicate(value.where, {
600
+ ...scope,
601
+ variables
602
+ }, src);
603
+ return "number";
604
+ }
605
+ return "unknown";
606
+ };
607
+ const requireType = (value, expected, scope, src, where) => {
608
+ const actual = valueType(value, scope, src);
609
+ if (actual === "unknown" || actual === expected) return;
610
+ const path = bindPath(value);
611
+ report({
612
+ code: "invalid-binding-type",
613
+ message: `${path === void 0 ? "This value" : `\`${display(path)}\``} is ${actual === "array" ? "an array" : `a ${actual}`}, and ${where} needs ${expected === "array" ? "an array" : `a ${expected}`}.`,
614
+ fix: expected === "number" ? "Bind a number field, or send the number in the payload." : `Bind a ${expected} field.`,
615
+ src
616
+ });
617
+ };
618
+ const checkFormat = (name, args, scope, src) => {
619
+ const expected = {
620
+ money: [
621
+ "number",
622
+ "string",
623
+ "string"
624
+ ],
625
+ number: [
626
+ "number",
627
+ void 0,
628
+ "string"
629
+ ],
630
+ date: [
631
+ "string",
632
+ "string",
633
+ "string"
634
+ ],
635
+ time: [
636
+ "string",
637
+ "string",
638
+ "string"
639
+ ],
640
+ plural: [
641
+ "number",
642
+ void 0,
643
+ "string"
644
+ ],
645
+ list: ["array", "string"]
646
+ };
647
+ for (const [index, argument] of args.entries()) {
648
+ const type = expected[name]?.[index];
649
+ if (type === void 0) valueType(argument, scope, src);
650
+ else requireType(argument, type, scope, src, `\`fmt.${name}\``);
651
+ }
652
+ };
653
+ const visitPredicate = (predicate, scope, src) => {
654
+ if ("present" in predicate) {
655
+ walkGuard(predicate.present, scope, src);
656
+ return;
657
+ }
658
+ if ("truthy" in predicate) {
659
+ const value = predicate.truthy;
660
+ const path = bindPath(value);
661
+ if (path !== void 0) walkGuard(path, scope, src);
662
+ else valueTypeGuard(value, scope, src);
663
+ return;
664
+ }
665
+ if ("not" in predicate) {
666
+ visitPredicate(predicate.not, scope, src);
667
+ return;
668
+ }
669
+ if ("and" in predicate) {
670
+ let running = scope;
671
+ for (const part of predicate.and) {
672
+ visitPredicate(part, running, src);
673
+ running = withGuards(running, guardsOf(part, true));
674
+ }
675
+ return;
676
+ }
677
+ if ("or" in predicate) {
678
+ let running = scope;
679
+ for (const part of predicate.or) {
680
+ visitPredicate(part, running, src);
681
+ running = withGuards(running, guardsOf(part, false));
682
+ }
683
+ return;
684
+ }
685
+ const numeric = predicate.cmp === "lt" || predicate.cmp === "gt" || predicate.cmp === "le" || predicate.cmp === "ge";
686
+ const strings = numeric && predicate.args.every((operand) => valueType(operand, scope, src) === "string");
687
+ for (const operand of predicate.args) {
688
+ const path = bindPath(operand);
689
+ if (strings) continue;
690
+ if (path !== void 0 && !numeric) walkGuard(path, scope, src);
691
+ else if (numeric) requireType(operand, "number", scope, src, "a comparison");
692
+ else valueTypeGuard(operand, scope, src);
693
+ }
694
+ };
695
+ /** Reading a field to test it is what a guard is; only the field itself must exist. */
696
+ const walkGuard = (path, scope, src) => {
697
+ walk(path, {
698
+ ...scope,
699
+ guards: /* @__PURE__ */ new Set([...scope.guards, ...prefixes(path)])
700
+ }, src);
701
+ };
702
+ const valueTypeGuard = (value, scope, src) => {
703
+ if (typeof value === "object" && value !== null && "length" in value) {
704
+ const path = bindPath(value.length);
705
+ if (path !== void 0) {
706
+ walkGuard(path, scope, src);
707
+ return;
708
+ }
709
+ }
710
+ valueType(value, scope, src);
711
+ };
712
+ const scalar = (value, scope, src, where) => {
713
+ const type = valueType(value, scope, src);
714
+ if (type === "array" || type === "object") {
715
+ const path = bindPath(value);
716
+ report({
717
+ code: "invalid-binding-type",
718
+ message: `${path === void 0 ? "This value" : `\`${display(path)}\``} is ${type === "array" ? "an array" : "an object"}, and ${where} takes a string or a number.`,
719
+ fix: type === "array" ? "Render the items with .map, or bind one of their fields." : "Bind one of its fields.",
720
+ src
721
+ });
722
+ }
723
+ };
724
+ const visitStyle = (style, scope, src) => {
725
+ if (style === void 0 || typeof style === "string") return;
726
+ if (Array.isArray(style)) {
727
+ for (const [, value] of style) scalar(value, scope, src, "a style value");
728
+ return;
729
+ }
730
+ const conditional = style;
731
+ visitPredicate(conditional.if, scope, src);
732
+ visitStyle(conditional.then, withGuards(scope, guardsOf(conditional.if, true)), src);
733
+ visitStyle(conditional.else, withGuards(scope, guardsOf(conditional.if, false)), src);
734
+ };
735
+ const visit = (nodes, scope, parent) => {
736
+ for (const node of nodes) {
737
+ if (isValueNode(node)) {
738
+ scalar(node, scope, parent, "text");
739
+ continue;
740
+ }
741
+ if ("el" in node) {
742
+ for (const value of Object.values(node.attrs ?? {})) scalar(value, scope, node.src, "an attribute");
743
+ visitStyle(node.style, scope, node.src);
744
+ visit(node.children ?? [], scope, node.src);
745
+ } else if ("if" in node) {
746
+ visitPredicate(node.if, scope, node.src);
747
+ visit(node.then, withGuards(scope, guardsOf(node.if, true)), node.src ?? parent);
748
+ visit(node.else ?? [], withGuards(scope, guardsOf(node.if, false)), node.src ?? parent);
749
+ } else if ("each" in node) {
750
+ const path = bindPath(node.each);
751
+ let listSchema;
752
+ if (path !== void 0) {
753
+ listSchema = walk(path, scope, node.src);
754
+ const type = typeOf(schema, listSchema);
755
+ if (type !== "array" && type !== "unknown") report({
756
+ code: "invalid-binding-type",
757
+ message: `\`${display(path)}\` is ${type === "object" ? "an object" : `a ${type}`}, and a loop needs an array.`,
758
+ fix: "Loop over an array field.",
759
+ src: node.src
760
+ });
761
+ } else valueType(node.each, scope, node.src);
762
+ let item;
763
+ if (listSchema !== void 0) {
764
+ for (const branch of branches(schema, listSchema)) if (isSchema(branch.items)) item = resolve(schema, branch.items);
765
+ }
766
+ const itemName = node.as.replace(/^\$/, "");
767
+ if (node.where !== void 0) visitPredicate(node.where, {
768
+ ...scope,
769
+ variables: new Map(scope.variables).set(itemName, item)
770
+ }, node.src ?? parent);
771
+ const variables = new Map(scope.variables);
772
+ variables.set(itemName, node.chunk === void 0 || item === void 0 ? item : {
773
+ type: "array",
774
+ items: item
775
+ });
776
+ if (node.index !== void 0) variables.set(node.index.replace(/^\$/, ""), { type: "integer" });
777
+ const kept = node.where === void 0 ? scope : withGuards(scope, guardsOf(node.where, true));
778
+ visit(node.children, {
779
+ ...kept,
780
+ variables
781
+ }, node.src ?? parent);
782
+ } else if ("raw" in node) {
783
+ for (const part of node.raw) if (typeof part !== "string") scalar("attr" in part ? part.attr : part.text, scope, node.src, "markup");
784
+ }
785
+ }
786
+ };
787
+ const base = {
788
+ variables: /* @__PURE__ */ new Map(),
789
+ guards: /* @__PURE__ */ new Set()
790
+ };
791
+ visit(roots, base);
792
+ for (const value of values) scalar(value, base, void 0, "the subject or preheader");
793
+ return findings;
794
+ };
795
+ /** The element content rules the serializer enforces, checked before any send. */
796
+ const checkStructure = (roots, report) => {
797
+ const flat = (nodes) => nodes.flatMap((node) => {
798
+ if (isValueNode(node)) return [node];
799
+ if ("if" in node) return [...flat(node.then), ...flat(node.else ?? [])];
800
+ if ("each" in node) return flat(node.children);
801
+ return [node];
802
+ });
803
+ const visit = (nodes) => {
804
+ for (const node of nodes) {
805
+ if (isValueNode(node)) continue;
806
+ if ("el" in node) checkElement(node);
807
+ else if ("if" in node) {
808
+ visit(node.then);
809
+ visit(node.else ?? []);
810
+ } else if ("each" in node) visit(node.children);
811
+ }
812
+ };
813
+ const checkElement = (element) => {
814
+ const spec = emailElement(element.el);
815
+ const kids = flat(element.children ?? []);
816
+ if (spec === void 0) return;
817
+ if (spec.content === "void" && kids.length > 0) report({
818
+ code: "void-element-children",
819
+ message: `<${element.el}> cannot have children.`,
820
+ src: element.src
821
+ });
822
+ else if (spec.content === "text" || spec.content === "raw") {
823
+ for (const kid of kids) if (!isValueNode(kid) && !("raw" in kid)) report({
824
+ code: "invalid-content",
825
+ message: `<${element.el}> accepts text only.`,
826
+ src: element.src
827
+ });
828
+ } else if (spec.childElements !== void 0) {
829
+ for (const kid of kids) if (isValueNode(kid)) {
830
+ if (typeof kid === "string" && kid.trim() === "") continue;
831
+ report({
832
+ code: "invalid-content",
833
+ message: `<${element.el}> cannot contain text directly; put it inside <${spec.childElements[0]}>.`,
834
+ src: element.src
835
+ });
836
+ } else if ("el" in kid && !spec.childElements.includes(kid.el)) report({
837
+ code: "invalid-content",
838
+ message: `<${element.el}> accepts only ${spec.childElements.map((name) => `<${name}>`).join(", ")}; found <${kid.el}>.`,
839
+ src: kid.src ?? element.src
840
+ });
841
+ }
842
+ visit(element.children ?? []);
843
+ };
844
+ visit(roots);
845
+ };
846
+ //#endregion
847
+ //#region src/sml/tokens.ts
848
+ const OPEN = "";
849
+ const CLOSE = "";
850
+ const TOKEN = /(\d+)/g;
851
+ /** Values and node lists a component call reads at render time, addressable from inside a string. */
852
+ var Tokens = class {
853
+ entries = [];
854
+ value(value) {
855
+ if (typeof value === "string") return value;
856
+ this.entries.push({ value });
857
+ return `${OPEN}${this.entries.length - 1}${CLOSE}`;
858
+ }
859
+ nodes(nodes) {
860
+ this.entries.push({ nodes });
861
+ return `${OPEN}${this.entries.length - 1}${CLOSE}`;
862
+ }
863
+ /** A string with tokens in it, as static text and the entries it names. */
864
+ parse(input) {
865
+ const parts = [];
866
+ let last = 0;
867
+ for (const match of input.matchAll(TOKEN)) {
868
+ if (match.index > last) parts.push(input.slice(last, match.index));
869
+ const entry = this.entries[Number(match[1])];
870
+ if (entry !== void 0) parts.push(entry);
871
+ last = match.index + match[0].length;
872
+ }
873
+ if (last < input.length) parts.push(input.slice(last));
874
+ return parts;
875
+ }
876
+ /** The value a string with tokens stands for: itself, one entry's value, or their concatenation. */
877
+ toValue(input) {
878
+ const parts = this.parse(input);
879
+ if (parts.length === 0) return "";
880
+ const values = [];
881
+ for (const part of parts) if (typeof part === "string") values.push(part);
882
+ else if ("value" in part) values.push(part.value);
883
+ else return void 0;
884
+ return values.length === 1 ? values[0] : { concat: values };
885
+ }
886
+ };
887
+ /** A prop value for a primitive: a literal stays itself and anything else becomes a token. */
888
+ const propFor = (tokens, value) => isStaticScalar(value) ? value : tokens.value(value);
889
+ const SOURCE = (source) => source === void 0 ? {} : { src: source };
890
+ /** Split compatibility markup into parts, deciding by position whether a token is in a tag. */
891
+ const rawParts = (tokens, html) => {
892
+ const parts = [];
893
+ let inTag = false;
894
+ let quote;
895
+ let buffer = "";
896
+ const flush = () => {
897
+ if (buffer !== "") parts.push(buffer);
898
+ buffer = "";
899
+ };
900
+ for (let index = 0; index < html.length; index++) {
901
+ const character = html[index];
902
+ if (character === OPEN) {
903
+ const end = html.indexOf(CLOSE, index);
904
+ const entry = tokens.parse(html.slice(index, end + 1))[0];
905
+ if (end > index && entry !== void 0 && typeof entry !== "string" && "value" in entry) {
906
+ flush();
907
+ parts.push(inTag ? { attr: entry.value } : { text: entry.value });
908
+ index = end;
909
+ continue;
910
+ }
911
+ }
912
+ if (!inTag && character === "<") inTag = true;
913
+ else if (inTag && quote === void 0 && (character === "\"" || character === "'")) quote = character;
914
+ else if (inTag && quote === character) quote = void 0;
915
+ else if (inTag && quote === void 0 && character === ">") inTag = false;
916
+ buffer += character;
917
+ }
918
+ flush();
919
+ return parts;
920
+ };
921
+ const styleList = (tokens, style) => {
922
+ if (!isObject(style)) return void 0;
923
+ const entries = [];
924
+ for (const [property, value] of Object.entries(style)) {
925
+ if (value === void 0 || value === null) continue;
926
+ if (typeof value === "number") entries.push([property, value]);
927
+ else if (typeof value === "string") {
928
+ const resolved = tokens.toValue(value);
929
+ if (resolved !== void 0) entries.push([property, resolved]);
930
+ }
931
+ }
932
+ return entries;
933
+ };
934
+ /** Turn the tree a primitive returned back into IR, restoring each token to what it stood for. */
935
+ const emailNodeToIr = (node, tokens, source) => {
936
+ if (node === null) return [];
937
+ switch (node.type) {
938
+ case "Text": {
939
+ const output = [];
940
+ for (const part of tokens.parse(node.value)) if (typeof part === "string") output.push(part);
941
+ else if ("value" in part) output.push(part.value);
942
+ else output.push(...part.nodes);
943
+ return output;
944
+ }
945
+ case "Fragment": return node.children.flatMap((childNode) => emailNodeToIr(childNode, tokens, source));
946
+ case "Raw": return [{
947
+ raw: rawParts(tokens, node.html),
948
+ ...node.text === "" ? {} : { text: node.text },
949
+ ...SOURCE(source)
950
+ }];
951
+ case "Element": {
952
+ const attrs = {};
953
+ let style;
954
+ for (const [key, value] of Object.entries(node.props)) {
955
+ if (key === "key" || value === void 0 || value === null || value === false) continue;
956
+ if (key === "style") {
957
+ style = styleList(tokens, value);
958
+ continue;
959
+ }
960
+ const name = key === "className" ? "class" : key;
961
+ if (typeof value === "string") attrs[name] = tokens.toValue(value) ?? value;
962
+ else if (typeof value === "number" || value === true) attrs[name] = value;
963
+ }
964
+ const reserved = node.props[BRAND_FOOTER_MARKER] === true;
965
+ return [{
966
+ el: node.tag,
967
+ ...Object.keys(attrs).length === 0 ? {} : { attrs },
968
+ ...style === void 0 || Array.isArray(style) && style.length === 0 ? {} : { style },
969
+ ...node.children.length === 0 ? {} : { children: node.children.flatMap((childNode) => emailNodeToIr(childNode, tokens, source)) },
970
+ ...reserved ? { reserved: true } : {},
971
+ ...SOURCE(source)
972
+ }];
973
+ }
974
+ }
975
+ };
976
+ //#endregion
977
+ //#region src/sml/components.ts
978
+ const NUMERIC = {
979
+ Button: [
980
+ "height",
981
+ "paddingX",
982
+ "borderRadius",
983
+ "borderWidth",
984
+ "fontSize",
985
+ "width"
986
+ ],
987
+ Spacer: ["height"],
988
+ Divider: ["thickness"],
989
+ BrandLogo: ["width"]
990
+ };
991
+ const COMPONENTS = {
992
+ Email,
993
+ Section,
994
+ Columns,
995
+ Column,
996
+ Button,
997
+ Spacer,
998
+ Divider,
999
+ Image,
1000
+ Link
1001
+ };
1002
+ const bindingToProp = (call, tokens, name, binding) => {
1003
+ switch (binding.k) {
1004
+ case "object": {
1005
+ const output = {};
1006
+ for (const [key, field] of binding.fields) output[key] = bindingToProp(call, tokens, key, field);
1007
+ return output;
1008
+ }
1009
+ case "nodes":
1010
+ call.lowerer.error(call.ctx, call.node, "dynamic-expression", `<${call.name} ${name}> takes text, not markup.`);
1011
+ return;
1012
+ default: {
1013
+ const value = bindingValue(binding);
1014
+ if (value === void 0) {
1015
+ call.lowerer.error(call.ctx, call.node, "dynamic-expression", `<${call.name} ${name}> cannot be the input as a whole.`, "Bind one field.");
1016
+ return;
1017
+ }
1018
+ return propFor(tokens, value);
1019
+ }
1020
+ }
1021
+ };
1022
+ /** Content between the tags, the way a primitive would have received it. */
1023
+ const childrenProp = (tokens, nodes) => {
1024
+ const only = nodes.length === 1 ? nodes[0] : void 0;
1025
+ if (only !== void 0 && isValueNode(only) && only !== null && typeof only !== "boolean") return typeof only === "string" ? only : tokens.value(only);
1026
+ return nodes.map((node) => isValueNode(node) ? typeof node === "string" ? node : tokens.value(node) : tokens.nodes([node]));
1027
+ };
1028
+ /** The width each cell of a `<Columns each>` row takes when it states none. */
1029
+ const equalWidth = (per, list) => {
1030
+ if (per !== void 0) return equalColumnWidth(per);
1031
+ const count = { length: list };
1032
+ return { concat: [{
1033
+ op: "/",
1034
+ args: [{
1035
+ op: "-",
1036
+ args: [100, {
1037
+ op: "%",
1038
+ args: [100, count]
1039
+ }]
1040
+ }, count]
1041
+ }, "%"] };
1042
+ };
1043
+ /**
1044
+ * `<Columns each={list} per={n}>{(item, i) => <Column … />}</Columns>`: one row per chunk of `n`
1045
+ * items (all in one row without `per`), each cell from the callback. Returns the nodes to place
1046
+ * inside the Columns table, and a wrapper that repeats that table per chunk.
1047
+ */
1048
+ const columnsEach = (call, bindings) => {
1049
+ const { lowerer, ctx, node } = call;
1050
+ const listValue = bindings.get("each");
1051
+ const list = listValue === void 0 ? void 0 : bindingValue(listValue);
1052
+ if (list === void 0) {
1053
+ lowerer.error(ctx, node, "invalid-columns-each", "<Columns each> takes a list from the input.", "Write each={input.items}.");
1054
+ return;
1055
+ }
1056
+ const perBinding = bindings.get("per");
1057
+ const perValue = perBinding?.k === "value" ? perBinding.value : void 0;
1058
+ if (perBinding !== void 0 && !(typeof perValue === "number" && Number.isInteger(perValue) && perValue >= 1)) {
1059
+ lowerer.error(ctx, node, "invalid-columns-per", "<Columns per> is a whole number of cells, one or more.", "Write per={2}.");
1060
+ return;
1061
+ }
1062
+ const per = typeof perValue === "number" ? perValue : void 0;
1063
+ const contents = children(node, "children").filter((item) => !(item.type === "JSXText" && (text(item, "value") ?? "").trim() === ""));
1064
+ const container = contents.length === 1 ? contents[0] : void 0;
1065
+ const expression = container?.type === "JSXExpressionContainer" ? child(container, "expression") : void 0;
1066
+ const fn = expression === void 0 ? void 0 : unwrap(expression);
1067
+ if (fn === void 0 || !/Function/.test(fn.type)) {
1068
+ lowerer.error(ctx, node, "invalid-columns-each", "<Columns each> takes a function of one item as its only child.", "Write {(item) => <Column>…</Column>}.");
1069
+ return;
1070
+ }
1071
+ const row = per === void 0 ? void 0 : lowerer.freshName(ctx, "row");
1072
+ const cellCtx = {
1073
+ ...ctx,
1074
+ loops: row === void 0 ? ctx.loops : [...ctx.loops, row],
1075
+ columnWidth: equalWidth(per, list)
1076
+ };
1077
+ const cells = lowerer.eachOver(cellCtx, node, row === void 0 ? list : { bind: `$${row}` }, fn);
1078
+ if (cells === void 0) return void 0;
1079
+ return {
1080
+ cells,
1081
+ wrap: (table) => {
1082
+ if (row === void 0 || per === void 0) return table;
1083
+ return [{
1084
+ each: list,
1085
+ as: row,
1086
+ chunk: per,
1087
+ children: table,
1088
+ src: lowerer.src(ctx.module, node)
1089
+ }];
1090
+ }
1091
+ };
1092
+ };
1093
+ /** Run a compiler primitive at compile time and lower what it returns. */
1094
+ const runBuiltinComponent = (call) => {
1095
+ const { lowerer, ctx, node, binding } = call;
1096
+ const specifier = binding.kind === "markup" ? binding.specifier : "samva:brand";
1097
+ if (binding.kind === "markup" && (specifier === "@samva/markup/sms" || specifier === "@samva/markup/whatsapp")) {
1098
+ const lowered = call.channel?.(call);
1099
+ if (lowered !== void 0) return lowered;
1100
+ lowerer.error(ctx, node, "wrong-channel", `<${call.name}> belongs in the ${specifier.split("/").pop()} body of a template.`, "Write it in the `sms` or `whatsapp` function of defineTemplate.");
1101
+ return [];
1102
+ }
1103
+ const exported = binding.imported;
1104
+ const isEmail = binding.kind === "markup" && specifier === "@samva/markup/email";
1105
+ const isBrand = binding.kind === "brand";
1106
+ if (!(isEmail || isBrand)) {
1107
+ lowerer.error(ctx, node, "unknown-component", `<${call.name}> is not a component the profile can read.`, "Import components from @samva/markup/email, samva:brand, or the project.");
1108
+ return [];
1109
+ }
1110
+ let fn;
1111
+ if (isEmail) fn = COMPONENTS[exported];
1112
+ else {
1113
+ if (call.brand === void 0) {
1114
+ lowerer.error(ctx, node, "brand-unavailable", `<${call.name}> comes from samva:brand, and this build has no brand to resolve it.`, "Compile with a brand, or remove the brand components.");
1115
+ return [];
1116
+ }
1117
+ const brand = brandComponents(call.brand);
1118
+ fn = exported === "BrandLogo" ? brand.BrandLogo : exported === "BrandFooter" ? brand.BrandFooter : void 0;
1119
+ }
1120
+ if (fn === void 0 || !Object.hasOwn(COMPONENTS, exported) && isEmail) {
1121
+ lowerer.error(ctx, node, "unknown-component", `\`${exported}\` is not an email component.`, `Use one of ${Object.keys(COMPONENTS).join(", ")}, BrandLogo or BrandFooter.`);
1122
+ return [];
1123
+ }
1124
+ const declared = lowerer.attributeBindings(ctx, call.attributes);
1125
+ if (declared === void 0) return [];
1126
+ let bindings = declared;
1127
+ let wrap = (table) => table;
1128
+ let kids;
1129
+ const childCtx = {
1130
+ ...ctx,
1131
+ columnWidth: void 0
1132
+ };
1133
+ if (exported === "Columns" && declared.has("each")) {
1134
+ const chunked = columnsEach(call, declared);
1135
+ if (chunked === void 0) return [];
1136
+ bindings = new Map([...declared].filter(([name]) => name !== "each" && name !== "per"));
1137
+ kids = [chunked.cells];
1138
+ wrap = chunked.wrap;
1139
+ } else kids = lowerer.jsxChildren(childCtx, node);
1140
+ const tokens = new Tokens();
1141
+ const props = {};
1142
+ for (const [name, bound] of bindings) props[name] = bindingToProp(call, tokens, name, bound);
1143
+ for (const name of NUMERIC[exported] ?? []) {
1144
+ const value = props[name];
1145
+ if (typeof value === "string" && !/^-?\d+(\.\d+)?$/.test(value) && value !== void 0) {
1146
+ lowerer.error(ctx, node, "dynamic-numeric-prop", `<${call.name} ${name}> sizes the layout at compile time, so it must be a number literal.`, `Write ${name}={44}.`);
1147
+ return [];
1148
+ }
1149
+ }
1150
+ if (exported === "Column" && props.width === void 0 && ctx.columnWidth !== void 0) props.width = propFor(tokens, ctx.columnWidth);
1151
+ if (kids.length > 0) props.children = childrenProp(tokens, kids);
1152
+ if (exported === "Button" && typeof props.children === "string" && props.children !== "" && props.width === void 0 && props.height !== void 0 && tokens.toValue(props.children) !== props.children) {
1153
+ 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} …>.");
1154
+ return [];
1155
+ }
1156
+ drainEmailDiagnostics();
1157
+ const output = fn(props);
1158
+ const location = lowerer.findings.locate(ctx.module.parsed, node);
1159
+ for (const diagnostic of drainEmailDiagnostics()) lowerer.findings.items.push({
1160
+ ...diagnostic,
1161
+ origins: [location]
1162
+ });
1163
+ if (output !== null && (!isObject(output) || typeof output.type !== "string")) return [];
1164
+ return wrap([...emailNodeToIr(output, tokens, lowerer.src(ctx.module, node))]);
1165
+ };
1166
+ //#endregion
1167
+ //#region src/sml/values.ts
1168
+ const fail = (module, node, reason) => ({
1169
+ ok: false,
1170
+ node,
1171
+ module,
1172
+ reason
1173
+ });
1174
+ const keyOf = (property) => {
1175
+ if (property.type === "ObjectProperty" && property.computed !== true) {
1176
+ const key = child(property, "key");
1177
+ if (key === void 0) return void 0;
1178
+ if (key.type === "Identifier") return text(key, "name");
1179
+ if (key.type === "StringLiteral") return text(key, "value");
1180
+ if (key.type === "NumericLiteral") return String(key.value);
1181
+ }
1182
+ };
1183
+ /** Resolve a top-level constant name in a module, following project imports and re-exports. */
1184
+ const resolveName = (context, module, name, seen = /* @__PURE__ */ new Set()) => {
1185
+ const key = `${module.path}\0${name}`;
1186
+ if (seen.has(key)) return void 0;
1187
+ seen.add(key);
1188
+ const declared = module.declarations.get(name);
1189
+ if (declared !== void 0) return {
1190
+ module,
1191
+ value: declared.value
1192
+ };
1193
+ const imported = module.imports.get(name);
1194
+ if (imported === void 0) return void 0;
1195
+ if (imported.kind === "asset") return { asset: imported.path };
1196
+ if (imported.kind !== "project") return void 0;
1197
+ const target = context.project.modules.get(imported.path);
1198
+ if (target === void 0) return void 0;
1199
+ if (imported.imported === "default") {
1200
+ const value = target.defaultExport;
1201
+ return value === void 0 ? void 0 : {
1202
+ module: target,
1203
+ value
1204
+ };
1205
+ }
1206
+ const local = target.exports.get(imported.imported);
1207
+ return local === void 0 ? void 0 : resolveName(context, target, local, seen);
1208
+ };
1209
+ /** Read a node as plain JSON-like data. */
1210
+ const readStatic = (context, module, node, seen = /* @__PURE__ */ new Set()) => {
1211
+ const current = unwrap(node);
1212
+ switch (current.type) {
1213
+ case "StringLiteral": return {
1214
+ ok: true,
1215
+ value: current.value
1216
+ };
1217
+ case "NumericLiteral": return {
1218
+ ok: true,
1219
+ value: current.value
1220
+ };
1221
+ case "BooleanLiteral": return {
1222
+ ok: true,
1223
+ value: current.value
1224
+ };
1225
+ case "NullLiteral": return {
1226
+ ok: true,
1227
+ value: null
1228
+ };
1229
+ case "TemplateLiteral": {
1230
+ if (children(current, "expressions").length > 0) return fail(module, current, "a template string with a `${}` part is computed at run time");
1231
+ const value = children(current, "quasis")[0]?.value;
1232
+ const cooked = value?.cooked ?? value?.raw;
1233
+ return {
1234
+ ok: true,
1235
+ value: typeof cooked === "string" ? cooked : ""
1236
+ };
1237
+ }
1238
+ case "UnaryExpression": {
1239
+ const argument = child(current, "argument");
1240
+ if (text(current, "operator") === "-" && argument !== void 0 && argument.type === "NumericLiteral" && typeof argument.value === "number") return {
1241
+ ok: true,
1242
+ value: -argument.value
1243
+ };
1244
+ return fail(module, current, "only a negative number literal is data");
1245
+ }
1246
+ case "Identifier": {
1247
+ const name = text(current, "name") ?? "";
1248
+ if (name === "undefined") return {
1249
+ ok: true,
1250
+ value: void 0
1251
+ };
1252
+ const resolved = resolveName(context, module, name);
1253
+ if (resolved === void 0) return fail(module, current, `\`${name}\` is not a constant in the project`);
1254
+ if ("asset" in resolved) {
1255
+ const url = context.assetUrl(resolved.asset);
1256
+ return url === void 0 ? fail(module, current, `\`${name}\` names an asset the build did not address`) : {
1257
+ ok: true,
1258
+ value: url
1259
+ };
1260
+ }
1261
+ const key = `${resolved.module.path}\0${name}\0${resolved.value.start}`;
1262
+ if (seen.has(key)) return fail(module, current, `\`${name}\` refers to itself`);
1263
+ seen.add(key);
1264
+ const read = readStatic(context, resolved.module, resolved.value, seen);
1265
+ seen.delete(key);
1266
+ return read;
1267
+ }
1268
+ case "ArrayExpression": {
1269
+ const output = [];
1270
+ for (const element of Array.isArray(current.elements) ? current.elements : []) {
1271
+ if (element === null || typeof element !== "object") return fail(module, current, "an array hole");
1272
+ const item = element;
1273
+ if (item.type === "SpreadElement") {
1274
+ const argument = child(item, "argument");
1275
+ if (argument === void 0) return fail(module, item, "an empty spread");
1276
+ const spread = readStatic(context, module, argument, seen);
1277
+ if (!spread.ok) return spread;
1278
+ if (!Array.isArray(spread.value)) return fail(module, item, "a spread of a non-array");
1279
+ output.push(...spread.value);
1280
+ continue;
1281
+ }
1282
+ const value = readStatic(context, module, item, seen);
1283
+ if (!value.ok) return value;
1284
+ output.push(value.value);
1285
+ }
1286
+ return {
1287
+ ok: true,
1288
+ value: output
1289
+ };
1290
+ }
1291
+ case "ObjectExpression": {
1292
+ const output = {};
1293
+ for (const property of children(current, "properties")) {
1294
+ if (property.type === "SpreadElement") {
1295
+ const argument = child(property, "argument");
1296
+ if (argument === void 0) return fail(module, property, "an empty spread");
1297
+ const spread = readStatic(context, module, argument, seen);
1298
+ if (!spread.ok) return spread;
1299
+ if (typeof spread.value !== "object" || spread.value === null || Array.isArray(spread.value)) return fail(module, property, "a spread of a non-object");
1300
+ Object.assign(output, spread.value);
1301
+ continue;
1302
+ }
1303
+ const key = keyOf(property);
1304
+ if (key === void 0) return fail(module, property, "a computed or method property");
1305
+ const valueNode = child(property, "value");
1306
+ if (valueNode === void 0) return fail(module, property, "a property without a value");
1307
+ const value = readStatic(context, module, valueNode, seen);
1308
+ if (!value.ok) return value;
1309
+ if (value.value !== void 0) output[key] = value.value;
1310
+ }
1311
+ return {
1312
+ ok: true,
1313
+ value: output
1314
+ };
1315
+ }
1316
+ case "CallExpression": {
1317
+ const callee = child(current, "callee");
1318
+ const first = children(current, "arguments")[0];
1319
+ if (callee?.type === "Identifier" && text(callee, "name") === "jsonSchema" && first !== void 0) return readStatic(context, module, first, seen);
1320
+ return fail(module, current, "a call is computed at run time");
1321
+ }
1322
+ default: return fail(module, current, `${current.type} is not data`);
1323
+ }
1324
+ };
1325
+ //#endregion
1326
+ //#region src/sml/lower.ts
1327
+ const MAX_DEPTH = 40;
1328
+ const ARITHMETIC = {
1329
+ "+": "+",
1330
+ "-": "-",
1331
+ "*": "*",
1332
+ "/": "/",
1333
+ "%": "%"
1334
+ };
1335
+ const COMPARISON = {
1336
+ "===": "eq",
1337
+ "!==": "ne",
1338
+ "<": "lt",
1339
+ ">": "gt",
1340
+ "<=": "le",
1341
+ ">=": "ge"
1342
+ };
1343
+ const FORMATTER_ARITY = {
1344
+ money: [2, 3],
1345
+ number: [1, 3],
1346
+ date: [1, 3],
1347
+ time: [1, 3],
1348
+ plural: [2, 3],
1349
+ list: [1, 2]
1350
+ };
1351
+ const RUN_TIME_METHODS = /* @__PURE__ */ new Set([
1352
+ "reduce",
1353
+ "filter",
1354
+ "slice",
1355
+ "join",
1356
+ "sort",
1357
+ "concat",
1358
+ "flatMap",
1359
+ "find",
1360
+ "some",
1361
+ "every",
1362
+ "toFixed",
1363
+ "toLocaleString",
1364
+ "toLocaleDateString",
1365
+ "toUpperCase",
1366
+ "toLowerCase",
1367
+ "trim",
1368
+ "split",
1369
+ "replace",
1370
+ "padStart",
1371
+ "includes",
1372
+ "indexOf",
1373
+ "at"
1374
+ ]);
1375
+ /** React's rule for JSX text: trim lines, drop blank ones, join the rest with a space. */
1376
+ const cleanJsxText = (raw) => {
1377
+ const lines = raw.split(/\r\n|\n|\r/);
1378
+ let lastNonEmpty = 0;
1379
+ for (const [index, line] of lines.entries()) if (/[^ \t]/.test(line)) lastNonEmpty = index;
1380
+ let output = "";
1381
+ for (const [index, line] of lines.entries()) {
1382
+ let trimmed = line.replace(/\t/g, " ");
1383
+ if (index !== 0) trimmed = trimmed.replace(/^ +/, "");
1384
+ if (index !== lines.length - 1) trimmed = trimmed.replace(/ +$/, "");
1385
+ if (trimmed !== "") {
1386
+ if (index !== lastNonEmpty) trimmed += " ";
1387
+ output += trimmed;
1388
+ }
1389
+ }
1390
+ return output;
1391
+ };
1392
+ const jsxName$1 = (name) => {
1393
+ if (name.type === "JSXIdentifier") return text(name, "name") ?? "";
1394
+ if (name.type === "JSXNamespacedName") return `${text(child(name, "namespace") ?? name, "name") ?? ""}:${text(child(name, "name") ?? name, "name") ?? ""}`;
1395
+ if (name.type === "JSXMemberExpression") return `${jsxName$1(child(name, "object") ?? name)}.${text(child(name, "property") ?? name, "name") ?? ""}`;
1396
+ return "";
1397
+ };
1398
+ const isStatic = (value) => value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
1399
+ const statementKind = (type) => {
1400
+ if (type === "VariableDeclaration") return "`const`";
1401
+ if (type === "IfStatement") return "`if`";
1402
+ if (/For|While/.test(type)) return "a loop";
1403
+ return "a statement";
1404
+ };
1405
+ var Lowerer = class {
1406
+ options;
1407
+ sources = [];
1408
+ uniqueId = 0;
1409
+ constructor(options) {
1410
+ this.options = options;
1411
+ }
1412
+ get findings() {
1413
+ return this.options.findings;
1414
+ }
1415
+ /** A stable source reference for a node: `[line, column]` in the entry, else with a file index. */
1416
+ src(module, node) {
1417
+ const at = module.parsed.position(node.start);
1418
+ if (module.path === this.options.project.entry.path) return [at.line, at.column];
1419
+ let index = this.sources.indexOf(module.path);
1420
+ if (index < 0) {
1421
+ index = this.sources.length;
1422
+ this.sources.push(module.path);
1423
+ }
1424
+ return [
1425
+ at.line,
1426
+ at.column,
1427
+ index + 1
1428
+ ];
1429
+ }
1430
+ error(ctx, node, code, message, fix) {
1431
+ this.findings.add(ctx.module.parsed, node, code, message, fix);
1432
+ }
1433
+ dynamic(ctx, node, what, fix) {
1434
+ this.error(ctx, node, "dynamic-expression", `${what} runs code, and templates render without running code.`, fix);
1435
+ }
1436
+ /** The expression a function returns, or a diagnostic when its body is more than one. */
1437
+ functionExpression(ctx, fn) {
1438
+ const body = child(fn, "body");
1439
+ if (body === void 0) return void 0;
1440
+ if (body.type !== "BlockStatement") return body;
1441
+ const statements = children(body, "body");
1442
+ const first = statements[0];
1443
+ if (statements.length === 1 && first?.type === "ReturnStatement") {
1444
+ const argument = child(first, "argument");
1445
+ if (argument !== void 0) return argument;
1446
+ }
1447
+ const offender = statements.find((statement) => statement.type !== "ReturnStatement") ?? body;
1448
+ const kind = statementKind(offender.type);
1449
+ this.error(ctx, offender, "statement-in-body", `A body is one expression, and ${kind} is a statement.`, "Write the value where it is used: `cond && <X />` or `cond ? <A /> : <B />` for branches, `.map` for lists. Compute anything else in the caller and send it in the payload.");
1450
+ }
1451
+ /** Bind a parameter pattern to a binding, adding the names it introduces to `env`. */
1452
+ bindPattern(ctx, pattern, binding, env, allowDefaults) {
1453
+ switch (pattern.type) {
1454
+ case "Identifier": {
1455
+ const name = text(pattern, "name") ?? "";
1456
+ env.set(name, binding ?? {
1457
+ k: "value",
1458
+ value: null
1459
+ });
1460
+ return;
1461
+ }
1462
+ case "AssignmentPattern": {
1463
+ const left = child(pattern, "left");
1464
+ const right = child(pattern, "right");
1465
+ if (left === void 0 || right === void 0) return;
1466
+ if (!allowDefaults) {
1467
+ this.error(ctx, pattern, "unsupported-pattern", "A default value on the input is never inserted, so it is not part of the profile.", "Remove the default; declare the field as required or guard it where it is read.");
1468
+ return;
1469
+ }
1470
+ let resolved = binding;
1471
+ if (resolved === void 0) {
1472
+ const defaultValue = readStatic(this.options.staticContext, ctx.module, right);
1473
+ if (!defaultValue.ok) {
1474
+ this.error(ctx, right, "dynamic-expression", "A default prop value must be a literal.", "Use a string, number or boolean literal.");
1475
+ return;
1476
+ }
1477
+ resolved = {
1478
+ k: "value",
1479
+ value: defaultValue.value
1480
+ };
1481
+ }
1482
+ this.bindPattern(ctx, left, resolved, env, allowDefaults);
1483
+ return;
1484
+ }
1485
+ case "ObjectPattern":
1486
+ for (const property of children(pattern, "properties")) {
1487
+ if (property.type === "RestElement") {
1488
+ this.error(ctx, property, "unsupported-pattern", "A rest element gathers props at run time.", "Name each prop you use.");
1489
+ continue;
1490
+ }
1491
+ const key = child(property, "key");
1492
+ const value = child(property, "value");
1493
+ const name = keyName(key);
1494
+ if (name === void 0 || value === void 0 || property.computed === true) {
1495
+ this.error(ctx, property, "unsupported-pattern", "A computed key cannot be read statically.");
1496
+ continue;
1497
+ }
1498
+ const field = binding === void 0 ? void 0 : this.field(ctx, binding, name, property);
1499
+ this.bindPattern(ctx, value, field, env, allowDefaults);
1500
+ }
1501
+ return;
1502
+ default: this.error(ctx, pattern, "unsupported-pattern", "A parameter is a name or an object pattern of names.", "Write `(input) =>` or `({ name, items }) =>`.");
1503
+ }
1504
+ }
1505
+ /** The binding for `base.name`. */
1506
+ field(ctx, base, name, at) {
1507
+ switch (base.k) {
1508
+ case "path":
1509
+ if (name === "length" && base.path !== "") return {
1510
+ k: "value",
1511
+ value: { length: { bind: base.path } }
1512
+ };
1513
+ return {
1514
+ k: "path",
1515
+ path: joinPath(base.path, name)
1516
+ };
1517
+ case "object": return base.fields.get(name);
1518
+ case "value":
1519
+ if (name === "length") return {
1520
+ k: "value",
1521
+ value: { length: base.value }
1522
+ };
1523
+ if (base.value === null) return {
1524
+ k: "value",
1525
+ value: null
1526
+ };
1527
+ this.error(ctx, at, "dynamic-expression", `\`.${name}\` reads a property of a computed value.`, "Read fields of the input directly.");
1528
+ return;
1529
+ case "nodes":
1530
+ this.error(ctx, at, "dynamic-expression", "`children` has no fields.", "Render `{children}` as is.");
1531
+ return;
1532
+ }
1533
+ }
1534
+ /** Resolve an identifier or member chain to a binding. */
1535
+ binding(ctx, expression) {
1536
+ const node = unwrap(expression);
1537
+ if (node.type === "Identifier") {
1538
+ const name = text(node, "name") ?? "";
1539
+ if (name === "undefined") return {
1540
+ k: "value",
1541
+ value: null
1542
+ };
1543
+ const found = ctx.env.get(name);
1544
+ if (found !== void 0) return found;
1545
+ const imported = ctx.module.imports.get(name);
1546
+ if (imported?.kind === "asset") {
1547
+ const url = this.options.assetUrl(imported.path);
1548
+ if (url !== void 0) return {
1549
+ k: "value",
1550
+ value: url
1551
+ };
1552
+ }
1553
+ this.error(ctx, node, "dynamic-expression", `\`${name}\` is not a binding the template declared.`, "Read the input through the function's parameter, for example `input.name`.");
1554
+ return;
1555
+ }
1556
+ if (node.type === "MemberExpression") {
1557
+ const object = child(node, "object");
1558
+ const property = child(node, "property");
1559
+ if (object === void 0 || property === void 0) return void 0;
1560
+ if (node.computed !== true && text(property, "name") === "length" && this.filterChain(ctx, object) !== void 0) {
1561
+ const source = this.filterChain(ctx, object);
1562
+ if (source.list === void 0) return void 0;
1563
+ const first = source.filters[0];
1564
+ const param = first === void 0 ? void 0 : children(first, "params")[0];
1565
+ const as = this.uniqueName(ctx, param?.type === "Identifier" ? text(param, "name") ?? "item" : "item");
1566
+ const where = this.whereClause({
1567
+ ...ctx,
1568
+ loops: [...ctx.loops, as]
1569
+ }, as, source.filters);
1570
+ return where === void 0 ? void 0 : {
1571
+ k: "value",
1572
+ value: {
1573
+ count: source.list,
1574
+ as,
1575
+ where
1576
+ }
1577
+ };
1578
+ }
1579
+ const base = this.binding(ctx, object);
1580
+ if (base === void 0) return void 0;
1581
+ let key;
1582
+ if (node.computed === true) {
1583
+ if (property.type === "StringLiteral") key = text(property, "value");
1584
+ else if (property.type === "NumericLiteral") key = String(property.value);
1585
+ if (key === void 0) {
1586
+ this.dynamic(ctx, node, "A computed property", "Use `.name`, or a numeric index such as `[0]`.");
1587
+ return;
1588
+ }
1589
+ } else key = text(property, "name");
1590
+ if (key === void 0) return void 0;
1591
+ return this.field(ctx, base, key, node);
1592
+ }
1593
+ if (node.type === "OptionalMemberExpression" || node.type === "OptionalCallExpression") {
1594
+ this.dynamic(ctx, node, "Optional chaining", "Guard the field: `input.discount && <p>{input.discount.label}</p>`.");
1595
+ return;
1596
+ }
1597
+ const value = this.value(ctx, node, false);
1598
+ return value === void 0 ? void 0 : {
1599
+ k: "value",
1600
+ value
1601
+ };
1602
+ }
1603
+ /** Lower an expression to a value read at render time. `arith` allows arithmetic operators. */
1604
+ value(ctx, expression, arith) {
1605
+ const node = unwrap(expression);
1606
+ switch (node.type) {
1607
+ case "StringLiteral": return this.checkedText(ctx, node, text(node, "value") ?? "");
1608
+ case "NumericLiteral": return typeof node.value === "number" ? node.value : void 0;
1609
+ case "BooleanLiteral": return node.value === true;
1610
+ case "NullLiteral": return null;
1611
+ case "Identifier":
1612
+ case "MemberExpression":
1613
+ case "OptionalMemberExpression": {
1614
+ const bound = this.binding(ctx, node);
1615
+ if (bound === void 0) return void 0;
1616
+ const value = bindingValue(bound);
1617
+ if (value !== void 0) return value;
1618
+ this.error(ctx, node, "dynamic-expression", bound.k === "path" ? "The input as a whole is not a value." : "An object or `children` is not text.", bound.k === "path" ? "Read one field: `input.name`." : "Render `{children}` as content, or read one field.");
1619
+ return;
1620
+ }
1621
+ case "TemplateLiteral": return this.template(ctx, node, arith);
1622
+ case "ConditionalExpression": {
1623
+ const test = child(node, "test");
1624
+ const consequent = child(node, "consequent");
1625
+ const alternate = child(node, "alternate");
1626
+ if (test === void 0 || consequent === void 0 || alternate === void 0) return void 0;
1627
+ const predicate = this.predicate(ctx, test);
1628
+ const then = this.value(ctx, consequent, arith);
1629
+ const otherwise = this.value(ctx, alternate, arith);
1630
+ if (predicate === void 0 || then === void 0 || otherwise === void 0) return void 0;
1631
+ return {
1632
+ if: predicate,
1633
+ then,
1634
+ else: otherwise
1635
+ };
1636
+ }
1637
+ case "LogicalExpression": {
1638
+ const operator = text(node, "operator");
1639
+ const left = child(node, "left");
1640
+ const right = child(node, "right");
1641
+ if (left === void 0 || right === void 0) return void 0;
1642
+ if (operator === "||") {
1643
+ const a = this.value(ctx, left, arith);
1644
+ const b = this.value(ctx, right, arith);
1645
+ if (a === void 0 || b === void 0) return void 0;
1646
+ return {
1647
+ if: { truthy: a },
1648
+ then: a,
1649
+ else: b
1650
+ };
1651
+ }
1652
+ if (operator === "??") {
1653
+ this.error(ctx, node, "dynamic-expression", "`??` is not one of the conditions the profile reads.", "Write the condition out: `input.name === null ? \"there\" : input.name`, or `input.name || \"there\"`.");
1654
+ return;
1655
+ }
1656
+ this.error(ctx, node, "dynamic-expression", "`&&` in a value position yields its left operand when that is falsy.", "Use a conditional: `cond ? value : fallback`, or `cond && <X />` in content.");
1657
+ return;
1658
+ }
1659
+ case "CallExpression": return this.call(ctx, node);
1660
+ case "BinaryExpression": {
1661
+ const operator = text(node, "operator") ?? "";
1662
+ const op = ARITHMETIC[operator];
1663
+ const left = child(node, "left");
1664
+ const right = child(node, "right");
1665
+ if (op === void 0 || left === void 0 || right === void 0) {
1666
+ this.error(ctx, node, "dynamic-expression", `\`${operator}\` produces a condition, not a value.`, "Use it as the test of `&&`, `? :` or `||`.");
1667
+ return;
1668
+ }
1669
+ if (!arith) {
1670
+ this.error(ctx, node, "dynamic-expression", "Arithmetic runs code, and it is allowed only inside a `fmt.*` argument or a condition.", "Wrap it: `fmt.number(item.price * item.qty)`, or send the computed value in the payload.");
1671
+ return;
1672
+ }
1673
+ if (op === "+" && [left, right].some((side) => /StringLiteral|TemplateLiteral/.test(unwrap(side).type))) {
1674
+ this.error(ctx, node, "dynamic-expression", "`+` with a string joins text at run time.", "Use a template string: `${a} ${b}`.");
1675
+ return;
1676
+ }
1677
+ const a = this.value(ctx, left, true);
1678
+ const b = this.value(ctx, right, true);
1679
+ return a === void 0 || b === void 0 ? void 0 : {
1680
+ op,
1681
+ args: [a, b]
1682
+ };
1683
+ }
1684
+ case "UnaryExpression": {
1685
+ const operator = text(node, "operator");
1686
+ const argument = child(node, "argument");
1687
+ if (operator === "-" && argument !== void 0) {
1688
+ const inner = unwrap(argument);
1689
+ if (inner.type === "NumericLiteral" && typeof inner.value === "number") return -inner.value;
1690
+ if (arith) {
1691
+ const value = this.value(ctx, argument, true);
1692
+ return value === void 0 ? void 0 : {
1693
+ op: "-",
1694
+ args: [0, value]
1695
+ };
1696
+ }
1697
+ }
1698
+ if (operator === "!") {
1699
+ this.error(ctx, node, "dynamic-expression", "`!` produces a condition, not a value.", "Use it as the test of `&&`, `? :` or `||`.");
1700
+ return;
1701
+ }
1702
+ return this.dynamic(ctx, node, `\`${operator ?? "?"}\``, "Send the computed value in the payload.");
1703
+ }
1704
+ case "JSXElement":
1705
+ case "JSXFragment":
1706
+ this.error(ctx, node, "dynamic-expression", "Markup is not a text value.", "Use it as content.");
1707
+ return;
1708
+ case "ArrowFunctionExpression":
1709
+ case "FunctionExpression": return this.dynamic(ctx, node, "A function", "Write the value directly.");
1710
+ case "NewExpression":
1711
+ case "AwaitExpression":
1712
+ case "AssignmentExpression":
1713
+ case "UpdateExpression":
1714
+ case "SequenceExpression":
1715
+ case "ObjectExpression":
1716
+ case "ArrayExpression":
1717
+ case "SpreadElement": return this.dynamic(ctx, node, `\`${node.type}\``, "Send the computed value in the payload.");
1718
+ default: return this.dynamic(ctx, node, `\`${node.type}\``, "Send the computed value in the payload.");
1719
+ }
1720
+ }
1721
+ checkedText(ctx, node, value) {
1722
+ if (/[-]/.test(value)) this.error(ctx, node, "reserved-character", "This text contains a private-use character the compiler reserves.", "Remove the characters U+E000 to U+E003.");
1723
+ return value;
1724
+ }
1725
+ template(ctx, node, arith) {
1726
+ const quasis = children(node, "quasis");
1727
+ const expressions = children(node, "expressions");
1728
+ const parts = [];
1729
+ let ok = true;
1730
+ for (const [index, quasi] of quasis.entries()) {
1731
+ const chunk = templateChunk(quasi);
1732
+ if (chunk !== "") parts.push(this.checkedText(ctx, quasi, chunk));
1733
+ const expression = expressions[index];
1734
+ if (expression !== void 0) {
1735
+ const value = this.value(ctx, expression, arith);
1736
+ if (value === void 0) ok = false;
1737
+ else parts.push(value);
1738
+ }
1739
+ }
1740
+ if (!ok) return void 0;
1741
+ return joinValues(parts);
1742
+ }
1743
+ /** A `fmt.*` call. Any other call is the dynamic expression the profile refuses. */
1744
+ call(ctx, node) {
1745
+ const callee = child(node, "callee");
1746
+ const args = children(node, "arguments");
1747
+ if (callee === void 0) return void 0;
1748
+ const target = unwrap(callee);
1749
+ if (target.type === "MemberExpression" && target.computed !== true) {
1750
+ const object = child(target, "object");
1751
+ const property = child(target, "property");
1752
+ const method = property === void 0 ? void 0 : text(property, "name");
1753
+ if (object !== void 0 && method !== void 0 && this.isFmt(ctx, object)) {
1754
+ if (!FORMATTER_NAMES.includes(method)) {
1755
+ this.error(ctx, node, "unknown-formatter", `\`fmt.${method}\` is not a formatter.`, `Use one of ${FORMATTER_NAMES.map((name) => `fmt.${name}`).join(", ")}.`);
1756
+ return;
1757
+ }
1758
+ const name = method;
1759
+ const [min, max] = FORMATTER_ARITY[name];
1760
+ if (args.length < min || args.length > max) {
1761
+ this.error(ctx, node, "formatter-arguments", `\`fmt.${name}\` takes ${min === max ? min : `${min} to ${max}`} arguments.`, "Check the formatter table in the reference.");
1762
+ return;
1763
+ }
1764
+ const lowered = [];
1765
+ for (const argument of args) {
1766
+ const inner = unwrap(argument);
1767
+ if (inner.type === "ObjectExpression") {
1768
+ const literal = readStatic(this.options.staticContext, ctx.module, inner);
1769
+ if (!literal.ok) {
1770
+ this.error(ctx, inner, "dynamic-expression", "Formatter options are literal.", "Write plain string and number values.");
1771
+ return;
1772
+ }
1773
+ lowered.push({ lit: literal.value });
1774
+ continue;
1775
+ }
1776
+ if (inner.type === "ArrayExpression") {
1777
+ const literal = readStatic(this.options.staticContext, ctx.module, inner);
1778
+ if (literal.ok) {
1779
+ lowered.push({ lit: literal.value });
1780
+ continue;
1781
+ }
1782
+ }
1783
+ const value = this.value(ctx, argument, true);
1784
+ if (value === void 0) return void 0;
1785
+ lowered.push(value);
1786
+ }
1787
+ return {
1788
+ format: name,
1789
+ args: lowered
1790
+ };
1791
+ }
1792
+ if (method !== void 0 && RUN_TIME_METHODS.has(method)) return this.dynamic(ctx, node, `\`.${method}(…)\``, "Send the computed value in the payload and bind it, or format with `fmt.*`.");
1793
+ }
1794
+ if (target.type === "Identifier" && /^use[A-Z]/.test(text(target, "name") ?? "")) {
1795
+ this.error(ctx, node, "hook-call", `\`${text(target, "name")}\` is a hook, and a template has no state, effects or context.`, "Read the input through the function's parameter; send anything else in the payload.");
1796
+ return;
1797
+ }
1798
+ const name = target.type === "Identifier" ? text(target, "name") ?? "" : target.type === "MemberExpression" ? `${jsxLike(child(target, "object"))}.${text(child(target, "property") ?? target, "name") ?? ""}` : "a call";
1799
+ return this.dynamic(ctx, node, `\`${name}(…)\``, "Send the computed value in the payload and bind it, or format with `fmt.money`, `fmt.number`, `fmt.date`, `fmt.time`, `fmt.plural` or `fmt.list`.");
1800
+ }
1801
+ isFmt(ctx, object) {
1802
+ const node = unwrap(object);
1803
+ if (node.type !== "Identifier") return false;
1804
+ const imported = ctx.module.imports.get(text(node, "name") ?? "");
1805
+ return imported?.kind === "markup" && imported.specifier === "@samva/markup/fmt";
1806
+ }
1807
+ predicate(ctx, expression) {
1808
+ const node = unwrap(expression);
1809
+ if (node.type === "LogicalExpression") {
1810
+ const operator = text(node, "operator");
1811
+ const left = child(node, "left");
1812
+ const right = child(node, "right");
1813
+ if (left !== void 0 && right !== void 0 && (operator === "&&" || operator === "||")) {
1814
+ const a = this.predicate(ctx, left);
1815
+ const b = this.predicate(ctx, right);
1816
+ if (a === void 0 || b === void 0) return void 0;
1817
+ if (operator === "&&") {
1818
+ const flat = (predicate) => "and" in predicate ? predicate.and : [predicate];
1819
+ return { and: [...flat(a), ...flat(b)] };
1820
+ }
1821
+ const flat = (predicate) => "or" in predicate ? predicate.or : [predicate];
1822
+ return { or: [...flat(a), ...flat(b)] };
1823
+ }
1824
+ }
1825
+ if (node.type === "UnaryExpression" && text(node, "operator") === "!") {
1826
+ const argument = child(node, "argument");
1827
+ const inner = argument === void 0 ? void 0 : this.predicate(ctx, argument);
1828
+ return inner === void 0 ? void 0 : { not: inner };
1829
+ }
1830
+ if (node.type === "BinaryExpression") {
1831
+ const operator = text(node, "operator") ?? "";
1832
+ const cmp = COMPARISON[operator];
1833
+ const left = child(node, "left");
1834
+ const right = child(node, "right");
1835
+ if (cmp === void 0 || left === void 0 || right === void 0) {
1836
+ this.error(ctx, node, "dynamic-expression", operator === "==" || operator === "!=" ? `\`${operator}\` coerces types at run time.` : `\`${operator}\` is not a comparison the profile reads.`, "Use `===`, `!==`, `<`, `>`, `<=` or `>=`.");
1837
+ return;
1838
+ }
1839
+ const a = this.value(ctx, left, true);
1840
+ const b = this.value(ctx, right, true);
1841
+ return a === void 0 || b === void 0 ? void 0 : {
1842
+ cmp,
1843
+ args: [a, b]
1844
+ };
1845
+ }
1846
+ const value = this.value(ctx, node, true);
1847
+ if (value === void 0) return void 0;
1848
+ if (typeof value === "object" && value !== null && "bind" in value) return { present: value.bind };
1849
+ if (isStatic(value)) return { truthy: value };
1850
+ return { truthy: value };
1851
+ }
1852
+ /** Lower the children of a JSX element or fragment. */
1853
+ jsxChildren(ctx, parent) {
1854
+ const output = [];
1855
+ for (const item of children(parent, "children")) switch (item.type) {
1856
+ case "JSXText": {
1857
+ const cleaned = cleanJsxText(text(item, "value") ?? "");
1858
+ if (cleaned !== "") output.push(this.checkedText(ctx, item, cleaned));
1859
+ break;
1860
+ }
1861
+ case "JSXExpressionContainer": {
1862
+ const expression = child(item, "expression");
1863
+ if (expression === void 0 || expression.type === "JSXEmptyExpression") break;
1864
+ output.push(...this.content(ctx, expression));
1865
+ break;
1866
+ }
1867
+ case "JSXElement":
1868
+ case "JSXFragment":
1869
+ output.push(...this.element(ctx, item));
1870
+ break;
1871
+ case "JSXSpreadChild": this.dynamic(ctx, item, "A spread child", "List the children.");
1872
+ }
1873
+ return output;
1874
+ }
1875
+ /** Lower an expression in content position. */
1876
+ content(ctx, expression) {
1877
+ if (ctx.depth > MAX_DEPTH) {
1878
+ this.error(ctx, expression, "nesting-limit", "The template nests too deeply.", "Flatten it.");
1879
+ return [];
1880
+ }
1881
+ const node = unwrap(expression);
1882
+ switch (node.type) {
1883
+ case "JSXElement":
1884
+ case "JSXFragment": return this.element(ctx, node);
1885
+ case "StringLiteral": {
1886
+ const value = text(node, "value") ?? "";
1887
+ return value === "" ? [] : [this.checkedText(ctx, node, value)];
1888
+ }
1889
+ case "NumericLiteral": return typeof node.value === "number" ? [node.value] : [];
1890
+ case "BooleanLiteral":
1891
+ case "NullLiteral": return [];
1892
+ case "Identifier": {
1893
+ const name = text(node, "name") ?? "";
1894
+ if (name === "undefined") return [];
1895
+ const found = ctx.env.get(name);
1896
+ if (found?.k === "nodes") return [...found.nodes];
1897
+ break;
1898
+ }
1899
+ case "LogicalExpression": {
1900
+ const operator = text(node, "operator");
1901
+ const left = child(node, "left");
1902
+ const right = child(node, "right");
1903
+ if (operator === "&&" && left !== void 0 && right !== void 0) {
1904
+ const test = this.predicate(ctx, left);
1905
+ const then = this.content(ctx, right);
1906
+ if (test === void 0) return [];
1907
+ return [{
1908
+ if: test,
1909
+ then,
1910
+ src: this.src(ctx.module, node)
1911
+ }];
1912
+ }
1913
+ break;
1914
+ }
1915
+ case "ConditionalExpression": {
1916
+ const test = child(node, "test");
1917
+ const consequent = child(node, "consequent");
1918
+ const alternate = child(node, "alternate");
1919
+ if (test === void 0 || consequent === void 0 || alternate === void 0) return [];
1920
+ const predicate = this.predicate(ctx, test);
1921
+ const then = this.content(ctx, consequent);
1922
+ const otherwise = this.content(ctx, alternate);
1923
+ if (predicate === void 0) return [];
1924
+ return [{
1925
+ if: predicate,
1926
+ then,
1927
+ ...otherwise.length === 0 ? {} : { else: otherwise },
1928
+ src: this.src(ctx.module, node)
1929
+ }];
1930
+ }
1931
+ case "CallExpression": {
1932
+ const callee = child(node, "callee");
1933
+ const target = callee === void 0 ? void 0 : unwrap(callee);
1934
+ const property = target === void 0 ? void 0 : child(target, "property");
1935
+ if (target?.type === "MemberExpression" && target.computed !== true && property !== void 0 && text(property, "name") === "map") return this.each(ctx, node, target);
1936
+ break;
1937
+ }
1938
+ }
1939
+ const value = this.value(ctx, node, false);
1940
+ return value === void 0 ? [] : [value];
1941
+ }
1942
+ /** A loop variable name that shadows none in scope. */
1943
+ freshName(ctx, base) {
1944
+ return this.uniqueName(ctx, base);
1945
+ }
1946
+ uniqueName(ctx, base) {
1947
+ if (!ctx.loops.includes(base)) return base;
1948
+ this.uniqueId += 1;
1949
+ return `${base}_${this.uniqueId}`;
1950
+ }
1951
+ each(ctx, call, callee) {
1952
+ const object = child(callee, "object");
1953
+ const callback = children(call, "arguments")[0];
1954
+ if (object === void 0 || callback === void 0) return [];
1955
+ const source = this.filterChain(ctx, object);
1956
+ const list = source === void 0 ? this.value(ctx, object, false) : source.list;
1957
+ const fn = unwrap(callback);
1958
+ if (fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression") {
1959
+ this.dynamic(ctx, callback, "A callback that is not an inline function", "Write `.map((item) => <Row item={item} />)`.");
1960
+ return [];
1961
+ }
1962
+ if (list === void 0) return [];
1963
+ const each = this.eachOver(ctx, call, list, fn, source?.filters ?? []);
1964
+ return each === void 0 ? [] : [each];
1965
+ }
1966
+ /** `list.filter(a).filter(b)`: the list underneath and the callbacks, innermost first. */
1967
+ filterChain(ctx, expression) {
1968
+ const filters = [];
1969
+ let current = unwrap(expression);
1970
+ for (;;) {
1971
+ if (current.type !== "CallExpression") break;
1972
+ const callee = child(current, "callee");
1973
+ const target = callee === void 0 ? void 0 : unwrap(callee);
1974
+ const property = target === void 0 ? void 0 : child(target, "property");
1975
+ if (target?.type !== "MemberExpression" || target.computed === true || property === void 0 || text(property, "name") !== "filter") break;
1976
+ const callback = children(current, "arguments")[0];
1977
+ const object = child(target, "object");
1978
+ if (callback === void 0 || object === void 0) return void 0;
1979
+ filters.unshift(unwrap(callback));
1980
+ current = unwrap(object);
1981
+ }
1982
+ if (filters.length === 0) return void 0;
1983
+ return {
1984
+ list: this.value(ctx, current, false),
1985
+ filters
1986
+ };
1987
+ }
1988
+ /** The condition `.filter(callbacks)` keeps items by, with the item bound as `$as`. */
1989
+ whereClause(ctx, as, filters) {
1990
+ const predicates = [];
1991
+ for (const filter of filters) {
1992
+ if (!/Function/.test(filter.type)) {
1993
+ this.dynamic(ctx, filter, "A `.filter` callback that is not an inline function", "Write `.filter((item) => item.done)`.");
1994
+ return;
1995
+ }
1996
+ const params = children(filter, "params");
1997
+ if (params.length > 1) {
1998
+ this.error(ctx, filter, "dynamic-expression", "A `.filter` callback reads the item; its index and the array are not part of the profile.", "Remove the extra parameters.");
1999
+ return;
2000
+ }
2001
+ const env = new Map(ctx.env);
2002
+ if (params[0] !== void 0) this.bindPattern(ctx, params[0], {
2003
+ k: "path",
2004
+ path: `$${as}`
2005
+ }, env, false);
2006
+ const scoped = {
2007
+ ...ctx,
2008
+ env
2009
+ };
2010
+ const body = this.functionExpression(scoped, filter);
2011
+ const predicate = body === void 0 ? void 0 : this.predicate(scoped, body);
2012
+ if (predicate === void 0) return void 0;
2013
+ predicates.push(predicate);
2014
+ }
2015
+ if (predicates.length === 0) return void 0;
2016
+ return predicates.length === 1 ? predicates[0] : { and: predicates };
2017
+ }
2018
+ /** Lower `list.map(fn)` to a loop, keeping only the items the filters keep. */
2019
+ eachOver(ctx, at, list, fn, filters = [], options = {}) {
2020
+ const params = children(fn, "params");
2021
+ if (params.length > 2) {
2022
+ this.error(ctx, fn, "dynamic-expression", "`.map` callbacks read the item and its index; a third parameter is the whole array.", "Remove the third parameter.");
2023
+ return;
2024
+ }
2025
+ const env = new Map(ctx.env);
2026
+ const loops = [...ctx.loops];
2027
+ let as = "";
2028
+ let index;
2029
+ const itemPattern = params[0];
2030
+ if (itemPattern !== void 0) {
2031
+ const raw = itemPattern.type === "Identifier" ? text(itemPattern, "name") ?? "item" : "item";
2032
+ as = this.uniqueName({
2033
+ ...ctx,
2034
+ loops
2035
+ }, options.name ?? raw);
2036
+ loops.push(as);
2037
+ this.bindPattern({
2038
+ ...ctx,
2039
+ loops
2040
+ }, itemPattern, {
2041
+ k: "path",
2042
+ path: `$${as}`
2043
+ }, env, false);
2044
+ } else {
2045
+ as = this.uniqueName(ctx, options.name ?? "item");
2046
+ loops.push(as);
2047
+ }
2048
+ const indexPattern = params[1];
2049
+ if (indexPattern !== void 0) {
2050
+ if (indexPattern.type !== "Identifier") this.error(ctx, indexPattern, "unsupported-pattern", "The index is a plain name.");
2051
+ else {
2052
+ index = this.uniqueName({
2053
+ ...ctx,
2054
+ loops
2055
+ }, text(indexPattern, "name") ?? "index");
2056
+ loops.push(index);
2057
+ env.set(text(indexPattern, "name") ?? "index", {
2058
+ k: "value",
2059
+ value: { bind: `$${index}` }
2060
+ });
2061
+ }
2062
+ }
2063
+ const where = filters.length === 0 ? void 0 : this.whereClause(ctx, as, filters);
2064
+ if (filters.length > 0 && where === void 0) return void 0;
2065
+ const inner = {
2066
+ ...ctx,
2067
+ env,
2068
+ loops,
2069
+ depth: ctx.depth + 1
2070
+ };
2071
+ const body = this.functionExpression(inner, fn);
2072
+ if (body === void 0) return void 0;
2073
+ const nodes = this.content(inner, body);
2074
+ return {
2075
+ each: list,
2076
+ as,
2077
+ ...index === void 0 ? {} : { index },
2078
+ ...where === void 0 ? {} : { where },
2079
+ ...options.chunk === void 0 ? {} : { chunk: options.chunk },
2080
+ children: nodes,
2081
+ src: this.src(ctx.module, at)
2082
+ };
2083
+ }
2084
+ /** Lower a JSX element or fragment to IR nodes. */
2085
+ element(ctx, node) {
2086
+ if (node.type === "JSXFragment") return this.jsxChildren({
2087
+ ...ctx,
2088
+ depth: ctx.depth + 1
2089
+ }, node);
2090
+ const opening = child(node, "openingElement");
2091
+ const nameNode = opening === void 0 ? void 0 : child(opening, "name");
2092
+ if (opening === void 0 || nameNode === void 0) return [];
2093
+ const name = jsxName$1(nameNode);
2094
+ const inner = {
2095
+ ...ctx,
2096
+ depth: ctx.depth + 1
2097
+ };
2098
+ if (inner.depth > MAX_DEPTH) {
2099
+ this.error(ctx, node, "nesting-limit", "The template nests too deeply.", "Flatten it.");
2100
+ return [];
2101
+ }
2102
+ if (/^[a-z]/.test(name) && !name.includes(".")) return this.intrinsic(inner, node, opening, name);
2103
+ return this.component(inner, node, opening, name);
2104
+ }
2105
+ attributeName(attribute) {
2106
+ const name = child(attribute, "name");
2107
+ return name === void 0 ? "" : jsxName$1(name);
2108
+ }
2109
+ intrinsic(ctx, node, opening, tag) {
2110
+ const spec = emailElement(tag);
2111
+ if (spec === void 0) {
2112
+ const near = didYouMean(tag, Object.keys(EMAIL_ELEMENTS));
2113
+ this.error(ctx, node, "unsupported-element", `<${tag}> is not part of the supported email vocabulary.`, near === void 0 ? `Use one of: ${Object.keys(EMAIL_ELEMENTS).join(", ")}.` : `Did you mean <${near}>? Supported elements: ${Object.keys(EMAIL_ELEMENTS).join(", ")}.`);
2114
+ return [];
2115
+ }
2116
+ const attrs = {};
2117
+ let style;
2118
+ const origin = [this.findings.locate(ctx.module.parsed, node)];
2119
+ for (const attribute of children(opening, "attributes")) {
2120
+ if (attribute.type === "JSXSpreadAttribute") {
2121
+ this.dynamic(ctx, attribute, "A spread attribute", "Write each attribute.");
2122
+ continue;
2123
+ }
2124
+ const name = this.attributeName(attribute);
2125
+ const valueNode = child(attribute, "value");
2126
+ if (name === "key") continue;
2127
+ if (name === "ref" || name === "dangerouslySetInnerHTML" || /^on[A-Z]/.test(name)) {
2128
+ const isHtml = name === "dangerouslySetInnerHTML";
2129
+ this.error(ctx, attribute, isHtml ? "dangerous-html" : name === "ref" ? "unsupported-attribute" : "event-handler", isHtml ? "`dangerouslySetInnerHTML` injects unescaped markup." : name === "ref" ? "A `ref` needs a running component." : `\`${name}\` is an event handler; email clients strip scripting and Samva refuses it.`, isHtml ? "Write the markup as elements." : "Remove it.");
2130
+ continue;
2131
+ }
2132
+ if (RESERVED_FOOTER_ATTRIBUTES.has(name)) {
2133
+ this.error(ctx, attribute, "reserved-attribute", `\`${name}\` is written only by BrandFooter from samva:brand.`, "Render <BrandFooter /> instead of marking an element as the footer.");
2134
+ continue;
2135
+ }
2136
+ if (name === "className" || name === "class") {
2137
+ const value = valueNode === void 0 ? "" : this.classValue(ctx, valueNode);
2138
+ if (value !== void 0 && value !== "") attrs.class = value;
2139
+ continue;
2140
+ }
2141
+ if (name === "style") {
2142
+ style = this.styleAttribute(ctx, valueNode);
2143
+ continue;
2144
+ }
2145
+ let value;
2146
+ if (valueNode === void 0) value = true;
2147
+ else if (valueNode.type === "StringLiteral") value = this.checkedText(ctx, valueNode, text(valueNode, "value") ?? "");
2148
+ else if (valueNode.type === "JSXExpressionContainer") {
2149
+ const expression = child(valueNode, "expression");
2150
+ value = expression === void 0 ? void 0 : this.value(ctx, expression, false);
2151
+ } else {
2152
+ this.error(ctx, valueNode, "dynamic-expression", "An attribute value is text, a number or a binding.");
2153
+ continue;
2154
+ }
2155
+ if (value === void 0) continue;
2156
+ if (value === false || value === null) continue;
2157
+ if (value === "%samva:unsubscribe-url%") {
2158
+ this.error(ctx, attribute, "reserved-url", "That URL is the unsubscribe placeholder only BrandFooter can write.", "Render <BrandFooter /> instead.");
2159
+ continue;
2160
+ }
2161
+ if (isStatic(value)) {
2162
+ const checked = checkAttribute(tag, name, value, origin);
2163
+ if (checked.text === void 0 && checked.diagnostics.length === 0) {
2164
+ const near = didYouMean(name, Object.keys({
2165
+ ...spec.attributes,
2166
+ id: 0,
2167
+ title: 0,
2168
+ lang: 0,
2169
+ dir: 0,
2170
+ role: 0
2171
+ }));
2172
+ this.error(ctx, attribute, "unsupported-attribute", `<${tag}> does not support the ${name} attribute.`, near === void 0 ? `Supported: ${Object.keys(spec.attributes).join(", ") || "none"}, plus id, title, lang, dir, role, class, style, data-* and aria-*.` : `Did you mean \`${near}\`?`);
2173
+ continue;
2174
+ }
2175
+ this.findings.items.push(...checked.diagnostics);
2176
+ }
2177
+ attrs[name] = value;
2178
+ }
2179
+ const kids = spec.content === "void" ? [] : this.jsxChildren(ctx, node);
2180
+ return [{
2181
+ el: tag,
2182
+ ...Object.keys(attrs).length === 0 ? {} : { attrs },
2183
+ ...style === void 0 ? {} : { style },
2184
+ ...kids.length === 0 ? {} : { children: kids },
2185
+ src: this.src(ctx.module, node)
2186
+ }];
2187
+ }
2188
+ /** A class value: literal classes, possibly chosen by a condition. Tailwind cannot see anything else. */
2189
+ classValue(ctx, valueNode) {
2190
+ const node = valueNode.type === "JSXExpressionContainer" ? child(valueNode, "expression") : valueNode;
2191
+ if (node === void 0) return void 0;
2192
+ const value = this.classExpression(ctx, node);
2193
+ if (value === void 0) this.error(ctx, valueNode, "dynamic-class", "This class is built at run time, and Tailwind cannot see class names it does not read.", "Write the classes literally, or choose between literal classes: `input.vip ? \"bg-amber-100\" : \"bg-white\"`.");
2194
+ return value;
2195
+ }
2196
+ classExpression(ctx, expression) {
2197
+ const node = unwrap(expression);
2198
+ switch (node.type) {
2199
+ case "StringLiteral": return text(node, "value") ?? "";
2200
+ case "NullLiteral": return "";
2201
+ case "BooleanLiteral": return "";
2202
+ case "Identifier":
2203
+ case "MemberExpression": {
2204
+ if (node.type === "Identifier" && text(node, "name") === "undefined") return "";
2205
+ const bound = this.binding(ctx, node);
2206
+ return bound?.k === "value" && isClassValue(bound.value) ? bound.value : void 0;
2207
+ }
2208
+ case "TemplateLiteral": {
2209
+ const quasis = children(node, "quasis");
2210
+ const expressions = children(node, "expressions");
2211
+ const parts = [];
2212
+ for (const [index, quasi] of quasis.entries()) {
2213
+ const chunk = templateChunk(quasi);
2214
+ if (chunk !== "") parts.push(chunk);
2215
+ const inner = expressions[index];
2216
+ if (inner !== void 0) {
2217
+ const value = this.classExpression(ctx, inner);
2218
+ if (value === void 0) return void 0;
2219
+ parts.push(value);
2220
+ }
2221
+ }
2222
+ return joinValues(parts);
2223
+ }
2224
+ case "ConditionalExpression": {
2225
+ const test = child(node, "test");
2226
+ const consequent = child(node, "consequent");
2227
+ const alternate = child(node, "alternate");
2228
+ if (test === void 0 || consequent === void 0 || alternate === void 0) return void 0;
2229
+ const predicate = this.predicate(ctx, test);
2230
+ const then = this.classExpression(ctx, consequent);
2231
+ const otherwise = this.classExpression(ctx, alternate);
2232
+ if (predicate === void 0 || then === void 0 || otherwise === void 0) return void 0;
2233
+ return {
2234
+ if: predicate,
2235
+ then,
2236
+ else: otherwise
2237
+ };
2238
+ }
2239
+ case "LogicalExpression": {
2240
+ const left = child(node, "left");
2241
+ const right = child(node, "right");
2242
+ if (text(node, "operator") !== "&&" || left === void 0 || right === void 0) return void 0;
2243
+ const predicate = this.predicate(ctx, left);
2244
+ const then = this.classExpression(ctx, right);
2245
+ if (predicate === void 0 || then === void 0) return void 0;
2246
+ return {
2247
+ if: predicate,
2248
+ then,
2249
+ else: ""
2250
+ };
2251
+ }
2252
+ case "BinaryExpression": {
2253
+ const left = child(node, "left");
2254
+ const right = child(node, "right");
2255
+ if (text(node, "operator") !== "+" || left === void 0 || right === void 0) return void 0;
2256
+ const a = this.classExpression(ctx, left);
2257
+ const b = this.classExpression(ctx, right);
2258
+ return a === void 0 || b === void 0 ? void 0 : joinValues([a, b]);
2259
+ }
2260
+ default: return;
2261
+ }
2262
+ }
2263
+ styleAttribute(ctx, valueNode) {
2264
+ const expression = valueNode === void 0 ? void 0 : child(valueNode, "expression");
2265
+ const object = expression === void 0 ? void 0 : unwrap(expression);
2266
+ if (object === void 0 || object.type !== "ObjectExpression") {
2267
+ if (valueNode !== void 0) this.error(ctx, valueNode, "invalid-style", "`style` is an object literal of CSS properties.", "Write style={{ backgroundColor: \"#fff\" }}, or use Tailwind classes.");
2268
+ return;
2269
+ }
2270
+ return this.styleObject(ctx, object);
2271
+ }
2272
+ /** A `style` object literal as declarations whose values may be read at render time. */
2273
+ styleObject(ctx, object) {
2274
+ const entries = [];
2275
+ for (const property of children(object, "properties")) {
2276
+ if (property.type !== "ObjectProperty" || property.computed === true) {
2277
+ this.dynamic(ctx, property, "A spread or computed style entry", "Write each property.");
2278
+ continue;
2279
+ }
2280
+ const key = child(property, "key");
2281
+ const value = child(property, "value");
2282
+ const name = keyName(key);
2283
+ if (name === void 0 || value === void 0) continue;
2284
+ const lowered = this.value(ctx, value, false);
2285
+ if (lowered === void 0) continue;
2286
+ if (lowered === null) continue;
2287
+ entries.push([name, lowered]);
2288
+ }
2289
+ return entries;
2290
+ }
2291
+ component(ctx, node, opening, name) {
2292
+ const head = name.split(".")[0] ?? name;
2293
+ const imported = ctx.module.imports.get(head);
2294
+ const local = ctx.module.declarations.get(head);
2295
+ const attributes = children(opening, "attributes");
2296
+ if (imported?.kind === "markup" || imported?.kind === "brand") return runBuiltinComponent({
2297
+ lowerer: this,
2298
+ ctx,
2299
+ node,
2300
+ attributes,
2301
+ name,
2302
+ binding: imported,
2303
+ brand: this.options.brand,
2304
+ channel: this.options.channel
2305
+ });
2306
+ let fn;
2307
+ if (imported?.kind === "project") {
2308
+ const target = this.options.project.modules.get(imported.path);
2309
+ if (target !== void 0) fn = this.exportedFunction(target, imported.imported, /* @__PURE__ */ new Set());
2310
+ } else if (local !== void 0) fn = {
2311
+ module: ctx.module,
2312
+ value: local.value
2313
+ };
2314
+ if (fn === void 0 || !/Function|Arrow/.test(fn.value.type)) {
2315
+ this.error(ctx, node, "unknown-component", `<${name}> is not a component the profile can read.`, "Import it from @samva/markup/email, or from a project file that exports it as a function.");
2316
+ return [];
2317
+ }
2318
+ return this.partial(ctx, node, attributes, name, fn.module, fn.value);
2319
+ }
2320
+ exportedFunction(module, exported, seen) {
2321
+ const key = `${module.path}\0${exported}`;
2322
+ if (seen.has(key)) return void 0;
2323
+ seen.add(key);
2324
+ if (exported === "default") {
2325
+ const value = module.defaultExport;
2326
+ if (value === void 0) return void 0;
2327
+ if (value.type === "Identifier") {
2328
+ const declared = module.declarations.get(text(value, "name") ?? "");
2329
+ return declared === void 0 ? void 0 : {
2330
+ module,
2331
+ value: declared.value
2332
+ };
2333
+ }
2334
+ return {
2335
+ module,
2336
+ value
2337
+ };
2338
+ }
2339
+ const local = module.exports.get(exported);
2340
+ if (local === void 0) return void 0;
2341
+ const declared = module.declarations.get(local);
2342
+ if (declared !== void 0) return {
2343
+ module,
2344
+ value: declared.value
2345
+ };
2346
+ const imported = module.imports.get(local);
2347
+ if (imported?.kind === "project") {
2348
+ const target = this.options.project.modules.get(imported.path);
2349
+ return target === void 0 ? void 0 : this.exportedFunction(target, imported.imported, seen);
2350
+ }
2351
+ }
2352
+ /** Bindings for the attributes written on a component tag. */
2353
+ attributeBindings(ctx, attributes) {
2354
+ const props = /* @__PURE__ */ new Map();
2355
+ let ok = true;
2356
+ for (const attribute of attributes) {
2357
+ if (attribute.type === "JSXSpreadAttribute") {
2358
+ this.dynamic(ctx, attribute, "A spread attribute", "Write each prop.");
2359
+ ok = false;
2360
+ continue;
2361
+ }
2362
+ const name = this.attributeName(attribute);
2363
+ if (name === "key") continue;
2364
+ const valueNode = child(attribute, "value");
2365
+ if (valueNode === void 0) {
2366
+ props.set(name, {
2367
+ k: "value",
2368
+ value: true
2369
+ });
2370
+ continue;
2371
+ }
2372
+ if (valueNode.type === "StringLiteral") {
2373
+ props.set(name, {
2374
+ k: "value",
2375
+ value: this.checkedText(ctx, valueNode, text(valueNode, "value") ?? "")
2376
+ });
2377
+ continue;
2378
+ }
2379
+ const expression = valueNode.type === "JSXExpressionContainer" ? child(valueNode, "expression") : void 0;
2380
+ if (expression === void 0) {
2381
+ this.error(ctx, valueNode, "dynamic-expression", "A prop value is text, a number, a binding or an object.");
2382
+ ok = false;
2383
+ continue;
2384
+ }
2385
+ const bound = this.propBinding(ctx, expression);
2386
+ if (bound === void 0) ok = false;
2387
+ else props.set(name, bound);
2388
+ }
2389
+ return ok ? props : void 0;
2390
+ }
2391
+ propBinding(ctx, expression) {
2392
+ const node = unwrap(expression);
2393
+ if (node.type === "ObjectExpression") {
2394
+ const fields = /* @__PURE__ */ new Map();
2395
+ for (const property of children(node, "properties")) {
2396
+ if (property.type !== "ObjectProperty" || property.computed === true) {
2397
+ this.dynamic(ctx, property, "A spread or computed entry", "Write each field.");
2398
+ return;
2399
+ }
2400
+ const key = child(property, "key");
2401
+ const value = child(property, "value");
2402
+ const name = key === void 0 ? void 0 : key.type === "Identifier" ? text(key, "name") : text(key, "value");
2403
+ if (name === void 0 || value === void 0) return void 0;
2404
+ const bound = this.propBinding(ctx, value);
2405
+ if (bound === void 0) return void 0;
2406
+ fields.set(name, bound);
2407
+ }
2408
+ return {
2409
+ k: "object",
2410
+ fields
2411
+ };
2412
+ }
2413
+ if (node.type === "Identifier" || node.type === "MemberExpression") return this.binding(ctx, node);
2414
+ if (node.type === "JSXElement" || node.type === "JSXFragment") return {
2415
+ k: "nodes",
2416
+ nodes: this.element(ctx, node)
2417
+ };
2418
+ const value = this.value(ctx, node, false);
2419
+ return value === void 0 ? void 0 : {
2420
+ k: "value",
2421
+ value
2422
+ };
2423
+ }
2424
+ partial(ctx, node, attributes, name, module, fn) {
2425
+ const id = `${module.path}#${fn.start}`;
2426
+ if (ctx.stack.includes(id)) {
2427
+ this.error(ctx, node, "recursive-partial", `<${name}> renders itself, and the compiler inlines partials.`, "Flatten the recursion, or repeat the structure with `.map`.");
2428
+ return [];
2429
+ }
2430
+ const props = this.attributeBindings(ctx, attributes);
2431
+ if (props === void 0) return [];
2432
+ const kids = this.jsxChildren(ctx, node);
2433
+ if (kids.length > 0) props.set("children", {
2434
+ k: "nodes",
2435
+ nodes: kids
2436
+ });
2437
+ const inner = {
2438
+ module,
2439
+ env: /* @__PURE__ */ new Map(),
2440
+ stack: [...ctx.stack, id],
2441
+ loops: ctx.loops,
2442
+ depth: ctx.depth + 1
2443
+ };
2444
+ const env = /* @__PURE__ */ new Map();
2445
+ const param = children(fn, "params")[0];
2446
+ if (param !== void 0) {
2447
+ const bag = {
2448
+ k: "object",
2449
+ fields: props
2450
+ };
2451
+ if (param.type === "Identifier") env.set(text(param, "name") ?? "props", bag);
2452
+ else this.bindPattern(inner, param, bag, env, true);
2453
+ }
2454
+ const scoped = {
2455
+ ...inner,
2456
+ env
2457
+ };
2458
+ const body = this.functionExpression(scoped, fn);
2459
+ return body === void 0 ? [] : this.content(scoped, body);
2460
+ }
2461
+ };
2462
+ const jsxLike = (node) => node === void 0 ? "" : node.type === "Identifier" ? text(node, "name") ?? "" : "…";
2463
+ /** A value made only of literal class lists and choices between them. */
2464
+ const isClassValue = (value) => {
2465
+ if (typeof value === "string") return true;
2466
+ if (typeof value !== "object" || value === null) return false;
2467
+ if ("if" in value) return isClassValue(value.then) && isClassValue(value.else);
2468
+ if ("concat" in value) return value.concat.every(isClassValue);
2469
+ return false;
2470
+ };
2471
+ /** The cooked text of one template-string segment. */
2472
+ const templateChunk = (quasi) => {
2473
+ const value = quasi.value;
2474
+ if (typeof value !== "object" || value === null) return "";
2475
+ const cooked = value.cooked;
2476
+ const raw = value.raw;
2477
+ return typeof cooked === "string" ? cooked : typeof raw === "string" ? raw : "";
2478
+ };
2479
+ /** Concatenate values, merging adjacent literals. A lone part stays itself. */
2480
+ const joinValues = (parts) => {
2481
+ const merged = [];
2482
+ for (const part of parts) {
2483
+ const previous = merged[merged.length - 1];
2484
+ if (typeof part === "string" && typeof previous === "string") merged[merged.length - 1] = previous + part;
2485
+ else if (typeof part === "object" && part !== null && "concat" in part) for (const inner of part.concat) {
2486
+ const last = merged[merged.length - 1];
2487
+ if (typeof inner === "string" && typeof last === "string") merged[merged.length - 1] = last + inner;
2488
+ else merged.push(inner);
2489
+ }
2490
+ else merged.push(part);
2491
+ }
2492
+ if (merged.length === 0) return "";
2493
+ if (merged.length === 1) return merged[0];
2494
+ return { concat: merged };
2495
+ };
2496
+ //#endregion
2497
+ //#region src/sml/channels.ts
2498
+ /**
2499
+ * The SMS and WhatsApp roots are read by `channelBodies` from the channel field, never lowered
2500
+ * as content, so a channel component met anywhere else is in the wrong channel.
2501
+ */
2502
+ const channelLowering = () => void 0;
2503
+ const HEADER_LIMIT = 60;
2504
+ const FOOTER_LIMIT = 60;
2505
+ const BODY_LIMIT = 1024;
2506
+ const BUTTON_TEXT_LIMIT = 25;
2507
+ const URL_BUTTON_LIMIT = 2;
2508
+ const PHONE_BUTTON_LIMIT = 1;
2509
+ const QUICK_REPLY_LIMIT = 10;
2510
+ const BUTTON_LIMIT = 10;
2511
+ const jsxName = (name) => {
2512
+ if (name.type === "JSXIdentifier") return text(name, "name") ?? "";
2513
+ if (name.type === "JSXMemberExpression") return `${jsxName(child(name, "object") ?? name)}.${text(child(name, "property") ?? name, "name") ?? ""}`;
2514
+ return "";
2515
+ };
2516
+ const tagOf = (element) => {
2517
+ const opening = child(element, "openingElement");
2518
+ const name = opening === void 0 ? void 0 : child(opening, "name");
2519
+ return name === void 0 ? "" : jsxName(name);
2520
+ };
2521
+ const attributesOf = (element) => {
2522
+ const opening = child(element, "openingElement");
2523
+ return opening === void 0 ? [] : children(opening, "attributes");
2524
+ };
2525
+ /** The root element of a channel body, when it is the channel's own component. */
2526
+ const readRoot = (input, field, imported, specifier) => {
2527
+ const opened = input.openFunction(field);
2528
+ if (opened === void 0) return void 0;
2529
+ const { ctx, body } = opened;
2530
+ const node = unwrap(body);
2531
+ const tag = node.type === "JSXElement" ? tagOf(node) : "";
2532
+ const binding = ctx.module.imports.get(tag);
2533
+ if (node.type !== "JSXElement" || binding?.kind !== "markup" || binding.specifier !== specifier || binding.imported !== imported) {
2534
+ input.lowerer.error(ctx, node, "wrong-channel", `The ${imported === "Sms" ? "sms" : "whatsapp"} channel returns one <${imported}> element imported from ${specifier}.`, `Write \`import { ${imported} } from "${specifier}"\` and return <${imported}>…</${imported}>.`);
2535
+ return;
2536
+ }
2537
+ return {
2538
+ ctx,
2539
+ node,
2540
+ local: tag
2541
+ };
2542
+ };
2543
+ /** The string-literal attributes of a channel root; anything else is reported. */
2544
+ const readAttributes = (input, root, allowed) => {
2545
+ const values = /* @__PURE__ */ new Map();
2546
+ for (const attribute of attributesOf(root.node)) {
2547
+ if (attribute.type === "JSXSpreadAttribute") {
2548
+ input.lowerer.error(root.ctx, attribute, "dynamic-expression", "A spread attribute runs code, and templates render without running code.", "Write each attribute as a string literal.");
2549
+ continue;
2550
+ }
2551
+ const nameNode = child(attribute, "name");
2552
+ const name = nameNode === void 0 ? "" : jsxName(nameNode);
2553
+ if (name === "key") continue;
2554
+ if (!allowed.includes(name)) {
2555
+ input.lowerer.error(root.ctx, attribute, "unknown-attribute", `\`${name}\` is not an attribute of this component.`, `The attributes are ${allowed.join(", ")}.`);
2556
+ continue;
2557
+ }
2558
+ const value = child(attribute, "value");
2559
+ const literal = value?.type === "JSXExpressionContainer" ? child(value, "expression") : value;
2560
+ if (literal?.type !== "StringLiteral") {
2561
+ input.lowerer.error(root.ctx, attribute, "dynamic-expression", `\`${name}\` is fixed when the template is compiled, so it is a string literal.`, `Write ${name}="…".`);
2562
+ continue;
2563
+ }
2564
+ values.set(name, text(literal, "value") ?? "");
2565
+ }
2566
+ return values;
2567
+ };
2568
+ const oneOf = (input, root, node, name, value, allowed) => {
2569
+ if (allowed.includes(value)) return true;
2570
+ input.lowerer.error(root.ctx, node, "invalid-attribute", `\`${value}\` is not a ${name} this channel accepts.`, `Use one of ${allowed.join(", ")}.`);
2571
+ return false;
2572
+ };
2573
+ const valueOnly = (nodes) => nodes.every((node) => isValueNode(node));
2574
+ const containsElement = (nodes) => nodes.some((node) => {
2575
+ if (isValueNode(node)) return false;
2576
+ if ("el" in node || "raw" in node) return true;
2577
+ if ("if" in node) return containsElement(node.then) || containsElement(node.else ?? []);
2578
+ if ("each" in node) return containsElement(node.children);
2579
+ return false;
2580
+ });
2581
+ const smsChannel = (input, field) => {
2582
+ const root = readRoot(input, field, "Sms", "@samva/markup/sms");
2583
+ if (root === void 0) return void 0;
2584
+ const attributes = readAttributes(input, root, SMS_ATTRIBUTES);
2585
+ const categoryNode = attributesOf(root.node).find((attribute) => text(child(attribute, "name") ?? attribute, "name") === "category");
2586
+ const category = attributes.get("category");
2587
+ if (category !== void 0 && categoryNode !== void 0 && !oneOf(input, root, categoryNode, "category", category, SMS_CATEGORIES)) return void 0;
2588
+ const body = input.lowerer.jsxChildren(root.ctx, root.node);
2589
+ if (containsElement(body)) input.lowerer.error(root.ctx, root.node, "sms-markup", "An SMS body is plain text, and this one contains an element.", "Write the message as text and bindings; use a newline in the text for a line break.");
2590
+ if (input.schema !== void 0) input.checks.push(...checkBindings(input.schema, body, []));
2591
+ return {
2592
+ ...category === void 0 ? {} : { category },
2593
+ body
2594
+ };
2595
+ };
2596
+ const staticLength = (value) => {
2597
+ if (typeof value === "string") return value.length;
2598
+ if (typeof value === "number") return String(value).length;
2599
+ if (typeof value === "object" && value !== null && "concat" in value) return value.concat.reduce((total, part) => total + staticLength(part), 0);
2600
+ return 0;
2601
+ };
2602
+ const ZERO = {
2603
+ url: 0,
2604
+ phone: 0,
2605
+ quickReply: 0,
2606
+ total: 0
2607
+ };
2608
+ /** The most buttons of each kind any combination of the conditions can produce. */
2609
+ const maximalCounts = (nodes) => {
2610
+ let counts = ZERO;
2611
+ for (const node of nodes) {
2612
+ let add;
2613
+ if ("if" in node) {
2614
+ const then = maximalCounts(node.then);
2615
+ const otherwise = maximalCounts(node.else ?? []);
2616
+ add = {
2617
+ url: Math.max(then.url, otherwise.url),
2618
+ phone: Math.max(then.phone, otherwise.phone),
2619
+ quickReply: Math.max(then.quickReply, otherwise.quickReply),
2620
+ total: Math.max(then.total, otherwise.total)
2621
+ };
2622
+ } else add = {
2623
+ url: node.type === "url" ? 1 : 0,
2624
+ phone: node.type === "phone" ? 1 : 0,
2625
+ quickReply: node.type === "quick-reply" ? 1 : 0,
2626
+ total: 1
2627
+ };
2628
+ counts = {
2629
+ url: counts.url + add.url,
2630
+ phone: counts.phone + add.phone,
2631
+ quickReply: counts.quickReply + add.quickReply,
2632
+ total: counts.total + add.total
2633
+ };
2634
+ }
2635
+ return counts;
2636
+ };
2637
+ const buttonNodes = (buttons) => buttons.flatMap((button) => {
2638
+ if ("if" in button) return [{
2639
+ if: button.if,
2640
+ then: buttonNodes(button.then),
2641
+ ...button.else === void 0 ? {} : { else: buttonNodes(button.else) },
2642
+ ...button.src === void 0 ? {} : { src: button.src }
2643
+ }];
2644
+ switch (button.type) {
2645
+ case "quick-reply": return [button.text];
2646
+ case "url": return [button.text, button.url];
2647
+ case "phone": return [button.text, button.phone];
2648
+ case "copy-code": return [button.code];
2649
+ }
2650
+ });
2651
+ const whatsappChannel = (input, field) => {
2652
+ const root = readRoot(input, field, "WhatsApp", "@samva/markup/whatsapp");
2653
+ if (root === void 0) return void 0;
2654
+ const { lowerer } = input;
2655
+ const { ctx } = root;
2656
+ const attributes = readAttributes(input, root, WHATSAPP_ATTRIBUTES);
2657
+ for (const required of ["category", "language"]) if (!attributes.has(required)) lowerer.error(ctx, root.node, "missing-attribute", `<WhatsApp> needs \`${required}\`.`, required === "category" ? `Write category="utility" (${WHATSAPP_CATEGORIES.join(", ")}).` : "Write language=\"en_US\".");
2658
+ const category = attributes.get("category");
2659
+ const categoryNode = attributesOf(root.node).find((attribute) => text(child(attribute, "name") ?? attribute, "name") === "category");
2660
+ if (category !== void 0 && categoryNode !== void 0 && !oneOf(input, root, categoryNode, "category", category, WHATSAPP_CATEGORIES)) return void 0;
2661
+ const parts = /* @__PURE__ */ new Map();
2662
+ for (const item of children(root.node, "children")) {
2663
+ if (item.type === "JSXText") {
2664
+ if ((text(item, "value") ?? "").trim() !== "") lowerer.error(ctx, item, "whatsapp-structure", "Text sits inside Header, Body, Footer or Buttons, not directly in <WhatsApp>.", "Move it into <WhatsApp.Body>.");
2665
+ continue;
2666
+ }
2667
+ const tag = item.type === "JSXElement" ? tagOf(item) : "";
2668
+ const part = tag.startsWith(`${root.local}.`) ? tag.slice(root.local.length + 1) : "";
2669
+ if (![
2670
+ "Header",
2671
+ "Body",
2672
+ "Footer",
2673
+ "Buttons"
2674
+ ].includes(part)) {
2675
+ lowerer.error(ctx, item, "whatsapp-structure", "<WhatsApp> holds <WhatsApp.Header>, <WhatsApp.Body>, <WhatsApp.Footer> and <WhatsApp.Buttons>.", "Wrap the content in one of them.");
2676
+ continue;
2677
+ }
2678
+ if (parts.has(part)) {
2679
+ lowerer.error(ctx, item, "whatsapp-structure", `<WhatsApp.${part}> appears twice.`, "A WhatsApp template has at most one of each part.");
2680
+ continue;
2681
+ }
2682
+ parts.set(part, item);
2683
+ }
2684
+ const roots = [];
2685
+ const lengthWarning = (part, node, length, limit) => {
2686
+ if (length > limit) lowerer.findings.add(ctx.module.parsed, node, "whatsapp-length", `The ${part} is at least ${length} characters, and Meta allows ${limit}.`, `Shorten the ${part}.`, "warning");
2687
+ };
2688
+ /** A text part: literal text and values, no branching. */
2689
+ const textPart = (name, node, limit) => {
2690
+ const nodes = lowerer.jsxChildren(ctx, node);
2691
+ if (!valueOnly(nodes)) {
2692
+ lowerer.error(ctx, node, "whatsapp-conditional", `A WhatsApp ${name} cannot branch or contain elements: Meta registers its text once.`, "Choose the wording as a parameter in the payload, or write separate templates.");
2693
+ return;
2694
+ }
2695
+ lengthWarning(name, node, nodes.reduce((total, part) => total + (isValueNode(part) ? staticLength(part) : 0), 0), limit);
2696
+ roots.push(...nodes);
2697
+ return nodes;
2698
+ };
2699
+ let header;
2700
+ const headerNode = parts.get("Header");
2701
+ if (headerNode !== void 0) {
2702
+ const typeAttribute = attributesOf(headerNode).find((attribute) => text(child(attribute, "name") ?? attribute, "name") === "type");
2703
+ const literal = typeAttribute === void 0 ? void 0 : child(typeAttribute, "value");
2704
+ const raw = literal?.type === "JSXExpressionContainer" ? child(literal, "expression") : literal;
2705
+ const typeValue = raw?.type === "StringLiteral" ? text(raw, "value") ?? "text" : "text";
2706
+ if (typeAttribute !== void 0 && raw?.type !== "StringLiteral") lowerer.error(ctx, typeAttribute, "dynamic-expression", "`type` is fixed when the template is compiled, so it is a string literal.", "Write type=\"image\".");
2707
+ else if (typeAttribute !== void 0 && !WHATSAPP_HEADER_TYPES.includes(typeValue)) lowerer.error(ctx, typeAttribute, "invalid-attribute", `\`${typeValue}\` is not a header type.`, `Use one of ${WHATSAPP_HEADER_TYPES.join(", ")}.`);
2708
+ else {
2709
+ const type = typeValue;
2710
+ const content = textPart("header", headerNode, type === "text" ? HEADER_LIMIT : Infinity);
2711
+ if (content !== void 0) {
2712
+ if (type === "text") {
2713
+ if (whatsappTemplate(content).params.length > 1) lowerer.error(ctx, headerNode, "whatsapp-header-variables", "A WhatsApp header text takes at most one variable.", "Keep one binding in the header and move the rest to the body.");
2714
+ } else if (content.length !== 1) lowerer.error(ctx, headerNode, "whatsapp-header-media", `A ${type} header is one URL.`, "Write the media URL as the only child: a literal or one binding.");
2715
+ header = {
2716
+ type,
2717
+ content
2718
+ };
2719
+ }
2720
+ }
2721
+ }
2722
+ let body = [];
2723
+ const bodyNode = parts.get("Body");
2724
+ if (bodyNode === void 0) lowerer.error(ctx, root.node, "whatsapp-body-required", "A WhatsApp template needs a body.", "Add <WhatsApp.Body>…</WhatsApp.Body>.");
2725
+ else {
2726
+ const lowered = textPart("body", bodyNode, BODY_LIMIT);
2727
+ body = lowered ?? [];
2728
+ if (lowered !== void 0 && lowered.length === 0) lowerer.error(ctx, bodyNode, "whatsapp-body-required", "The WhatsApp body is empty.", "Write the message text.");
2729
+ }
2730
+ const footerNode = parts.get("Footer");
2731
+ const footer = footerNode === void 0 ? void 0 : textPart("footer", footerNode, FOOTER_LIMIT);
2732
+ const buttonPart = parts.get("Buttons");
2733
+ const buttons = buttonPart === void 0 ? void 0 : buttonList(input, ctx, root.local, buttonPart);
2734
+ if (buttons !== void 0) {
2735
+ const counts = maximalCounts(buttons);
2736
+ const over = (kind, count, limit) => {
2737
+ if (count > limit) lowerer.error(ctx, buttonPart, "whatsapp-button-limit", `A WhatsApp template allows ${limit} ${kind}, and these conditions can produce ${count}.`, `Remove ${count - limit}, or split the template.`);
2738
+ };
2739
+ over("URL buttons", counts.url, URL_BUTTON_LIMIT);
2740
+ over("phone buttons", counts.phone, PHONE_BUTTON_LIMIT);
2741
+ over("quick-reply buttons", counts.quickReply, QUICK_REPLY_LIMIT);
2742
+ over("buttons in all", counts.total, BUTTON_LIMIT);
2743
+ roots.push(...buttonNodes(buttons));
2744
+ }
2745
+ if (input.schema !== void 0) input.checks.push(...checkBindings(input.schema, roots, []));
2746
+ if (category === void 0 || !attributes.has("language")) return void 0;
2747
+ const name = attributes.get("name");
2748
+ return {
2749
+ ...name === void 0 ? {} : { name },
2750
+ category,
2751
+ language: attributes.get("language"),
2752
+ ...header === void 0 ? {} : { header },
2753
+ body,
2754
+ ...footer === void 0 ? {} : { footer },
2755
+ ...buttons === void 0 ? {} : { buttons }
2756
+ };
2757
+ };
2758
+ const BUTTON_TAGS = [
2759
+ "UrlButton",
2760
+ "QuickReplyButton",
2761
+ "PhoneButton",
2762
+ "CopyCodeButton"
2763
+ ];
2764
+ const buttonList = (input, ctx, local, parent) => {
2765
+ const { lowerer } = input;
2766
+ const label = (element, what) => {
2767
+ const nodes = lowerer.jsxChildren(ctx, element);
2768
+ if (!valueOnly(nodes) || nodes.length === 0) {
2769
+ lowerer.error(ctx, element, nodes.length === 0 ? "whatsapp-button-text" : "whatsapp-conditional", nodes.length === 0 ? `A button needs its ${what}.` : `A button ${what} cannot branch or contain elements.`, `Write the ${what} as text between the tags.`);
2770
+ return;
2771
+ }
2772
+ const value = joinValues(nodes);
2773
+ if (what === "label" && staticLength(value) > BUTTON_TEXT_LIMIT) lowerer.findings.add(ctx.module.parsed, element, "whatsapp-length", `The button label is at least ${staticLength(value)} characters, and Meta allows ${BUTTON_TEXT_LIMIT}.`, "Shorten the label.", "warning");
2774
+ return value;
2775
+ };
2776
+ const attribute = (element, name) => {
2777
+ const bindings = lowerer.attributeBindings(ctx, attributesOf(element));
2778
+ if (bindings === void 0) return void 0;
2779
+ const known = attributesOf(element).map((item) => text(child(item, "name") ?? item, "name"));
2780
+ for (const item of known) if (item !== void 0 && item !== name && item !== "key") lowerer.error(ctx, element, "unknown-attribute", `\`${item}\` is not an attribute of this button.`, name === void 0 ? "Remove it." : `The attribute is ${name}.`);
2781
+ if (name === void 0) return void 0;
2782
+ const bound = bindings.get(name);
2783
+ const value = bound === void 0 ? void 0 : bindingValue(bound);
2784
+ if (value === void 0) lowerer.error(ctx, element, "missing-attribute", `This button needs \`${name}\`.`, `Write ${name}="…" or ${name}={input.field}.`);
2785
+ return value;
2786
+ };
2787
+ const single = (element) => {
2788
+ const tag = tagOf(element);
2789
+ switch (tag.startsWith(`${local}.`) ? tag.slice(local.length + 1) : "") {
2790
+ case "UrlButton": {
2791
+ const url = attribute(element, "url");
2792
+ const label_ = label(element, "label");
2793
+ return url === void 0 || label_ === void 0 ? void 0 : {
2794
+ type: "url",
2795
+ text: label_,
2796
+ url
2797
+ };
2798
+ }
2799
+ case "PhoneButton": {
2800
+ const phone = attribute(element, "phone");
2801
+ const label_ = label(element, "label");
2802
+ return phone === void 0 || label_ === void 0 ? void 0 : {
2803
+ type: "phone",
2804
+ text: label_,
2805
+ phone
2806
+ };
2807
+ }
2808
+ case "QuickReplyButton": {
2809
+ attribute(element, void 0);
2810
+ const label_ = label(element, "label");
2811
+ return label_ === void 0 ? void 0 : {
2812
+ type: "quick-reply",
2813
+ text: label_
2814
+ };
2815
+ }
2816
+ case "CopyCodeButton": {
2817
+ const code = label(element, "code");
2818
+ return code === void 0 ? void 0 : {
2819
+ type: "copy-code",
2820
+ code
2821
+ };
2822
+ }
2823
+ default:
2824
+ lowerer.error(ctx, element, "whatsapp-structure", `<WhatsApp.Buttons> holds ${BUTTON_TAGS.map((name) => `<WhatsApp.${name}>`).join(", ")}.`, "Use one of the button components.");
2825
+ return;
2826
+ }
2827
+ };
2828
+ const expression = (node) => {
2829
+ const inner = unwrap(node);
2830
+ switch (inner.type) {
2831
+ case "JSXElement": {
2832
+ const button = single(inner);
2833
+ return button === void 0 ? [] : [button];
2834
+ }
2835
+ case "JSXFragment": return fragment(inner);
2836
+ case "NullLiteral":
2837
+ case "BooleanLiteral": return [];
2838
+ case "LogicalExpression": {
2839
+ const left = child(inner, "left");
2840
+ const right = child(inner, "right");
2841
+ if (text(inner, "operator") === "&&" && left !== void 0 && right !== void 0) {
2842
+ const test = lowerer.predicate(ctx, left);
2843
+ const then = expression(right);
2844
+ return test === void 0 ? [] : [{
2845
+ if: test,
2846
+ then,
2847
+ src: lowerer.src(ctx.module, inner)
2848
+ }];
2849
+ }
2850
+ break;
2851
+ }
2852
+ case "ConditionalExpression": {
2853
+ const test = child(inner, "test");
2854
+ const consequent = child(inner, "consequent");
2855
+ const alternate = child(inner, "alternate");
2856
+ if (test === void 0 || consequent === void 0 || alternate === void 0) return [];
2857
+ const predicate = lowerer.predicate(ctx, test);
2858
+ const then = expression(consequent);
2859
+ const otherwise = expression(alternate);
2860
+ return predicate === void 0 ? [] : [{
2861
+ if: predicate,
2862
+ then,
2863
+ ...otherwise.length === 0 ? {} : { else: otherwise },
2864
+ src: lowerer.src(ctx.module, inner)
2865
+ }];
2866
+ }
2867
+ }
2868
+ lowerer.error(ctx, inner, "dynamic-expression", "Buttons are fixed by the template, and this expression builds them at render time.", "List each button; wrap one in `condition && <WhatsApp.UrlButton … />` to make it optional.");
2869
+ return [];
2870
+ };
2871
+ const fragment = (node) => {
2872
+ const output = [];
2873
+ for (const item of children(node, "children")) if (item.type === "JSXText") {
2874
+ if ((text(item, "value") ?? "").trim() !== "") lowerer.error(ctx, item, "whatsapp-structure", "Text cannot sit between buttons.", "Put the label inside the button.");
2875
+ } else if (item.type === "JSXExpressionContainer") {
2876
+ const inner = child(item, "expression");
2877
+ if (inner !== void 0 && inner.type !== "JSXEmptyExpression") output.push(...expression(inner));
2878
+ } else output.push(...expression(item));
2879
+ return output;
2880
+ };
2881
+ return fragment(parent);
2882
+ };
2883
+ const channelBodies = (input) => {
2884
+ const smsField = input.fields.get("sms");
2885
+ const whatsappField = input.fields.get("whatsapp");
2886
+ const sms = smsField === void 0 ? void 0 : smsChannel(input, smsField);
2887
+ const whatsapp = whatsappField === void 0 ? void 0 : whatsappChannel(input, whatsappField);
2888
+ return {
2889
+ ...sms === void 0 ? {} : { sms },
2890
+ ...whatsapp === void 0 ? {} : { whatsapp }
2891
+ };
2892
+ };
2893
+ //#endregion
2894
+ //#region src/sml/styles.ts
2895
+ const MAX_CLASS_VARIANTS = 8;
2896
+ const product = (left, right) => {
2897
+ if ("leaf" in left) {
2898
+ const prefix = left.leaf;
2899
+ const prepend = (tree) => "leaf" in tree ? { leaf: prefix + tree.leaf } : {
2900
+ pred: tree.pred,
2901
+ then: prepend(tree.then),
2902
+ else: prepend(tree.else)
2903
+ };
2904
+ return prepend(right);
2905
+ }
2906
+ return {
2907
+ pred: left.pred,
2908
+ then: product(left.then, right),
2909
+ else: product(left.else, right)
2910
+ };
2911
+ };
2912
+ const classTree = (value) => {
2913
+ if (typeof value === "string") return { leaf: value };
2914
+ if (typeof value !== "object" || value === null) return void 0;
2915
+ if ("if" in value) {
2916
+ const then = classTree(value.then);
2917
+ const otherwise = classTree(value.else);
2918
+ return then === void 0 || otherwise === void 0 ? void 0 : {
2919
+ pred: value.if,
2920
+ then,
2921
+ else: otherwise
2922
+ };
2923
+ }
2924
+ if ("concat" in value) {
2925
+ let tree = { leaf: "" };
2926
+ for (const part of value.concat) {
2927
+ const next = classTree(part);
2928
+ if (next === void 0) return void 0;
2929
+ tree = product(tree, next);
2930
+ }
2931
+ return tree;
2932
+ }
2933
+ };
2934
+ const leavesOf = (tree) => "leaf" in tree ? [tree.leaf] : [...leavesOf(tree.then), ...leavesOf(tree.else)];
2935
+ const mapTree = (tree, leafStyle, counter = { next: 0 }) => {
2936
+ if ("leaf" in tree) return leafStyle(counter.next++);
2937
+ const then = mapTree(tree.then, leafStyle, counter);
2938
+ const otherwise = mapTree(tree.else, leafStyle, counter);
2939
+ return JSON.stringify(then) === JSON.stringify(otherwise) ? then : {
2940
+ if: tree.pred,
2941
+ then,
2942
+ else: otherwise
2943
+ };
2944
+ };
2945
+ const collect = (nodes, into) => {
2946
+ for (const node of nodes) {
2947
+ if (isValueNode(node)) continue;
2948
+ if ("el" in node) {
2949
+ into.push({
2950
+ kind: "el",
2951
+ el: node
2952
+ });
2953
+ collect(node.children ?? [], into);
2954
+ } else if ("if" in node) {
2955
+ collect(node.then, into);
2956
+ collect(node.else ?? [], into);
2957
+ } else if ("each" in node) collect(node.children, into);
2958
+ else if ("raw" in node) into.push({
2959
+ kind: "raw",
2960
+ raw: node
2961
+ });
2962
+ }
2963
+ };
2964
+ const rawHtml = (tokens, parts) => parts.map((part) => typeof part === "string" ? part : tokens.value("attr" in part ? part.attr : part.text)).join("");
2965
+ /** Resolve the sheet against a lowered tree and write each element's inline style. */
2966
+ const applyStyles = (root, sheet, options) => {
2967
+ const items = [];
2968
+ collect([root], items);
2969
+ const tokens = new Tokens();
2970
+ const trees = items.map((item) => {
2971
+ if (item.kind !== "el") return void 0;
2972
+ const value = item.el.attrs?.class;
2973
+ if (value === void 0) return void 0;
2974
+ const tree = classTree(value);
2975
+ if (tree === void 0) return void 0;
2976
+ return tree;
2977
+ });
2978
+ const leafCounts = trees.map((tree) => tree === void 0 ? 1 : leavesOf(tree).length);
2979
+ for (const [index, count] of leafCounts.entries()) if (count > MAX_CLASS_VARIANTS) {
2980
+ const item = items[index];
2981
+ const at = item?.kind === "el" ? options.locate(item.el.src) : void 0;
2982
+ options.report({
2983
+ code: "too-many-class-variants",
2984
+ severity: "error",
2985
+ message: `This class list chooses between ${count} literal lists; the compiler resolves at most ${MAX_CLASS_VARIANTS}.`,
2986
+ fix: "Split the choice into fewer conditions, or move part of it to a partial.",
2987
+ origins: at === void 0 ? [] : [at]
2988
+ });
2989
+ return {
2990
+ root,
2991
+ headCss: "",
2992
+ fontFaceCss: ""
2993
+ };
2994
+ }
2995
+ const passes = Math.max(1, ...leafCounts);
2996
+ const shadow = (nodes, pass, cursor) => nodes.flatMap((node) => {
2997
+ if (isValueNode(node)) return [];
2998
+ if ("el" in node) {
2999
+ const index = cursor.next++;
3000
+ const tree = trees[index];
3001
+ const leaves = tree === void 0 ? void 0 : leavesOf(tree);
3002
+ const props = {};
3003
+ for (const [name, value] of Object.entries(node.attrs ?? {})) {
3004
+ if (name === "class") continue;
3005
+ if (typeof value === "string" || typeof value === "number") props[name] = value;
3006
+ else if (typeof value === "object" && value !== null) props[name] = tokens.value(value);
3007
+ }
3008
+ if (leaves !== void 0) props.class = leaves[Math.min(pass, leaves.length - 1)];
3009
+ else if (node.attrs?.class !== void 0) props.class = tokens.value(node.attrs.class);
3010
+ const style = node.style;
3011
+ if (Array.isArray(style)) {
3012
+ const declared = {};
3013
+ for (const [property, value] of style) {
3014
+ if (value === null || typeof value === "boolean") continue;
3015
+ declared[property] = typeof value === "number" ? value : tokens.value(value);
3016
+ }
3017
+ props.style = declared;
3018
+ } else if (typeof style === "string" && style !== "") {
3019
+ const declared = {};
3020
+ for (const part of style.split(";")) {
3021
+ const at = part.indexOf(":");
3022
+ if (at > 0) declared[part.slice(0, at)] = part.slice(at + 1);
3023
+ }
3024
+ props.style = declared;
3025
+ }
3026
+ const at = options.locate(node.src);
3027
+ return [EmailNode.Element({
3028
+ tag: node.el,
3029
+ props,
3030
+ children: shadow(node.children ?? [], pass, cursor),
3031
+ origins: at === void 0 ? [] : [at],
3032
+ authored: true
3033
+ })];
3034
+ }
3035
+ if ("if" in node) return [EmailNode.Fragment({ children: [...shadow(node.then, pass, cursor), ...shadow(node.else ?? [], pass, cursor)] })];
3036
+ if ("each" in node) return [EmailNode.Fragment({ children: shadow(node.children, pass, cursor) })];
3037
+ if ("raw" in node) {
3038
+ cursor.next++;
3039
+ return [EmailNode.Raw({
3040
+ html: rawHtml(tokens, node.raw),
3041
+ text: "",
3042
+ origins: []
3043
+ })];
3044
+ }
3045
+ return [];
3046
+ });
3047
+ const read = (nodes, into) => {
3048
+ for (const node of nodes) if (node.type === "Fragment") read(node.children, into);
3049
+ else if (node.type === "Raw") into.raws.push(node.html);
3050
+ else if (node.type === "Element") {
3051
+ into.styles.push(node.props.style === void 0 ? void 0 : node.props.style);
3052
+ into.bgcolors.push(typeof node.props.bgcolor === "string" ? node.props.bgcolor : void 0);
3053
+ read(node.children, into);
3054
+ }
3055
+ };
3056
+ const results = [];
3057
+ let headCss = "";
3058
+ let fontFaceCss = "";
3059
+ const seen = /* @__PURE__ */ new Set();
3060
+ for (let pass = 0; pass < passes; pass++) {
3061
+ const tree = shadow([root], pass, { next: 0 });
3062
+ const applied = applyStylesheet(tree[0] ?? EmailNode.Fragment({ children: [] }), sheet);
3063
+ if (pass === 0) {
3064
+ headCss = applied.headCss;
3065
+ fontFaceCss = applied.fontFaceCss;
3066
+ }
3067
+ for (const diagnostic of applied.diagnostics) {
3068
+ const key = `${diagnostic.code}\0${diagnostic.message}`;
3069
+ if (seen.has(key)) continue;
3070
+ seen.add(key);
3071
+ options.report(diagnostic);
3072
+ }
3073
+ const result = {
3074
+ styles: [],
3075
+ bgcolors: [],
3076
+ raws: []
3077
+ };
3078
+ read([applied.tree], result);
3079
+ results.push(result);
3080
+ }
3081
+ const styleOf = (declarations, element) => {
3082
+ if (declarations === void 0) return "";
3083
+ const entries = Object.entries(declarations);
3084
+ if (entries.length === 0) return "";
3085
+ const at = options.locate(element.src);
3086
+ const dynamic = entries.some(([, value]) => value.includes(""));
3087
+ const staticOnly = Object.fromEntries(entries.filter(([, value]) => !value.includes("")));
3088
+ const checked = serializeStyle(staticOnly, at === void 0 ? [] : [at]);
3089
+ for (const diagnostic of checked.diagnostics) options.report(diagnostic);
3090
+ if (!dynamic) return checked.css;
3091
+ return entries.map(([property, value]) => {
3092
+ return [property, tokens.toValue(value) ?? value];
3093
+ });
3094
+ };
3095
+ let cursor = 0;
3096
+ let elementCursor = 0;
3097
+ let rawCursor = 0;
3098
+ const rebuild = (nodes) => nodes.map((node) => {
3099
+ if (isValueNode(node)) return node;
3100
+ if ("el" in node) {
3101
+ const index = cursor++;
3102
+ const ordinal = elementCursor++;
3103
+ const tree = trees[index];
3104
+ const first = results[0];
3105
+ const baseStyle = first?.styles[ordinal];
3106
+ const style = tree !== void 0 && leavesOf(tree).length > 1 ? mapTree(tree, (leaf) => styleOf(results[Math.min(leaf, results.length - 1)]?.styles[ordinal], node)) : styleOf(baseStyle, node);
3107
+ const bgcolor = first?.bgcolors[ordinal];
3108
+ const attrs = typeof node.attrs?.bgcolor === "string" && bgcolor !== void 0 && bgcolor !== node.attrs.bgcolor ? {
3109
+ ...node.attrs,
3110
+ bgcolor: tokens.toValue(bgcolor) ?? bgcolor
3111
+ } : node.attrs;
3112
+ const kids = rebuild(node.children ?? []);
3113
+ const { style: _authored, children: _children, attrs: _attrs, ...rest } = node;
3114
+ return {
3115
+ ...rest,
3116
+ ...attrs === void 0 ? {} : { attrs },
3117
+ ...style === "" ? {} : { style },
3118
+ ...kids.length === 0 ? {} : { children: kids }
3119
+ };
3120
+ }
3121
+ if ("if" in node) {
3122
+ const then = rebuild(node.then);
3123
+ const otherwise = rebuild(node.else ?? []);
3124
+ return {
3125
+ ...node,
3126
+ then,
3127
+ ...node.else === void 0 ? {} : { else: otherwise }
3128
+ };
3129
+ }
3130
+ if ("each" in node) return {
3131
+ ...node,
3132
+ children: rebuild(node.children)
3133
+ };
3134
+ if ("raw" in node) {
3135
+ cursor++;
3136
+ const html = results[0]?.raws[rawCursor++];
3137
+ return html === void 0 ? node : {
3138
+ ...node,
3139
+ raw: rawParts(tokens, html)
3140
+ };
3141
+ }
3142
+ return node;
3143
+ });
3144
+ return {
3145
+ root: rebuild([root])[0] ?? root,
3146
+ headCss,
3147
+ fontFaceCss
3148
+ };
3149
+ };
3150
+ //#endregion
3151
+ //#region src/sml/lower-project.ts
3152
+ const ENTRY_CALL = "defineTemplate";
3153
+ const CHANNELS = [
3154
+ "email",
3155
+ "sms",
3156
+ "whatsapp"
3157
+ ];
3158
+ const readDefinition = (project, findings) => {
3159
+ const module = project.entry;
3160
+ let expression = module.defaultExport;
3161
+ if (expression?.type === "Identifier") expression = module.declarations.get(text(expression, "name") ?? "")?.value;
3162
+ const fail = (message, fix, code = "no-template") => {
3163
+ findings.add(module.parsed, expression ?? module.parsed.program, code, message, fix);
3164
+ };
3165
+ if (expression === void 0 || expression.type !== "CallExpression") return fail("The entry must default-export defineTemplate({ ... }).", "Write `export default defineTemplate({ id, schema, fixtures, email })`.");
3166
+ const callee = child(expression, "callee");
3167
+ const name = callee === void 0 ? void 0 : text(callee, "name");
3168
+ const imported = name === void 0 ? void 0 : module.imports.get(name);
3169
+ if (name === "defineEmail") return fail("`defineEmail` runs a render function, which the static profile does not read.", "Use `defineTemplate` from @samva/markup with `email: { subject, preheader, body }`.", "legacy-definition");
3170
+ if (imported?.kind !== "markup" || imported.imported !== ENTRY_CALL) return fail("The entry must default-export defineTemplate({ ... }) imported from @samva/markup.", "Add `import { defineTemplate } from \"@samva/markup\";`.");
3171
+ const argument = children(expression, "arguments")[0];
3172
+ const object = argument === void 0 ? void 0 : unwrap(argument);
3173
+ if (object === void 0 || object.type !== "ObjectExpression") return fail("defineTemplate takes one object literal.", "Write defineTemplate({ id, schema, fixtures, email }).");
3174
+ const fields = /* @__PURE__ */ new Map();
3175
+ for (const property of children(object, "properties")) {
3176
+ const key = keyOf(property);
3177
+ const value = child(property, "value");
3178
+ if (key === void 0 || value === void 0) {
3179
+ findings.add(module.parsed, property, "dynamic-definition", "A definition field is written out, not spread or computed.", "Write each field as `name: value`.");
3180
+ continue;
3181
+ }
3182
+ fields.set(key, value);
3183
+ }
3184
+ return {
3185
+ module,
3186
+ call: expression,
3187
+ fields
3188
+ };
3189
+ };
3190
+ /** A function written in place, or named and declared in the project. */
3191
+ const resolveFunction = (project, module, node) => {
3192
+ const value = unwrap(node);
3193
+ if (/Function/.test(value.type)) return {
3194
+ module,
3195
+ fn: value
3196
+ };
3197
+ if (value.type === "Identifier") {
3198
+ const name = text(value, "name") ?? "";
3199
+ const declared = module.declarations.get(name);
3200
+ if (declared !== void 0) return resolveFunction(project, module, declared.value);
3201
+ const imported = module.imports.get(name);
3202
+ if (imported?.kind === "project") {
3203
+ const target = project.modules.get(imported.path);
3204
+ if (target === void 0) return void 0;
3205
+ const local = imported.imported === "default" ? void 0 : target.exports.get(imported.imported);
3206
+ if (imported.imported === "default" && target.defaultExport !== void 0) return resolveFunction(project, target, target.defaultExport);
3207
+ const declaration = local === void 0 ? void 0 : target.declarations.get(local);
3208
+ return declaration === void 0 ? void 0 : resolveFunction(project, target, declaration.value);
3209
+ }
3210
+ }
3211
+ };
3212
+ const stylesheetOrder = (project) => {
3213
+ const order = [];
3214
+ const visited = /* @__PURE__ */ new Set();
3215
+ const visit = (module) => {
3216
+ if (visited.has(module.path)) return;
3217
+ visited.add(module.path);
3218
+ for (const item of module.ordered) if (item.kind === "css") {
3219
+ if (!order.includes(item.path)) order.push(item.path);
3220
+ } else {
3221
+ const target = project.modules.get(item.path);
3222
+ if (target !== void 0) visit(target);
3223
+ }
3224
+ };
3225
+ visit(project.entry);
3226
+ return order;
3227
+ };
3228
+ /** Rebuild a tree with `edit` applied to the first element it matches. */
3229
+ const editFirst = (nodes, matches, edit) => {
3230
+ let done = false;
3231
+ const walk = (list) => list.map((node) => {
3232
+ if (done || isValueNode(node)) return node;
3233
+ if ("el" in node) {
3234
+ if (matches(node)) {
3235
+ done = true;
3236
+ return edit(node);
3237
+ }
3238
+ return {
3239
+ ...node,
3240
+ ...node.children === void 0 ? {} : { children: walk(node.children) }
3241
+ };
3242
+ }
3243
+ if ("if" in node) {
3244
+ const then = walk(node.then);
3245
+ const otherwise = walk(node.else ?? []);
3246
+ return {
3247
+ ...node,
3248
+ then,
3249
+ ...node.else === void 0 ? {} : { else: otherwise }
3250
+ };
3251
+ }
3252
+ if ("each" in node) return {
3253
+ ...node,
3254
+ children: walk(node.children)
3255
+ };
3256
+ return node;
3257
+ });
3258
+ return {
3259
+ nodes: walk(nodes),
3260
+ done
3261
+ };
3262
+ };
3263
+ const HEAD_COMMENT_OPEN = { raw: ["<!--[if !mso]><!-->"] };
3264
+ const HEAD_COMMENT_CLOSE = { raw: ["<!--<![endif]-->"] };
3265
+ const completed = (findings, ir, fixtures = {}, assets = []) => ({
3266
+ ir: findings.blocking ? void 0 : ir,
3267
+ fixtures,
3268
+ assets,
3269
+ diagnostics: findings.items
3270
+ });
3271
+ /**
3272
+ * Lower a loaded project to IR: the part of the compile that reads structure and needs no Tailwind
3273
+ * or brand. `checkStaticProfile` runs it with empty stylesheets.
3274
+ */
3275
+ const lowerProject = (options, project, findings, inputs) => {
3276
+ const done = (ir, fixtures = {}, assets = inputs.assets) => completed(findings, ir, fixtures, assets);
3277
+ const urlFor = inputs.urlFor;
3278
+ const staticContext = {
3279
+ project,
3280
+ assetUrl: urlFor
3281
+ };
3282
+ const definition = readDefinition(project, findings);
3283
+ if (definition === void 0) return done(void 0);
3284
+ const { module, fields } = definition;
3285
+ const at = (name) => fields.get(name) ?? definition.call;
3286
+ const id = fields.has("id") ? readStatic(staticContext, module, fields.get("id")) : void 0;
3287
+ if (id === void 0 || !id.ok || typeof id.value !== "string") findings.add(module.parsed, at("id"), "invalid-template-id", "The template id is a string literal.", "Write id: \"order-shipped\".");
3288
+ else if (!TEMPLATE_ID_PATTERN.test(id.value)) findings.add(module.parsed, at("id"), "invalid-template-id", `Template id ${JSON.stringify(id.value)} is not lowercase kebab-case.`, "Write lowercase letters and digits joined by single hyphens, for example \"order-shipped\".");
3289
+ const templateId = id !== void 0 && id.ok && typeof id.value === "string" ? id.value : "";
3290
+ let schemaJson;
3291
+ const schemaNode = fields.get("schema");
3292
+ if (schemaNode === void 0) findings.add(module.parsed, definition.call, "missing-schema", "The template declares no schema.", "Add `schema: jsonSchema<Input>({ ... })`.");
3293
+ else {
3294
+ const read = readStatic(staticContext, module, schemaNode);
3295
+ if (read.ok && typeof read.value === "object" && read.value !== null) schemaJson = read.value;
3296
+ else if (options.schema !== void 0) schemaJson = { ...options.schema };
3297
+ else findings.add(read.ok ? module.parsed : read.module.parsed, read.ok ? schemaNode : read.node, "schema-not-static", `The schema cannot be read without running it${read.ok ? "" : ` (${read.reason})`}.`, "Write the schema as a literal: `jsonSchema<Input>({ type: \"object\", properties: { … } })`, or compile with the schema a library already converted.");
3298
+ }
3299
+ let validate = () => true;
3300
+ if (schemaJson !== void 0) try {
3301
+ const converted = inputSchema(jsonSchema(schemaJson));
3302
+ validate = converted.validate;
3303
+ schemaJson = converted.schema;
3304
+ } catch (error) {
3305
+ if (!(error instanceof TypeError)) throw error;
3306
+ findings.add(module.parsed, at("schema"), "invalid-schema", error.message, "Use only the JSON Schema keywords the README lists.");
3307
+ schemaJson = void 0;
3308
+ }
3309
+ const fixtures = {};
3310
+ const fixtureNode = fields.get("fixtures");
3311
+ if (fixtureNode === void 0) findings.add(module.parsed, definition.call, "missing-fixtures", "The template declares no fixtures.", "Add `fixtures: { default: { … } }`.");
3312
+ else {
3313
+ const read = readStatic(staticContext, module, fixtureNode);
3314
+ if (!read.ok) findings.add(read.module.parsed, read.node, "fixtures-not-static", `Fixtures are data, and this one is computed (${read.reason}).`, "Write each fixture as a literal object; an imported asset is allowed.");
3315
+ else if (typeof read.value !== "object" || read.value === null || Object.keys(read.value).length === 0) findings.add(module.parsed, fixtureNode, "missing-fixtures", "The template declares no fixtures.", "Declare at least one: `fixtures: { default: { … } }`.");
3316
+ else {
3317
+ const object = unwrap(fixtureNode);
3318
+ const locations = /* @__PURE__ */ new Map();
3319
+ if (object.type === "ObjectExpression") for (const property of children(object, "properties")) {
3320
+ const key = keyOf(property);
3321
+ if (key !== void 0) locations.set(key, child(property, "key") ?? property);
3322
+ }
3323
+ for (const [name, value] of Object.entries(read.value)) {
3324
+ if (!TEMPLATE_NAME_PATTERN.test(name)) {
3325
+ findings.add(module.parsed, locations.get(name) ?? fixtureNode, "invalid-fixture-name", `Fixture key ${JSON.stringify(name)} is not one path segment.`, "Use letters, digits, `.`, `_` and `-`, starting with a letter or digit.");
3326
+ continue;
3327
+ }
3328
+ fixtures[name] = value;
3329
+ if (schemaJson !== void 0 && !validate(value)) {
3330
+ const first = validate.errors?.at(-1) ?? validate.errors?.[0];
3331
+ findings.add(module.parsed, locations.get(name) ?? fixtureNode, "fixture-invalid", `Fixture ${JSON.stringify(name)} does not match the schema${first === void 0 ? "" : `: ${first.instanceLocation === "#" || first.instanceLocation === void 0 ? "" : `${first.instanceLocation.replace(/^#/, "")} `}${first.error ?? "is invalid"}`}.`, "Change the fixture, or the schema, so a fixture is a payload a send could make.");
3332
+ }
3333
+ }
3334
+ }
3335
+ }
3336
+ const localeRead = fields.has("locale") ? readStatic(staticContext, module, fields.get("locale")) : void 0;
3337
+ const locale = localeRead?.ok === true && typeof localeRead.value === "string" ? localeRead.value : void 0;
3338
+ 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\".");
3339
+ const lowerer = new Lowerer({
3340
+ project,
3341
+ findings,
3342
+ staticContext,
3343
+ assetUrl: urlFor,
3344
+ brand: options.brand,
3345
+ brand: options.brand,
3346
+ channel: channelLowering
3347
+ });
3348
+ const rootContext = (target) => ({
3349
+ module: target,
3350
+ env: /* @__PURE__ */ new Map(),
3351
+ stack: [],
3352
+ loops: [],
3353
+ depth: 0
3354
+ });
3355
+ /** Bind the function's parameter to the input and return the single expression it returns. */
3356
+ const openFunction = (node) => {
3357
+ const resolved = resolveFunction(project, module, node);
3358
+ if (resolved === void 0) {
3359
+ findings.add(module.parsed, node, "not-a-function", "A channel field is a function of the input, written in place or declared in the project.", "Write `(input) => …`.");
3360
+ return;
3361
+ }
3362
+ const ctx = rootContext(resolved.module);
3363
+ const env = /* @__PURE__ */ new Map();
3364
+ const parameter = children(resolved.fn, "params")[0];
3365
+ if (parameter !== void 0) lowerer.bindPattern(ctx, parameter, {
3366
+ k: "path",
3367
+ path: ""
3368
+ }, env, false);
3369
+ const scoped = {
3370
+ ...ctx,
3371
+ env
3372
+ };
3373
+ const body = lowerer.functionExpression(scoped, resolved.fn);
3374
+ return body === void 0 ? void 0 : {
3375
+ ctx: scoped,
3376
+ body
3377
+ };
3378
+ };
3379
+ for (const name of fields.keys()) if (![
3380
+ "id",
3381
+ "schema",
3382
+ "fixtures",
3383
+ "locale",
3384
+ ...CHANNELS
3385
+ ].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.");
3386
+ let ir;
3387
+ const checks = [];
3388
+ const emailField = fields.get("email");
3389
+ let emailIr;
3390
+ if (emailField !== void 0) {
3391
+ const object = unwrap(emailField);
3392
+ const parts = /* @__PURE__ */ new Map();
3393
+ if (object.type === "ObjectExpression") for (const property of children(object, "properties")) {
3394
+ const key = keyOf(property);
3395
+ const value = child(property, "value");
3396
+ if (key !== void 0 && value !== void 0) parts.set(key, value);
3397
+ }
3398
+ else findings.add(module.parsed, emailField, "invalid-channel", "The email channel is an object: `{ subject, preheader, body }`.", "Write email: { subject: …, body: … }.");
3399
+ for (const key of parts.keys()) if (![
3400
+ "subject",
3401
+ "preheader",
3402
+ "body"
3403
+ ].includes(key)) findings.add(module.parsed, parts.get(key), "unknown-field", `\`${key}\` is not an email field.`, "The fields are subject, preheader and body.");
3404
+ const value = (name) => {
3405
+ const node = parts.get(name);
3406
+ if (node === void 0) return void 0;
3407
+ const opened = openFunction(node);
3408
+ return opened === void 0 ? void 0 : lowerer.value(opened.ctx, opened.body, false);
3409
+ };
3410
+ const subject = parts.has("subject") ? value("subject") : void 0;
3411
+ if (!parts.has("subject") && object.type === "ObjectExpression") findings.add(module.parsed, emailField, "missing-subject", "The email channel needs a subject.", "Add `subject: (input) => …`.");
3412
+ const preheader = value("preheader");
3413
+ let body;
3414
+ const bodyNode = parts.get("body");
3415
+ 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>)`.");
3416
+ else if (bodyNode !== void 0) {
3417
+ const opened = openFunction(bodyNode);
3418
+ if (opened !== void 0) {
3419
+ const nodes = lowerer.content(opened.ctx, opened.body);
3420
+ if (nodes.length === 1) body = nodes[0];
3421
+ else if (!findings.blocking) findings.add(opened.ctx.module.parsed, opened.body, "multiple-roots", nodes.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>.");
3422
+ }
3423
+ }
3424
+ if (subject !== void 0 && body !== void 0) {
3425
+ const sheets = [];
3426
+ if (inputs.tailwindSheet !== void 0) sheets.push(inputs.tailwindSheet);
3427
+ for (const path of stylesheetOrder(project)) {
3428
+ const sheet = inputs.stylesheets.get(path);
3429
+ if (sheet !== void 0) sheets.push(sheet);
3430
+ }
3431
+ const sources = [options.entry, ...lowerer.sources];
3432
+ const locate = (source) => {
3433
+ if (source === void 0) return void 0;
3434
+ return {
3435
+ fileName: sources[source[2] ?? 0] ?? options.entry,
3436
+ lineNumber: source[0],
3437
+ columnNumber: source[1]
3438
+ };
3439
+ };
3440
+ const styled = applyStyles(body, mergeStylesheets(sheets), {
3441
+ locate,
3442
+ report: (diagnostic) => {
3443
+ findings.items.push(diagnostic);
3444
+ }
3445
+ });
3446
+ let root = styled.root;
3447
+ if (styled.headCss !== "" || styled.fontFaceCss !== "") {
3448
+ const additions = [...styled.headCss === "" ? [] : [{
3449
+ el: "style",
3450
+ children: [styled.headCss]
3451
+ }], ...styled.fontFaceCss === "" ? [] : [
3452
+ HEAD_COMMENT_OPEN,
3453
+ {
3454
+ el: "style",
3455
+ children: [styled.fontFaceCss]
3456
+ },
3457
+ HEAD_COMMENT_CLOSE
3458
+ ]];
3459
+ const edited = editFirst([root], (element) => element.el === "head", (head) => ({
3460
+ ...head,
3461
+ children: [...head.children ?? [], ...additions]
3462
+ }));
3463
+ if (!edited.done) findings.items.push({
3464
+ code: "missing-head",
3465
+ severity: "error",
3466
+ message: "Styles that cannot be inlined — media queries, dark-scheme rules and pseudo-selectors — need a <head> to live in.",
3467
+ fix: "Wrap the message in <Email>, or write those declarations as inline styles.",
3468
+ origins: []
3469
+ });
3470
+ else root = edited.nodes[0] ?? root;
3471
+ }
3472
+ if (preheader !== void 0) {
3473
+ const edited = editFirst([root], (element) => element.el === "body", (bodyElement) => ({
3474
+ ...bodyElement,
3475
+ children: [{ preheader: true }, ...bodyElement.children ?? []]
3476
+ }));
3477
+ if (!edited.done) findings.add(module.parsed, at("email"), "preheader-without-shell", "A preheader is the hidden inbox preview inside <body>, and this body has no <Email> shell.", "Wrap the body in <Email>.");
3478
+ else root = edited.nodes[0] ?? root;
3479
+ }
3480
+ checkStructure([root], (finding) => checks.push(finding));
3481
+ if (schemaJson !== void 0) checks.push(...checkBindings(schemaJson, [root], [subject, ...preheader === void 0 ? [] : [preheader]]));
3482
+ emailIr = {
3483
+ subject,
3484
+ ...preheader === void 0 ? {} : { preheader },
3485
+ body: root
3486
+ };
3487
+ }
3488
+ }
3489
+ const channelIr = channelBodies({
3490
+ lowerer,
3491
+ findings,
3492
+ fields,
3493
+ module,
3494
+ openFunction,
3495
+ checks,
3496
+ schema: schemaJson
3497
+ });
3498
+ const sources = [options.entry, ...lowerer.sources];
3499
+ for (const finding of checks) {
3500
+ const source = finding.src;
3501
+ const file = sources[source?.[2] ?? 0] ?? options.entry;
3502
+ findings.items.push({
3503
+ code: finding.code,
3504
+ severity: "error",
3505
+ message: finding.message,
3506
+ ...finding.fix === void 0 ? {} : { fix: finding.fix },
3507
+ origins: source === void 0 ? [{
3508
+ fileName: options.entry,
3509
+ lineNumber: 1,
3510
+ columnNumber: 1
3511
+ }] : [{
3512
+ fileName: file,
3513
+ lineNumber: source[0],
3514
+ columnNumber: source[1]
3515
+ }]
3516
+ });
3517
+ }
3518
+ if (schemaJson !== void 0 && templateId !== "" && (emailIr !== void 0 || channelIr.sms !== void 0 || channelIr.whatsapp !== void 0)) ir = {
3519
+ sml: 1,
3520
+ template: templateId,
3521
+ ...locale === void 0 ? {} : { locale },
3522
+ sources,
3523
+ ...emailIr === void 0 ? {} : { email: emailIr },
3524
+ ...channelIr.sms === void 0 ? {} : { sms: channelIr.sms },
3525
+ ...channelIr.whatsapp === void 0 ? {} : { whatsapp: channelIr.whatsapp },
3526
+ schema: schemaJson
3527
+ };
3528
+ 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`.");
3529
+ return done(ir, fixtures);
3530
+ };
3531
+ //#endregion
3532
+ //#region src/sml/project.ts
3533
+ const MARKUP_SPECIFIER = "@samva/markup";
3534
+ const MARKUP_PREFIX = "@samva/markup/";
3535
+ /** Entries a template could import that moved; each is reported with its replacement. */
3536
+ const MOVED_MARKUP_ENTRIES = { "@samva/markup/email/components": "@samva/markup/email" };
3537
+ const SCRIPT_EXTENSIONS = [".tsx", ".ts"];
3538
+ /** Resolve a relative specifier to a project file, or `undefined`. */
3539
+ const resolveSpecifier = (from, specifier, files) => {
3540
+ const base = resolveFromDirectory(projectDirectory(from), specifier.replace(/[?#].*$/, ""));
3541
+ if (base === void 0) return void 0;
3542
+ if (Object.hasOwn(files, base)) return base;
3543
+ for (const extension of SCRIPT_EXTENSIONS) if (Object.hasOwn(files, `${base}${extension}`)) return `${base}${extension}`;
3544
+ for (const extension of SCRIPT_EXTENSIONS) if (Object.hasOwn(files, `${base}/index${extension}`)) return `${base}/index${extension}`;
3545
+ };
3546
+ const isScriptPath = (path) => /\.[cm]?[jt]sx?$/.test(path);
3547
+ const declaredNames = (pattern) => pattern.type === "Identifier" ? [text(pattern, "name") ?? ""] : [];
3548
+ /** Load the entry and everything it imports from the project. */
3549
+ const loadProject = (files, entryPath, findings, loadOptions = {}) => {
3550
+ const modules = /* @__PURE__ */ new Map();
3551
+ const unresolved = /* @__PURE__ */ new Set();
3552
+ const loading = /* @__PURE__ */ new Set();
3553
+ let failed = false;
3554
+ const load = (path) => {
3555
+ const existing = modules.get(path);
3556
+ if (existing !== void 0) return existing;
3557
+ if (loading.has(path)) return void 0;
3558
+ const raw = files[path];
3559
+ if (raw === void 0) return void 0;
3560
+ const source = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
3561
+ let parsed;
3562
+ try {
3563
+ parsed = parseSource(path, source);
3564
+ } catch (error) {
3565
+ if (!(error instanceof SourceSyntaxError)) throw error;
3566
+ findings.items.push({
3567
+ code: "syntax-error",
3568
+ severity: "error",
3569
+ message: error.message,
3570
+ origins: [{
3571
+ fileName: path,
3572
+ lineNumber: error.line,
3573
+ columnNumber: error.column
3574
+ }]
3575
+ });
3576
+ failed = true;
3577
+ return;
3578
+ }
3579
+ loading.add(path);
3580
+ const imports = /* @__PURE__ */ new Map();
3581
+ const ordered = [];
3582
+ const declarations = /* @__PURE__ */ new Map();
3583
+ const exports = /* @__PURE__ */ new Map();
3584
+ let defaultExport;
3585
+ const noteDeclaration = (node) => {
3586
+ if (node.type === "VariableDeclaration") for (const declarator of children(node, "declarations")) {
3587
+ const id = child(declarator, "id");
3588
+ const init = child(declarator, "init");
3589
+ if (id === void 0 || init === void 0) continue;
3590
+ for (const name of declaredNames(id)) declarations.set(name, {
3591
+ value: unwrap(init),
3592
+ node
3593
+ });
3594
+ }
3595
+ else if (node.type === "FunctionDeclaration") {
3596
+ const name = text(child(node, "id") ?? node, "name");
3597
+ if (name !== void 0) declarations.set(name, {
3598
+ value: node,
3599
+ node
3600
+ });
3601
+ }
3602
+ };
3603
+ for (const statement of children(parsed.program, "body")) switch (statement.type) {
3604
+ case "ImportDeclaration": {
3605
+ const specifier = text(child(statement, "source") ?? statement, "value") ?? "";
3606
+ const typeOnly = text(statement, "importKind") === "type";
3607
+ const specifiers = children(statement, "specifiers");
3608
+ if (typeOnly) break;
3609
+ const local = (node) => text(child(node, "local") ?? node, "name") ?? "";
3610
+ const importedName = (node) => {
3611
+ if (node.type === "ImportDefaultSpecifier") return "default";
3612
+ if (node.type === "ImportNamespaceSpecifier") return "*";
3613
+ const imported = child(node, "imported");
3614
+ return imported === void 0 ? local(node) : text(imported, "name") ?? text(imported, "value") ?? local(node);
3615
+ };
3616
+ const moved = MOVED_MARKUP_ENTRIES[specifier];
3617
+ if (moved !== void 0) {
3618
+ findings.add(parsed, child(statement, "source") ?? statement, "moved-import", `\`${specifier}\` moved to \`${moved}\`; the imported names are unchanged.`, `Import from "${moved}".`);
3619
+ failed = true;
3620
+ break;
3621
+ }
3622
+ if (specifier === MARKUP_SPECIFIER || specifier.startsWith(MARKUP_PREFIX)) {
3623
+ for (const item of specifiers) {
3624
+ if (text(item, "importKind") === "type") continue;
3625
+ imports.set(local(item), {
3626
+ kind: "markup",
3627
+ specifier,
3628
+ imported: importedName(item),
3629
+ node: item
3630
+ });
3631
+ }
3632
+ break;
3633
+ }
3634
+ if (/^samva:brand(\/|$)/.test(specifier)) {
3635
+ for (const item of specifiers) if (text(item, "importKind") !== "type") imports.set(local(item), {
3636
+ kind: "brand",
3637
+ specifier,
3638
+ imported: importedName(item),
3639
+ node: item
3640
+ });
3641
+ break;
3642
+ }
3643
+ if (!/^\.{0,2}\//.test(specifier)) {
3644
+ findings.add(parsed, statement, "non-project-import", `\`${specifier}\` is not part of the project; templates import only @samva/markup and files inside the project.`, "Remove the import, or copy the code you need into a project file.");
3645
+ failed = true;
3646
+ break;
3647
+ }
3648
+ const resolved = resolveSpecifier(path, specifier, files);
3649
+ if (resolved === void 0 && loadOptions.lenient === true) {
3650
+ const missing = resolveFromDirectory(projectDirectory(path), specifier.replace(/[?#].*$/, ""));
3651
+ const asset = missing !== void 0 && isAssetPath(missing);
3652
+ const script = missing !== void 0 && (isScriptPath(missing) || !/\.[a-z0-9]+$/i.test(missing));
3653
+ if (missing !== void 0 && (missing.endsWith(".css") || asset || script)) {
3654
+ if (!missing.endsWith(".css")) for (const item of specifiers) if (asset) imports.set(local(item), {
3655
+ kind: "asset",
3656
+ path: missing,
3657
+ node: item
3658
+ });
3659
+ else {
3660
+ unresolved.add(local(item));
3661
+ imports.set(local(item), {
3662
+ kind: "project",
3663
+ path: missing,
3664
+ imported: importedName(item),
3665
+ node: item
3666
+ });
3667
+ }
3668
+ break;
3669
+ }
3670
+ }
3671
+ if (resolved === void 0) {
3672
+ findings.add(parsed, statement, "unresolved-import", `\`${specifier}\` names no file in the project.`, "Check the path and the file extension.");
3673
+ failed = true;
3674
+ break;
3675
+ }
3676
+ if (resolved.endsWith(".css")) {
3677
+ ordered.push({
3678
+ kind: "css",
3679
+ path: resolved
3680
+ });
3681
+ break;
3682
+ }
3683
+ if (isAssetPath(resolved)) {
3684
+ for (const item of specifiers) imports.set(local(item), {
3685
+ kind: "asset",
3686
+ path: resolved,
3687
+ node: item
3688
+ });
3689
+ break;
3690
+ }
3691
+ if (isScriptPath(resolved)) {
3692
+ ordered.push({
3693
+ kind: "script",
3694
+ path: resolved
3695
+ });
3696
+ for (const item of specifiers) imports.set(local(item), {
3697
+ kind: "project",
3698
+ path: resolved,
3699
+ imported: importedName(item),
3700
+ node: item
3701
+ });
3702
+ break;
3703
+ }
3704
+ findings.add(parsed, statement, "unresolved-import", `\`${specifier}\` is a file type templates cannot import.`, "Import a script, a stylesheet or an asset such as .png or .woff2.");
3705
+ failed = true;
3706
+ break;
3707
+ }
3708
+ case "ExportNamedDeclaration": {
3709
+ const declaration = child(statement, "declaration");
3710
+ if (declaration !== void 0) {
3711
+ noteDeclaration(declaration);
3712
+ if (declaration.type === "VariableDeclaration") for (const declarator of children(declaration, "declarations")) for (const name of declaredNames(child(declarator, "id") ?? declarator)) exports.set(name, name);
3713
+ else if (declaration.type === "FunctionDeclaration") {
3714
+ const name = text(child(declaration, "id") ?? declaration, "name");
3715
+ if (name !== void 0) exports.set(name, name);
3716
+ }
3717
+ } else for (const item of children(statement, "specifiers")) {
3718
+ const exported = child(item, "exported");
3719
+ const localNode = child(item, "local");
3720
+ const exportedName = exported === void 0 ? void 0 : text(exported, "name") ?? text(exported, "value");
3721
+ const localName = localNode === void 0 ? void 0 : text(localNode, "name");
3722
+ if (exportedName !== void 0 && localName !== void 0) exports.set(exportedName, localName);
3723
+ }
3724
+ break;
3725
+ }
3726
+ case "ExportDefaultDeclaration": {
3727
+ const declaration = child(statement, "declaration");
3728
+ if (declaration !== void 0) defaultExport = unwrap(declaration);
3729
+ break;
3730
+ }
3731
+ case "VariableDeclaration":
3732
+ case "FunctionDeclaration": noteDeclaration(statement);
3733
+ }
3734
+ const module = {
3735
+ path,
3736
+ parsed,
3737
+ imports,
3738
+ ordered,
3739
+ declarations,
3740
+ exports,
3741
+ defaultExport
3742
+ };
3743
+ modules.set(path, module);
3744
+ loading.delete(path);
3745
+ for (const item of ordered) if (item.kind === "script") load(item.path);
3746
+ return module;
3747
+ };
3748
+ const entry = load(entryPath);
3749
+ if (entry === void 0 || failed) return void 0;
3750
+ return {
3751
+ modules,
3752
+ entry,
3753
+ unresolved
3754
+ };
3755
+ };
3756
+ //#endregion
3757
+ //#region src/sml/profile.ts
3758
+ const DEFAULT_ENTRY = "template.tsx";
3759
+ /** Findings that come from the inputs this check leaves out, not from the source. */
3760
+ const MISSING_INPUT_CODES = /* @__PURE__ */ new Set(["brand-unavailable", "unmatched-class"]);
3761
+ const asFiles = (files, entry) => typeof files === "string" ? { [entry]: files } : files;
3762
+ /**
3763
+ * Check an entry against the static profile. Pass the entry's source alone, or the project's files
3764
+ * and the entry's path. Imports of files that are absent are trusted to exist, and the brand,
3765
+ * Tailwind and theme are not resolved, so the result holds only findings the source itself owns.
3766
+ */
3767
+ const checkStaticProfile = (files, entry = DEFAULT_ENTRY) => {
3768
+ const findings = new Findings();
3769
+ const project = loadProject(asFiles(files, entry), entry, findings, { lenient: true });
3770
+ if (project === void 0) return findings.items;
3771
+ lowerProject({
3772
+ entry,
3773
+ brand: void 0
3774
+ }, project, findings, {
3775
+ assets: [],
3776
+ stylesheets: /* @__PURE__ */ new Map(),
3777
+ tailwindSheet: void 0,
3778
+ urlFor: (path) => `https://assets.invalid/${path}`
3779
+ });
3780
+ const unresolved = [...project.unresolved];
3781
+ 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}\``)));
3782
+ };
3783
+ const key = (item) => `${item.code}\0${item.message}`;
3784
+ /**
3785
+ * The errors in `after` that `before` did not already have, compared by code and message so a
3786
+ * finding that only moved does not count as new.
3787
+ */
3788
+ const introducedProfileErrors = (before, after) => {
3789
+ const known = /* @__PURE__ */ new Map();
3790
+ for (const item of before) if (item.severity === "error") known.set(key(item), (known.get(key(item)) ?? 0) + 1);
3791
+ const added = [];
3792
+ for (const item of after) {
3793
+ if (item.severity !== "error") continue;
3794
+ const remaining = known.get(key(item)) ?? 0;
3795
+ if (remaining > 0) known.set(key(item), remaining - 1);
3796
+ else added.push(item);
3797
+ }
3798
+ return added;
3799
+ };
3800
+ //#endregion
3801
+ export { lowerProject as a, WHATSAPP_HEADER_TYPES as c, parseBrandSpecifier as d, projectDirectory as f, completed as i, Findings as l, resolveThemeImports as m, introducedProfileErrors as n, SMS_CATEGORIES as o, resolveFromDirectory as p, loadProject as r, WHATSAPP_CATEGORIES as s, checkStaticProfile as t, BRAND_SPECIFIER as u };
3802
+
3803
+ //# sourceMappingURL=profile-pWGtWZXF.js.map