@samva/markup 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/AGENTS.md +90 -37
  2. package/README.md +188 -52
  3. package/dist/assets-C5duwTya.d.ts +40 -0
  4. package/dist/{brand-DcoVkh_S.d.ts → brand-B-0bN_Ir.d.ts} +2 -2
  5. package/dist/{render-vAv_4r9v.js → cascade-t6kxlgEI.js} +93 -280
  6. package/dist/channel-edit.d.ts +121 -0
  7. package/dist/channel-edit.js +609 -0
  8. package/dist/channel-specs-B6M_HlaV.js +29 -0
  9. package/dist/channel-specs-C2feJwuP.d.ts +13 -0
  10. package/dist/compatibility-DbISoFIq.js +589 -0
  11. package/dist/compiler.d.ts +10 -257
  12. package/dist/compiler.js +10 -730
  13. package/dist/{css-CS2jLID6.d.ts → css-BVQKICKC.d.ts} +3 -3
  14. package/dist/diagnostic-codes.d.ts +403 -0
  15. package/dist/diagnostic-codes.js +399 -0
  16. package/dist/{diagnostics-BMYtBCQk.d.ts → diagnostics-B-Gym34R.d.ts} +8 -4
  17. package/dist/diagnostics.d.ts +31 -2
  18. package/dist/diagnostics.js +1 -19
  19. package/dist/{elements-BzjjnoCS.d.ts → elements-cqldpaJt.d.ts} +3 -3
  20. package/dist/email/assets.d.ts +2 -40
  21. package/dist/email/brand-footer.d.ts +2 -2
  22. package/dist/email/brand-footer.js +4 -3
  23. package/dist/email/brand.d.ts +1 -1
  24. package/dist/email/brand.js +1 -1
  25. package/dist/email/components.d.ts +21 -10
  26. package/dist/email/components.js +46 -11
  27. package/dist/email/diagnostics.d.ts +2 -2
  28. package/dist/email/diagnostics.js +2 -1
  29. package/dist/email/elements.d.ts +1 -1
  30. package/dist/email/html.d.ts +2 -2
  31. package/dist/email/html.js +45 -2
  32. package/dist/email/jsx-dev-runtime.d.ts +2 -2
  33. package/dist/email/jsx-runtime.d.ts +1 -1
  34. package/dist/email/render.d.ts +8 -18
  35. package/dist/email/render.js +163 -3
  36. package/dist/email/text.d.ts +1 -1
  37. package/dist/fmt.d.ts +32 -0
  38. package/dist/fmt.js +174 -0
  39. package/dist/{html-Bl9BAdqc.d.ts → html-BJepOwZ4.d.ts} +19 -5
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.js +2 -0
  42. package/dist/ir-aaqNE987.d.ts +252 -0
  43. package/dist/ir.d.ts +2 -0
  44. package/dist/ir.js +7 -0
  45. package/dist/preview-Ekdg1dXG.d.ts +100 -0
  46. package/dist/preview.d.ts +2 -0
  47. package/dist/preview.js +160 -0
  48. package/dist/profile-BH-OtdDA.js +3670 -0
  49. package/dist/render-ir.d.ts +19 -0
  50. package/dist/render-ir.js +826 -0
  51. package/dist/sms-segments.d.ts +13 -2
  52. package/dist/sms.d.ts +16 -0
  53. package/dist/sms.js +12 -0
  54. package/dist/{email/source-edit.d.ts → source-edit.d.ts} +55 -6
  55. package/dist/source-edit.js +343 -0
  56. package/dist/source-locations-C8wCsiAy.d.ts +23 -0
  57. package/dist/source-locations.d.ts +2 -2
  58. package/dist/source-locations.js +3 -89
  59. package/dist/static.d.ts +57 -0
  60. package/dist/static.js +69 -0
  61. package/dist/tailwind-CMYpZf1v.js +614 -0
  62. package/dist/template-_QE_rQND.d.ts +65 -0
  63. package/dist/template.d.ts +2 -2
  64. package/dist/template.js +15 -10
  65. package/dist/theme-css.d.ts +1 -1
  66. package/dist/theme-imports-lvBw3AD1.js +109 -0
  67. package/dist/transform-Ct2OMkFN.d.ts +153 -0
  68. package/dist/whatsapp.d.ts +57 -0
  69. package/dist/whatsapp.js +22 -0
  70. package/generated/contract.json +1782 -0
  71. package/generated/reference.md +353 -0
  72. package/package.json +41 -151
  73. package/skills/sml/SKILL.md +123 -0
  74. package/dist/ast-BrDtApwS.d.ts +0 -107
  75. package/dist/ast.d.ts +0 -2
  76. package/dist/ast.js +0 -124
  77. package/dist/candidate.d.ts +0 -29
  78. package/dist/candidate.js +0 -88
  79. package/dist/catalog-CKuOooB8.d.ts +0 -354
  80. package/dist/checks-D73Xizbw.js +0 -59366
  81. package/dist/checks.d.ts +0 -33
  82. package/dist/checks.js +0 -2
  83. package/dist/classes.d.ts +0 -26
  84. package/dist/classes.js +0 -184
  85. package/dist/compile-sms.d.ts +0 -28
  86. package/dist/compile-sms.js +0 -44
  87. package/dist/compile-whatsapp.d.ts +0 -56
  88. package/dist/compile-whatsapp.js +0 -186
  89. package/dist/compile.d.ts +0 -18
  90. package/dist/compile.js +0 -29
  91. package/dist/components.d.ts +0 -289
  92. package/dist/components.js +0 -106
  93. package/dist/conditionals.d.ts +0 -10
  94. package/dist/conditionals.js +0 -74
  95. package/dist/corpus-CPPfgAHN.d.ts +0 -106
  96. package/dist/diagnostics-OcQKQYSY.d.ts +0 -49
  97. package/dist/email/selection.d.ts +0 -2
  98. package/dist/email/selection.js +0 -91
  99. package/dist/email/source-edit.js +0 -185
  100. package/dist/email/stylesheets.d.ts +0 -15
  101. package/dist/email/stylesheets.js +0 -48
  102. package/dist/emit.d.ts +0 -51
  103. package/dist/emit.js +0 -151
  104. package/dist/interpolate.d.ts +0 -24
  105. package/dist/interpolate.js +0 -43
  106. package/dist/jsx-dev-runtime.d.ts +0 -11
  107. package/dist/jsx-dev-runtime.js +0 -15
  108. package/dist/jsx-runtime-I-1nCtl2.d.ts +0 -57
  109. package/dist/jsx-runtime.d.ts +0 -2
  110. package/dist/jsx-runtime.js +0 -121
  111. package/dist/lexer.d.ts +0 -60
  112. package/dist/lexer.js +0 -281
  113. package/dist/model.d.ts +0 -24
  114. package/dist/model.js +0 -77
  115. package/dist/ops-BvwqHl7V.js +0 -630
  116. package/dist/ops.d.ts +0 -114
  117. package/dist/ops.js +0 -2
  118. package/dist/parser-t29cHLVL.d.ts +0 -396
  119. package/dist/parser.d.ts +0 -2
  120. package/dist/parser.js +0 -859
  121. package/dist/predicate-BI6uRRM-.js +0 -549
  122. package/dist/predicate-M-05Nd7R.d.ts +0 -56
  123. package/dist/predicate.d.ts +0 -2
  124. package/dist/predicate.js +0 -2
  125. package/dist/printer.d.ts +0 -6
  126. package/dist/printer.js +0 -122
  127. package/dist/registry-Dcp6QEK_.d.ts +0 -73
  128. package/dist/registry.d.ts +0 -2
  129. package/dist/registry.js +0 -646
  130. package/dist/render-template.d.ts +0 -45
  131. package/dist/render-template.js +0 -76
  132. package/dist/selection-CCArx9lU.d.ts +0 -91
  133. package/dist/showcase-egkusQBf.d.ts +0 -415
  134. package/dist/sms-segments-y4FpPilJ.d.ts +0 -13
  135. package/dist/source-locations-DrCM82Ym.d.ts +0 -67
  136. package/dist/starters/category-mapping.d.ts +0 -13
  137. package/dist/starters/category-mapping.js +0 -49
  138. package/dist/starters/corpus.d.ts +0 -2
  139. package/dist/starters/corpus.js +0 -194
  140. package/dist/starters/library.d.ts +0 -18
  141. package/dist/starters/library.js +0 -692
  142. package/dist/starters/public-gallery.d.ts +0 -266
  143. package/dist/starters/public-gallery.js +0 -916
  144. package/dist/starters/public-handoff.d.ts +0 -164
  145. package/dist/starters/public-handoff.js +0 -361
  146. package/dist/starters/public-preview-artifacts.d.ts +0 -43
  147. package/dist/starters/public-preview-artifacts.js +0 -104
  148. package/dist/starters/public-preview.d.ts +0 -61
  149. package/dist/starters/public-preview.js +0 -83
  150. package/dist/starters/showcase.d.ts +0 -2
  151. package/dist/starters/showcase.js +0 -335
  152. package/dist/starters/sources.d.ts +0 -45
  153. package/dist/starters/sources.js +0 -7762
  154. package/dist/starters/theme.d.ts +0 -45
  155. package/dist/starters/theme.js +0 -77
  156. package/dist/starters.d.ts +0 -2
  157. package/dist/starters.js +0 -874
  158. package/dist/tailwind-BYq7Lo6u.js +0 -727
  159. package/dist/template-DWftqh4x.d.ts +0 -60
  160. package/dist/tsx-source-instrumentation.d.ts +0 -30
  161. package/dist/tsx-source-instrumentation.js +0 -115
  162. package/dist/tsx-tracer.d.ts +0 -112
  163. package/dist/tsx-tracer.js +0 -985
  164. package/dist/variables-J_L3qxRa.d.ts +0 -77
  165. package/dist/variables.d.ts +0 -2
  166. package/dist/variables.js +0 -141
  167. package/dist/version-BITr58kk.js +0 -96
  168. package/generated/sml-agent-contract.json +0 -709
  169. package/generated/sml-email-agent-contract.json +0 -615
  170. package/generated/sml-email-language-reference.md +0 -94
  171. package/generated/sml-language-reference.md +0 -195
@@ -1,2 +1,13 @@
1
- import { i as smsSegmentInfo, n as SmsSegmentInfo, r as hasSmsOptOut, t as SmsEncoding } from "./sms-segments-y4FpPilJ.js";
2
- export { SmsEncoding, SmsSegmentInfo, hasSmsOptOut, smsSegmentInfo };
1
+ //#region src/sms-segments.d.ts
2
+ /** The encoding a message needs: GSM-7 when every character fits, UCS-2 otherwise. */
3
+ type SmsEncoding = "gsm7" | "ucs2";
4
+ interface SmsSegmentInfo {
5
+ readonly encoding: SmsEncoding;
6
+ readonly characterCount: number;
7
+ readonly segmentCount: number;
8
+ }
9
+ declare const smsSegmentInfo: (text: string) => SmsSegmentInfo;
10
+ declare const hasSmsOptOut: (text: string) => boolean;
11
+ //#endregion
12
+ export { SmsEncoding, SmsSegmentInfo, hasSmsOptOut, smsSegmentInfo };
13
+ //# sourceMappingURL=sms-segments.d.ts.map
package/dist/sms.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { t as SmsCategory } from "./channel-specs-C2feJwuP.js";
2
+ import { f as EmailChild, m as EmailNode } from "./elements-cqldpaJt.js";
3
+ //#region src/sms.d.ts
4
+ interface SmsProps {
5
+ readonly category?: SmsCategory | undefined;
6
+ readonly children?: EmailChild;
7
+ }
8
+ /**
9
+ * The root of an SMS body: plain text with bindings, conditionals and formatters. The compiler
10
+ * reads it from source; executed, it is a fragment of its children so a template file that also
11
+ * declares an email body still evaluates.
12
+ */
13
+ declare const Sms: (props: SmsProps) => EmailNode;
14
+ //#endregion
15
+ export { Sms, type SmsCategory, SmsProps };
16
+ //# sourceMappingURL=sms.d.ts.map
package/dist/sms.js ADDED
@@ -0,0 +1,12 @@
1
+ import { Fragment, jsx } from "./email/jsx-runtime.js";
2
+ //#region src/sms.ts
3
+ /**
4
+ * The root of an SMS body: plain text with bindings, conditionals and formatters. The compiler
5
+ * reads it from source; executed, it is a fragment of its children so a template file that also
6
+ * declares an email body still evaluates.
7
+ */
8
+ const Sms = (props) => jsx(Fragment, { children: props.children });
9
+ //#endregion
10
+ export { Sms };
11
+
12
+ //# sourceMappingURL=sms.js.map
@@ -1,5 +1,5 @@
1
- import { i as JsxSourceLocation } from "../source-locations-DrCM82Ym.js";
2
- //#region src/email/source-edit.d.ts
1
+ import { r as JsxSourceLocation } from "./source-locations-C8wCsiAy.js";
2
+ //#region src/source-edit.d.ts
3
3
  /** One exact source replacement; before text guards against concurrent changes. */
4
4
  interface SourceReplacement {
5
5
  readonly start: number;
@@ -33,6 +33,13 @@ interface SourceElement {
33
33
  readonly hasSpread: boolean;
34
34
  readonly properties: readonly SourceProperty[];
35
35
  readonly structural: boolean;
36
+ /** The JSX element sibling directly before / after this one, when both share a JSX parent. */
37
+ readonly previous: SourceRange | null;
38
+ readonly next: SourceRange | null;
39
+ }
40
+ interface SourceRange {
41
+ readonly start: number;
42
+ readonly end: number;
36
43
  }
37
44
  /** Resolve a renderer origin against the unchanged authored entry, never against HTML offsets. */
38
45
  declare const inspectSourceElement: (source: string, file: string, origin: JsxSourceLocation) => SourceElement | null;
@@ -40,11 +47,39 @@ declare const inspectSourceElement: (source: string, file: string, origin: JsxSo
40
47
  declare const literalReplacement: (source: string, field: SourceProperty, value: string) => SourceReplacement | null;
41
48
  /** Explicit expression changes are proposals: callers show before/after and require Apply. */
42
49
  declare const expressionReplacement: (source: string, field: SourceProperty, expression: string) => SourceReplacement;
50
+ /**
51
+ * Where an edited file sits in its template project. Every edit of a template is checked against
52
+ * the static profile: one that would leave it is refused, so a visual edit can never produce a
53
+ * template the compiler rejects.
54
+ */
55
+ interface ProfileGuard {
56
+ /** The template entry the profile check reads. */
57
+ readonly entry: string;
58
+ /** The file the edit changes; the entry when omitted. A partial names itself here. */
59
+ readonly file?: string | undefined;
60
+ /** The project's other files, when the edited file is not the entry. */
61
+ readonly files?: Readonly<Record<string, string>> | undefined;
62
+ }
63
+ interface ApplyOptions {
64
+ readonly profile?: ProfileGuard | undefined;
65
+ }
43
66
  /** Apply exact, nonoverlapping edits and refuse syntax damage without changing the original. */
44
- declare const applySourceReplacements: (source: string, edits: readonly SourceReplacement[]) => SourceEditResult;
67
+ declare const applySourceReplacements: (source: string, edits: readonly SourceReplacement[], options?: ApplyOptions) => SourceEditResult;
68
+ type SourceProposal = {
69
+ readonly ok: true;
70
+ readonly edits: readonly SourceReplacement[];
71
+ } | {
72
+ readonly ok: false;
73
+ readonly reason: string;
74
+ };
45
75
  /** Structural edits name a whole authored block; loop iterations are never source nodes. */
46
76
  declare const structuralReplacements: (source: string, element: SourceElement, operation: {
47
77
  readonly kind: "delete";
78
+ } | {
79
+ readonly kind: "duplicate";
80
+ } | {
81
+ readonly kind: "reorder";
82
+ readonly direction: "before" | "after";
48
83
  } | {
49
84
  readonly kind: "insert";
50
85
  readonly tsx: string;
@@ -60,8 +95,22 @@ interface SourceFixture {
60
95
  }
61
96
  /** Declared fixture values are separate source targets from body bindings and literals. */
62
97
  declare const inspectSourceFixtures: (source: string) => readonly SourceFixture[];
63
- /** Adding a style literal is unambiguous only when no spread can supply that property. */
64
- declare const addStyleClasses: (element: SourceElement, value: string) => SourceReplacement | null;
98
+ type ClassNameOperation = {
99
+ readonly kind: "set";
100
+ readonly value: string;
101
+ } | {
102
+ readonly kind: "add";
103
+ readonly tokens: string;
104
+ } | {
105
+ readonly kind: "remove";
106
+ readonly tokens: string;
107
+ };
108
+ /**
109
+ * Edit an element's class list where it is written. A literal `className` is replaced, extended or
110
+ * trimmed in place; a missing one is added; one that is a condition or any other expression is
111
+ * refused, because rewriting it would discard the choice the author encoded.
112
+ */
113
+ declare const classNameEdit: (source: string, element: SourceElement, operation: ClassNameOperation) => SourceProposal;
65
114
  //#endregion
66
- export { SourceEditResult, SourceElement, SourceFixture, SourceProperty, SourceReplacement, addStyleClasses, applySourceReplacements, expressionReplacement, inspectSourceElement, inspectSourceFixtures, literalReplacement, structuralReplacements };
115
+ export { ApplyOptions, ClassNameOperation, ProfileGuard, SourceEditResult, SourceElement, SourceFixture, SourceProperty, SourceProposal, SourceRange, SourceReplacement, applySourceReplacements, classNameEdit, expressionReplacement, inspectSourceElement, inspectSourceFixtures, literalReplacement, structuralReplacements };
67
116
  //# sourceMappingURL=source-edit.d.ts.map
@@ -0,0 +1,343 @@
1
+ import { n as isObject } from "./guards-eV1qLbf2.js";
2
+ import { buildLineMap } from "./diagnostics.js";
3
+ import { n as introducedProfileErrors, t as checkStaticProfile } from "./profile-BH-OtdDA.js";
4
+ import { parse } from "@babel/parser";
5
+ //#region src/source-edit.ts
6
+ 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";
7
+ const children = (value) => Array.isArray(value) ? value.filter(node) : [];
8
+ const walk = (value, visit, parent = null) => {
9
+ if (Array.isArray(value)) {
10
+ for (const item of value) walk(item, visit, parent);
11
+ return;
12
+ }
13
+ if (!node(value)) return;
14
+ visit(value, parent);
15
+ for (const [key, child] of Object.entries(value)) if (key !== "loc" && key !== "extra" && key !== "comments") walk(child, visit, value);
16
+ };
17
+ const parsed = (source) => {
18
+ try {
19
+ const result = parse(source, {
20
+ sourceType: "module",
21
+ plugins: ["typescript", "jsx"]
22
+ });
23
+ return node(result) ? result : null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ };
28
+ const binding = (value) => value.type === "Identifier" || value.type === "MemberExpression" && value.computed === false && node(value.object) && binding(value.object);
29
+ /** JSX trims each line of text and joins the non-empty lines with one space. */
30
+ const collapseJsxText = (value) => value.split(/\r\n|\n|\r|\u2028|\u2029/).map((line) => line.trim()).filter((line) => line !== "").join(" ");
31
+ const property = (name, value, source, syntax) => {
32
+ const expression = value.type === "JSXExpressionContainer" && node(value.expression) ? value.expression : value;
33
+ const literal = expression.type === "StringLiteral" || expression.type === "NumericLiteral" || expression.type === "BooleanLiteral" || expression.type === "JSXText";
34
+ const raw = source.slice(expression.start, expression.end);
35
+ const inset = expression.type === "JSXText" ? {
36
+ start: expression.start + (raw.length - raw.trimStart().length),
37
+ end: expression.end - (raw.length - raw.trimEnd().length)
38
+ } : {
39
+ start: value.start,
40
+ end: value.end
41
+ };
42
+ return {
43
+ name,
44
+ literalType: expression.type === "NumericLiteral" ? "number" : expression.type === "BooleanLiteral" ? "boolean" : "string",
45
+ kind: literal ? "literal" : binding(expression) ? "binding" : "computed",
46
+ 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),
47
+ start: inset.start,
48
+ end: inset.end,
49
+ syntax
50
+ };
51
+ };
52
+ /** The JSX elements directly before and after `element` in its parent's child list. */
53
+ const siblingsOf = (element, parent, source) => {
54
+ if (parent?.type !== "JSXElement" && parent?.type !== "JSXFragment") return {
55
+ previous: null,
56
+ next: null
57
+ };
58
+ const list = children(parent.children).filter((child) => child.type !== "JSXText" || source.slice(child.start, child.end).trim() !== "");
59
+ const index = list.findIndex((child) => child.start === element.start);
60
+ const range = (child) => child?.type === "JSXElement" ? {
61
+ start: child.start,
62
+ end: child.end
63
+ } : null;
64
+ return {
65
+ previous: range(list[index - 1]),
66
+ next: range(list[index + 1])
67
+ };
68
+ };
69
+ /** Resolve a renderer origin against the unchanged authored entry, never against HTML offsets. */
70
+ const inspectSourceElement = (source, file, origin) => {
71
+ if (file !== origin.fileName) return null;
72
+ const tree = parsed(source);
73
+ if (tree === null) return null;
74
+ const lines = buildLineMap(source);
75
+ const lineStart = lines[origin.lineNumber - 1];
76
+ if (lineStart === void 0 || !Number.isInteger(origin.columnNumber) || origin.columnNumber < 1) return null;
77
+ const offset = lineStart + origin.columnNumber - 1;
78
+ if (offset >= (lines[origin.lineNumber] ?? source.length)) return null;
79
+ let found = null;
80
+ walk(tree, (element, parent) => {
81
+ if (element.type !== "JSXElement" || element.start !== offset || !node(element.openingElement)) return;
82
+ const opening = element.openingElement;
83
+ const properties = [];
84
+ for (const attribute of children(opening.attributes)) if (attribute.type === "JSXAttribute" && node(attribute.name) && typeof attribute.name.name === "string" && node(attribute.value)) {
85
+ const value = attribute.value;
86
+ const expression = value.type === "JSXExpressionContainer" && node(value.expression) ? value.expression : null;
87
+ if (attribute.name.name === "style" && expression?.type === "ObjectExpression" && !children(expression.properties).some((member) => member.type === "SpreadElement")) for (const member of children(expression.properties)) {
88
+ if (member.type !== "ObjectProperty" || member.computed === true || !node(member.key) || !node(member.value)) continue;
89
+ const key = member.key.type === "Identifier" ? member.key.name : member.key.value;
90
+ if (typeof key === "string") properties.push(property(`style.${key}`, member.value, source, "value"));
91
+ }
92
+ else properties.push(property(attribute.name.name, value, source, "attribute"));
93
+ }
94
+ const meaningful = children(element.children).filter((child) => child.type !== "JSXText" || source.slice(child.start, child.end).trim() !== "");
95
+ if (meaningful.length === 1 && meaningful[0] !== void 0 && meaningful[0].type !== "JSXElement" && meaningful[0].type !== "JSXFragment") properties.unshift(property("children", meaningful[0], source, "child"));
96
+ else if (meaningful.length > 1) {
97
+ const texts = meaningful.flatMap((child, index) => child.type === "JSXText" ? [property(`text.${index}`, child, source, "child")] : []);
98
+ properties.unshift(...texts);
99
+ }
100
+ found = {
101
+ start: element.start,
102
+ end: element.end,
103
+ contentStart: opening.selfClosing === true ? null : opening.end,
104
+ contentEnd: node(element.closingElement) ? element.closingElement.start : null,
105
+ tag: node(opening.name) ? source.slice(opening.name.start, opening.name.end) : "element",
106
+ properties,
107
+ attributeInsertion: (() => {
108
+ const last = children(opening.attributes).at(-1);
109
+ if (last !== void 0) return last.end;
110
+ return node(opening.name) ? opening.name.end : opening.end - 1;
111
+ })(),
112
+ hasSpread: children(opening.attributes).some((attribute) => attribute.type === "JSXSpreadAttribute"),
113
+ structural: parent?.type === "JSXElement" || parent?.type === "JSXFragment",
114
+ ...siblingsOf(element, parent, source)
115
+ };
116
+ });
117
+ return found;
118
+ };
119
+ const PLAIN_JSX_TEXT = /^[^\s{}<>&](?:[^{}<>&\r\n\u2028\u2029]*[^\s{}<>&])?$/;
120
+ /** Literal controls cannot overwrite a binding or computed expression. */
121
+ const literalReplacement = (source, field, value) => {
122
+ if (field.kind !== "literal") return null;
123
+ if (field.literalType === "number" && (value.trim() === "" || !Number.isFinite(Number(value)))) return null;
124
+ if (field.literalType === "boolean" && value !== "true" && value !== "false") return null;
125
+ const literal = field.literalType === "string" ? JSON.stringify(value) : field.literalType === "number" ? String(Number(value)) : value;
126
+ const before = source.slice(field.start, field.end);
127
+ const plainText = field.syntax === "child" && !before.startsWith("{") && PLAIN_JSX_TEXT.test(value);
128
+ return {
129
+ start: field.start,
130
+ end: field.end,
131
+ before,
132
+ after: plainText ? value : field.syntax === "child" || field.syntax === "attribute" && field.literalType !== "string" ? `{${literal}}` : literal
133
+ };
134
+ };
135
+ /** Explicit expression changes are proposals: callers show before/after and require Apply. */
136
+ const expressionReplacement = (source, field, expression) => ({
137
+ start: field.start,
138
+ end: field.end,
139
+ before: source.slice(field.start, field.end),
140
+ after: field.syntax === "value" ? expression : `{${expression}}`
141
+ });
142
+ const profileRefusal = (source, result, guard) => {
143
+ const file = guard.file ?? guard.entry;
144
+ const project = (content) => ({
145
+ ...guard.files,
146
+ [file]: content
147
+ });
148
+ const introduced = introducedProfileErrors(checkStaticProfile(project(source), guard.entry), checkStaticProfile(project(result), guard.entry));
149
+ const first = introduced[0];
150
+ if (first === void 0) return null;
151
+ const more = introduced.length > 1 ? ` (and ${introduced.length - 1} more)` : "";
152
+ return `This change leaves the static profile: ${first.message}${first.fix === void 0 ? "" : ` ${first.fix}`}${more}`;
153
+ };
154
+ /** Apply exact, nonoverlapping edits and refuse syntax damage without changing the original. */
155
+ const applySourceReplacements = (source, edits, options = {}) => {
156
+ const sorted = [...edits].sort((a, b) => b.start - a.start);
157
+ let boundary = source.length;
158
+ let result = source;
159
+ for (const edit of sorted) {
160
+ if (edit.start < 0 || edit.end < edit.start || edit.end > boundary || source.slice(edit.start, edit.end) !== edit.before) return {
161
+ ok: false,
162
+ reason: "Source changed at this target. Select it again before applying the edit."
163
+ };
164
+ result = result.slice(0, edit.start) + edit.after + result.slice(edit.end);
165
+ boundary = edit.start;
166
+ }
167
+ if (parsed(result) === null) return {
168
+ ok: false,
169
+ reason: "This change is not valid TSX. Review it in Source."
170
+ };
171
+ if (options.profile !== void 0 && result !== source) {
172
+ const refusal = profileRefusal(source, result, options.profile);
173
+ if (refusal !== null) return {
174
+ ok: false,
175
+ reason: refusal
176
+ };
177
+ }
178
+ return {
179
+ ok: true,
180
+ source: result
181
+ };
182
+ };
183
+ const indentation = (source, offset) => {
184
+ const lineStart = Math.max(source.lastIndexOf("\n", offset - 1), source.lastIndexOf("\r", offset - 1));
185
+ const lead = source.slice(lineStart + 1, offset);
186
+ return /^[ \t]*$/.test(lead) ? lead : null;
187
+ };
188
+ /** The whole lines an element alone occupies, so removing it leaves no blank line behind. */
189
+ const ownLines = (source, element) => {
190
+ if (indentation(source, element.start) === null) return element;
191
+ const trailing = /^[ \t]*(\r\n|\n|\r)/.exec(source.slice(element.end));
192
+ if (trailing === null) return element;
193
+ return {
194
+ start: element.start - (indentation(source, element.start) ?? "").length,
195
+ end: element.end + trailing[0].length
196
+ };
197
+ };
198
+ /** Structural edits name a whole authored block; loop iterations are never source nodes. */
199
+ const structuralReplacements = (source, element, operation) => {
200
+ if (operation.kind === "insert") {
201
+ if (element.contentEnd === null) return null;
202
+ return [{
203
+ start: element.contentEnd,
204
+ end: element.contentEnd,
205
+ before: "",
206
+ after: operation.tsx
207
+ }];
208
+ }
209
+ if (!element.structural) return null;
210
+ const text = source.slice(element.start, element.end);
211
+ if (operation.kind === "duplicate") {
212
+ const indent = indentation(source, element.start);
213
+ return [{
214
+ start: element.end,
215
+ end: element.end,
216
+ before: "",
217
+ after: indent === null ? text : `\n${indent}${text}`
218
+ }];
219
+ }
220
+ if (operation.kind === "reorder") {
221
+ const neighbour = operation.direction === "before" ? element.previous : element.next;
222
+ if (neighbour === null) return null;
223
+ return [{
224
+ start: neighbour.start,
225
+ end: neighbour.end,
226
+ before: source.slice(neighbour.start, neighbour.end),
227
+ after: text
228
+ }, {
229
+ start: element.start,
230
+ end: element.end,
231
+ before: text,
232
+ after: source.slice(neighbour.start, neighbour.end)
233
+ }];
234
+ }
235
+ const removal = ownLines(source, element);
236
+ const removed = {
237
+ start: removal.start,
238
+ end: removal.end,
239
+ before: source.slice(removal.start, removal.end),
240
+ after: ""
241
+ };
242
+ if (operation.kind === "delete") return [removed];
243
+ const destination = operation.destination.contentEnd;
244
+ if (destination === null || destination >= element.start && destination <= element.end) return null;
245
+ return [removed, {
246
+ start: destination,
247
+ end: destination,
248
+ before: "",
249
+ after: text
250
+ }];
251
+ };
252
+ /** The object literal a top-level `const name = { ... }` holds. */
253
+ const topLevelObject = (tree, name) => {
254
+ const program = tree.program;
255
+ if (!node(program)) return null;
256
+ for (const item of children(program.body)) {
257
+ const declaration = item.type === "ExportNamedDeclaration" && node(item.declaration) ? item.declaration : item;
258
+ if (declaration.type !== "VariableDeclaration") continue;
259
+ 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;
260
+ }
261
+ return null;
262
+ };
263
+ /** Declared fixture values are separate source targets from body bindings and literals. */
264
+ const inspectSourceFixtures = (source) => {
265
+ const tree = parsed(source);
266
+ if (tree === null) return [];
267
+ const found = [];
268
+ walk(tree, (call) => {
269
+ if (call.type !== "CallExpression" || !node(call.callee) || call.callee.type !== "Identifier" || call.callee.name !== "defineTemplate") return;
270
+ const definition = children(call.arguments)[0];
271
+ if (definition?.type !== "ObjectExpression") return;
272
+ const fixtures = children(definition.properties).find((field) => field.type === "ObjectProperty" && node(field.key) && field.key.name === "fixtures" && field.computed !== true);
273
+ if (fixtures === void 0 || !node(fixtures.value) || fixtures.value.type !== "ObjectExpression") return;
274
+ for (const field of children(fixtures.value.properties)) {
275
+ if (field.type !== "ObjectProperty" || field.computed === true || !node(field.key) || !node(field.value)) continue;
276
+ const name = field.key.type === "Identifier" ? field.key.name : field.key.value;
277
+ const value = field.value.type === "Identifier" && typeof field.value.name === "string" ? topLevelObject(tree, field.value.name) ?? field.value : field.value;
278
+ if (typeof name === "string") found.push({
279
+ name,
280
+ start: value.start,
281
+ end: value.end,
282
+ source: source.slice(value.start, value.end)
283
+ });
284
+ }
285
+ });
286
+ return found;
287
+ };
288
+ const tokens = (value) => value.split(/\s+/).filter((item) => item !== "");
289
+ /**
290
+ * Edit an element's class list where it is written. A literal `className` is replaced, extended or
291
+ * trimmed in place; a missing one is added; one that is a condition or any other expression is
292
+ * refused, because rewriting it would discard the choice the author encoded.
293
+ */
294
+ const classNameEdit = (source, element, operation) => {
295
+ const field = element.properties.find((item) => item.name === "className");
296
+ const next = (current) => {
297
+ if (operation.kind === "set") return tokens(operation.value).join(" ");
298
+ const wanted = tokens(operation.tokens);
299
+ return operation.kind === "add" ? [...current, ...wanted.filter((item) => !current.includes(item))].join(" ") : current.filter((item) => !wanted.includes(item)).join(" ");
300
+ };
301
+ if (field === void 0) {
302
+ if (element.hasSpread) return {
303
+ ok: false,
304
+ reason: "A spread attribute can supply className here, so a class list cannot be added safely. Edit it in Source."
305
+ };
306
+ if (operation.kind === "remove") return {
307
+ ok: true,
308
+ edits: []
309
+ };
310
+ const value = next([]);
311
+ if (value === "") return {
312
+ ok: true,
313
+ edits: []
314
+ };
315
+ return {
316
+ ok: true,
317
+ edits: [{
318
+ start: element.attributeInsertion,
319
+ end: element.attributeInsertion,
320
+ before: "",
321
+ after: ` className=${JSON.stringify(value)}`
322
+ }]
323
+ };
324
+ }
325
+ if (field.kind !== "literal" || field.literalType !== "string") return {
326
+ ok: false,
327
+ 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.`
328
+ };
329
+ const value = next(tokens(field.value));
330
+ return {
331
+ ok: true,
332
+ edits: [{
333
+ start: field.start,
334
+ end: field.end,
335
+ before: source.slice(field.start, field.end),
336
+ after: JSON.stringify(value)
337
+ }]
338
+ };
339
+ };
340
+ //#endregion
341
+ export { applySourceReplacements, classNameEdit, expressionReplacement, inspectSourceElement, inspectSourceFixtures, literalReplacement, structuralReplacements };
342
+
343
+ //# sourceMappingURL=source-edit.js.map
@@ -0,0 +1,23 @@
1
+ //#region src/source-locations.d.ts
2
+ /**
3
+ * The source payload a JSX transform may attach to an element. The transform may omit any field
4
+ * for synthetic calls; only a complete payload can identify a source origin safely.
5
+ */
6
+ interface JsxSource {
7
+ readonly fileName?: string | undefined;
8
+ readonly lineNumber?: number | undefined;
9
+ readonly columnNumber?: number | undefined;
10
+ }
11
+ /** A complete, normalized source origin for a JSX element. */
12
+ interface JsxSourceLocation {
13
+ readonly fileName: string;
14
+ readonly lineNumber: number;
15
+ readonly columnNumber: number;
16
+ }
17
+ /** The JSX prop that carries a {@link JsxSource} to the email JSX runtime. */
18
+ declare const JSX_SOURCE_LOCATION_PROP: "__samva_source_location";
19
+ /** Normalize the optional payload a JSX transform attaches. */
20
+ declare const completeJsxSourceLocation: (source: JsxSource | null | undefined) => JsxSourceLocation | undefined;
21
+ //#endregion
22
+ export { completeJsxSourceLocation as i, JsxSource as n, JsxSourceLocation as r, JSX_SOURCE_LOCATION_PROP as t };
23
+ //# sourceMappingURL=source-locations-C8wCsiAy.d.ts.map
@@ -1,2 +1,2 @@
1
- import { a as SourceLocationUnavailableReason, c as attachJsxSourceLocation, d as mapJsxSourceLocations, f as sameTemplateSourceLocations, i as JsxSourceLocation, l as completeJsxSourceLocation, n as JSX_SOURCE_LOCATION_PROP, o as TemplateSourceLocations, r as JsxSource, s as UnboundTemplateSourceLocations, t as JSX_SOURCE_LOCATION, u as jsxSourceLocationOf } from "./source-locations-DrCM82Ym.js";
2
- export { JSX_SOURCE_LOCATION, JSX_SOURCE_LOCATION_PROP, JsxSource, JsxSourceLocation, SourceLocationUnavailableReason, TemplateSourceLocations, UnboundTemplateSourceLocations, attachJsxSourceLocation, completeJsxSourceLocation, jsxSourceLocationOf, mapJsxSourceLocations, sameTemplateSourceLocations };
1
+ import { i as completeJsxSourceLocation, n as JsxSource, r as JsxSourceLocation, t as JSX_SOURCE_LOCATION_PROP } from "./source-locations-C8wCsiAy.js";
2
+ export { JSX_SOURCE_LOCATION_PROP, JsxSource, JsxSourceLocation, completeJsxSourceLocation };
@@ -1,14 +1,7 @@
1
- import { t as hasProperty } from "./guards-eV1qLbf2.js";
2
1
  //#region src/source-locations.ts
3
- /**
4
- * Runtime-only carrier for JSX source metadata. It is global so separately
5
- * bundled `jsx-dev-runtime` and `render-template` entries still agree on the
6
- * sidecar key, and it is a Symbol so it cannot enter SML, HTML, or JSON.
7
- */
8
- const JSX_SOURCE_LOCATION = Symbol.for("@samva/markup/jsx-source-location");
9
- /** Internal JSX prop used only by an editor preview source transform. */
2
+ /** The JSX prop that carries a {@link JsxSource} to the email JSX runtime. */
10
3
  const JSX_SOURCE_LOCATION_PROP = "__samva_source_location";
11
- /** Normalize the optional payload accepted by `jsxDEV`. */
4
+ /** Normalize the optional payload a JSX transform attaches. */
12
5
  const completeJsxSourceLocation = (source) => {
13
6
  if (source?.fileName === void 0 || source.lineNumber === void 0 || source.columnNumber === void 0) return;
14
7
  return Object.freeze({
@@ -17,86 +10,7 @@ const completeJsxSourceLocation = (source) => {
17
10
  columnNumber: source.columnNumber
18
11
  });
19
12
  };
20
- /** Attach a source origin without changing the serialized draft shape. */
21
- const attachJsxSourceLocation = (node, source) => {
22
- if (Object.hasOwn(node, JSX_SOURCE_LOCATION)) return;
23
- Object.defineProperty(node, JSX_SOURCE_LOCATION, {
24
- configurable: false,
25
- enumerable: false,
26
- value: source,
27
- writable: false
28
- });
29
- };
30
- const isJsxSourceLocation = (value) => hasProperty(value, "fileName") && typeof value.fileName === "string" && hasProperty(value, "lineNumber") && typeof value.lineNumber === "number" && Number.isInteger(value.lineNumber) && hasProperty(value, "columnNumber") && typeof value.columnNumber === "number" && Number.isInteger(value.columnNumber);
31
- /** Read the ephemeral origin, if this draft element has one. */
32
- const jsxSourceLocationOf = (node) => {
33
- if (node.kind !== "element") return void 0;
34
- const value = Object.getOwnPropertyDescriptor(node, JSX_SOURCE_LOCATION)?.value;
35
- return isJsxSourceLocation(value) ? value : void 0;
36
- };
37
- const elementChildren = (node) => node.children.filter((child) => child.kind === "element");
38
- const sourceOriginKey = (source) => JSON.stringify([
39
- source.fileName,
40
- source.lineNumber,
41
- source.columnNumber
42
- ]);
43
- /**
44
- * Pair a draft tree with its parsed SML tree by element preorder. Text nodes
45
- * are intentionally ignored: the SML parser normalizes inline whitespace, so
46
- * only the element structure is stable enough to be a source-map contract.
47
- */
48
- const mapJsxSourceLocations = (draft, parsed) => {
49
- const byNodeId = {};
50
- const origins = /* @__PURE__ */ new Set();
51
- let duplicateOrigin = false;
52
- const visit = (draftNode, parsedNode) => {
53
- if (draftNode.tag !== parsedNode.tag) return false;
54
- const source = jsxSourceLocationOf(draftNode);
55
- if (source !== void 0) {
56
- const origin = sourceOriginKey(source);
57
- if (origins.has(origin)) duplicateOrigin = true;
58
- origins.add(origin);
59
- if (Object.hasOwn(byNodeId, parsedNode.id)) return false;
60
- byNodeId[parsedNode.id] = source;
61
- }
62
- const draftChildren = elementChildren(draftNode);
63
- const parsedChildren = elementChildren(parsedNode);
64
- if (draftChildren.length !== parsedChildren.length) return false;
65
- for (let index = 0; index < draftChildren.length; index++) if (!visit(draftChildren[index], parsedChildren[index])) return false;
66
- return true;
67
- };
68
- if (!visit(draft, parsed)) return {
69
- kind: "unavailable",
70
- reason: "structure-mismatch"
71
- };
72
- if (duplicateOrigin) return {
73
- kind: "unavailable",
74
- reason: "duplicate-source-origin"
75
- };
76
- if (Object.keys(byNodeId).length === 0) return {
77
- kind: "unavailable",
78
- reason: "no-source-metadata"
79
- };
80
- return {
81
- kind: "available",
82
- byNodeId: Object.freeze(byNodeId)
83
- };
84
- };
85
- /** Compare source-map statuses for the emission determinism guard. */
86
- const sameTemplateSourceLocations = (left, right) => {
87
- if (left.kind !== right.kind) return false;
88
- if (left.kind === "unavailable") return right.kind === "unavailable" && left.reason === right.reason;
89
- if (right.kind === "unavailable") return false;
90
- const leftEntries = Object.entries(left.byNodeId);
91
- const rightEntries = Object.entries(right.byNodeId);
92
- if (leftEntries.length !== rightEntries.length) return false;
93
- for (const [nodeId, source] of leftEntries) {
94
- const other = right.byNodeId[nodeId];
95
- if (other === void 0 || other.fileName !== source.fileName || other.lineNumber !== source.lineNumber || other.columnNumber !== source.columnNumber) return false;
96
- }
97
- return true;
98
- };
99
13
  //#endregion
100
- export { JSX_SOURCE_LOCATION, JSX_SOURCE_LOCATION_PROP, attachJsxSourceLocation, completeJsxSourceLocation, jsxSourceLocationOf, mapJsxSourceLocations, sameTemplateSourceLocations };
14
+ export { JSX_SOURCE_LOCATION_PROP, completeJsxSourceLocation };
101
15
 
102
16
  //# sourceMappingURL=source-locations.js.map