@samva/markup 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +104 -37
- package/README.md +188 -52
- package/dist/assets-C5duwTya.d.ts +40 -0
- package/dist/brand-B-0bN_Ir.d.ts +88 -0
- package/dist/{render-aaJBnsXK.js → cascade-t6kxlgEI.js} +130 -160
- package/dist/channel-edit.d.ts +121 -0
- package/dist/channel-edit.js +609 -0
- package/dist/channel-specs-B6M_HlaV.js +29 -0
- package/dist/channel-specs-C2feJwuP.d.ts +13 -0
- package/dist/compatibility-DbISoFIq.js +589 -0
- package/dist/compiler.d.ts +19 -249
- package/dist/compiler.js +10 -1408
- package/dist/{css-COcFOT1y.d.ts → css-BVQKICKC.d.ts} +30 -4
- package/dist/diagnostic-codes.d.ts +403 -0
- package/dist/diagnostic-codes.js +399 -0
- package/dist/{diagnostics-BMYtBCQk.d.ts → diagnostics-B-Gym34R.d.ts} +8 -4
- package/dist/diagnostics.d.ts +31 -2
- package/dist/diagnostics.js +1 -19
- package/dist/{elements-BzjjnoCS.d.ts → elements-cqldpaJt.d.ts} +3 -3
- package/dist/email/assets.d.ts +2 -0
- package/dist/email/assets.js +63 -0
- package/dist/email/brand-footer.d.ts +34 -0
- package/dist/email/brand-footer.js +63 -0
- package/dist/email/brand.d.ts +2 -0
- package/dist/email/brand.js +148 -0
- package/dist/email/components.d.ts +21 -10
- package/dist/email/components.js +46 -11
- package/dist/email/diagnostics.d.ts +2 -2
- package/dist/email/diagnostics.js +2 -1
- package/dist/email/elements.d.ts +1 -1
- package/dist/email/html.d.ts +2 -42
- package/dist/email/html.js +56 -2
- package/dist/email/jsx-dev-runtime.d.ts +2 -2
- package/dist/email/jsx-runtime.d.ts +1 -1
- package/dist/email/render.d.ts +10 -13
- package/dist/email/render.js +163 -6
- package/dist/email/text.d.ts +1 -1
- package/dist/fmt.d.ts +32 -0
- package/dist/fmt.js +174 -0
- package/dist/html-BJepOwZ4.d.ts +56 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/ir-aaqNE987.d.ts +252 -0
- package/dist/ir.d.ts +2 -0
- package/dist/ir.js +7 -0
- package/dist/preview-Ekdg1dXG.d.ts +100 -0
- package/dist/preview.d.ts +2 -0
- package/dist/preview.js +160 -0
- package/dist/profile-BH-OtdDA.js +3670 -0
- package/dist/render-ir.d.ts +19 -0
- package/dist/render-ir.js +826 -0
- package/dist/sms-segments.d.ts +13 -2
- package/dist/sms.d.ts +16 -0
- package/dist/sms.js +12 -0
- package/dist/{email/source-edit.d.ts → source-edit.d.ts} +55 -6
- package/dist/source-edit.js +343 -0
- package/dist/source-locations-C8wCsiAy.d.ts +23 -0
- package/dist/source-locations.d.ts +2 -2
- package/dist/source-locations.js +3 -89
- package/dist/static.d.ts +57 -0
- package/dist/static.js +69 -0
- package/dist/tailwind-CMYpZf1v.js +614 -0
- package/dist/template-_QE_rQND.d.ts +65 -0
- package/dist/template.d.ts +2 -2
- package/dist/template.js +15 -10
- package/dist/theme-css-hFqcPSar.js +587 -0
- package/dist/theme-css.d.ts +57 -16
- package/dist/theme-css.js +2 -509
- package/dist/theme-imports-lvBw3AD1.js +109 -0
- package/dist/transform-Ct2OMkFN.d.ts +153 -0
- package/dist/unsubscribe-link-CoesnQDU.js +26 -0
- package/dist/whatsapp.d.ts +57 -0
- package/dist/whatsapp.js +22 -0
- package/generated/contract.json +1782 -0
- package/generated/reference.md +353 -0
- package/package.json +56 -154
- package/samva-brand.d.ts +14 -0
- 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-60eaYC7b.d.ts +0 -356
- package/dist/checks-DBweSIPN.js +0 -58954
- package/dist/checks.d.ts +0 -33
- package/dist/checks.js +0 -2
- package/dist/classes.d.ts +0 -27
- 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-BjYMqOoY.d.ts +0 -106
- package/dist/diagnostics-OcQKQYSY.d.ts +0 -49
- package/dist/email/selection.d.ts +0 -91
- package/dist/email/selection.js +0 -91
- package/dist/email/source-edit.js +0 -185
- package/dist/email/stylesheets.d.ts +0 -15
- package/dist/email/stylesheets.js +0 -45
- package/dist/emit.d.ts +0 -51
- package/dist/emit.js +0 -151
- package/dist/footer.d.ts +0 -66
- package/dist/footer.js +0 -93
- 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-2aiiYO7m.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/presets/category-mapping.d.ts +0 -13
- package/dist/presets/category-mapping.js +0 -49
- package/dist/presets/corpus.d.ts +0 -2
- package/dist/presets/corpus.js +0 -194
- package/dist/presets/public-gallery.d.ts +0 -266
- package/dist/presets/public-gallery.js +0 -916
- package/dist/presets/public-handoff.d.ts +0 -164
- package/dist/presets/public-handoff.js +0 -361
- package/dist/presets/public-preview-artifacts.d.ts +0 -43
- package/dist/presets/public-preview-artifacts.js +0 -104
- package/dist/presets/public-preview.d.ts +0 -61
- package/dist/presets/public-preview.js +0 -83
- package/dist/presets/showcase.d.ts +0 -2
- package/dist/presets/showcase.js +0 -627
- package/dist/presets/sources.d.ts +0 -38
- package/dist/presets/sources.js +0 -6083
- package/dist/presets.d.ts +0 -2
- package/dist/presets.js +0 -881
- package/dist/printer.d.ts +0 -6
- package/dist/printer.js +0 -122
- package/dist/registry.d.ts +0 -42
- package/dist/registry.js +0 -646
- package/dist/render-template.d.ts +0 -45
- package/dist/render-template.js +0 -76
- package/dist/showcase-DGZAWn4Z.d.ts +0 -456
- package/dist/sms-segments-y4FpPilJ.d.ts +0 -13
- package/dist/source-locations-DrCM82Ym.d.ts +0 -67
- package/dist/template-DWftqh4x.d.ts +0 -60
- package/dist/theme-6ApjjiBo.d.ts +0 -116
- package/dist/theme.d.ts +0 -2
- package/dist/theme.js +0 -118
- package/dist/tokens.d.ts +0 -6
- package/dist/tokens.js +0 -417
- 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,609 @@
|
|
|
1
|
+
import { n as isObject } from "./guards-eV1qLbf2.js";
|
|
2
|
+
import { a as WHATSAPP_HEADER_TYPES, i as WHATSAPP_CATEGORIES, n as SMS_CATEGORIES } from "./channel-specs-B6M_HlaV.js";
|
|
3
|
+
import { parse } from "@babel/parser";
|
|
4
|
+
//#region src/channel-edit.ts
|
|
5
|
+
const isNode = (value) => isObject(value) && typeof value.type === "string" && typeof value.start === "number" && typeof value.end === "number";
|
|
6
|
+
const nodeList = (value) => Array.isArray(value) ? value.filter(isNode) : [];
|
|
7
|
+
const nodeOf = (parent, key) => {
|
|
8
|
+
const value = parent[key];
|
|
9
|
+
return isNode(value) ? value : null;
|
|
10
|
+
};
|
|
11
|
+
const parseSource = (source) => {
|
|
12
|
+
try {
|
|
13
|
+
const result = parse(source, {
|
|
14
|
+
sourceType: "module",
|
|
15
|
+
plugins: ["typescript", "jsx"]
|
|
16
|
+
});
|
|
17
|
+
return isNode(result) ? result : null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const walk = (value, visit) => {
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
for (const item of value) walk(item, visit);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!isNode(value)) return;
|
|
28
|
+
if (visit(value) === false) return;
|
|
29
|
+
for (const [key, child] of Object.entries(value)) if (key !== "loc" && key !== "extra" && key !== "comments") walk(child, visit);
|
|
30
|
+
};
|
|
31
|
+
const jsxName = (name) => {
|
|
32
|
+
if (name === null) return "";
|
|
33
|
+
if (name.type === "JSXIdentifier") return typeof name.name === "string" ? name.name : "";
|
|
34
|
+
if (name.type === "JSXMemberExpression") return `${jsxName(nodeOf(name, "object"))}.${jsxName(nodeOf(name, "property"))}`;
|
|
35
|
+
return "";
|
|
36
|
+
};
|
|
37
|
+
const elementOf = (node) => {
|
|
38
|
+
if (node.type !== "JSXElement") return null;
|
|
39
|
+
const opening = nodeOf(node, "openingElement");
|
|
40
|
+
if (opening === null) return null;
|
|
41
|
+
const closing = nodeOf(node, "closingElement");
|
|
42
|
+
return {
|
|
43
|
+
node,
|
|
44
|
+
opening,
|
|
45
|
+
closing,
|
|
46
|
+
tag: jsxName(nodeOf(opening, "name")),
|
|
47
|
+
children: nodeList(node.children),
|
|
48
|
+
attributes: nodeList(opening.attributes),
|
|
49
|
+
selfClosing: opening.selfClosing === true,
|
|
50
|
+
contentStart: opening.end,
|
|
51
|
+
contentEnd: closing === null ? opening.end : closing.start
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const returnedElement = (fn) => {
|
|
55
|
+
const body = nodeOf(fn, "body");
|
|
56
|
+
if (body === null) return null;
|
|
57
|
+
if (body.type === "JSXElement") return body;
|
|
58
|
+
if (body.type !== "BlockStatement") return null;
|
|
59
|
+
const statements = nodeList(body.body).filter((statement) => statement.type !== "EmptyStatement");
|
|
60
|
+
const only = statements.length === 1 ? statements[0] : void 0;
|
|
61
|
+
const argument = only?.type === "ReturnStatement" && only !== void 0 ? nodeOf(only, "argument") : null;
|
|
62
|
+
return argument !== null && argument.type === "JSXElement" ? argument : null;
|
|
63
|
+
};
|
|
64
|
+
const propertyName = (property) => {
|
|
65
|
+
if (property.computed === true) return null;
|
|
66
|
+
const key = nodeOf(property, "key");
|
|
67
|
+
if (key === null) return null;
|
|
68
|
+
if (key.type === "Identifier") return typeof key.name === "string" ? key.name : null;
|
|
69
|
+
return key.type === "StringLiteral" && typeof key.value === "string" ? key.value : null;
|
|
70
|
+
};
|
|
71
|
+
/** The function behind `defineTemplate({ [channel]: … })`, when it is written in place. */
|
|
72
|
+
const channelFunction = (tree, channel) => {
|
|
73
|
+
let found = null;
|
|
74
|
+
walk(tree, (call) => {
|
|
75
|
+
if (found !== null) return false;
|
|
76
|
+
if (call.type !== "CallExpression") return;
|
|
77
|
+
const callee = nodeOf(call, "callee");
|
|
78
|
+
if (callee?.type !== "Identifier" || callee.name !== "defineTemplate") return;
|
|
79
|
+
const definition = nodeList(call.arguments)[0];
|
|
80
|
+
if (definition?.type !== "ObjectExpression") return false;
|
|
81
|
+
for (const property of nodeList(definition.properties)) {
|
|
82
|
+
if (propertyName(property) !== channel) continue;
|
|
83
|
+
const fn = property.type === "ObjectMethod" ? property : property.type === "ObjectProperty" ? nodeOf(property, "value") : null;
|
|
84
|
+
if (fn === null || fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression" && fn.type !== "ObjectMethod") return false;
|
|
85
|
+
const returned = returnedElement(fn);
|
|
86
|
+
const root = returned === null ? null : elementOf(returned);
|
|
87
|
+
if (root !== null) found = {
|
|
88
|
+
root,
|
|
89
|
+
parameter: nodeList(fn.params)[0] ?? null
|
|
90
|
+
};
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
});
|
|
95
|
+
return found;
|
|
96
|
+
};
|
|
97
|
+
/** The name an import binds a component to, or the component's own name when nothing is imported. */
|
|
98
|
+
const importedAs = (tree, specifier, imported) => {
|
|
99
|
+
let local = imported;
|
|
100
|
+
walk(tree, (node) => {
|
|
101
|
+
if (node.type !== "ImportDeclaration") return;
|
|
102
|
+
if (nodeOf(node, "source")?.value !== specifier) return false;
|
|
103
|
+
for (const item of nodeList(node.specifiers)) {
|
|
104
|
+
const name = nodeOf(item, "imported");
|
|
105
|
+
const alias = nodeOf(item, "local");
|
|
106
|
+
if (item.type === "ImportSpecifier" && (name?.name === imported || name?.value === imported)) local = typeof alias?.name === "string" ? alias.name : imported;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
});
|
|
110
|
+
return local;
|
|
111
|
+
};
|
|
112
|
+
/** JSX text as the compiler reads it: lines are trimmed and joined, blank lines dropped. */
|
|
113
|
+
const jsxTextValue = (value) => {
|
|
114
|
+
const lines = value.split(/\r\n|\n|\r/);
|
|
115
|
+
let lastNonEmpty = 0;
|
|
116
|
+
lines.forEach((line, index) => {
|
|
117
|
+
if (/[^ \t]/.test(line)) lastNonEmpty = index;
|
|
118
|
+
});
|
|
119
|
+
let result = "";
|
|
120
|
+
lines.forEach((line, index) => {
|
|
121
|
+
let trimmed = line.replace(/\t/g, " ");
|
|
122
|
+
if (index !== 0) trimmed = trimmed.replace(/^ +/, "");
|
|
123
|
+
if (index !== lines.length - 1) trimmed = trimmed.replace(/ +$/, "");
|
|
124
|
+
if (trimmed === "") return;
|
|
125
|
+
result += index === lastNonEmpty ? trimmed : `${trimmed} `;
|
|
126
|
+
});
|
|
127
|
+
return result;
|
|
128
|
+
};
|
|
129
|
+
/** The value of a container that holds only a string, or null for any other expression. */
|
|
130
|
+
const stringContainerValue = (container) => {
|
|
131
|
+
const expression = nodeOf(container, "expression");
|
|
132
|
+
if (expression === null) return null;
|
|
133
|
+
if (expression.type === "StringLiteral" && typeof expression.value === "string") return expression.value;
|
|
134
|
+
if (expression.type === "TemplateLiteral" && nodeList(expression.expressions).length === 0) {
|
|
135
|
+
const cooked = nodeList(expression.quasis)[0]?.value;
|
|
136
|
+
return isObject(cooked) && typeof cooked.cooked === "string" ? cooked.cooked : null;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
};
|
|
140
|
+
const summarize = (source, node) => {
|
|
141
|
+
const text = source.slice(node.start, node.end).replace(/\s+/g, " ").trim();
|
|
142
|
+
return text.length > 60 ? `${text.slice(0, 59)}…` : text;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* The editable text and locked expressions of an element's children, in order. Text is a segment
|
|
146
|
+
* between locked children, so a text segment exists (possibly empty) before, between and after
|
|
147
|
+
* them.
|
|
148
|
+
*/
|
|
149
|
+
const segmentsOf = (source, element) => {
|
|
150
|
+
const segments = [];
|
|
151
|
+
let start = element.contentStart;
|
|
152
|
+
let text = "";
|
|
153
|
+
const flush = (end) => {
|
|
154
|
+
segments.push({
|
|
155
|
+
kind: "text",
|
|
156
|
+
text,
|
|
157
|
+
start,
|
|
158
|
+
end
|
|
159
|
+
});
|
|
160
|
+
text = "";
|
|
161
|
+
};
|
|
162
|
+
for (const child of element.children) {
|
|
163
|
+
if (child.type === "JSXText") {
|
|
164
|
+
text += jsxTextValue(typeof child.value === "string" ? child.value : "");
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (child.type === "JSXExpressionContainer") {
|
|
168
|
+
const value = stringContainerValue(child);
|
|
169
|
+
if (value !== null) {
|
|
170
|
+
text += value;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
flush(child.start);
|
|
175
|
+
segments.push({
|
|
176
|
+
kind: "locked",
|
|
177
|
+
summary: summarize(source, child),
|
|
178
|
+
start: child.start,
|
|
179
|
+
end: child.end
|
|
180
|
+
});
|
|
181
|
+
start = child.end;
|
|
182
|
+
}
|
|
183
|
+
flush(element.contentEnd);
|
|
184
|
+
return segments;
|
|
185
|
+
};
|
|
186
|
+
const isSafeRun = (char) => !/[{}<>&\r\n\t]/.test(char);
|
|
187
|
+
/**
|
|
188
|
+
* JSX for the given text. Characters JSX text cannot carry, and whitespace at an edge the
|
|
189
|
+
* compiler would trim, are written as string expressions; the rest stays plain text.
|
|
190
|
+
*/
|
|
191
|
+
const encodeText = (text, trimStart, trimEnd) => {
|
|
192
|
+
const chars = [...text];
|
|
193
|
+
const unsafe = chars.map((char) => !isSafeRun(char));
|
|
194
|
+
if (trimStart) for (let i = 0; i < chars.length && /\s/.test(chars[i]); i++) unsafe[i] = true;
|
|
195
|
+
if (trimEnd) for (let i = chars.length - 1; i >= 0 && /\s/.test(chars[i]); i--) unsafe[i] = true;
|
|
196
|
+
let result = "";
|
|
197
|
+
let run = "";
|
|
198
|
+
let runUnsafe = false;
|
|
199
|
+
const flush = () => {
|
|
200
|
+
if (run === "") return;
|
|
201
|
+
result += runUnsafe ? `{${JSON.stringify(run)}}` : run;
|
|
202
|
+
run = "";
|
|
203
|
+
};
|
|
204
|
+
chars.forEach((char, index) => {
|
|
205
|
+
if (run !== "" && unsafe[index] !== runUnsafe) flush();
|
|
206
|
+
runUnsafe = unsafe[index];
|
|
207
|
+
run += char;
|
|
208
|
+
});
|
|
209
|
+
flush();
|
|
210
|
+
return result;
|
|
211
|
+
};
|
|
212
|
+
/** Write `text` over the gap at `segment`, keeping the newlines and indentation around it. */
|
|
213
|
+
const writeText = (source, element, segments, index, text) => {
|
|
214
|
+
const segment = segments[index];
|
|
215
|
+
if (segment?.kind !== "text") return {
|
|
216
|
+
ok: false,
|
|
217
|
+
reason: "That part of the body is locked."
|
|
218
|
+
};
|
|
219
|
+
if (segment.text === text) return {
|
|
220
|
+
ok: true,
|
|
221
|
+
edits: []
|
|
222
|
+
};
|
|
223
|
+
if (element.selfClosing) {
|
|
224
|
+
const opening = element.opening;
|
|
225
|
+
let start = opening.end - 2;
|
|
226
|
+
while (start > opening.start && /\s/.test(source[start - 1])) start--;
|
|
227
|
+
return {
|
|
228
|
+
ok: true,
|
|
229
|
+
edits: [{
|
|
230
|
+
start,
|
|
231
|
+
end: opening.end,
|
|
232
|
+
before: source.slice(start, opening.end),
|
|
233
|
+
after: `>${encodeText(text, false, false)}</${element.tag}>`
|
|
234
|
+
}]
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const raw = source.slice(segment.start, segment.end);
|
|
238
|
+
const leading = /^\s*/.exec(raw)?.[0] ?? "";
|
|
239
|
+
const prefix = /[\r\n]/.test(leading) ? leading : "";
|
|
240
|
+
const rest = raw.slice(prefix.length);
|
|
241
|
+
const trailing = /\s*$/.exec(rest)?.[0] ?? "";
|
|
242
|
+
const suffix = /[\r\n]/.test(trailing) ? trailing : "";
|
|
243
|
+
const start = segment.start + prefix.length;
|
|
244
|
+
const end = segment.end - suffix.length;
|
|
245
|
+
return {
|
|
246
|
+
ok: true,
|
|
247
|
+
edits: [{
|
|
248
|
+
start,
|
|
249
|
+
end,
|
|
250
|
+
before: source.slice(start, end),
|
|
251
|
+
after: encodeText(text, prefix !== "", suffix !== "")
|
|
252
|
+
}]
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
const attributeNamed = (element, name) => element.attributes.find((attribute) => attribute.type === "JSXAttribute" && jsxName(nodeOf(attribute, "name")) === name) ?? null;
|
|
256
|
+
const attributeState = (source, element, name) => {
|
|
257
|
+
const attribute = attributeNamed(element, name);
|
|
258
|
+
if (attribute === null) return { kind: "absent" };
|
|
259
|
+
const value = nodeOf(attribute, "value");
|
|
260
|
+
if (value?.type === "StringLiteral" && typeof value.value === "string") return {
|
|
261
|
+
kind: "literal",
|
|
262
|
+
value: value.value
|
|
263
|
+
};
|
|
264
|
+
if (value?.type === "JSXExpressionContainer") {
|
|
265
|
+
const literal = stringContainerValue(value);
|
|
266
|
+
if (literal !== null) return {
|
|
267
|
+
kind: "literal",
|
|
268
|
+
value: literal
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
kind: "expression",
|
|
273
|
+
source: summarize(source, value ?? attribute)
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
const encodeAttribute = (value) => /["&\\\r\n]/.test(value) ? `{${JSON.stringify(value)}}` : `"${value}"`;
|
|
277
|
+
const replacement = (source, start, end, after) => ({
|
|
278
|
+
start,
|
|
279
|
+
end,
|
|
280
|
+
before: source.slice(start, end),
|
|
281
|
+
after
|
|
282
|
+
});
|
|
283
|
+
/** Set, add or remove one string attribute of an element. */
|
|
284
|
+
const setAttribute = (source, element, name, value) => {
|
|
285
|
+
const attribute = attributeNamed(element, name);
|
|
286
|
+
if (attribute === null) {
|
|
287
|
+
if (value === void 0) return {
|
|
288
|
+
ok: true,
|
|
289
|
+
edits: []
|
|
290
|
+
};
|
|
291
|
+
const anchor = element.attributes.at(-1)?.end ?? nodeOf(element.opening, "name")?.end ?? element.opening.end;
|
|
292
|
+
return {
|
|
293
|
+
ok: true,
|
|
294
|
+
edits: [replacement(source, anchor, anchor, ` ${name}=${encodeAttribute(value)}`)]
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (attributeState(source, element, name).kind === "expression") return {
|
|
298
|
+
ok: false,
|
|
299
|
+
reason: `\`${name}\` reads the input. Edit it in Source.`
|
|
300
|
+
};
|
|
301
|
+
if (value === void 0) {
|
|
302
|
+
let start = attribute.start;
|
|
303
|
+
while (start > element.opening.start && /\s/.test(source[start - 1])) start--;
|
|
304
|
+
return {
|
|
305
|
+
ok: true,
|
|
306
|
+
edits: [replacement(source, start, attribute.end, "")]
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const literal = nodeOf(attribute, "value");
|
|
310
|
+
return {
|
|
311
|
+
ok: true,
|
|
312
|
+
edits: literal === null ? [replacement(source, attribute.end, attribute.end, `=${encodeAttribute(value)}`)] : [replacement(source, literal.start, literal.end, encodeAttribute(value))]
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
const INDENT = " ";
|
|
316
|
+
/** The indentation of the line `position` is on, when only whitespace precedes it there. */
|
|
317
|
+
const ownLineIndent = (source, position) => {
|
|
318
|
+
const lineStart = source.lastIndexOf("\n", position - 1) + 1;
|
|
319
|
+
const lead = source.slice(lineStart, position);
|
|
320
|
+
return /^[ \t]*$/.test(lead) ? lead : null;
|
|
321
|
+
};
|
|
322
|
+
/** Remove a child, and its line when it is alone on one. */
|
|
323
|
+
const removeChild = (source, node) => {
|
|
324
|
+
const indent = ownLineIndent(source, node.start);
|
|
325
|
+
const after = /^[ \t]*(\r?\n)/.exec(source.slice(node.end));
|
|
326
|
+
if (indent === null || after === null) return replacement(source, node.start, node.end, "");
|
|
327
|
+
const start = node.start - indent.length;
|
|
328
|
+
return replacement(source, start, node.end + after[0].length, "");
|
|
329
|
+
};
|
|
330
|
+
/** Insert markup on its own line before or after a sibling, or inline when the sibling is not alone. */
|
|
331
|
+
const insertBeside = (source, sibling, side, text) => {
|
|
332
|
+
const indent = ownLineIndent(source, sibling.start);
|
|
333
|
+
if (side === "before") return replacement(source, sibling.start, sibling.start, indent === null ? text : `${text}\n${indent}`);
|
|
334
|
+
return replacement(source, sibling.end, sibling.end, indent === null ? text : `\n${indent}${text}`);
|
|
335
|
+
};
|
|
336
|
+
/** The input fields read by the expressions inside `root`, as dotted paths. */
|
|
337
|
+
const variablesOf = (source, root, parameter) => {
|
|
338
|
+
const names = /* @__PURE__ */ new Set();
|
|
339
|
+
const scan = (text) => {
|
|
340
|
+
if (parameter?.type === "Identifier" && typeof parameter.name === "string") {
|
|
341
|
+
const pattern = new RegExp(`\\b${parameter.name}((?:\\??\\.[A-Za-z_$][\\w$]*)+)`, "g");
|
|
342
|
+
for (const match of text.matchAll(pattern)) names.add(match[1].replace(/\?/g, "").slice(1));
|
|
343
|
+
} else if (parameter?.type === "ObjectPattern") for (const property of nodeList(parameter.properties)) {
|
|
344
|
+
const key = nodeOf(property, "key");
|
|
345
|
+
if (key?.type === "Identifier" && typeof key.name === "string") {
|
|
346
|
+
if (new RegExp(`\\b${key.name}\\b`).test(text)) names.add(key.name);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
walk(root.node, (node) => {
|
|
351
|
+
if (node.type === "JSXExpressionContainer" && stringContainerValue(node) === null) scan(source.slice(node.start, node.end));
|
|
352
|
+
});
|
|
353
|
+
return [...names].sort();
|
|
354
|
+
};
|
|
355
|
+
const smsChannel = (source) => {
|
|
356
|
+
const tree = parseSource(source);
|
|
357
|
+
if (tree === null) return null;
|
|
358
|
+
const channel = channelFunction(tree, "sms");
|
|
359
|
+
if (channel === null || channel.root.tag !== importedAs(tree, "@samva/markup/sms", "Sms")) return null;
|
|
360
|
+
return {
|
|
361
|
+
...channel,
|
|
362
|
+
tree
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
/** The form model of the template's SMS body, or null when it is not a `<Sms>` written in place. */
|
|
366
|
+
const readSmsChannel = (source) => {
|
|
367
|
+
const channel = smsChannel(source);
|
|
368
|
+
if (channel === null) return null;
|
|
369
|
+
return {
|
|
370
|
+
category: attributeState(source, channel.root, "category"),
|
|
371
|
+
segments: segmentsOf(source, channel.root),
|
|
372
|
+
variables: variablesOf(source, channel.root, channel.parameter)
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
/** The source replacements that apply one form change to the SMS body. */
|
|
376
|
+
const editSmsChannel = (source, edit) => {
|
|
377
|
+
const channel = smsChannel(source);
|
|
378
|
+
if (channel === null) return {
|
|
379
|
+
ok: false,
|
|
380
|
+
reason: "The SMS body is not editable here."
|
|
381
|
+
};
|
|
382
|
+
if (edit.kind === "category") {
|
|
383
|
+
if (edit.value !== void 0 && !SMS_CATEGORIES.includes(edit.value)) return {
|
|
384
|
+
ok: false,
|
|
385
|
+
reason: `\`${edit.value}\` is not an SMS category.`
|
|
386
|
+
};
|
|
387
|
+
return setAttribute(source, channel.root, "category", edit.value);
|
|
388
|
+
}
|
|
389
|
+
return writeText(source, channel.root, segmentsOf(source, channel.root), edit.segment, edit.text);
|
|
390
|
+
};
|
|
391
|
+
const BUTTON_TAGS = {
|
|
392
|
+
"quick-reply": "QuickReplyButton",
|
|
393
|
+
url: "UrlButton",
|
|
394
|
+
phone: "PhoneButton",
|
|
395
|
+
"copy-code": "CopyCodeButton"
|
|
396
|
+
};
|
|
397
|
+
const BUTTON_DEFAULTS = {
|
|
398
|
+
"quick-reply": "Reply",
|
|
399
|
+
url: "Visit site",
|
|
400
|
+
phone: "Call us",
|
|
401
|
+
"copy-code": "CODE"
|
|
402
|
+
};
|
|
403
|
+
const kindOfTag = (tag, local) => {
|
|
404
|
+
if (!tag.startsWith(`${local}.`)) return null;
|
|
405
|
+
const part = tag.slice(local.length + 1);
|
|
406
|
+
return Object.keys(BUTTON_TAGS).find((kind) => BUTTON_TAGS[kind] === part) ?? null;
|
|
407
|
+
};
|
|
408
|
+
const whatsappChannel = (source) => {
|
|
409
|
+
const tree = parseSource(source);
|
|
410
|
+
if (tree === null) return null;
|
|
411
|
+
const channel = channelFunction(tree, "whatsapp");
|
|
412
|
+
const local = importedAs(tree, "@samva/markup/whatsapp", "WhatsApp");
|
|
413
|
+
if (channel === null || channel.root.tag !== local) return null;
|
|
414
|
+
const part = (name) => {
|
|
415
|
+
for (const child of channel.root.children) {
|
|
416
|
+
const element = elementOf(child);
|
|
417
|
+
if (element?.tag === `${local}.${name}`) return element;
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
};
|
|
421
|
+
return {
|
|
422
|
+
...channel,
|
|
423
|
+
local,
|
|
424
|
+
header: part("Header"),
|
|
425
|
+
body: part("Body"),
|
|
426
|
+
footer: part("Footer"),
|
|
427
|
+
buttons: part("Buttons")
|
|
428
|
+
};
|
|
429
|
+
};
|
|
430
|
+
const buttonRows = (source, channel) => {
|
|
431
|
+
if (channel.buttons === null) return [];
|
|
432
|
+
const rows = [];
|
|
433
|
+
for (const child of channel.buttons.children) {
|
|
434
|
+
if (child.type === "JSXText" && jsxTextValue(String(child.value ?? "")) === "") continue;
|
|
435
|
+
const element = elementOf(child);
|
|
436
|
+
const kind = element === null ? null : kindOfTag(element.tag, channel.local);
|
|
437
|
+
if (element === null || kind === null) {
|
|
438
|
+
rows.push({
|
|
439
|
+
row: {
|
|
440
|
+
kind: "locked",
|
|
441
|
+
summary: summarize(source, child)
|
|
442
|
+
},
|
|
443
|
+
node: child
|
|
444
|
+
});
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const segments = segmentsOf(source, element);
|
|
448
|
+
const label = segments[0];
|
|
449
|
+
rows.push({
|
|
450
|
+
node: child,
|
|
451
|
+
row: segments.length === 1 && label?.kind === "text" ? {
|
|
452
|
+
kind: "editable",
|
|
453
|
+
type: kind,
|
|
454
|
+
text: label.text,
|
|
455
|
+
url: attributeState(source, element, "url"),
|
|
456
|
+
phone: attributeState(source, element, "phone")
|
|
457
|
+
} : {
|
|
458
|
+
kind: "locked",
|
|
459
|
+
summary: summarize(source, child)
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return rows;
|
|
464
|
+
};
|
|
465
|
+
/** The form model of the template's WhatsApp body, or null when it is not a `<WhatsApp>` written in place. */
|
|
466
|
+
const readWhatsAppChannel = (source) => {
|
|
467
|
+
const channel = whatsappChannel(source);
|
|
468
|
+
if (channel === null || channel.body === null) return null;
|
|
469
|
+
const { root } = channel;
|
|
470
|
+
return {
|
|
471
|
+
name: attributeState(source, root, "name"),
|
|
472
|
+
language: attributeState(source, root, "language"),
|
|
473
|
+
category: attributeState(source, root, "category"),
|
|
474
|
+
header: channel.header === null ? null : segmentsOf(source, channel.header),
|
|
475
|
+
headerType: channel.header === null ? { kind: "absent" } : attributeState(source, channel.header, "type"),
|
|
476
|
+
body: segmentsOf(source, channel.body),
|
|
477
|
+
footer: channel.footer === null ? null : segmentsOf(source, channel.footer),
|
|
478
|
+
buttons: buttonRows(source, channel).map(({ row }) => row),
|
|
479
|
+
variables: variablesOf(source, root, channel.parameter)
|
|
480
|
+
};
|
|
481
|
+
};
|
|
482
|
+
const buttonMarkup = (local, type, text, attributes) => {
|
|
483
|
+
const tag = `${local}.${BUTTON_TAGS[type]}`;
|
|
484
|
+
return `<${tag}${type === "url" && attributes.url !== void 0 ? ` url=${encodeAttribute(attributes.url)}` : type === "phone" && attributes.phone !== void 0 ? ` phone=${encodeAttribute(attributes.phone)}` : ""}>${encodeText(text, false, false)}</${tag}>`;
|
|
485
|
+
};
|
|
486
|
+
const newButton = (local, type) => buttonMarkup(local, type, BUTTON_DEFAULTS[type], {
|
|
487
|
+
url: "https://example.com",
|
|
488
|
+
phone: "+15555550100"
|
|
489
|
+
});
|
|
490
|
+
const editPart = (source, channel, part, index, text) => {
|
|
491
|
+
const element = channel[part];
|
|
492
|
+
if (element === null) {
|
|
493
|
+
if (part === "body" || text === "" || index !== 0) return {
|
|
494
|
+
ok: true,
|
|
495
|
+
edits: []
|
|
496
|
+
};
|
|
497
|
+
const tag = `${channel.local}.${part === "header" ? "Header" : "Footer"}`;
|
|
498
|
+
const markup = `<${tag}>${encodeText(text, false, false)}</${tag}>`;
|
|
499
|
+
const anchor = part === "header" ? channel.body.node : channel.buttons?.node ?? channel.body.node;
|
|
500
|
+
return {
|
|
501
|
+
ok: true,
|
|
502
|
+
edits: [insertBeside(source, anchor, part === "header" ? "before" : "after", markup)]
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const segments = segmentsOf(source, element);
|
|
506
|
+
if (part !== "body" && text === "" && segments.length === 1) return {
|
|
507
|
+
ok: true,
|
|
508
|
+
edits: [removeChild(source, element.node)]
|
|
509
|
+
};
|
|
510
|
+
return writeText(source, element, segments, index, text);
|
|
511
|
+
};
|
|
512
|
+
/** The source replacements that apply one form change to the WhatsApp body. */
|
|
513
|
+
const editWhatsAppChannel = (source, edit) => {
|
|
514
|
+
const channel = whatsappChannel(source);
|
|
515
|
+
if (channel === null || channel.body === null) return {
|
|
516
|
+
ok: false,
|
|
517
|
+
reason: "The WhatsApp body is not editable here."
|
|
518
|
+
};
|
|
519
|
+
const { root, local } = channel;
|
|
520
|
+
switch (edit.kind) {
|
|
521
|
+
case "attribute": {
|
|
522
|
+
if (edit.name === "category" && !WHATSAPP_CATEGORIES.includes(edit.value ?? "")) return {
|
|
523
|
+
ok: false,
|
|
524
|
+
reason: "Choose a WhatsApp category."
|
|
525
|
+
};
|
|
526
|
+
const value = edit.name === "name" && edit.value === "" ? void 0 : edit.value ?? "";
|
|
527
|
+
return setAttribute(source, root, edit.name, value);
|
|
528
|
+
}
|
|
529
|
+
case "headerType":
|
|
530
|
+
if (channel.header === null) return {
|
|
531
|
+
ok: false,
|
|
532
|
+
reason: "Add the header text before choosing its type."
|
|
533
|
+
};
|
|
534
|
+
if (!WHATSAPP_HEADER_TYPES.includes(edit.value)) return {
|
|
535
|
+
ok: false,
|
|
536
|
+
reason: `\`${edit.value}\` is not a header type.`
|
|
537
|
+
};
|
|
538
|
+
return setAttribute(source, channel.header, "type", edit.value === "text" ? void 0 : edit.value);
|
|
539
|
+
case "text": return editPart(source, channel, edit.part, edit.segment, edit.text);
|
|
540
|
+
case "buttonAdd": {
|
|
541
|
+
const button = newButton(local, edit.type);
|
|
542
|
+
const buttons = channel.buttons;
|
|
543
|
+
if (buttons === null) {
|
|
544
|
+
const anchor = channel.footer ?? channel.body;
|
|
545
|
+
const tag = `${local}.Buttons`;
|
|
546
|
+
const indent = ownLineIndent(source, anchor.node.start);
|
|
547
|
+
const markup = indent === null ? `<${tag}>${button}</${tag}>` : `<${tag}>\n${indent}${INDENT}${button}\n${indent}</${tag}>`;
|
|
548
|
+
return {
|
|
549
|
+
ok: true,
|
|
550
|
+
edits: [insertBeside(source, anchor.node, "after", markup)]
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
if (buttons.selfClosing) {
|
|
554
|
+
const tag = `${local}.Buttons`;
|
|
555
|
+
const indent = ownLineIndent(source, buttons.node.start);
|
|
556
|
+
const markup = indent === null ? `<${tag}>${button}</${tag}>` : `<${tag}>\n${indent}${INDENT}${button}\n${indent}</${tag}>`;
|
|
557
|
+
return {
|
|
558
|
+
ok: true,
|
|
559
|
+
edits: [replacement(source, buttons.node.start, buttons.node.end, markup)]
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
const last = buttons.children.findLast((child) => child.type !== "JSXText" || jsxTextValue(String(child.value ?? "")) !== "");
|
|
563
|
+
if (last !== void 0) return {
|
|
564
|
+
ok: true,
|
|
565
|
+
edits: [insertBeside(source, last, "after", button)]
|
|
566
|
+
};
|
|
567
|
+
const indent = ownLineIndent(source, buttons.node.start);
|
|
568
|
+
return {
|
|
569
|
+
ok: true,
|
|
570
|
+
edits: [replacement(source, buttons.contentStart, buttons.contentEnd, indent === null ? button : `\n${indent}${INDENT}${button}\n${indent}`)]
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const rows = buttonRows(source, channel);
|
|
575
|
+
const target = rows[edit.button];
|
|
576
|
+
if (target === void 0 || channel.buttons === null) return {
|
|
577
|
+
ok: false,
|
|
578
|
+
reason: "That button no longer exists."
|
|
579
|
+
};
|
|
580
|
+
if (edit.kind === "buttonRemove") return {
|
|
581
|
+
ok: true,
|
|
582
|
+
edits: [removeChild(source, rows.length === 1 ? channel.buttons.node : target.node)]
|
|
583
|
+
};
|
|
584
|
+
const element = elementOf(target.node);
|
|
585
|
+
if (target.row.kind === "locked" || element === null) return {
|
|
586
|
+
ok: false,
|
|
587
|
+
reason: "That button is locked. Edit it in Source."
|
|
588
|
+
};
|
|
589
|
+
switch (edit.kind) {
|
|
590
|
+
case "buttonText": return writeText(source, element, segmentsOf(source, element), 0, edit.text);
|
|
591
|
+
case "buttonAttribute": return setAttribute(source, element, edit.name, edit.value);
|
|
592
|
+
default: {
|
|
593
|
+
const { row } = target;
|
|
594
|
+
const keep = (state) => state.kind === "literal" ? state.value : void 0;
|
|
595
|
+
const markup = buttonMarkup(local, edit.type, row.text, {
|
|
596
|
+
url: keep(row.url) ?? "https://example.com",
|
|
597
|
+
phone: keep(row.phone) ?? "+15555550100"
|
|
598
|
+
});
|
|
599
|
+
return {
|
|
600
|
+
ok: true,
|
|
601
|
+
edits: [replacement(source, target.node.start, target.node.end, markup)]
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
//#endregion
|
|
607
|
+
export { editSmsChannel, editWhatsAppChannel, readSmsChannel, readWhatsAppChannel };
|
|
608
|
+
|
|
609
|
+
//# sourceMappingURL=channel-edit.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/channel-specs.ts
|
|
2
|
+
const SMS_CATEGORIES = [
|
|
3
|
+
"transactional",
|
|
4
|
+
"promotional",
|
|
5
|
+
"service_implicit"
|
|
6
|
+
];
|
|
7
|
+
const WHATSAPP_CATEGORIES = [
|
|
8
|
+
"marketing",
|
|
9
|
+
"utility",
|
|
10
|
+
"authentication"
|
|
11
|
+
];
|
|
12
|
+
const WHATSAPP_HEADER_TYPES = [
|
|
13
|
+
"text",
|
|
14
|
+
"image",
|
|
15
|
+
"video",
|
|
16
|
+
"document"
|
|
17
|
+
];
|
|
18
|
+
/** The attributes `<Sms>` accepts. */
|
|
19
|
+
const SMS_ATTRIBUTES = ["category"];
|
|
20
|
+
/** The attributes `<WhatsApp>` accepts. */
|
|
21
|
+
const WHATSAPP_ATTRIBUTES = [
|
|
22
|
+
"name",
|
|
23
|
+
"language",
|
|
24
|
+
"category"
|
|
25
|
+
];
|
|
26
|
+
//#endregion
|
|
27
|
+
export { WHATSAPP_HEADER_TYPES as a, WHATSAPP_CATEGORIES as i, SMS_CATEGORIES as n, WHATSAPP_ATTRIBUTES as r, SMS_ATTRIBUTES as t };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=channel-specs-B6M_HlaV.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/channel-specs.d.ts
|
|
2
|
+
declare const SMS_CATEGORIES: readonly ["transactional", "promotional", "service_implicit"];
|
|
3
|
+
declare const WHATSAPP_CATEGORIES: readonly ["marketing", "utility", "authentication"];
|
|
4
|
+
declare const WHATSAPP_HEADER_TYPES: readonly ["text", "image", "video", "document"];
|
|
5
|
+
/** The SMS categories `<Sms category>` accepts. */
|
|
6
|
+
type SmsCategory = (typeof SMS_CATEGORIES)[number];
|
|
7
|
+
/** The Meta template categories `<WhatsApp category>` accepts. */
|
|
8
|
+
type WhatsAppCategory = (typeof WHATSAPP_CATEGORIES)[number];
|
|
9
|
+
/** The header kinds `<WhatsApp.Header type>` accepts. */
|
|
10
|
+
type WhatsAppHeaderType = (typeof WHATSAPP_HEADER_TYPES)[number];
|
|
11
|
+
//#endregion
|
|
12
|
+
export { WhatsAppCategory as n, WhatsAppHeaderType as r, SmsCategory as t };
|
|
13
|
+
//# sourceMappingURL=channel-specs-C2feJwuP.d.ts.map
|