@samva/markup 0.7.0 → 0.9.0

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