@samva/markup 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/AGENTS.md +107 -46
  2. package/CHANGELOG.md +345 -0
  3. package/README.md +188 -52
  4. package/dist/{brand-DcoVkh_S.d.ts → assets-BIsyMQ92.d.ts} +41 -11
  5. package/dist/{email/brand.js → brand-C0MKTwmX.js} +64 -4
  6. package/dist/{theme-css.d.ts → brand.d.ts} +12 -3
  7. package/dist/brand.js +4 -0
  8. package/dist/channel-specs-C2feJwuP.d.ts +13 -0
  9. package/dist/compiler.d.ts +88 -115
  10. package/dist/compiler.js +750 -171
  11. package/dist/components-BZCqegS8.js +1864 -0
  12. package/dist/diagnostic-model-DJtqbETd.js +38 -0
  13. package/dist/{diagnostics-OcQKQYSY.d.ts → diagnostic-model-DxthUZBT.d.ts} +3 -21
  14. package/dist/{diagnostics-BMYtBCQk.d.ts → diagnostics-B-Gym34R.d.ts} +8 -4
  15. package/dist/{email/diagnostics.js → diagnostics-CnKFefKz.js} +4 -3
  16. package/dist/diagnostics.d.ts +410 -2
  17. package/dist/diagnostics.js +402 -52
  18. package/dist/edit.d.ts +234 -0
  19. package/dist/edit.js +947 -0
  20. package/dist/email/jsx-dev-runtime.d.ts +2 -2
  21. package/dist/email/jsx-dev-runtime.js +1 -2
  22. package/dist/email/jsx-runtime.d.ts +1 -1
  23. package/dist/email/jsx-runtime.js +1 -85
  24. package/dist/{email/components.d.ts → email.d.ts} +22 -11
  25. package/dist/email.js +2 -0
  26. package/dist/fmt.d.ts +32 -0
  27. package/dist/fmt.js +174 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +2 -0
  30. package/dist/{render-vAv_4r9v.js → ir-CLhlklKV.js} +96 -279
  31. package/dist/jsx-runtime-CBlexoeR.js +98 -0
  32. package/dist/{elements-BzjjnoCS.d.ts → jsx-runtime-ylIOnaSf.d.ts} +76 -76
  33. package/dist/profile-pWGtWZXF.js +3803 -0
  34. package/dist/render-BPonDpCl.d.ts +539 -0
  35. package/dist/render-BQmfh94k.js +257 -0
  36. package/dist/render.d.ts +35 -0
  37. package/dist/render.js +49 -0
  38. package/dist/sms.d.ts +16 -0
  39. package/dist/sms.js +12 -0
  40. package/dist/source-locations-C8wCsiAy.d.ts +23 -0
  41. package/dist/template-DMPwxMSq.d.ts +65 -0
  42. package/dist/template-zLUjcwfF.js +42 -0
  43. package/dist/{theme-css-hFqcPSar.js → theme-css-JtRCm1fb.js} +2 -2
  44. package/dist/whatsapp.d.ts +57 -0
  45. package/dist/whatsapp.js +22 -0
  46. package/generated/contract.json +1788 -0
  47. package/generated/reference.md +360 -0
  48. package/package.json +46 -239
  49. package/samva-brand.d.ts +4 -4
  50. package/skills/sml/SKILL.md +123 -0
  51. package/dist/ast-BrDtApwS.d.ts +0 -107
  52. package/dist/ast.d.ts +0 -2
  53. package/dist/ast.js +0 -124
  54. package/dist/candidate.d.ts +0 -29
  55. package/dist/candidate.js +0 -88
  56. package/dist/catalog-CKuOooB8.d.ts +0 -354
  57. package/dist/checks-D73Xizbw.js +0 -59366
  58. package/dist/checks.d.ts +0 -33
  59. package/dist/checks.js +0 -2
  60. package/dist/classes.d.ts +0 -26
  61. package/dist/classes.js +0 -184
  62. package/dist/compile-sms.d.ts +0 -28
  63. package/dist/compile-sms.js +0 -44
  64. package/dist/compile-whatsapp.d.ts +0 -56
  65. package/dist/compile-whatsapp.js +0 -186
  66. package/dist/compile.d.ts +0 -18
  67. package/dist/compile.js +0 -29
  68. package/dist/components.d.ts +0 -289
  69. package/dist/components.js +0 -106
  70. package/dist/conditionals.d.ts +0 -10
  71. package/dist/conditionals.js +0 -74
  72. package/dist/corpus-CPPfgAHN.d.ts +0 -106
  73. package/dist/css-CS2jLID6.d.ts +0 -109
  74. package/dist/email/assets.d.ts +0 -40
  75. package/dist/email/assets.js +0 -63
  76. package/dist/email/brand-footer.d.ts +0 -34
  77. package/dist/email/brand-footer.js +0 -62
  78. package/dist/email/brand.d.ts +0 -2
  79. package/dist/email/components.js +0 -240
  80. package/dist/email/diagnostics.d.ts +0 -2
  81. package/dist/email/elements.d.ts +0 -2
  82. package/dist/email/elements.js +0 -299
  83. package/dist/email/html.d.ts +0 -2
  84. package/dist/email/html.js +0 -278
  85. package/dist/email/render.d.ts +0 -50
  86. package/dist/email/render.js +0 -3
  87. package/dist/email/selection.d.ts +0 -2
  88. package/dist/email/selection.js +0 -91
  89. package/dist/email/source-edit.d.ts +0 -67
  90. package/dist/email/source-edit.js +0 -185
  91. package/dist/email/stylesheets.d.ts +0 -15
  92. package/dist/email/stylesheets.js +0 -48
  93. package/dist/email/text.d.ts +0 -7
  94. package/dist/email/text.js +0 -100
  95. package/dist/emit.d.ts +0 -51
  96. package/dist/emit.js +0 -151
  97. package/dist/html-Bl9BAdqc.d.ts +0 -42
  98. package/dist/interpolate.d.ts +0 -24
  99. package/dist/interpolate.js +0 -43
  100. package/dist/jsx-dev-runtime.d.ts +0 -11
  101. package/dist/jsx-dev-runtime.js +0 -15
  102. package/dist/jsx-runtime-I-1nCtl2.d.ts +0 -57
  103. package/dist/jsx-runtime.d.ts +0 -2
  104. package/dist/jsx-runtime.js +0 -121
  105. package/dist/lexer.d.ts +0 -60
  106. package/dist/lexer.js +0 -281
  107. package/dist/model.d.ts +0 -24
  108. package/dist/model.js +0 -77
  109. package/dist/ops-BvwqHl7V.js +0 -630
  110. package/dist/ops.d.ts +0 -114
  111. package/dist/ops.js +0 -2
  112. package/dist/parser-t29cHLVL.d.ts +0 -396
  113. package/dist/parser.d.ts +0 -2
  114. package/dist/parser.js +0 -859
  115. package/dist/predicate-BI6uRRM-.js +0 -549
  116. package/dist/predicate-M-05Nd7R.d.ts +0 -56
  117. package/dist/predicate.d.ts +0 -2
  118. package/dist/predicate.js +0 -2
  119. package/dist/printer.d.ts +0 -6
  120. package/dist/printer.js +0 -122
  121. package/dist/registry-Dcp6QEK_.d.ts +0 -73
  122. package/dist/registry.d.ts +0 -2
  123. package/dist/registry.js +0 -646
  124. package/dist/render-template.d.ts +0 -45
  125. package/dist/render-template.js +0 -76
  126. package/dist/selection-CCArx9lU.d.ts +0 -91
  127. package/dist/showcase-egkusQBf.d.ts +0 -415
  128. package/dist/sms-segments-y4FpPilJ.d.ts +0 -13
  129. package/dist/sms-segments.d.ts +0 -2
  130. package/dist/sms-segments.js +0 -46
  131. package/dist/source-locations-DrCM82Ym.d.ts +0 -67
  132. package/dist/source-locations.d.ts +0 -2
  133. package/dist/source-locations.js +0 -102
  134. package/dist/starters/category-mapping.d.ts +0 -13
  135. package/dist/starters/category-mapping.js +0 -49
  136. package/dist/starters/corpus.d.ts +0 -2
  137. package/dist/starters/corpus.js +0 -194
  138. package/dist/starters/library.d.ts +0 -18
  139. package/dist/starters/library.js +0 -692
  140. package/dist/starters/public-gallery.d.ts +0 -266
  141. package/dist/starters/public-gallery.js +0 -916
  142. package/dist/starters/public-handoff.d.ts +0 -164
  143. package/dist/starters/public-handoff.js +0 -361
  144. package/dist/starters/public-preview-artifacts.d.ts +0 -43
  145. package/dist/starters/public-preview-artifacts.js +0 -104
  146. package/dist/starters/public-preview.d.ts +0 -61
  147. package/dist/starters/public-preview.js +0 -83
  148. package/dist/starters/showcase.d.ts +0 -2
  149. package/dist/starters/showcase.js +0 -335
  150. package/dist/starters/sources.d.ts +0 -45
  151. package/dist/starters/sources.js +0 -7762
  152. package/dist/starters/theme.d.ts +0 -45
  153. package/dist/starters/theme.js +0 -77
  154. package/dist/starters.d.ts +0 -2
  155. package/dist/starters.js +0 -874
  156. package/dist/tailwind-BYq7Lo6u.js +0 -727
  157. package/dist/template-DWftqh4x.d.ts +0 -60
  158. package/dist/template.d.ts +0 -2
  159. package/dist/template.js +0 -37
  160. package/dist/theme-css.js +0 -2
  161. package/dist/tsx-source-instrumentation.d.ts +0 -30
  162. package/dist/tsx-source-instrumentation.js +0 -115
  163. package/dist/tsx-tracer.d.ts +0 -112
  164. package/dist/tsx-tracer.js +0 -985
  165. package/dist/variables-J_L3qxRa.d.ts +0 -77
  166. package/dist/variables.d.ts +0 -2
  167. package/dist/variables.js +0 -141
  168. package/dist/version-BITr58kk.js +0 -96
  169. package/generated/sml-agent-contract.json +0 -709
  170. package/generated/sml-email-agent-contract.json +0 -615
  171. package/generated/sml-email-language-reference.md +0 -94
  172. package/generated/sml-language-reference.md +0 -195
package/dist/edit.js ADDED
@@ -0,0 +1,947 @@
1
+ import { n as isObject } from "./guards-eV1qLbf2.js";
2
+ import { d as completeJsxSourceLocation, u as JSX_SOURCE_LOCATION_PROP } from "./jsx-runtime-CBlexoeR.js";
3
+ import { t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
4
+ import { c as WHATSAPP_HEADER_TYPES, n as introducedProfileErrors, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES, t as checkStaticProfile } from "./profile-pWGtWZXF.js";
5
+ import { parse } from "@babel/parser";
6
+ //#region src/source-edit.ts
7
+ const node = (value) => isObject(value) && "type" in value && typeof value.type === "string" && "start" in value && typeof value.start === "number" && "end" in value && typeof value.end === "number";
8
+ const children = (value) => Array.isArray(value) ? value.filter(node) : [];
9
+ const walk$1 = (value, visit, parent = null) => {
10
+ if (Array.isArray(value)) {
11
+ for (const item of value) walk$1(item, visit, parent);
12
+ return;
13
+ }
14
+ if (!node(value)) return;
15
+ visit(value, parent);
16
+ for (const [key, child] of Object.entries(value)) if (key !== "loc" && key !== "extra" && key !== "comments") walk$1(child, visit, value);
17
+ };
18
+ const parsed = (source) => {
19
+ try {
20
+ const result = parse(source, {
21
+ sourceType: "module",
22
+ plugins: ["typescript", "jsx"]
23
+ });
24
+ return node(result) ? result : null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ };
29
+ const binding = (value) => value.type === "Identifier" || value.type === "MemberExpression" && value.computed === false && node(value.object) && binding(value.object);
30
+ /** JSX trims each line of text and joins the non-empty lines with one space. */
31
+ const collapseJsxText = (value) => value.split(/\r\n|\n|\r|\u2028|\u2029/).map((line) => line.trim()).filter((line) => line !== "").join(" ");
32
+ const property = (name, value, source, syntax) => {
33
+ const expression = value.type === "JSXExpressionContainer" && node(value.expression) ? value.expression : value;
34
+ const literal = expression.type === "StringLiteral" || expression.type === "NumericLiteral" || expression.type === "BooleanLiteral" || expression.type === "JSXText";
35
+ const raw = source.slice(expression.start, expression.end);
36
+ const inset = expression.type === "JSXText" ? {
37
+ start: expression.start + (raw.length - raw.trimStart().length),
38
+ end: expression.end - (raw.length - raw.trimEnd().length)
39
+ } : {
40
+ start: value.start,
41
+ end: value.end
42
+ };
43
+ return {
44
+ name,
45
+ literalType: expression.type === "NumericLiteral" ? "number" : expression.type === "BooleanLiteral" ? "boolean" : "string",
46
+ kind: literal ? "literal" : binding(expression) ? "binding" : "computed",
47
+ value: literal && expression.type === "JSXText" && typeof expression.value === "string" ? collapseJsxText(expression.value) : literal && (typeof expression.value === "string" || typeof expression.value === "number" || typeof expression.value === "boolean") ? String(expression.value) : source.slice(expression.start, expression.end),
48
+ start: inset.start,
49
+ end: inset.end,
50
+ syntax
51
+ };
52
+ };
53
+ /** The JSX elements directly before and after `element` in its parent's child list. */
54
+ const siblingsOf = (element, parent, source) => {
55
+ if (parent?.type !== "JSXElement" && parent?.type !== "JSXFragment") return {
56
+ previous: null,
57
+ next: null
58
+ };
59
+ const list = children(parent.children).filter((child) => child.type !== "JSXText" || source.slice(child.start, child.end).trim() !== "");
60
+ const index = list.findIndex((child) => child.start === element.start);
61
+ const range = (child) => child?.type === "JSXElement" ? {
62
+ start: child.start,
63
+ end: child.end
64
+ } : null;
65
+ return {
66
+ previous: range(list[index - 1]),
67
+ next: range(list[index + 1])
68
+ };
69
+ };
70
+ /** Resolve a renderer origin against the unchanged authored entry, never against HTML offsets. */
71
+ const inspectSourceElement = (source, file, origin) => {
72
+ if (file !== origin.fileName) return null;
73
+ const tree = parsed(source);
74
+ if (tree === null) return null;
75
+ const lines = buildLineMap(source);
76
+ const lineStart = lines[origin.lineNumber - 1];
77
+ if (lineStart === void 0 || !Number.isInteger(origin.columnNumber) || origin.columnNumber < 1) return null;
78
+ const offset = lineStart + origin.columnNumber - 1;
79
+ if (offset >= (lines[origin.lineNumber] ?? source.length)) return null;
80
+ let found = null;
81
+ walk$1(tree, (element, parent) => {
82
+ if (element.type !== "JSXElement" || element.start !== offset || !node(element.openingElement)) return;
83
+ const opening = element.openingElement;
84
+ const properties = [];
85
+ for (const attribute of children(opening.attributes)) if (attribute.type === "JSXAttribute" && node(attribute.name) && typeof attribute.name.name === "string" && node(attribute.value)) {
86
+ const value = attribute.value;
87
+ const expression = value.type === "JSXExpressionContainer" && node(value.expression) ? value.expression : null;
88
+ if (attribute.name.name === "style" && expression?.type === "ObjectExpression" && !children(expression.properties).some((member) => member.type === "SpreadElement")) for (const member of children(expression.properties)) {
89
+ if (member.type !== "ObjectProperty" || member.computed === true || !node(member.key) || !node(member.value)) continue;
90
+ const key = member.key.type === "Identifier" ? member.key.name : member.key.value;
91
+ if (typeof key === "string") properties.push(property(`style.${key}`, member.value, source, "value"));
92
+ }
93
+ else properties.push(property(attribute.name.name, value, source, "attribute"));
94
+ }
95
+ const meaningful = children(element.children).filter((child) => child.type !== "JSXText" || source.slice(child.start, child.end).trim() !== "");
96
+ if (meaningful.length === 1 && meaningful[0] !== void 0 && meaningful[0].type !== "JSXElement" && meaningful[0].type !== "JSXFragment") properties.unshift(property("children", meaningful[0], source, "child"));
97
+ else if (meaningful.length > 1) {
98
+ const texts = meaningful.flatMap((child, index) => child.type === "JSXText" ? [property(`text.${index}`, child, source, "child")] : []);
99
+ properties.unshift(...texts);
100
+ }
101
+ found = {
102
+ start: element.start,
103
+ end: element.end,
104
+ contentStart: opening.selfClosing === true ? null : opening.end,
105
+ contentEnd: node(element.closingElement) ? element.closingElement.start : null,
106
+ tag: node(opening.name) ? source.slice(opening.name.start, opening.name.end) : "element",
107
+ properties,
108
+ attributeInsertion: (() => {
109
+ const last = children(opening.attributes).at(-1);
110
+ if (last !== void 0) return last.end;
111
+ return node(opening.name) ? opening.name.end : opening.end - 1;
112
+ })(),
113
+ hasSpread: children(opening.attributes).some((attribute) => attribute.type === "JSXSpreadAttribute"),
114
+ structural: parent?.type === "JSXElement" || parent?.type === "JSXFragment",
115
+ ...siblingsOf(element, parent, source)
116
+ };
117
+ });
118
+ return found;
119
+ };
120
+ const PLAIN_JSX_TEXT = /^[^\s{}<>&](?:[^{}<>&\r\n\u2028\u2029]*[^\s{}<>&])?$/;
121
+ /** Literal controls cannot overwrite a binding or computed expression. */
122
+ const literalReplacement = (source, field, value) => {
123
+ if (field.kind !== "literal") return null;
124
+ if (field.literalType === "number" && (value.trim() === "" || !Number.isFinite(Number(value)))) return null;
125
+ if (field.literalType === "boolean" && value !== "true" && value !== "false") return null;
126
+ const literal = field.literalType === "string" ? JSON.stringify(value) : field.literalType === "number" ? String(Number(value)) : value;
127
+ const before = source.slice(field.start, field.end);
128
+ const plainText = field.syntax === "child" && !before.startsWith("{") && PLAIN_JSX_TEXT.test(value);
129
+ return {
130
+ start: field.start,
131
+ end: field.end,
132
+ before,
133
+ after: plainText ? value : field.syntax === "child" || field.syntax === "attribute" && field.literalType !== "string" ? `{${literal}}` : literal
134
+ };
135
+ };
136
+ /** Explicit expression changes are proposals: callers show before/after and require Apply. */
137
+ const expressionReplacement = (source, field, expression) => ({
138
+ start: field.start,
139
+ end: field.end,
140
+ before: source.slice(field.start, field.end),
141
+ after: field.syntax === "value" ? expression : `{${expression}}`
142
+ });
143
+ const profileRefusal = (source, result, guard) => {
144
+ const file = guard.file ?? guard.entry;
145
+ const project = (content) => ({
146
+ ...guard.files,
147
+ [file]: content
148
+ });
149
+ const introduced = introducedProfileErrors(checkStaticProfile(project(source), guard.entry), checkStaticProfile(project(result), guard.entry));
150
+ const first = introduced[0];
151
+ if (first === void 0) return null;
152
+ const more = introduced.length > 1 ? ` (and ${introduced.length - 1} more)` : "";
153
+ return `This change leaves the static profile: ${first.message}${first.fix === void 0 ? "" : ` ${first.fix}`}${more}`;
154
+ };
155
+ /** Apply exact, nonoverlapping edits and refuse syntax damage without changing the original. */
156
+ const applySourceReplacements = (source, edits, options = {}) => {
157
+ const sorted = [...edits].sort((a, b) => b.start - a.start);
158
+ let boundary = source.length;
159
+ let result = source;
160
+ for (const edit of sorted) {
161
+ if (edit.start < 0 || edit.end < edit.start || edit.end > boundary || source.slice(edit.start, edit.end) !== edit.before) return {
162
+ ok: false,
163
+ reason: "Source changed at this target. Select it again before applying the edit."
164
+ };
165
+ result = result.slice(0, edit.start) + edit.after + result.slice(edit.end);
166
+ boundary = edit.start;
167
+ }
168
+ if (parsed(result) === null) return {
169
+ ok: false,
170
+ reason: "This change is not valid TSX. Review it in Source."
171
+ };
172
+ if (options.profile !== void 0 && result !== source) {
173
+ const refusal = profileRefusal(source, result, options.profile);
174
+ if (refusal !== null) return {
175
+ ok: false,
176
+ reason: refusal
177
+ };
178
+ }
179
+ return {
180
+ ok: true,
181
+ source: result
182
+ };
183
+ };
184
+ const indentation = (source, offset) => {
185
+ const lineStart = Math.max(source.lastIndexOf("\n", offset - 1), source.lastIndexOf("\r", offset - 1));
186
+ const lead = source.slice(lineStart + 1, offset);
187
+ return /^[ \t]*$/.test(lead) ? lead : null;
188
+ };
189
+ /** The whole lines an element alone occupies, so removing it leaves no blank line behind. */
190
+ const ownLines = (source, element) => {
191
+ if (indentation(source, element.start) === null) return element;
192
+ const trailing = /^[ \t]*(\r\n|\n|\r)/.exec(source.slice(element.end));
193
+ if (trailing === null) return element;
194
+ return {
195
+ start: element.start - (indentation(source, element.start) ?? "").length,
196
+ end: element.end + trailing[0].length
197
+ };
198
+ };
199
+ /** Structural edits name a whole authored block; loop iterations are never source nodes. */
200
+ const structuralReplacements = (source, element, operation) => {
201
+ if (operation.kind === "insert") {
202
+ if (element.contentEnd === null) return null;
203
+ return [{
204
+ start: element.contentEnd,
205
+ end: element.contentEnd,
206
+ before: "",
207
+ after: operation.tsx
208
+ }];
209
+ }
210
+ if (!element.structural) return null;
211
+ const text = source.slice(element.start, element.end);
212
+ if (operation.kind === "duplicate") {
213
+ const indent = indentation(source, element.start);
214
+ return [{
215
+ start: element.end,
216
+ end: element.end,
217
+ before: "",
218
+ after: indent === null ? text : `\n${indent}${text}`
219
+ }];
220
+ }
221
+ if (operation.kind === "reorder") {
222
+ const neighbour = operation.direction === "before" ? element.previous : element.next;
223
+ if (neighbour === null) return null;
224
+ return [{
225
+ start: neighbour.start,
226
+ end: neighbour.end,
227
+ before: source.slice(neighbour.start, neighbour.end),
228
+ after: text
229
+ }, {
230
+ start: element.start,
231
+ end: element.end,
232
+ before: text,
233
+ after: source.slice(neighbour.start, neighbour.end)
234
+ }];
235
+ }
236
+ const removal = ownLines(source, element);
237
+ const removed = {
238
+ start: removal.start,
239
+ end: removal.end,
240
+ before: source.slice(removal.start, removal.end),
241
+ after: ""
242
+ };
243
+ if (operation.kind === "delete") return [removed];
244
+ const destination = operation.destination.contentEnd;
245
+ if (destination === null || destination >= element.start && destination <= element.end) return null;
246
+ return [removed, {
247
+ start: destination,
248
+ end: destination,
249
+ before: "",
250
+ after: text
251
+ }];
252
+ };
253
+ /** The object literal a top-level `const name = { ... }` holds. */
254
+ const topLevelObject = (tree, name) => {
255
+ const program = tree.program;
256
+ if (!node(program)) return null;
257
+ for (const item of children(program.body)) {
258
+ const declaration = item.type === "ExportNamedDeclaration" && node(item.declaration) ? item.declaration : item;
259
+ if (declaration.type !== "VariableDeclaration") continue;
260
+ for (const declarator of children(declaration.declarations)) if (node(declarator.id) && declarator.id.name === name && node(declarator.init) && declarator.init.type === "ObjectExpression") return declarator.init;
261
+ }
262
+ return null;
263
+ };
264
+ /** Declared fixture values are separate source targets from body bindings and literals. */
265
+ const inspectSourceFixtures = (source) => {
266
+ const tree = parsed(source);
267
+ if (tree === null) return [];
268
+ const found = [];
269
+ walk$1(tree, (call) => {
270
+ if (call.type !== "CallExpression" || !node(call.callee) || call.callee.type !== "Identifier" || call.callee.name !== "defineTemplate") return;
271
+ const definition = children(call.arguments)[0];
272
+ if (definition?.type !== "ObjectExpression") return;
273
+ const fixtures = children(definition.properties).find((field) => field.type === "ObjectProperty" && node(field.key) && field.key.name === "fixtures" && field.computed !== true);
274
+ if (fixtures === void 0 || !node(fixtures.value) || fixtures.value.type !== "ObjectExpression") return;
275
+ for (const field of children(fixtures.value.properties)) {
276
+ if (field.type !== "ObjectProperty" || field.computed === true || !node(field.key) || !node(field.value)) continue;
277
+ const name = field.key.type === "Identifier" ? field.key.name : field.key.value;
278
+ const value = field.value.type === "Identifier" && typeof field.value.name === "string" ? topLevelObject(tree, field.value.name) ?? field.value : field.value;
279
+ if (typeof name === "string") found.push({
280
+ name,
281
+ start: value.start,
282
+ end: value.end,
283
+ source: source.slice(value.start, value.end)
284
+ });
285
+ }
286
+ });
287
+ return found;
288
+ };
289
+ const tokens = (value) => value.split(/\s+/).filter((item) => item !== "");
290
+ /**
291
+ * Edit an element's class list where it is written. A literal `className` is replaced, extended or
292
+ * trimmed in place; a missing one is added; one that is a condition or any other expression is
293
+ * refused, because rewriting it would discard the choice the author encoded.
294
+ */
295
+ const classNameEdit = (source, element, operation) => {
296
+ const field = element.properties.find((item) => item.name === "className");
297
+ const next = (current) => {
298
+ if (operation.kind === "set") return tokens(operation.value).join(" ");
299
+ const wanted = tokens(operation.tokens);
300
+ return operation.kind === "add" ? [...current, ...wanted.filter((item) => !current.includes(item))].join(" ") : current.filter((item) => !wanted.includes(item)).join(" ");
301
+ };
302
+ if (field === void 0) {
303
+ if (element.hasSpread) return {
304
+ ok: false,
305
+ reason: "A spread attribute can supply className here, so a class list cannot be added safely. Edit it in Source."
306
+ };
307
+ if (operation.kind === "remove") return {
308
+ ok: true,
309
+ edits: []
310
+ };
311
+ const value = next([]);
312
+ if (value === "") return {
313
+ ok: true,
314
+ edits: []
315
+ };
316
+ return {
317
+ ok: true,
318
+ edits: [{
319
+ start: element.attributeInsertion,
320
+ end: element.attributeInsertion,
321
+ before: "",
322
+ after: ` className=${JSON.stringify(value)}`
323
+ }]
324
+ };
325
+ }
326
+ if (field.kind !== "literal" || field.literalType !== "string") return {
327
+ ok: false,
328
+ reason: `className is ${field.kind === "binding" ? "bound to" : "computed from"} \`${field.value}\`, so editing it would discard that choice. Change the literal classes inside it in Source.`
329
+ };
330
+ const value = next(tokens(field.value));
331
+ return {
332
+ ok: true,
333
+ edits: [{
334
+ start: field.start,
335
+ end: field.end,
336
+ before: source.slice(field.start, field.end),
337
+ after: JSON.stringify(value)
338
+ }]
339
+ };
340
+ };
341
+ //#endregion
342
+ //#region src/channel-edit.ts
343
+ const isNode = (value) => isObject(value) && typeof value.type === "string" && typeof value.start === "number" && typeof value.end === "number";
344
+ const nodeList = (value) => Array.isArray(value) ? value.filter(isNode) : [];
345
+ const nodeOf = (parent, key) => {
346
+ const value = parent[key];
347
+ return isNode(value) ? value : null;
348
+ };
349
+ const parseSource = (source) => {
350
+ try {
351
+ const result = parse(source, {
352
+ sourceType: "module",
353
+ plugins: ["typescript", "jsx"]
354
+ });
355
+ return isNode(result) ? result : null;
356
+ } catch {
357
+ return null;
358
+ }
359
+ };
360
+ const walk = (value, visit) => {
361
+ if (Array.isArray(value)) {
362
+ for (const item of value) walk(item, visit);
363
+ return;
364
+ }
365
+ if (!isNode(value)) return;
366
+ if (visit(value) === false) return;
367
+ for (const [key, child] of Object.entries(value)) if (key !== "loc" && key !== "extra" && key !== "comments") walk(child, visit);
368
+ };
369
+ const jsxName = (name) => {
370
+ if (name === null) return "";
371
+ if (name.type === "JSXIdentifier") return typeof name.name === "string" ? name.name : "";
372
+ if (name.type === "JSXMemberExpression") return `${jsxName(nodeOf(name, "object"))}.${jsxName(nodeOf(name, "property"))}`;
373
+ return "";
374
+ };
375
+ const elementOf = (node) => {
376
+ if (node.type !== "JSXElement") return null;
377
+ const opening = nodeOf(node, "openingElement");
378
+ if (opening === null) return null;
379
+ const closing = nodeOf(node, "closingElement");
380
+ return {
381
+ node,
382
+ opening,
383
+ closing,
384
+ tag: jsxName(nodeOf(opening, "name")),
385
+ children: nodeList(node.children),
386
+ attributes: nodeList(opening.attributes),
387
+ selfClosing: opening.selfClosing === true,
388
+ contentStart: opening.end,
389
+ contentEnd: closing === null ? opening.end : closing.start
390
+ };
391
+ };
392
+ const returnedElement = (fn) => {
393
+ const body = nodeOf(fn, "body");
394
+ if (body === null) return null;
395
+ if (body.type === "JSXElement") return body;
396
+ if (body.type !== "BlockStatement") return null;
397
+ const statements = nodeList(body.body).filter((statement) => statement.type !== "EmptyStatement");
398
+ const only = statements.length === 1 ? statements[0] : void 0;
399
+ const argument = only?.type === "ReturnStatement" && only !== void 0 ? nodeOf(only, "argument") : null;
400
+ return argument !== null && argument.type === "JSXElement" ? argument : null;
401
+ };
402
+ const propertyName = (property) => {
403
+ if (property.computed === true) return null;
404
+ const key = nodeOf(property, "key");
405
+ if (key === null) return null;
406
+ if (key.type === "Identifier") return typeof key.name === "string" ? key.name : null;
407
+ return key.type === "StringLiteral" && typeof key.value === "string" ? key.value : null;
408
+ };
409
+ /** The function behind `defineTemplate({ [channel]: … })`, when it is written in place. */
410
+ const channelFunction = (tree, channel) => {
411
+ let found = null;
412
+ walk(tree, (call) => {
413
+ if (found !== null) return false;
414
+ if (call.type !== "CallExpression") return;
415
+ const callee = nodeOf(call, "callee");
416
+ if (callee?.type !== "Identifier" || callee.name !== "defineTemplate") return;
417
+ const definition = nodeList(call.arguments)[0];
418
+ if (definition?.type !== "ObjectExpression") return false;
419
+ for (const property of nodeList(definition.properties)) {
420
+ if (propertyName(property) !== channel) continue;
421
+ const fn = property.type === "ObjectMethod" ? property : property.type === "ObjectProperty" ? nodeOf(property, "value") : null;
422
+ if (fn === null || fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression" && fn.type !== "ObjectMethod") return false;
423
+ const returned = returnedElement(fn);
424
+ const root = returned === null ? null : elementOf(returned);
425
+ if (root !== null) found = {
426
+ root,
427
+ parameter: nodeList(fn.params)[0] ?? null
428
+ };
429
+ return false;
430
+ }
431
+ return false;
432
+ });
433
+ return found;
434
+ };
435
+ /** The name an import binds a component to, or the component's own name when nothing is imported. */
436
+ const importedAs = (tree, specifier, imported) => {
437
+ let local = imported;
438
+ walk(tree, (node) => {
439
+ if (node.type !== "ImportDeclaration") return;
440
+ if (nodeOf(node, "source")?.value !== specifier) return false;
441
+ for (const item of nodeList(node.specifiers)) {
442
+ const name = nodeOf(item, "imported");
443
+ const alias = nodeOf(item, "local");
444
+ if (item.type === "ImportSpecifier" && (name?.name === imported || name?.value === imported)) local = typeof alias?.name === "string" ? alias.name : imported;
445
+ }
446
+ return false;
447
+ });
448
+ return local;
449
+ };
450
+ /** JSX text as the compiler reads it: lines are trimmed and joined, blank lines dropped. */
451
+ const jsxTextValue = (value) => {
452
+ const lines = value.split(/\r\n|\n|\r/);
453
+ let lastNonEmpty = 0;
454
+ lines.forEach((line, index) => {
455
+ if (/[^ \t]/.test(line)) lastNonEmpty = index;
456
+ });
457
+ let result = "";
458
+ lines.forEach((line, index) => {
459
+ let trimmed = line.replace(/\t/g, " ");
460
+ if (index !== 0) trimmed = trimmed.replace(/^ +/, "");
461
+ if (index !== lines.length - 1) trimmed = trimmed.replace(/ +$/, "");
462
+ if (trimmed === "") return;
463
+ result += index === lastNonEmpty ? trimmed : `${trimmed} `;
464
+ });
465
+ return result;
466
+ };
467
+ /** The value of a container that holds only a string, or null for any other expression. */
468
+ const stringContainerValue = (container) => {
469
+ const expression = nodeOf(container, "expression");
470
+ if (expression === null) return null;
471
+ if (expression.type === "StringLiteral" && typeof expression.value === "string") return expression.value;
472
+ if (expression.type === "TemplateLiteral" && nodeList(expression.expressions).length === 0) {
473
+ const cooked = nodeList(expression.quasis)[0]?.value;
474
+ return isObject(cooked) && typeof cooked.cooked === "string" ? cooked.cooked : null;
475
+ }
476
+ return null;
477
+ };
478
+ const summarize = (source, node) => {
479
+ const text = source.slice(node.start, node.end).replace(/\s+/g, " ").trim();
480
+ return text.length > 60 ? `${text.slice(0, 59)}…` : text;
481
+ };
482
+ /**
483
+ * The editable text and locked expressions of an element's children, in order. Text is a segment
484
+ * between locked children, so a text segment exists (possibly empty) before, between and after
485
+ * them.
486
+ */
487
+ const segmentsOf = (source, element) => {
488
+ const segments = [];
489
+ let start = element.contentStart;
490
+ let text = "";
491
+ const flush = (end) => {
492
+ segments.push({
493
+ kind: "text",
494
+ text,
495
+ start,
496
+ end
497
+ });
498
+ text = "";
499
+ };
500
+ for (const child of element.children) {
501
+ if (child.type === "JSXText") {
502
+ text += jsxTextValue(typeof child.value === "string" ? child.value : "");
503
+ continue;
504
+ }
505
+ if (child.type === "JSXExpressionContainer") {
506
+ const value = stringContainerValue(child);
507
+ if (value !== null) {
508
+ text += value;
509
+ continue;
510
+ }
511
+ }
512
+ flush(child.start);
513
+ segments.push({
514
+ kind: "locked",
515
+ summary: summarize(source, child),
516
+ start: child.start,
517
+ end: child.end
518
+ });
519
+ start = child.end;
520
+ }
521
+ flush(element.contentEnd);
522
+ return segments;
523
+ };
524
+ const isSafeRun = (char) => !/[{}<>&\r\n\t]/.test(char);
525
+ /**
526
+ * JSX for the given text. Characters JSX text cannot carry, and whitespace at an edge the
527
+ * compiler would trim, are written as string expressions; the rest stays plain text.
528
+ */
529
+ const encodeText = (text, trimStart, trimEnd) => {
530
+ const chars = [...text];
531
+ const unsafe = chars.map((char) => !isSafeRun(char));
532
+ if (trimStart) for (let i = 0; i < chars.length && /\s/.test(chars[i]); i++) unsafe[i] = true;
533
+ if (trimEnd) for (let i = chars.length - 1; i >= 0 && /\s/.test(chars[i]); i--) unsafe[i] = true;
534
+ let result = "";
535
+ let run = "";
536
+ let runUnsafe = false;
537
+ const flush = () => {
538
+ if (run === "") return;
539
+ result += runUnsafe ? `{${JSON.stringify(run)}}` : run;
540
+ run = "";
541
+ };
542
+ chars.forEach((char, index) => {
543
+ if (run !== "" && unsafe[index] !== runUnsafe) flush();
544
+ runUnsafe = unsafe[index];
545
+ run += char;
546
+ });
547
+ flush();
548
+ return result;
549
+ };
550
+ /** Write `text` over the gap at `segment`, keeping the newlines and indentation around it. */
551
+ const writeText = (source, element, segments, index, text) => {
552
+ const segment = segments[index];
553
+ if (segment?.kind !== "text") return {
554
+ ok: false,
555
+ reason: "That part of the body is locked."
556
+ };
557
+ if (segment.text === text) return {
558
+ ok: true,
559
+ edits: []
560
+ };
561
+ if (element.selfClosing) {
562
+ const opening = element.opening;
563
+ let start = opening.end - 2;
564
+ while (start > opening.start && /\s/.test(source[start - 1])) start--;
565
+ return {
566
+ ok: true,
567
+ edits: [{
568
+ start,
569
+ end: opening.end,
570
+ before: source.slice(start, opening.end),
571
+ after: `>${encodeText(text, false, false)}</${element.tag}>`
572
+ }]
573
+ };
574
+ }
575
+ const raw = source.slice(segment.start, segment.end);
576
+ const leading = /^\s*/.exec(raw)?.[0] ?? "";
577
+ const prefix = /[\r\n]/.test(leading) ? leading : "";
578
+ const rest = raw.slice(prefix.length);
579
+ const trailing = /\s*$/.exec(rest)?.[0] ?? "";
580
+ const suffix = /[\r\n]/.test(trailing) ? trailing : "";
581
+ const start = segment.start + prefix.length;
582
+ const end = segment.end - suffix.length;
583
+ return {
584
+ ok: true,
585
+ edits: [{
586
+ start,
587
+ end,
588
+ before: source.slice(start, end),
589
+ after: encodeText(text, prefix !== "", suffix !== "")
590
+ }]
591
+ };
592
+ };
593
+ const attributeNamed = (element, name) => element.attributes.find((attribute) => attribute.type === "JSXAttribute" && jsxName(nodeOf(attribute, "name")) === name) ?? null;
594
+ const attributeState = (source, element, name) => {
595
+ const attribute = attributeNamed(element, name);
596
+ if (attribute === null) return { kind: "absent" };
597
+ const value = nodeOf(attribute, "value");
598
+ if (value?.type === "StringLiteral" && typeof value.value === "string") return {
599
+ kind: "literal",
600
+ value: value.value
601
+ };
602
+ if (value?.type === "JSXExpressionContainer") {
603
+ const literal = stringContainerValue(value);
604
+ if (literal !== null) return {
605
+ kind: "literal",
606
+ value: literal
607
+ };
608
+ }
609
+ return {
610
+ kind: "expression",
611
+ source: summarize(source, value ?? attribute)
612
+ };
613
+ };
614
+ const encodeAttribute = (value) => /["&\\\r\n]/.test(value) ? `{${JSON.stringify(value)}}` : `"${value}"`;
615
+ const replacement = (source, start, end, after) => ({
616
+ start,
617
+ end,
618
+ before: source.slice(start, end),
619
+ after
620
+ });
621
+ /** Set, add or remove one string attribute of an element. */
622
+ const setAttribute = (source, element, name, value) => {
623
+ const attribute = attributeNamed(element, name);
624
+ if (attribute === null) {
625
+ if (value === void 0) return {
626
+ ok: true,
627
+ edits: []
628
+ };
629
+ const anchor = element.attributes.at(-1)?.end ?? nodeOf(element.opening, "name")?.end ?? element.opening.end;
630
+ return {
631
+ ok: true,
632
+ edits: [replacement(source, anchor, anchor, ` ${name}=${encodeAttribute(value)}`)]
633
+ };
634
+ }
635
+ if (attributeState(source, element, name).kind === "expression") return {
636
+ ok: false,
637
+ reason: `\`${name}\` reads the input. Edit it in Source.`
638
+ };
639
+ if (value === void 0) {
640
+ let start = attribute.start;
641
+ while (start > element.opening.start && /\s/.test(source[start - 1])) start--;
642
+ return {
643
+ ok: true,
644
+ edits: [replacement(source, start, attribute.end, "")]
645
+ };
646
+ }
647
+ const literal = nodeOf(attribute, "value");
648
+ return {
649
+ ok: true,
650
+ edits: literal === null ? [replacement(source, attribute.end, attribute.end, `=${encodeAttribute(value)}`)] : [replacement(source, literal.start, literal.end, encodeAttribute(value))]
651
+ };
652
+ };
653
+ const INDENT = " ";
654
+ /** The indentation of the line `position` is on, when only whitespace precedes it there. */
655
+ const ownLineIndent = (source, position) => {
656
+ const lineStart = source.lastIndexOf("\n", position - 1) + 1;
657
+ const lead = source.slice(lineStart, position);
658
+ return /^[ \t]*$/.test(lead) ? lead : null;
659
+ };
660
+ /** Remove a child, and its line when it is alone on one. */
661
+ const removeChild = (source, node) => {
662
+ const indent = ownLineIndent(source, node.start);
663
+ const after = /^[ \t]*(\r?\n)/.exec(source.slice(node.end));
664
+ if (indent === null || after === null) return replacement(source, node.start, node.end, "");
665
+ const start = node.start - indent.length;
666
+ return replacement(source, start, node.end + after[0].length, "");
667
+ };
668
+ /** Insert markup on its own line before or after a sibling, or inline when the sibling is not alone. */
669
+ const insertBeside = (source, sibling, side, text) => {
670
+ const indent = ownLineIndent(source, sibling.start);
671
+ if (side === "before") return replacement(source, sibling.start, sibling.start, indent === null ? text : `${text}\n${indent}`);
672
+ return replacement(source, sibling.end, sibling.end, indent === null ? text : `\n${indent}${text}`);
673
+ };
674
+ /** The input fields read by the expressions inside `root`, as dotted paths. */
675
+ const variablesOf = (source, root, parameter) => {
676
+ const names = /* @__PURE__ */ new Set();
677
+ const scan = (text) => {
678
+ if (parameter?.type === "Identifier" && typeof parameter.name === "string") {
679
+ const pattern = new RegExp(`\\b${parameter.name}((?:\\??\\.[A-Za-z_$][\\w$]*)+)`, "g");
680
+ for (const match of text.matchAll(pattern)) names.add(match[1].replace(/\?/g, "").slice(1));
681
+ } else if (parameter?.type === "ObjectPattern") for (const property of nodeList(parameter.properties)) {
682
+ const key = nodeOf(property, "key");
683
+ if (key?.type === "Identifier" && typeof key.name === "string") {
684
+ if (new RegExp(`\\b${key.name}\\b`).test(text)) names.add(key.name);
685
+ }
686
+ }
687
+ };
688
+ walk(root.node, (node) => {
689
+ if (node.type === "JSXExpressionContainer" && stringContainerValue(node) === null) scan(source.slice(node.start, node.end));
690
+ });
691
+ return [...names].sort();
692
+ };
693
+ const smsChannel = (source) => {
694
+ const tree = parseSource(source);
695
+ if (tree === null) return null;
696
+ const channel = channelFunction(tree, "sms");
697
+ if (channel === null || channel.root.tag !== importedAs(tree, "@samva/markup/sms", "Sms")) return null;
698
+ return {
699
+ ...channel,
700
+ tree
701
+ };
702
+ };
703
+ /** The form model of the template's SMS body, or null when it is not a `<Sms>` written in place. */
704
+ const readSmsChannel = (source) => {
705
+ const channel = smsChannel(source);
706
+ if (channel === null) return null;
707
+ return {
708
+ category: attributeState(source, channel.root, "category"),
709
+ segments: segmentsOf(source, channel.root),
710
+ variables: variablesOf(source, channel.root, channel.parameter)
711
+ };
712
+ };
713
+ /** The source replacements that apply one form change to the SMS body. */
714
+ const editSmsChannel = (source, edit) => {
715
+ const channel = smsChannel(source);
716
+ if (channel === null) return {
717
+ ok: false,
718
+ reason: "The SMS body is not editable here."
719
+ };
720
+ if (edit.kind === "category") {
721
+ if (edit.value !== void 0 && !SMS_CATEGORIES.includes(edit.value)) return {
722
+ ok: false,
723
+ reason: `\`${edit.value}\` is not an SMS category.`
724
+ };
725
+ return setAttribute(source, channel.root, "category", edit.value);
726
+ }
727
+ return writeText(source, channel.root, segmentsOf(source, channel.root), edit.segment, edit.text);
728
+ };
729
+ const BUTTON_TAGS = {
730
+ "quick-reply": "QuickReplyButton",
731
+ url: "UrlButton",
732
+ phone: "PhoneButton",
733
+ "copy-code": "CopyCodeButton"
734
+ };
735
+ const BUTTON_DEFAULTS = {
736
+ "quick-reply": "Reply",
737
+ url: "Visit site",
738
+ phone: "Call us",
739
+ "copy-code": "CODE"
740
+ };
741
+ const kindOfTag = (tag, local) => {
742
+ if (!tag.startsWith(`${local}.`)) return null;
743
+ const part = tag.slice(local.length + 1);
744
+ return Object.keys(BUTTON_TAGS).find((kind) => BUTTON_TAGS[kind] === part) ?? null;
745
+ };
746
+ const whatsappChannel = (source) => {
747
+ const tree = parseSource(source);
748
+ if (tree === null) return null;
749
+ const channel = channelFunction(tree, "whatsapp");
750
+ const local = importedAs(tree, "@samva/markup/whatsapp", "WhatsApp");
751
+ if (channel === null || channel.root.tag !== local) return null;
752
+ const part = (name) => {
753
+ for (const child of channel.root.children) {
754
+ const element = elementOf(child);
755
+ if (element?.tag === `${local}.${name}`) return element;
756
+ }
757
+ return null;
758
+ };
759
+ return {
760
+ ...channel,
761
+ local,
762
+ header: part("Header"),
763
+ body: part("Body"),
764
+ footer: part("Footer"),
765
+ buttons: part("Buttons")
766
+ };
767
+ };
768
+ const buttonRows = (source, channel) => {
769
+ if (channel.buttons === null) return [];
770
+ const rows = [];
771
+ for (const child of channel.buttons.children) {
772
+ if (child.type === "JSXText" && jsxTextValue(String(child.value ?? "")) === "") continue;
773
+ const element = elementOf(child);
774
+ const kind = element === null ? null : kindOfTag(element.tag, channel.local);
775
+ if (element === null || kind === null) {
776
+ rows.push({
777
+ row: {
778
+ kind: "locked",
779
+ summary: summarize(source, child)
780
+ },
781
+ node: child
782
+ });
783
+ continue;
784
+ }
785
+ const segments = segmentsOf(source, element);
786
+ const label = segments[0];
787
+ rows.push({
788
+ node: child,
789
+ row: segments.length === 1 && label?.kind === "text" ? {
790
+ kind: "editable",
791
+ type: kind,
792
+ text: label.text,
793
+ url: attributeState(source, element, "url"),
794
+ phone: attributeState(source, element, "phone")
795
+ } : {
796
+ kind: "locked",
797
+ summary: summarize(source, child)
798
+ }
799
+ });
800
+ }
801
+ return rows;
802
+ };
803
+ /** The form model of the template's WhatsApp body, or null when it is not a `<WhatsApp>` written in place. */
804
+ const readWhatsAppChannel = (source) => {
805
+ const channel = whatsappChannel(source);
806
+ if (channel === null || channel.body === null) return null;
807
+ const { root } = channel;
808
+ return {
809
+ name: attributeState(source, root, "name"),
810
+ language: attributeState(source, root, "language"),
811
+ category: attributeState(source, root, "category"),
812
+ header: channel.header === null ? null : segmentsOf(source, channel.header),
813
+ headerType: channel.header === null ? { kind: "absent" } : attributeState(source, channel.header, "type"),
814
+ body: segmentsOf(source, channel.body),
815
+ footer: channel.footer === null ? null : segmentsOf(source, channel.footer),
816
+ buttons: buttonRows(source, channel).map(({ row }) => row),
817
+ variables: variablesOf(source, root, channel.parameter)
818
+ };
819
+ };
820
+ const buttonMarkup = (local, type, text, attributes) => {
821
+ const tag = `${local}.${BUTTON_TAGS[type]}`;
822
+ 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}>`;
823
+ };
824
+ const newButton = (local, type) => buttonMarkup(local, type, BUTTON_DEFAULTS[type], {
825
+ url: "https://example.com",
826
+ phone: "+15555550100"
827
+ });
828
+ const editPart = (source, channel, part, index, text) => {
829
+ const element = channel[part];
830
+ if (element === null) {
831
+ if (part === "body" || text === "" || index !== 0) return {
832
+ ok: true,
833
+ edits: []
834
+ };
835
+ const tag = `${channel.local}.${part === "header" ? "Header" : "Footer"}`;
836
+ const markup = `<${tag}>${encodeText(text, false, false)}</${tag}>`;
837
+ const anchor = part === "header" ? channel.body.node : channel.buttons?.node ?? channel.body.node;
838
+ return {
839
+ ok: true,
840
+ edits: [insertBeside(source, anchor, part === "header" ? "before" : "after", markup)]
841
+ };
842
+ }
843
+ const segments = segmentsOf(source, element);
844
+ if (part !== "body" && text === "" && segments.length === 1) return {
845
+ ok: true,
846
+ edits: [removeChild(source, element.node)]
847
+ };
848
+ return writeText(source, element, segments, index, text);
849
+ };
850
+ /** The source replacements that apply one form change to the WhatsApp body. */
851
+ const editWhatsAppChannel = (source, edit) => {
852
+ const channel = whatsappChannel(source);
853
+ if (channel === null || channel.body === null) return {
854
+ ok: false,
855
+ reason: "The WhatsApp body is not editable here."
856
+ };
857
+ const { root, local } = channel;
858
+ switch (edit.kind) {
859
+ case "attribute": {
860
+ if (edit.name === "category" && !WHATSAPP_CATEGORIES.includes(edit.value ?? "")) return {
861
+ ok: false,
862
+ reason: "Choose a WhatsApp category."
863
+ };
864
+ const value = edit.name === "name" && edit.value === "" ? void 0 : edit.value ?? "";
865
+ return setAttribute(source, root, edit.name, value);
866
+ }
867
+ case "headerType":
868
+ if (channel.header === null) return {
869
+ ok: false,
870
+ reason: "Add the header text before choosing its type."
871
+ };
872
+ if (!WHATSAPP_HEADER_TYPES.includes(edit.value)) return {
873
+ ok: false,
874
+ reason: `\`${edit.value}\` is not a header type.`
875
+ };
876
+ return setAttribute(source, channel.header, "type", edit.value === "text" ? void 0 : edit.value);
877
+ case "text": return editPart(source, channel, edit.part, edit.segment, edit.text);
878
+ case "buttonAdd": {
879
+ const button = newButton(local, edit.type);
880
+ const buttons = channel.buttons;
881
+ if (buttons === null) {
882
+ const anchor = channel.footer ?? channel.body;
883
+ const tag = `${local}.Buttons`;
884
+ const indent = ownLineIndent(source, anchor.node.start);
885
+ const markup = indent === null ? `<${tag}>${button}</${tag}>` : `<${tag}>\n${indent}${INDENT}${button}\n${indent}</${tag}>`;
886
+ return {
887
+ ok: true,
888
+ edits: [insertBeside(source, anchor.node, "after", markup)]
889
+ };
890
+ }
891
+ if (buttons.selfClosing) {
892
+ const tag = `${local}.Buttons`;
893
+ const indent = ownLineIndent(source, buttons.node.start);
894
+ const markup = indent === null ? `<${tag}>${button}</${tag}>` : `<${tag}>\n${indent}${INDENT}${button}\n${indent}</${tag}>`;
895
+ return {
896
+ ok: true,
897
+ edits: [replacement(source, buttons.node.start, buttons.node.end, markup)]
898
+ };
899
+ }
900
+ const last = buttons.children.findLast((child) => child.type !== "JSXText" || jsxTextValue(String(child.value ?? "")) !== "");
901
+ if (last !== void 0) return {
902
+ ok: true,
903
+ edits: [insertBeside(source, last, "after", button)]
904
+ };
905
+ const indent = ownLineIndent(source, buttons.node.start);
906
+ return {
907
+ ok: true,
908
+ edits: [replacement(source, buttons.contentStart, buttons.contentEnd, indent === null ? button : `\n${indent}${INDENT}${button}\n${indent}`)]
909
+ };
910
+ }
911
+ }
912
+ const rows = buttonRows(source, channel);
913
+ const target = rows[edit.button];
914
+ if (target === void 0 || channel.buttons === null) return {
915
+ ok: false,
916
+ reason: "That button no longer exists."
917
+ };
918
+ if (edit.kind === "buttonRemove") return {
919
+ ok: true,
920
+ edits: [removeChild(source, rows.length === 1 ? channel.buttons.node : target.node)]
921
+ };
922
+ const element = elementOf(target.node);
923
+ if (target.row.kind === "locked" || element === null) return {
924
+ ok: false,
925
+ reason: "That button is locked. Edit it in Source."
926
+ };
927
+ switch (edit.kind) {
928
+ case "buttonText": return writeText(source, element, segmentsOf(source, element), 0, edit.text);
929
+ case "buttonAttribute": return setAttribute(source, element, edit.name, edit.value);
930
+ default: {
931
+ const { row } = target;
932
+ const keep = (state) => state.kind === "literal" ? state.value : void 0;
933
+ const markup = buttonMarkup(local, edit.type, row.text, {
934
+ url: keep(row.url) ?? "https://example.com",
935
+ phone: keep(row.phone) ?? "+15555550100"
936
+ });
937
+ return {
938
+ ok: true,
939
+ edits: [replacement(source, target.node.start, target.node.end, markup)]
940
+ };
941
+ }
942
+ }
943
+ };
944
+ //#endregion
945
+ export { JSX_SOURCE_LOCATION_PROP, applySourceReplacements, classNameEdit, completeJsxSourceLocation, editSmsChannel, editWhatsAppChannel, expressionReplacement, inspectSourceElement, inspectSourceFixtures, literalReplacement, readSmsChannel, readWhatsAppChannel, structuralReplacements };
946
+
947
+ //# sourceMappingURL=edit.js.map