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