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