@samva/markup 0.1.2 → 0.3.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 (93) hide show
  1. package/AGENTS.md +25 -5
  2. package/README.md +92 -26
  3. package/dist/{ast-DAhnAvQN.d.ts → ast-63I-VLQx.d.ts} +15 -2
  4. package/dist/ast.d.ts +2 -2
  5. package/dist/ast.js +14 -1
  6. package/dist/{checks-D-zFQt5R.js → checks-DXuERO4B.js} +36 -2
  7. package/dist/checks.d.ts +8 -1
  8. package/dist/checks.js +2 -2
  9. package/dist/compile-sms.d.ts +2 -2
  10. package/dist/compile-whatsapp.d.ts +2 -2
  11. package/dist/compile.d.ts +9 -84
  12. package/dist/compile.js +11 -920
  13. package/dist/compiler.d.ts +303 -0
  14. package/dist/compiler.js +1390 -0
  15. package/dist/components.d.ts +3 -3
  16. package/dist/corpus-qrUD5O6r.d.ts +254 -0
  17. package/dist/css-ar2lU3NC.d.ts +84 -0
  18. package/dist/diagnostics-Djd9ckLg.d.ts +90 -0
  19. package/dist/diagnostics.js +4 -1
  20. package/dist/elements-DtwKlFVH.d.ts +820 -0
  21. package/dist/email/components.d.ts +125 -0
  22. package/dist/email/components.js +210 -0
  23. package/dist/email/diagnostics.d.ts +2 -0
  24. package/dist/email/diagnostics.js +56 -0
  25. package/dist/email/elements.d.ts +2 -0
  26. package/dist/email/elements.js +299 -0
  27. package/dist/email/html.d.ts +42 -0
  28. package/dist/email/html.js +266 -0
  29. package/dist/email/jsx-dev-runtime.d.ts +26 -0
  30. package/dist/email/jsx-dev-runtime.js +12 -0
  31. package/dist/email/jsx-runtime.d.ts +2 -0
  32. package/dist/email/jsx-runtime.js +60 -0
  33. package/dist/email/render.d.ts +43 -0
  34. package/dist/email/render.js +6 -0
  35. package/dist/email/selection.d.ts +102 -0
  36. package/dist/email/selection.js +143 -0
  37. package/dist/email/source-edit.d.ts +67 -0
  38. package/dist/email/source-edit.js +185 -0
  39. package/dist/email/stylesheets.d.ts +15 -0
  40. package/dist/email/stylesheets.js +45 -0
  41. package/dist/email/text.d.ts +7 -0
  42. package/dist/email/text.js +100 -0
  43. package/dist/emit.d.ts +13 -12
  44. package/dist/emit.js +63 -63
  45. package/dist/input-schema-BIfzI63B.d.ts +23 -0
  46. package/dist/input-schema.d.ts +2 -0
  47. package/dist/input-schema.js +170 -0
  48. package/dist/jsx-dev-runtime.d.ts +4 -3
  49. package/dist/jsx-dev-runtime.js +9 -1
  50. package/dist/{jsx-runtime-eNIMY87F.d.ts → jsx-runtime-C5k_Zq0A.d.ts} +3 -3
  51. package/dist/jsx-runtime.d.ts +1 -1
  52. package/dist/jsx-runtime.js +20 -1
  53. package/dist/model.d.ts +1 -1
  54. package/dist/ops.d.ts +1 -1
  55. package/dist/ops.js +1 -1
  56. package/dist/{parser-DkQKIjDa.d.ts → parser-CGBS73Gz.d.ts} +2 -2
  57. package/dist/parser.d.ts +1 -1
  58. package/dist/presets/category-mapping.d.ts +1 -1
  59. package/dist/presets/corpus.d.ts +2 -0
  60. package/dist/presets/corpus.js +247 -0
  61. package/dist/presets/public-gallery.d.ts +290 -2
  62. package/dist/presets/public-gallery.js +428 -6
  63. package/dist/presets/public-handoff.d.ts +164 -2
  64. package/dist/presets/public-handoff.js +229 -61
  65. package/dist/presets/public-preview-artifacts.d.ts +2 -2
  66. package/dist/presets/public-preview-artifacts.js +25 -31
  67. package/dist/presets/public-preview.d.ts +65 -2
  68. package/dist/presets/public-preview.js +402 -1152
  69. package/dist/presets/sources.d.ts +35 -4
  70. package/dist/presets/sources.js +2328 -43
  71. package/dist/presets.d.ts +39 -5
  72. package/dist/presets.js +96 -0
  73. package/dist/printer.d.ts +1 -1
  74. package/dist/render-template.d.ts +5 -2
  75. package/dist/render-template.js +3 -1
  76. package/dist/render-ykQhtMs_.js +592 -0
  77. package/dist/source-locations-BCJQnIfU.d.ts +89 -0
  78. package/dist/source-locations.d.ts +2 -0
  79. package/dist/source-locations.js +125 -0
  80. package/dist/template-Dy5yOPyp.d.ts +60 -0
  81. package/dist/template.d.ts +2 -0
  82. package/dist/template.js +37 -0
  83. package/dist/tsx-source-instrumentation.d.ts +23 -0
  84. package/dist/tsx-source-instrumentation.js +97 -0
  85. package/dist/tsx-tracer.d.ts +116 -0
  86. package/dist/tsx-tracer.js +985 -0
  87. package/generated/sml-agent-contract.json +574 -782
  88. package/generated/sml-email-agent-contract.json +502 -677
  89. package/generated/sml-email-language-reference.md +94 -160
  90. package/generated/sml-language-reference.md +195 -220
  91. package/package.json +111 -9
  92. package/dist/public-gallery-D0Zv0pZE.d.ts +0 -611
  93. package/dist/public-preview-u1Z9aaGq.d.ts +0 -1168
@@ -0,0 +1,985 @@
1
+ import { buildLineMap } from "./diagnostics.js";
2
+ import { SML_TAGS, componentNameForSmlTag, smlTagForComponentName } from "./ast.js";
3
+ import { decodeEntities } from "./lexer.js";
4
+ import { parseSml } from "./parser.js";
5
+ import { applyOps } from "./ops.js";
6
+ import { Data, Predicate, Result } from "effect";
7
+ import { parse } from "@babel/parser";
8
+ //#region src/tsx-tracer.ts
9
+ var TsxHeadingTraceError = class extends Data.TaggedError("TsxHeadingTraceError") {};
10
+ const ZERO_SPAN = {
11
+ start: 0,
12
+ end: 0
13
+ };
14
+ const diagnostic = (code, message, span = ZERO_SPAN) => ({
15
+ code,
16
+ message,
17
+ span
18
+ });
19
+ const failure = (...diagnostics) => Result.fail(new TsxHeadingTraceError({ diagnostics }));
20
+ const isRecord = (value) => Predicate.isObject(value) && !Array.isArray(value);
21
+ const isAstNode = (value) => isRecord(value) && typeof value.type === "string" && typeof value.start === "number" && typeof value.end === "number";
22
+ const childNode = (node, key) => {
23
+ const value = node[key];
24
+ return isAstNode(value) ? value : null;
25
+ };
26
+ const stringProperty = (node, key) => {
27
+ const value = node[key];
28
+ return typeof value === "string" ? value : null;
29
+ };
30
+ const astNodeSpan = (node) => ({
31
+ start: node.start,
32
+ end: node.end
33
+ });
34
+ const walkAst = (value, visit) => {
35
+ if (Array.isArray(value)) {
36
+ for (const item of value) walkAst(item, visit);
37
+ return;
38
+ }
39
+ if (!isRecord(value)) return;
40
+ if (isAstNode(value)) visit(value);
41
+ for (const [key, child] of Object.entries(value)) {
42
+ if (key === "loc" || key === "extra" || key === "comments" || key === "tokens") continue;
43
+ walkAst(child, visit);
44
+ }
45
+ };
46
+ const jsxName = (node) => {
47
+ if (node.type === "JSXIdentifier") {
48
+ const name = stringProperty(node, "name");
49
+ return name === null ? { kind: "dynamic" } : {
50
+ kind: "literal",
51
+ name
52
+ };
53
+ }
54
+ return { kind: "dynamic" };
55
+ };
56
+ const jsxOpeningName = (node) => {
57
+ const opening = childNode(node, "openingElement");
58
+ const name = opening === null ? null : childNode(opening, "name");
59
+ return name === null ? { kind: "dynamic" } : jsxName(name);
60
+ };
61
+ /**
62
+ * The text of a `{"…"}` child, or `null` when the container holds anything
63
+ * else. A template literal qualifies only with no substitutions, so an
64
+ * interpolation like `{v.tagline}` or `` {`${a}b`} `` stays opaque.
65
+ */
66
+ const staticStringLiteralText = (node) => {
67
+ if (node.type !== "JSXExpressionContainer") return null;
68
+ const expression = node.expression;
69
+ if (!isAstNode(expression)) return null;
70
+ if (expression.type === "StringLiteral") return typeof expression.value === "string" ? expression.value : null;
71
+ if (expression.type !== "TemplateLiteral") return null;
72
+ const expressions = expression.expressions;
73
+ if (Array.isArray(expressions) && expressions.length > 0) return null;
74
+ const quasis = Array.isArray(expression.quasis) ? expression.quasis.filter(isAstNode) : [];
75
+ if (quasis.length !== 1) return null;
76
+ const cooked = isRecord(quasis[0].value) ? quasis[0].value.cooked : void 0;
77
+ return typeof cooked === "string" ? cooked : null;
78
+ };
79
+ /** True for bare JSX text that carries no visible characters. */
80
+ const isBlankJsxText = (node, source) => node.type === "JSXText" && source.slice(node.start, node.end).trim().length === 0;
81
+ const escapeJsxText = (text) => text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("{", "&#123;");
82
+ /**
83
+ * Write static text back in the form the author used. A string literal is
84
+ * serialized with `JSON.stringify`, so braces, angle brackets, and quotes
85
+ * round-trip without JSX-text escaping.
86
+ */
87
+ const serializeStaticTextChild = (form, text) => form === "string-literal" ? `{${JSON.stringify(text)}}` : escapeJsxText(text);
88
+ const directLiteral = (node, source) => {
89
+ const children = node.children;
90
+ if (!Array.isArray(children)) return null;
91
+ const significant = children.filter((child) => isAstNode(child) && !isBlankJsxText(child, source));
92
+ if (significant.length !== 1) return null;
93
+ const child = significant[0];
94
+ const literal = staticStringLiteralText(child);
95
+ if (literal !== null) {
96
+ if (literal.trim().length === 0) return null;
97
+ return {
98
+ nodeSpan: astNodeSpan(node),
99
+ text: literal,
100
+ span: {
101
+ start: child.start,
102
+ end: child.end
103
+ },
104
+ form: "string-literal"
105
+ };
106
+ }
107
+ if (child.type !== "JSXText") return null;
108
+ const raw = source.slice(child.start, child.end);
109
+ const leading = raw.search(/\S/u);
110
+ if (leading < 0) return null;
111
+ let trailing = raw.length;
112
+ while (trailing > leading && /\s/u.test(raw[trailing - 1])) trailing -= 1;
113
+ const text = decodeEntities(raw.slice(leading, trailing));
114
+ if (text.trim().length === 0) return null;
115
+ return {
116
+ nodeSpan: astNodeSpan(node),
117
+ text,
118
+ span: {
119
+ start: child.start + leading,
120
+ end: child.start + trailing
121
+ },
122
+ form: "jsx-text"
123
+ };
124
+ };
125
+ const normalizeText = (text) => text.replace(/\s+/gu, " ").trim();
126
+ const elementById = (node, id) => {
127
+ if (node.kind !== "element") return null;
128
+ if (node.id === id) return node;
129
+ for (const child of node.children) {
130
+ const found = elementById(child, id);
131
+ if (found !== null) return found;
132
+ }
133
+ return null;
134
+ };
135
+ const headingText = (node) => {
136
+ if (node.children.length !== 1 || node.children[0]?.kind !== "text") return null;
137
+ return node.children[0].value;
138
+ };
139
+ const parseAuthored = (source) => {
140
+ try {
141
+ const parsed = parse(source, {
142
+ plugins: ["typescript", "jsx"],
143
+ sourceType: "module"
144
+ });
145
+ return Result.succeed(parsed.program);
146
+ } catch (error) {
147
+ return Result.fail(diagnostic("tsx-invalid-authored-source", error instanceof Error ? error.message : "The authored TSX could not be parsed."));
148
+ }
149
+ };
150
+ const isSpanLocator = (locator) => "span" in locator;
151
+ const locatorOffset = (source, locator) => {
152
+ if (isSpanLocator(locator)) return null;
153
+ if (!Number.isInteger(locator.lineNumber) || !Number.isInteger(locator.columnNumber) || locator.lineNumber < 1 || locator.columnNumber < 1) return null;
154
+ const lineMap = buildLineMap(source);
155
+ if (locator.lineNumber > lineMap.length) return null;
156
+ const lineStart = lineMap[locator.lineNumber - 1];
157
+ const lineEnd = lineMap[locator.lineNumber] ?? source.length;
158
+ const offset = lineStart + locator.columnNumber - 1;
159
+ return offset < lineStart || offset >= lineEnd ? null : offset;
160
+ };
161
+ const locatorMatches = (source, authoredFile, locator, nodeSpan) => {
162
+ if (locator.fileName !== authoredFile) return false;
163
+ if (isSpanLocator(locator)) return Number.isInteger(locator.span.start) && Number.isInteger(locator.span.end) && locator.span.start === nodeSpan.start && locator.span.end === nodeSpan.end;
164
+ return locatorOffset(source, locator) === nodeSpan.start;
165
+ };
166
+ const locatorSpan = (locator) => locator !== void 0 && isSpanLocator(locator) ? locator.span : ZERO_SPAN;
167
+ /** Resolve one rendered Heading to one direct-literal JSX text range. */
168
+ const traceTsxHeadingTarget = (input) => {
169
+ const parsed = parseSml(input.emittedSource);
170
+ if (Result.isFailure(parsed)) return failure(...parsed.failure.diagnostics.map(({ message, span }) => diagnostic("tsx-invalid-emitted-source", message, span)));
171
+ const target = elementById(parsed.success.root, input.nodeId);
172
+ if (target === null) return failure(diagnostic("tsx-target-not-found", `rendered node "${input.nodeId}" was not found in the emitted SML source`));
173
+ if (target.tag !== "Heading") return failure(diagnostic("tsx-unsupported-target", `rendered node "${input.nodeId}" is <${target.tag}>; only a Heading can be edited`, target.span));
174
+ const renderedText = headingText(target);
175
+ if (renderedText === null) return failure(diagnostic("tsx-dynamic-target", "the rendered Heading contains dynamic or structured children and has no safe literal text range", target.span));
176
+ const locator = input.sourceLocator;
177
+ if (locator === void 0) return failure(diagnostic("tsx-unsupported-target", "the selected Heading has no exact authored TSX source locator; refusing to guess a range", target.span));
178
+ if (locator.fileName !== input.authoredFile) return failure(diagnostic("tsx-unsupported-target", `the source locator points to "${locator.fileName}", not the authored file "${input.authoredFile}"`, locatorSpan(locator)));
179
+ if (isSpanLocator(locator)) {
180
+ if (!Number.isInteger(locator.span.start) || !Number.isInteger(locator.span.end) || locator.span.start < 0 || locator.span.end <= locator.span.start || locator.span.end > input.authoredSource.length) return failure(diagnostic("tsx-unsupported-target", "the source locator span is outside the authored TSX source", locator.span));
181
+ } else if (locatorOffset(input.authoredSource, locator) === null) return failure(diagnostic("tsx-unsupported-target", "the source locator does not identify a position in the authored TSX source"));
182
+ const authored = parseAuthored(input.authoredSource);
183
+ if (Result.isFailure(authored)) return failure(authored.failure);
184
+ const candidates = [];
185
+ const dynamicSpans = [];
186
+ const unsupportedSpans = [];
187
+ walkAst(authored.success, (node) => {
188
+ if (node.type !== "JSXElement") return;
189
+ const name = jsxOpeningName(node);
190
+ if (name.kind === "dynamic") {
191
+ unsupportedSpans.push(astNodeSpan(node));
192
+ return;
193
+ }
194
+ if (name.name !== "Heading") {
195
+ unsupportedSpans.push(astNodeSpan(node));
196
+ return;
197
+ }
198
+ const literal = directLiteral(node, input.authoredSource);
199
+ if (literal === null) dynamicSpans.push(astNodeSpan(node));
200
+ else candidates.push(literal);
201
+ });
202
+ const mappedCandidates = candidates.filter((candidate) => locatorMatches(input.authoredSource, input.authoredFile, locator, candidate.nodeSpan));
203
+ const mappedDynamicSpans = dynamicSpans.filter((span) => locatorMatches(input.authoredSource, input.authoredFile, locator, span));
204
+ const mappedUnsupportedSpans = unsupportedSpans.filter((span) => locatorMatches(input.authoredSource, input.authoredFile, locator, span));
205
+ if (mappedDynamicSpans.length > 0) return failure(diagnostic("tsx-dynamic-target", "the source locator identifies a Heading with dynamic or structured children; refusing to edit it", mappedDynamicSpans[0]));
206
+ if (mappedUnsupportedSpans.length > 0) return failure(diagnostic("tsx-unsupported-target", "the source locator identifies a JSX component that is not the editable Heading primitive", mappedUnsupportedSpans[0]));
207
+ if (mappedCandidates.length === 0) return failure(diagnostic("tsx-target-not-found", "the source locator does not identify a Heading element in the authored TSX source", locatorSpan(locator)));
208
+ if (mappedCandidates.length !== 1) return failure(diagnostic("tsx-ambiguous-target", `the source locator identifies ${mappedCandidates.length} authored Heading elements; refusing to guess a source range`, mappedCandidates[0].nodeSpan));
209
+ const candidate = mappedCandidates[0];
210
+ if (normalizeText(renderedText) !== normalizeText(candidate.text)) return failure(diagnostic("tsx-ambiguous-target", "the selected emitted Heading does not match the located authored literal; refusing to edit stale source", candidate.nodeSpan));
211
+ return Result.succeed({
212
+ nodeId: input.nodeId,
213
+ text: candidate.text,
214
+ span: candidate.span,
215
+ form: candidate.form
216
+ });
217
+ };
218
+ /** Replace only the mapped literal bytes; all other authored TSX stays untouched. */
219
+ const replaceTsxHeadingText = (input) => {
220
+ const traced = traceTsxHeadingTarget(input);
221
+ if (Result.isFailure(traced)) return Result.fail(traced.failure);
222
+ const { start, end } = traced.success.span;
223
+ const replacement = serializeStaticTextChild(traced.success.form, input.text);
224
+ return Result.succeed(`${input.authoredSource.slice(0, start)}${replacement}${input.authoredSource.slice(end)}`);
225
+ };
226
+ var TsxSmlOpsError = class extends Data.TaggedError("TsxSmlOpsError") {};
227
+ const sourceDiagnostic = (code, message, span = ZERO_SPAN) => ({
228
+ code,
229
+ message,
230
+ span
231
+ });
232
+ const sourceFailure = (...diagnostics) => Result.fail(new TsxSmlOpsError({ diagnostics }));
233
+ const astNodes = (value) => {
234
+ if (!Array.isArray(value)) return [];
235
+ return value.filter(isAstNode);
236
+ };
237
+ const astNumber = (node, key) => {
238
+ const value = node[key];
239
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
240
+ };
241
+ const astValue = (node, key) => node[key];
242
+ /** The authored JSX component name, or `null` when the name is computed. */
243
+ const sourceComponentName = (node) => {
244
+ const name = jsxOpeningName(node);
245
+ return name.kind === "literal" ? name.name : null;
246
+ };
247
+ /**
248
+ * The SML tag an authored JSX element emits. `<Strong>` emits `strong`, so the
249
+ * authored name is resolved through the component registry before it is
250
+ * compared with an emitted node. A name outside the registry is kept as-is, so
251
+ * an imported custom component still fails `isSmlTag` exactly as before.
252
+ */
253
+ const sourceElementTag = (node) => {
254
+ const name = sourceComponentName(node);
255
+ if (name === null) return null;
256
+ return smlTagForComponentName(name) ?? name;
257
+ };
258
+ const isSmlTag = (value) => value !== null && SML_TAGS.includes(value);
259
+ const sourceAttributeName = (node) => {
260
+ const name = childNode(node, "name");
261
+ if (name === null || name.type !== "JSXIdentifier") return null;
262
+ return stringProperty(name, "name");
263
+ };
264
+ const sourceAttributes = (opening, source) => {
265
+ const attributes = [];
266
+ for (const attribute of astNodes(astValue(opening, "attributes"))) {
267
+ if (attribute.type === "JSXSpreadAttribute") {
268
+ attributes.push({
269
+ name: "...",
270
+ start: attribute.start,
271
+ end: attribute.end,
272
+ nameStart: attribute.start,
273
+ valueStart: attribute.start,
274
+ valueEnd: attribute.end,
275
+ kind: "dynamic",
276
+ value: null
277
+ });
278
+ continue;
279
+ }
280
+ if (attribute.type !== "JSXAttribute") continue;
281
+ const name = sourceAttributeName(attribute);
282
+ const nameNode = childNode(attribute, "name");
283
+ const valueNode = astValue(attribute, "value");
284
+ if (name === null || nameNode === null) {
285
+ attributes.push({
286
+ name: name ?? "?",
287
+ start: attribute.start,
288
+ end: attribute.end,
289
+ nameStart: attribute.start,
290
+ valueStart: attribute.start,
291
+ valueEnd: attribute.end,
292
+ kind: "dynamic",
293
+ value: null
294
+ });
295
+ continue;
296
+ }
297
+ if (isAstNode(valueNode) && valueNode.type === "StringLiteral") {
298
+ const value = stringProperty(valueNode, "value");
299
+ if (value !== null) {
300
+ attributes.push({
301
+ name,
302
+ start: attribute.start,
303
+ end: attribute.end,
304
+ nameStart: nameNode.start,
305
+ valueStart: valueNode.start,
306
+ valueEnd: valueNode.end,
307
+ kind: "string",
308
+ value
309
+ });
310
+ continue;
311
+ }
312
+ }
313
+ if (isAstNode(valueNode) && valueNode.type === "JSXExpressionContainer") {
314
+ const expression = childNode(valueNode, "expression");
315
+ const value = expression === null ? null : astNumber(expression, "value");
316
+ if (expression !== null && expression.type === "NumericLiteral" && value !== null) {
317
+ attributes.push({
318
+ name,
319
+ start: attribute.start,
320
+ end: attribute.end,
321
+ nameStart: nameNode.start,
322
+ valueStart: expression.start,
323
+ valueEnd: expression.end,
324
+ kind: "number",
325
+ value
326
+ });
327
+ continue;
328
+ }
329
+ }
330
+ attributes.push({
331
+ name,
332
+ start: attribute.start,
333
+ end: attribute.end,
334
+ nameStart: nameNode.start,
335
+ valueStart: valueNode && isAstNode(valueNode) ? valueNode.start : attribute.end,
336
+ valueEnd: valueNode && isAstNode(valueNode) ? valueNode.end : attribute.end,
337
+ kind: "dynamic",
338
+ value: null
339
+ });
340
+ }
341
+ return attributes;
342
+ };
343
+ const sourceChildParts = (node, source, parent, built) => {
344
+ const children = [];
345
+ for (const child of astNodes(astValue(node, "children"))) {
346
+ if (child.type === "JSXElement") {
347
+ children.push(buildSourceElement(child, source, parent, built));
348
+ continue;
349
+ }
350
+ if (child.type === "JSXText") {
351
+ children.push({
352
+ kind: "text",
353
+ start: child.start,
354
+ end: child.end,
355
+ form: "jsx-text"
356
+ });
357
+ continue;
358
+ }
359
+ const literal = staticStringLiteralText(child);
360
+ if (literal !== null) {
361
+ children.push({
362
+ kind: "text",
363
+ start: child.start,
364
+ end: child.end,
365
+ form: "string-literal",
366
+ literal
367
+ });
368
+ continue;
369
+ }
370
+ children.push({
371
+ kind: "opaque",
372
+ start: child.start,
373
+ end: child.end
374
+ });
375
+ }
376
+ return children;
377
+ };
378
+ const buildSourceElement = (node, source, parent, built) => {
379
+ const opening = childNode(node, "openingElement");
380
+ const closing = childNode(node, "closingElement");
381
+ const openingStart = opening?.start ?? node.start;
382
+ const openingEnd = opening?.end ?? node.start;
383
+ const selfClosing = source.slice(Math.max(openingEnd - 2, openingStart), openingEnd) === "/>";
384
+ const element = {
385
+ kind: "element",
386
+ ast: node,
387
+ tag: sourceElementTag(node),
388
+ componentName: sourceComponentName(node),
389
+ start: node.start,
390
+ end: node.end,
391
+ openingStart,
392
+ openingEnd,
393
+ closingStart: closing?.start ?? null,
394
+ closingEnd: closing?.end ?? null,
395
+ selfClosing,
396
+ attributes: opening === null ? [] : sourceAttributes(opening, source),
397
+ children: [],
398
+ parent,
399
+ active: true,
400
+ propEdits: /* @__PURE__ */ new Map()
401
+ };
402
+ built.add(node);
403
+ element.children = sourceChildParts(node, source, element, built);
404
+ return element;
405
+ };
406
+ const collectSourceElements = (program, source) => {
407
+ const built = /* @__PURE__ */ new Set();
408
+ const elements = [];
409
+ const collect = (element) => {
410
+ elements.push(element);
411
+ for (const child of element.children) if (child.kind === "element") collect(child);
412
+ };
413
+ walkAst(program, (node) => {
414
+ if (node.type !== "JSXElement" || built.has(node)) return;
415
+ collect(buildSourceElement(node, source, null, built));
416
+ });
417
+ return elements;
418
+ };
419
+ const sourceSpan = (element) => ({
420
+ start: element.start,
421
+ end: element.end
422
+ });
423
+ const sourceTextValue = (part, source) => part.form === "string-literal" ? part.literal ?? "" : decodeEntities(source.slice(part.start, part.end));
424
+ /** The name to print in a diagnostic or a synthesized closing tag: what the author wrote. */
425
+ const sourceTagName = (element) => element.componentName ?? "<dynamic JSX component>";
426
+ const sourceElementChildren = (element) => element.children.filter((child) => child.kind === "element");
427
+ const sourceIsAttached = (element, root) => {
428
+ if (!element.active) return false;
429
+ let current = element;
430
+ while (current !== null && current !== root) current = current.parent;
431
+ return current === root;
432
+ };
433
+ const markSourceSubtree = (element, active) => {
434
+ element.active = active;
435
+ for (const child of element.children) if (child.kind === "element") markSourceSubtree(child, active);
436
+ };
437
+ const sourceLocatorIsValid = (source, locator) => {
438
+ if (isSpanLocator(locator)) return Number.isInteger(locator.span.start) && Number.isInteger(locator.span.end) && locator.span.start >= 0 && locator.span.end > locator.span.start && locator.span.end <= source.length;
439
+ return locatorOffset(source, locator) !== null;
440
+ };
441
+ const renderStaticAttribute = (name, value) => value.kind === "number" ? `${name}={${value.value}}` : `${name}="${value.value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")}"`;
442
+ const sourceValueMatches = (left, right) => left !== void 0 && right !== void 0 && left.kind === right.kind && left.value === right.value;
443
+ const sourceAttributeValue = (attribute) => {
444
+ if (attribute.kind === "string" && typeof attribute.value === "string") return {
445
+ kind: "string",
446
+ value: attribute.value
447
+ };
448
+ if (attribute.kind === "number" && typeof attribute.value === "number") return {
449
+ kind: "number",
450
+ value: attribute.value
451
+ };
452
+ };
453
+ const sourceStaticAttributes = (element) => {
454
+ const values = /* @__PURE__ */ new Map();
455
+ for (const attribute of element.attributes) {
456
+ const value = sourceAttributeValue(attribute);
457
+ if (attribute.kind === "dynamic" || value === void 0) return Result.fail(sourceDiagnostic("tsx-dynamic-target", `authored <${sourceTagName(element)}> has a dynamic or spread attribute; refusing to rewrite it`, {
458
+ start: attribute.start,
459
+ end: attribute.end
460
+ }));
461
+ if (values.has(attribute.name)) return Result.fail(sourceDiagnostic("tsx-ambiguous-target", `authored <${sourceTagName(element)}> repeats attribute "${attribute.name}"; refusing to choose one`, {
462
+ start: attribute.start,
463
+ end: attribute.end
464
+ }));
465
+ values.set(attribute.name, value);
466
+ }
467
+ return Result.succeed(values);
468
+ };
469
+ const currentSourceAttributes = (element) => {
470
+ const original = sourceStaticAttributes(element);
471
+ if (Result.isFailure(original)) return original;
472
+ const values = new Map(original.success);
473
+ for (const [name, value] of element.propEdits) if (value === null || value.kind === "string" && value.value === "") values.delete(name);
474
+ else values.set(name, value);
475
+ return Result.succeed(values);
476
+ };
477
+ const emittedAttributes = (element) => new Map(element.attributes.map((attribute) => [attribute.name, attribute.value]));
478
+ const attributesMatch = (source, emitted) => {
479
+ if (source.size !== emitted.size) return false;
480
+ for (const [name, value] of source) if (!sourceValueMatches(value, emitted.get(name))) return false;
481
+ return true;
482
+ };
483
+ const validateMappedElement = (source, emitted, root, authoredSource, expectedAttributes, validateAttributes = true) => {
484
+ if (!sourceIsAttached(source, root)) return Result.fail(sourceDiagnostic("tsx-target-not-found", `the authored JSX node for emitted "${emitted.id}" is no longer in the active source tree`, sourceSpan(source)));
485
+ if (!isSmlTag(source.tag)) return Result.fail(sourceDiagnostic("tsx-unsupported-target", `authored ${sourceTagName(source)} is not a recognized Samva primitive`, sourceSpan(source)));
486
+ if (source.tag !== emitted.tag) return Result.fail(sourceDiagnostic("tsx-ambiguous-target", `emitted node "${emitted.id}" is <${emitted.tag}> but its locator identifies authored <${sourceTagName(source)}>`, sourceSpan(source)));
487
+ if (validateAttributes) {
488
+ const sourceValues = currentSourceAttributes(source);
489
+ if (Result.isFailure(sourceValues)) return Result.fail(sourceValues.failure);
490
+ const emittedValues = expectedAttributes ?? emittedAttributes(emitted);
491
+ if (!attributesMatch(sourceValues.success, emittedValues)) return Result.fail(sourceDiagnostic("tsx-ambiguous-target", `emitted <${emitted.tag}> does not match the located authored JSX attributes; refusing to edit stale source`, sourceSpan(source)));
492
+ }
493
+ if (source.end > authoredSource.length || source.start < 0) return Result.fail(sourceDiagnostic("tsx-unsupported-target", "the mapped JSX element lies outside the authored source", sourceSpan(source)));
494
+ return Result.succeed(source);
495
+ };
496
+ const sourceElementForLocator = (sourceElements, authoredSource, authoredFile, locator) => {
497
+ if (locator.fileName !== authoredFile) return Result.fail(sourceDiagnostic("tsx-unsupported-target", `the source locator points to "${locator.fileName}", not the authored file "${authoredFile}"`, locatorSpan(locator)));
498
+ if (!sourceLocatorIsValid(authoredSource, locator)) return Result.fail(sourceDiagnostic("tsx-unsupported-target", "the source locator does not identify a position in the authored TSX source", locatorSpan(locator)));
499
+ const candidates = sourceElements.filter((element) => locatorMatches(authoredSource, authoredFile, locator, sourceSpan(element)));
500
+ if (candidates.length === 0) return Result.fail(sourceDiagnostic("tsx-target-not-found", "the source locator does not identify a JSX element in the authored TSX source", locatorSpan(locator)));
501
+ if (candidates.length !== 1) return Result.fail(sourceDiagnostic("tsx-ambiguous-target", `the source locator identifies ${candidates.length} JSX elements; refusing to guess a source range`, sourceSpan(candidates[0])));
502
+ return Result.succeed(candidates[0]);
503
+ };
504
+ const sourceElementForId = (id, emittedRoot, sourceElementsById, sourceMapErrors) => {
505
+ const emitted = elementById(emittedRoot, id);
506
+ if (emitted === null) return Result.fail(sourceDiagnostic("tsx-target-not-found", `emitted node "${id}" was not found in the SML source`));
507
+ const mapError = sourceMapErrors.get(id);
508
+ if (mapError !== void 0) return Result.fail(mapError);
509
+ const source = sourceElementsById.get(id);
510
+ if (source === void 0) return Result.fail(sourceDiagnostic("tsx-unsupported-target", `emitted node "${id}" has no exact authored TSX source locator; refusing to guess a range`, emitted.span));
511
+ return Result.succeed({
512
+ source,
513
+ emitted
514
+ });
515
+ };
516
+ const renderSourceOpening = (element, authoredSource) => {
517
+ if (element.propEdits.size === 0) return authoredSource.slice(element.openingStart, element.openingEnd);
518
+ const edits = [];
519
+ const originalNames = new Set(element.attributes.map((attribute) => attribute.name));
520
+ for (const attribute of element.attributes) {
521
+ if (attribute.kind === "dynamic") continue;
522
+ if (!element.propEdits.has(attribute.name)) continue;
523
+ const value = element.propEdits.get(attribute.name);
524
+ if (value === void 0) continue;
525
+ if (value === null || value.kind === "string" && value.value === "") {
526
+ edits.push({
527
+ start: attribute.start > element.openingStart && authoredSource[attribute.start - 1] === " " ? attribute.start - 1 : attribute.start,
528
+ end: attribute.end,
529
+ value: ""
530
+ });
531
+ continue;
532
+ }
533
+ const original = sourceAttributeValue(attribute);
534
+ if (original !== void 0 && sourceValueMatches(original, value)) continue;
535
+ if (original?.kind === value.kind) {
536
+ if (value.kind === "string") edits.push({
537
+ start: attribute.valueStart + 1,
538
+ end: Math.max(attribute.valueStart + 1, attribute.valueEnd - 1),
539
+ value: value.value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")
540
+ });
541
+ else edits.push({
542
+ start: attribute.valueStart,
543
+ end: attribute.valueEnd,
544
+ value: String(value.value)
545
+ });
546
+ } else edits.push({
547
+ start: attribute.start,
548
+ end: attribute.end,
549
+ value: renderStaticAttribute(attribute.name, value)
550
+ });
551
+ }
552
+ const added = [];
553
+ for (const [name, value] of element.propEdits) if (!originalNames.has(name) && value !== null && !(value.kind === "string" && value.value === "")) added.push([name, value]);
554
+ if (added.length > 0) {
555
+ const closingLength = element.selfClosing ? 2 : 1;
556
+ const insertion = element.openingEnd - closingLength;
557
+ const prefix = insertion > element.openingStart && /\s/u.test(authoredSource[insertion - 1] ?? "") ? "" : " ";
558
+ edits.push({
559
+ start: insertion,
560
+ end: insertion,
561
+ value: `${prefix}${added.map(([name, value]) => renderStaticAttribute(name, value)).join(" ")}`
562
+ });
563
+ }
564
+ let opening = authoredSource.slice(element.openingStart, element.openingEnd);
565
+ for (const edit of edits.toSorted((left, right) => right.start - left.start)) {
566
+ const relativeStart = edit.start - element.openingStart;
567
+ const relativeEnd = edit.end - element.openingStart;
568
+ opening = `${opening.slice(0, relativeStart)}${edit.value}${opening.slice(relativeEnd)}`;
569
+ }
570
+ return opening;
571
+ };
572
+ const renderSourceElement = (element, authoredSource) => {
573
+ const opening = renderSourceOpening(element, authoredSource);
574
+ const children = element.children.map((child) => {
575
+ if (child.kind === "raw") return child.source;
576
+ if (child.kind === "text") return child.replacement ?? authoredSource.slice(child.start, child.end);
577
+ if (child.kind === "opaque") return authoredSource.slice(child.start, child.end);
578
+ return renderSourceElement(child, authoredSource);
579
+ });
580
+ if (children.length === 0) {
581
+ if (element.selfClosing) return opening;
582
+ return `${opening.slice(0, -1).trimEnd()} />`;
583
+ }
584
+ const open = element.selfClosing ? `${opening.slice(0, -2).trimEnd()}>` : opening;
585
+ const close = element.closingStart === null || element.closingEnd === null ? `</${sourceTagName(element)}>` : authoredSource.slice(element.closingStart, element.closingEnd);
586
+ return `${open}${children.join("")}${close}`;
587
+ };
588
+ const sourceElementParentIndex = (parent, child) => parent.children.findIndex((candidate) => candidate === child);
589
+ const elementChildInsertionIndex = (children, insertionIndex) => {
590
+ if (insertionIndex <= 0) {
591
+ const first = children.findIndex((child) => child.kind === "element");
592
+ return first < 0 ? children.length : first;
593
+ }
594
+ let seen = 0;
595
+ for (let index = 0; index < children.length; index++) {
596
+ if (children[index]?.kind !== "element") continue;
597
+ if (seen === insertionIndex) return index;
598
+ seen++;
599
+ }
600
+ return children.length;
601
+ };
602
+ const activeDescendant = (ancestor, candidate) => {
603
+ let current = candidate.parent;
604
+ while (current !== null) {
605
+ if (current === ancestor) return true;
606
+ current = current.parent;
607
+ }
608
+ return false;
609
+ };
610
+ const detachSourceElement = (element) => {
611
+ if (element.parent === null) return false;
612
+ const parent = element.parent;
613
+ const index = sourceElementParentIndex(parent, element);
614
+ if (index < 0) return false;
615
+ parent.children.splice(index, 1);
616
+ element.parent = null;
617
+ markSourceSubtree(element, false);
618
+ return true;
619
+ };
620
+ const attachSourceElement = (element, parent, index) => {
621
+ element.parent = parent;
622
+ parent.children.splice(index, 0, element);
623
+ markSourceSubtree(element, true);
624
+ };
625
+ const escapedStaticText = (value) => escapeJsxText(value);
626
+ const renderStaticSmlNode = (node) => {
627
+ if (node.kind === "interpolation") return Result.fail(sourceDiagnostic("tsx-dynamic-target", `cannot safely print interpolation {{${node.name}}} into a TSX child list without a variable binding`, node.span));
628
+ if (node.kind === "text") return Result.succeed(escapedStaticText(node.value));
629
+ if (!isSmlTag(node.tag)) return Result.fail(sourceDiagnostic("tsx-unsupported-target", `cannot print unknown SML tag <${node.tag}>`, node.span));
630
+ const attributes = node.attributes.map((attribute) => renderStaticAttribute(attribute.name, attribute.value)).join(" ");
631
+ const name = componentNameForSmlTag(node.tag);
632
+ const open = attributes.length === 0 ? `<${name}` : `<${name} ${attributes}`;
633
+ if (node.children.length === 0) return Result.succeed(`${open} />`);
634
+ const children = [];
635
+ for (const child of node.children) {
636
+ const rendered = renderStaticSmlNode(child);
637
+ if (Result.isFailure(rendered)) return rendered;
638
+ children.push(rendered.success);
639
+ }
640
+ return Result.succeed(`${open}>${children.join("")}</${name}>`);
641
+ };
642
+ const renderStaticSmlChildren = (children) => {
643
+ const rendered = [];
644
+ for (const child of children) {
645
+ const value = renderStaticSmlNode(child);
646
+ if (Result.isFailure(value)) return value;
647
+ rendered.push(value.success);
648
+ }
649
+ return Result.succeed(rendered.join(""));
650
+ };
651
+ /**
652
+ * The one static text child an inline edit may rewrite. A formatter puts a
653
+ * `{"…"}` child on its own line, so blank JSX text around it is ignored; any
654
+ * other child kind makes the element structured and disqualifies it.
655
+ */
656
+ const directTextPart = (element, source) => {
657
+ if (element.children.some((child) => child.kind !== "text")) return null;
658
+ const parts = element.children.filter((child) => child.kind === "text" && sourceTextValue(child, source).trim().length > 0);
659
+ return parts.length === 1 ? parts[0] : null;
660
+ };
661
+ const replaceDirectText = (element, emitted, source, text) => {
662
+ if (emitted.children.length !== 1 || emitted.children[0]?.kind !== "text") return Result.succeed(false);
663
+ const part = directTextPart(element, source);
664
+ if (part === null) return Result.succeed(false);
665
+ const current = sourceTextValue(part, source);
666
+ if (normalizeText(current) !== normalizeText(emitted.children[0].value)) return Result.fail(sourceDiagnostic("tsx-ambiguous-target", `emitted <${emitted.tag}> text does not match the located authored literal; refusing to edit stale source`, sourceSpan(element)));
667
+ if (part.form === "string-literal") {
668
+ part.replacement = serializeStaticTextChild("string-literal", text);
669
+ return Result.succeed(true);
670
+ }
671
+ const raw = source.slice(part.start, part.end);
672
+ const leading = raw.search(/\S/u);
673
+ if (leading < 0) return Result.succeed(false);
674
+ let trailing = raw.length;
675
+ while (trailing > leading && /\s/u.test(raw[trailing - 1])) trailing -= 1;
676
+ part.replacement = `${raw.slice(0, leading)}${escapedStaticText(text)}${raw.slice(trailing)}`;
677
+ return Result.succeed(true);
678
+ };
679
+ const validateStaticSnippet = (source, snippet) => {
680
+ let parsed;
681
+ try {
682
+ parsed = parse(`const __samvaSnippet = <>${snippet}</>;`, {
683
+ plugins: ["typescript", "jsx"],
684
+ sourceType: "module"
685
+ }).program;
686
+ } catch (error) {
687
+ return Result.fail(sourceDiagnostic("tsx-invalid-snippet", error instanceof Error ? error.message : "the insert snippet is not valid TSX"));
688
+ }
689
+ const fragments = [];
690
+ walkAst(parsed, (node) => {
691
+ if (node.type === "JSXFragment") fragments.push(node);
692
+ });
693
+ const fragment = fragments[0];
694
+ if (fragment === void 0) return Result.fail(sourceDiagnostic("tsx-invalid-snippet", "the insert snippet has no JSX fragment"));
695
+ const visitElement = (node) => {
696
+ const tag = sourceElementTag(node);
697
+ if (!isSmlTag(tag)) return sourceDiagnostic("tsx-unsupported-target", `insert snippet contains ${tag === null ? "a dynamic JSX component" : `<${tag}>`}, not a recognized Samva primitive`, astNodeSpan(node));
698
+ const opening = childNode(node, "openingElement");
699
+ if (opening !== null) {
700
+ for (const attribute of sourceAttributes(opening, source)) if (attribute.kind === "dynamic") return sourceDiagnostic("tsx-dynamic-target", `insert snippet <${tag}> contains a dynamic or spread attribute`, {
701
+ start: attribute.start,
702
+ end: attribute.end
703
+ });
704
+ }
705
+ for (const child of astNodes(astValue(node, "children"))) {
706
+ if (child.type === "JSXText") continue;
707
+ if (child.type !== "JSXElement") return sourceDiagnostic("tsx-dynamic-target", `insert snippet <${tag}> contains a dynamic or opaque child`, astNodeSpan(child));
708
+ const childFailure = visitElement(child);
709
+ if (childFailure !== null) return childFailure;
710
+ }
711
+ return null;
712
+ };
713
+ const directChildren = astNodes(astValue(fragment, "children"));
714
+ let elementCount = 0;
715
+ for (const child of directChildren) {
716
+ if (child.type === "JSXText") {
717
+ if (source.slice(child.start, child.end).trim().length > 0) return Result.fail(sourceDiagnostic("tsx-invalid-snippet", "insert snippet must contain only JSX elements", astNodeSpan(child)));
718
+ continue;
719
+ }
720
+ if (child.type !== "JSXElement") return Result.fail(sourceDiagnostic("tsx-dynamic-target", "insert snippet contains an opaque JSX child", astNodeSpan(child)));
721
+ elementCount++;
722
+ const childFailure = visitElement(child);
723
+ if (childFailure !== null) return Result.fail(childFailure);
724
+ }
725
+ return elementCount === 0 ? Result.fail(sourceDiagnostic("tsx-invalid-snippet", "insert snippet contains no elements")) : Result.succeed(void 0);
726
+ };
727
+ const allEmittedElements = (root) => {
728
+ const elements = [];
729
+ const visit = (node) => {
730
+ elements.push(node);
731
+ for (const child of node.children) if (child.kind === "element") visit(child);
732
+ };
733
+ visit(root);
734
+ return elements;
735
+ };
736
+ const sourceTargetChildrenAreStatic = (target) => {
737
+ for (const child of target.children) if (child.kind === "opaque" || child.kind === "raw") return Result.fail(sourceDiagnostic("tsx-dynamic-target", `authored <${sourceTagName(target)}> has a dynamic or opaque child; refusing to replace its complete child list`, child.kind === "raw" ? sourceSpan(target) : {
738
+ start: child.start,
739
+ end: child.end
740
+ }));
741
+ return Result.succeed(void 0);
742
+ };
743
+ const staticSourceTreeMatches = (source, emitted, authoredSource) => {
744
+ if (source.tag !== emitted.tag) return false;
745
+ const attributes = sourceStaticAttributes(source);
746
+ if (Result.isFailure(attributes) || !attributesMatch(attributes.success, emittedAttributes(emitted))) return false;
747
+ const sourceChildren = source.children.filter((child) => child.kind !== "text" || normalizeText(sourceTextValue(child, authoredSource)) !== "");
748
+ const emittedChildren = emitted.children.filter((child) => child.kind !== "text" || normalizeText(child.value) !== "");
749
+ if (sourceChildren.length !== emittedChildren.length) return false;
750
+ for (let index = 0; index < sourceChildren.length; index++) {
751
+ const sourceChild = sourceChildren[index];
752
+ const emittedChild = emittedChildren[index];
753
+ if (sourceChild.kind === "text" && emittedChild.kind === "text") {
754
+ if (normalizeText(sourceTextValue(sourceChild, authoredSource)) !== normalizeText(emittedChild.value)) return false;
755
+ continue;
756
+ }
757
+ if (sourceChild.kind !== "element" || emittedChild.kind !== "element" || !staticSourceTreeMatches(sourceChild, emittedChild, authoredSource)) return false;
758
+ }
759
+ return true;
760
+ };
761
+ const removeEmittedSubtree = (values, node) => {
762
+ values.delete(node.id);
763
+ for (const child of node.children) if (child.kind === "element") removeEmittedSubtree(values, child);
764
+ };
765
+ /**
766
+ * Apply semantic editor operations to the authored TSX entry.
767
+ *
768
+ * The emitted SML document remains the validation oracle: operations are
769
+ * preflighted through the same markup op implementation first, then each
770
+ * mapped target is checked against the authored JSX AST and exact source
771
+ * provenance. The mutable source tree below only moves original AST nodes or
772
+ * inserts validated raw fragments; it never reprints the user's module.
773
+ */
774
+ const applyTsxSmlOps = (input) => {
775
+ const emitted = parseSml(input.emittedSource);
776
+ if (Result.isFailure(emitted)) return sourceFailure(...emitted.failure.diagnostics.map(({ message, span }) => sourceDiagnostic("tsx-invalid-emitted-source", message, span)));
777
+ const authored = parseAuthored(input.authoredSource);
778
+ if (Result.isFailure(authored)) return sourceFailure(authored.failure);
779
+ if (input.ops.length === 0) return Result.succeed(input.authoredSource);
780
+ const preflight = applyOps(input.emittedSource, input.ops);
781
+ if (Result.isFailure(preflight)) return sourceFailure(...preflight.failure.diagnostics.map(({ message, span, code }) => sourceDiagnostic(code === "op-target-not-found" ? "tsx-target-not-found" : "tsx-unsupported-target", message, span)));
782
+ const failedOperation = preflight.success.results.find((result) => result.status === "failed");
783
+ if (failedOperation !== void 0) return sourceFailure(...failedOperation.diagnostics.map(({ message, span, code }) => sourceDiagnostic(code === "op-target-not-found" ? "tsx-target-not-found" : "tsx-unsupported-target", message, span)));
784
+ const sourceElements = collectSourceElements(authored.success, input.authoredSource);
785
+ const sourceElementsById = /* @__PURE__ */ new Map();
786
+ const sourceIdByElement = /* @__PURE__ */ new Map();
787
+ const sourceMapErrors = /* @__PURE__ */ new Map();
788
+ const firstIdBySourceElement = /* @__PURE__ */ new Map();
789
+ for (const [id, locator] of Object.entries(input.sourceLocations.byNodeId)) {
790
+ const resolved = sourceElementForLocator(sourceElements, input.authoredSource, input.authoredFile, locator);
791
+ if (Result.isFailure(resolved)) {
792
+ sourceMapErrors.set(id, resolved.failure);
793
+ continue;
794
+ }
795
+ const existingId = firstIdBySourceElement.get(resolved.success);
796
+ if (existingId !== void 0) {
797
+ const duplicate = sourceDiagnostic("tsx-ambiguous-target", `source locators for emitted nodes "${existingId}" and "${id}" identify the same authored JSX element`, sourceSpan(resolved.success));
798
+ sourceMapErrors.set(existingId, duplicate);
799
+ sourceMapErrors.set(id, duplicate);
800
+ continue;
801
+ }
802
+ firstIdBySourceElement.set(resolved.success, id);
803
+ sourceElementsById.set(id, resolved.success);
804
+ sourceIdByElement.set(resolved.success, id);
805
+ }
806
+ const rootId = emitted.success.root.id;
807
+ const mappedRoot = sourceElementForId(rootId, emitted.success.root, sourceElementsById, sourceMapErrors);
808
+ if (Result.isFailure(mappedRoot)) return sourceFailure(mappedRoot.failure);
809
+ if (mappedRoot.success.source.parent !== null) return sourceFailure(sourceDiagnostic("tsx-unsupported-target", "the emitted root locator identifies a nested JSX element, not the template root", sourceSpan(mappedRoot.success.source)));
810
+ const root = mappedRoot.success.source;
811
+ const rootValidation = validateMappedElement(root, mappedRoot.success.emitted, root, input.authoredSource);
812
+ if (Result.isFailure(rootValidation)) return sourceFailure(rootValidation.failure);
813
+ for (const element of sourceElements) markSourceSubtree(element, false);
814
+ markSourceSubtree(root, true);
815
+ const virtualAttributes = new Map(allEmittedElements(emitted.success.root).map((element) => [element.id, new Map(emittedAttributes(element))]));
816
+ const resolveTarget = (id) => {
817
+ const resolved = sourceElementForId(id, emitted.success.root, sourceElementsById, sourceMapErrors);
818
+ if (Result.isFailure(resolved)) return Result.fail(resolved.failure);
819
+ const validated = validateMappedElement(resolved.success.source, resolved.success.emitted, root, input.authoredSource, virtualAttributes.get(id));
820
+ if (Result.isFailure(validated)) return Result.fail(validated.failure);
821
+ return Result.succeed(resolved.success);
822
+ };
823
+ const resolveParentFor = (source) => {
824
+ if (source.parent === null) return Result.fail(sourceDiagnostic("tsx-target-not-found", `authored <${sourceTagName(source)}> has no active JSX parent`, sourceSpan(source)));
825
+ const id = sourceIdByElement.get(source.parent);
826
+ if (id === void 0) return Result.fail(sourceDiagnostic("tsx-unsupported-target", `authored parent <${sourceTagName(source.parent)}> has no exact source-map entry`, sourceSpan(source.parent)));
827
+ const resolved = resolveTarget(id);
828
+ if (Result.isFailure(resolved)) return Result.fail(resolved.failure);
829
+ return Result.succeed(source.parent);
830
+ };
831
+ const resolveRootTarget = () => {
832
+ const validated = resolveTarget(rootId);
833
+ if (Result.isFailure(validated)) return Result.fail(validated.failure);
834
+ return Result.succeed(validated.success.source);
835
+ };
836
+ const applySetProp = (op) => {
837
+ if (op.name === "src:dark") return Result.fail(sourceDiagnostic("tsx-unsupported-operation", "src:dark is a derived SML source and has no direct TSX attribute splice"));
838
+ const resolved = resolveTarget(op.nodeId);
839
+ if (Result.isFailure(resolved)) return Result.fail(resolved.failure);
840
+ const value = op.value?.kind === "string" && op.value.value === "" ? null : op.value;
841
+ resolved.success.source.propEdits.set(op.name, value);
842
+ const attributes = virtualAttributes.get(op.nodeId);
843
+ if (attributes === void 0) return Result.fail(sourceDiagnostic("tsx-target-not-found", `emitted node "${op.nodeId}" was not found in the SML attribute map`));
844
+ if (value === null) attributes.delete(op.name);
845
+ else attributes.set(op.name, value);
846
+ return Result.succeed(void 0);
847
+ };
848
+ const applySetChildren = (op) => {
849
+ const mapped = sourceElementForId(op.nodeId, emitted.success.root, sourceElementsById, sourceMapErrors);
850
+ if (Result.isFailure(mapped)) return Result.fail(mapped.failure);
851
+ const mappedIdentity = validateMappedElement(mapped.success.source, mapped.success.emitted, root, input.authoredSource, void 0, false);
852
+ if (Result.isFailure(mappedIdentity)) return Result.fail(mappedIdentity.failure);
853
+ const rendered = renderStaticSmlChildren(op.children);
854
+ if (Result.isFailure(rendered)) return Result.fail(rendered.failure);
855
+ if (op.children.length === 1 && op.children[0]?.kind === "text") {
856
+ const direct = replaceDirectText(mapped.success.source, mapped.success.emitted, input.authoredSource, op.children[0].value);
857
+ if (Result.isFailure(direct)) return Result.fail(direct.failure);
858
+ if (direct.success) return Result.succeed(void 0);
859
+ }
860
+ const resolved = resolveTarget(op.nodeId);
861
+ if (Result.isFailure(resolved)) return Result.fail(resolved.failure);
862
+ const { source, emitted: emittedElement } = resolved.success;
863
+ const staticChildren = sourceTargetChildrenAreStatic(source);
864
+ if (Result.isFailure(staticChildren)) return staticChildren;
865
+ if (!staticSourceTreeMatches(source, emittedElement, input.authoredSource)) return Result.fail(sourceDiagnostic("tsx-ambiguous-target", `emitted <${emittedElement.tag}> does not match the located static authored JSX subtree`, sourceSpan(source)));
866
+ for (const child of sourceElementChildren(source)) {
867
+ child.parent = null;
868
+ markSourceSubtree(child, false);
869
+ }
870
+ source.children = rendered.success.length === 0 ? [] : [{
871
+ kind: "raw",
872
+ source: rendered.success
873
+ }];
874
+ return Result.succeed(void 0);
875
+ };
876
+ const applyInsertSnippet = (op) => {
877
+ const validSnippet = validateStaticSnippet(input.authoredSource, op.snippet);
878
+ if (Result.isFailure(validSnippet)) return validSnippet;
879
+ let parent;
880
+ let insertionIndex;
881
+ if (op.referenceId === null || op.referenceId === rootId) {
882
+ const resolvedRoot = resolveRootTarget();
883
+ if (Result.isFailure(resolvedRoot)) return Result.fail(resolvedRoot.failure);
884
+ parent = resolvedRoot.success;
885
+ insertionIndex = op.position === "before" ? 0 : parent.children.length;
886
+ } else {
887
+ const reference = resolveTarget(op.referenceId);
888
+ if (Result.isFailure(reference)) return Result.fail(reference.failure);
889
+ if (op.position === "inside") {
890
+ parent = reference.success.source;
891
+ insertionIndex = parent.children.length;
892
+ } else {
893
+ const resolvedParent = resolveParentFor(reference.success.source);
894
+ if (Result.isFailure(resolvedParent)) return Result.fail(resolvedParent.failure);
895
+ parent = resolvedParent.success;
896
+ const referenceIndex = sourceElementParentIndex(parent, reference.success.source);
897
+ if (referenceIndex < 0) return Result.fail(sourceDiagnostic("tsx-target-not-found", `reference node "${op.referenceId}" is no longer attached`));
898
+ insertionIndex = referenceIndex + (op.position === "after" ? 1 : 0);
899
+ }
900
+ }
901
+ const parentId = sourceIdByElement.get(parent);
902
+ if (parentId !== void 0) {
903
+ const resolvedParent = resolveTarget(parentId);
904
+ if (Result.isFailure(resolvedParent)) return Result.fail(resolvedParent.failure);
905
+ }
906
+ parent.children.splice(insertionIndex, 0, {
907
+ kind: "raw",
908
+ source: op.snippet
909
+ });
910
+ return Result.succeed(void 0);
911
+ };
912
+ const applyDuplicate = (op) => {
913
+ const resolved = resolveTarget(op.nodeId);
914
+ if (Result.isFailure(resolved)) return Result.fail(resolved.failure);
915
+ if (resolved.success.source === root) return Result.fail(sourceDiagnostic("tsx-unsupported-operation", "the document root cannot be duplicated"));
916
+ const parent = resolveParentFor(resolved.success.source);
917
+ if (Result.isFailure(parent)) return Result.fail(parent.failure);
918
+ const index = sourceElementParentIndex(parent.success, resolved.success.source);
919
+ if (index < 0) return Result.fail(sourceDiagnostic("tsx-target-not-found", "the node is no longer attached"));
920
+ parent.success.children.splice(index + 1, 0, {
921
+ kind: "raw",
922
+ source: renderSourceElement(resolved.success.source, input.authoredSource)
923
+ });
924
+ return Result.succeed(void 0);
925
+ };
926
+ const applyRemove = (op) => {
927
+ const resolved = resolveTarget(op.nodeId);
928
+ if (Result.isFailure(resolved)) return Result.fail(resolved.failure);
929
+ if (resolved.success.source === root) return Result.fail(sourceDiagnostic("tsx-unsupported-operation", "the document root cannot be removed"));
930
+ const parent = resolveParentFor(resolved.success.source);
931
+ if (Result.isFailure(parent)) return Result.fail(parent.failure);
932
+ if (!detachSourceElement(resolved.success.source)) return Result.fail(sourceDiagnostic("tsx-target-not-found", `node "${op.nodeId}" is no longer attached`));
933
+ const emittedNode = resolved.success.emitted;
934
+ removeEmittedSubtree(virtualAttributes, emittedNode);
935
+ return Result.succeed(void 0);
936
+ };
937
+ const applyMove = (op) => {
938
+ const moved = resolveTarget(op.nodeId);
939
+ if (Result.isFailure(moved)) return Result.fail(moved.failure);
940
+ const parent = resolveTarget(op.targetParentId);
941
+ if (Result.isFailure(parent)) return Result.fail(parent.failure);
942
+ if (moved.success.source === root) return Result.fail(sourceDiagnostic("tsx-unsupported-operation", "the document root cannot be moved"));
943
+ if (moved.success.source === parent.success.source || activeDescendant(moved.success.source, parent.success.source)) return Result.fail(sourceDiagnostic("tsx-unsupported-operation", "a node cannot be moved into itself or its descendants"));
944
+ const oldParent = resolveParentFor(moved.success.source);
945
+ if (Result.isFailure(oldParent)) return Result.fail(oldParent.failure);
946
+ const oldIndex = sourceElementParentIndex(oldParent.success, moved.success.source);
947
+ if (oldIndex < 0) return Result.fail(sourceDiagnostic("tsx-target-not-found", `node "${op.nodeId}" is no longer attached`));
948
+ oldParent.success.children.splice(oldIndex, 1);
949
+ moved.success.source.parent = null;
950
+ const insertionIndex = elementChildInsertionIndex(parent.success.source.children, op.insertionIndex);
951
+ attachSourceElement(moved.success.source, parent.success.source, insertionIndex);
952
+ return Result.succeed(void 0);
953
+ };
954
+ for (const op of input.ops) {
955
+ let result;
956
+ switch (op._tag) {
957
+ case "setProp":
958
+ result = applySetProp(op);
959
+ break;
960
+ case "setChildren":
961
+ result = applySetChildren(op);
962
+ break;
963
+ case "insertSnippet":
964
+ result = applyInsertSnippet(op);
965
+ break;
966
+ case "duplicateNode":
967
+ result = applyDuplicate(op);
968
+ break;
969
+ case "removeNode":
970
+ result = applyRemove(op);
971
+ break;
972
+ case "moveNode":
973
+ result = applyMove(op);
974
+ break;
975
+ case "insertBlock": result = Result.fail(sourceDiagnostic("tsx-unsupported-operation", "insertBlock has no authored TSX fragment; use insertSnippet with an explicit JSX fragment"));
976
+ }
977
+ if (Result.isFailure(result)) return sourceFailure(result.failure);
978
+ }
979
+ const renderedRoot = renderSourceElement(root, input.authoredSource);
980
+ return Result.succeed(`${input.authoredSource.slice(0, root.start)}${renderedRoot}${input.authoredSource.slice(root.end)}`);
981
+ };
982
+ //#endregion
983
+ export { TsxHeadingTraceError, TsxSmlOpsError, applyTsxSmlOps, replaceTsxHeadingText, traceTsxHeadingTarget };
984
+
985
+ //# sourceMappingURL=tsx-tracer.js.map