@samva/markup 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/AGENTS.md +90 -37
  2. package/README.md +188 -52
  3. package/dist/assets-C5duwTya.d.ts +40 -0
  4. package/dist/{brand-DcoVkh_S.d.ts → brand-B-0bN_Ir.d.ts} +2 -2
  5. package/dist/{render-vAv_4r9v.js → cascade-t6kxlgEI.js} +93 -280
  6. package/dist/channel-edit.d.ts +121 -0
  7. package/dist/channel-edit.js +609 -0
  8. package/dist/channel-specs-B6M_HlaV.js +29 -0
  9. package/dist/channel-specs-C2feJwuP.d.ts +13 -0
  10. package/dist/compatibility-DbISoFIq.js +589 -0
  11. package/dist/compiler.d.ts +10 -257
  12. package/dist/compiler.js +10 -730
  13. package/dist/{css-CS2jLID6.d.ts → css-BVQKICKC.d.ts} +3 -3
  14. package/dist/diagnostic-codes.d.ts +403 -0
  15. package/dist/diagnostic-codes.js +399 -0
  16. package/dist/{diagnostics-BMYtBCQk.d.ts → diagnostics-B-Gym34R.d.ts} +8 -4
  17. package/dist/diagnostics.d.ts +31 -2
  18. package/dist/diagnostics.js +1 -19
  19. package/dist/{elements-BzjjnoCS.d.ts → elements-cqldpaJt.d.ts} +3 -3
  20. package/dist/email/assets.d.ts +2 -40
  21. package/dist/email/brand-footer.d.ts +2 -2
  22. package/dist/email/brand-footer.js +4 -3
  23. package/dist/email/brand.d.ts +1 -1
  24. package/dist/email/brand.js +1 -1
  25. package/dist/email/components.d.ts +21 -10
  26. package/dist/email/components.js +46 -11
  27. package/dist/email/diagnostics.d.ts +2 -2
  28. package/dist/email/diagnostics.js +2 -1
  29. package/dist/email/elements.d.ts +1 -1
  30. package/dist/email/html.d.ts +2 -2
  31. package/dist/email/html.js +45 -2
  32. package/dist/email/jsx-dev-runtime.d.ts +2 -2
  33. package/dist/email/jsx-runtime.d.ts +1 -1
  34. package/dist/email/render.d.ts +8 -18
  35. package/dist/email/render.js +163 -3
  36. package/dist/email/text.d.ts +1 -1
  37. package/dist/fmt.d.ts +32 -0
  38. package/dist/fmt.js +174 -0
  39. package/dist/{html-Bl9BAdqc.d.ts → html-BJepOwZ4.d.ts} +19 -5
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.js +2 -0
  42. package/dist/ir-aaqNE987.d.ts +252 -0
  43. package/dist/ir.d.ts +2 -0
  44. package/dist/ir.js +7 -0
  45. package/dist/preview-Ekdg1dXG.d.ts +100 -0
  46. package/dist/preview.d.ts +2 -0
  47. package/dist/preview.js +160 -0
  48. package/dist/profile-BH-OtdDA.js +3670 -0
  49. package/dist/render-ir.d.ts +19 -0
  50. package/dist/render-ir.js +826 -0
  51. package/dist/sms-segments.d.ts +13 -2
  52. package/dist/sms.d.ts +16 -0
  53. package/dist/sms.js +12 -0
  54. package/dist/{email/source-edit.d.ts → source-edit.d.ts} +55 -6
  55. package/dist/source-edit.js +343 -0
  56. package/dist/source-locations-C8wCsiAy.d.ts +23 -0
  57. package/dist/source-locations.d.ts +2 -2
  58. package/dist/source-locations.js +3 -89
  59. package/dist/static.d.ts +57 -0
  60. package/dist/static.js +69 -0
  61. package/dist/tailwind-CMYpZf1v.js +614 -0
  62. package/dist/template-_QE_rQND.d.ts +65 -0
  63. package/dist/template.d.ts +2 -2
  64. package/dist/template.js +15 -10
  65. package/dist/theme-css.d.ts +1 -1
  66. package/dist/theme-imports-lvBw3AD1.js +109 -0
  67. package/dist/transform-Ct2OMkFN.d.ts +153 -0
  68. package/dist/whatsapp.d.ts +57 -0
  69. package/dist/whatsapp.js +22 -0
  70. package/generated/contract.json +1782 -0
  71. package/generated/reference.md +353 -0
  72. package/package.json +41 -151
  73. package/skills/sml/SKILL.md +123 -0
  74. package/dist/ast-BrDtApwS.d.ts +0 -107
  75. package/dist/ast.d.ts +0 -2
  76. package/dist/ast.js +0 -124
  77. package/dist/candidate.d.ts +0 -29
  78. package/dist/candidate.js +0 -88
  79. package/dist/catalog-CKuOooB8.d.ts +0 -354
  80. package/dist/checks-D73Xizbw.js +0 -59366
  81. package/dist/checks.d.ts +0 -33
  82. package/dist/checks.js +0 -2
  83. package/dist/classes.d.ts +0 -26
  84. package/dist/classes.js +0 -184
  85. package/dist/compile-sms.d.ts +0 -28
  86. package/dist/compile-sms.js +0 -44
  87. package/dist/compile-whatsapp.d.ts +0 -56
  88. package/dist/compile-whatsapp.js +0 -186
  89. package/dist/compile.d.ts +0 -18
  90. package/dist/compile.js +0 -29
  91. package/dist/components.d.ts +0 -289
  92. package/dist/components.js +0 -106
  93. package/dist/conditionals.d.ts +0 -10
  94. package/dist/conditionals.js +0 -74
  95. package/dist/corpus-CPPfgAHN.d.ts +0 -106
  96. package/dist/diagnostics-OcQKQYSY.d.ts +0 -49
  97. package/dist/email/selection.d.ts +0 -2
  98. package/dist/email/selection.js +0 -91
  99. package/dist/email/source-edit.js +0 -185
  100. package/dist/email/stylesheets.d.ts +0 -15
  101. package/dist/email/stylesheets.js +0 -48
  102. package/dist/emit.d.ts +0 -51
  103. package/dist/emit.js +0 -151
  104. package/dist/interpolate.d.ts +0 -24
  105. package/dist/interpolate.js +0 -43
  106. package/dist/jsx-dev-runtime.d.ts +0 -11
  107. package/dist/jsx-dev-runtime.js +0 -15
  108. package/dist/jsx-runtime-I-1nCtl2.d.ts +0 -57
  109. package/dist/jsx-runtime.d.ts +0 -2
  110. package/dist/jsx-runtime.js +0 -121
  111. package/dist/lexer.d.ts +0 -60
  112. package/dist/lexer.js +0 -281
  113. package/dist/model.d.ts +0 -24
  114. package/dist/model.js +0 -77
  115. package/dist/ops-BvwqHl7V.js +0 -630
  116. package/dist/ops.d.ts +0 -114
  117. package/dist/ops.js +0 -2
  118. package/dist/parser-t29cHLVL.d.ts +0 -396
  119. package/dist/parser.d.ts +0 -2
  120. package/dist/parser.js +0 -859
  121. package/dist/predicate-BI6uRRM-.js +0 -549
  122. package/dist/predicate-M-05Nd7R.d.ts +0 -56
  123. package/dist/predicate.d.ts +0 -2
  124. package/dist/predicate.js +0 -2
  125. package/dist/printer.d.ts +0 -6
  126. package/dist/printer.js +0 -122
  127. package/dist/registry-Dcp6QEK_.d.ts +0 -73
  128. package/dist/registry.d.ts +0 -2
  129. package/dist/registry.js +0 -646
  130. package/dist/render-template.d.ts +0 -45
  131. package/dist/render-template.js +0 -76
  132. package/dist/selection-CCArx9lU.d.ts +0 -91
  133. package/dist/showcase-egkusQBf.d.ts +0 -415
  134. package/dist/sms-segments-y4FpPilJ.d.ts +0 -13
  135. package/dist/source-locations-DrCM82Ym.d.ts +0 -67
  136. package/dist/starters/category-mapping.d.ts +0 -13
  137. package/dist/starters/category-mapping.js +0 -49
  138. package/dist/starters/corpus.d.ts +0 -2
  139. package/dist/starters/corpus.js +0 -194
  140. package/dist/starters/library.d.ts +0 -18
  141. package/dist/starters/library.js +0 -692
  142. package/dist/starters/public-gallery.d.ts +0 -266
  143. package/dist/starters/public-gallery.js +0 -916
  144. package/dist/starters/public-handoff.d.ts +0 -164
  145. package/dist/starters/public-handoff.js +0 -361
  146. package/dist/starters/public-preview-artifacts.d.ts +0 -43
  147. package/dist/starters/public-preview-artifacts.js +0 -104
  148. package/dist/starters/public-preview.d.ts +0 -61
  149. package/dist/starters/public-preview.js +0 -83
  150. package/dist/starters/showcase.d.ts +0 -2
  151. package/dist/starters/showcase.js +0 -335
  152. package/dist/starters/sources.d.ts +0 -45
  153. package/dist/starters/sources.js +0 -7762
  154. package/dist/starters/theme.d.ts +0 -45
  155. package/dist/starters/theme.js +0 -77
  156. package/dist/starters.d.ts +0 -2
  157. package/dist/starters.js +0 -874
  158. package/dist/tailwind-BYq7Lo6u.js +0 -727
  159. package/dist/template-DWftqh4x.d.ts +0 -60
  160. package/dist/tsx-source-instrumentation.d.ts +0 -30
  161. package/dist/tsx-source-instrumentation.js +0 -115
  162. package/dist/tsx-tracer.d.ts +0 -112
  163. package/dist/tsx-tracer.js +0 -985
  164. package/dist/variables-J_L3qxRa.d.ts +0 -77
  165. package/dist/variables.d.ts +0 -2
  166. package/dist/variables.js +0 -141
  167. package/dist/version-BITr58kk.js +0 -96
  168. package/generated/sml-agent-contract.json +0 -709
  169. package/generated/sml-email-agent-contract.json +0 -615
  170. package/generated/sml-email-language-reference.md +0 -94
  171. 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