@samva/markup 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/AGENTS.md +107 -46
  2. package/CHANGELOG.md +345 -0
  3. package/README.md +188 -52
  4. package/dist/{brand-DcoVkh_S.d.ts → assets-BIsyMQ92.d.ts} +41 -11
  5. package/dist/{email/brand.js → brand-C0MKTwmX.js} +64 -4
  6. package/dist/{theme-css.d.ts → brand.d.ts} +12 -3
  7. package/dist/brand.js +4 -0
  8. package/dist/channel-specs-C2feJwuP.d.ts +13 -0
  9. package/dist/compiler.d.ts +88 -115
  10. package/dist/compiler.js +750 -171
  11. package/dist/components-BZCqegS8.js +1864 -0
  12. package/dist/diagnostic-model-DJtqbETd.js +38 -0
  13. package/dist/{diagnostics-OcQKQYSY.d.ts → diagnostic-model-DxthUZBT.d.ts} +3 -21
  14. package/dist/{diagnostics-BMYtBCQk.d.ts → diagnostics-B-Gym34R.d.ts} +8 -4
  15. package/dist/{email/diagnostics.js → diagnostics-CnKFefKz.js} +4 -3
  16. package/dist/diagnostics.d.ts +410 -2
  17. package/dist/diagnostics.js +402 -52
  18. package/dist/edit.d.ts +234 -0
  19. package/dist/edit.js +947 -0
  20. package/dist/email/jsx-dev-runtime.d.ts +2 -2
  21. package/dist/email/jsx-dev-runtime.js +1 -2
  22. package/dist/email/jsx-runtime.d.ts +1 -1
  23. package/dist/email/jsx-runtime.js +1 -85
  24. package/dist/{email/components.d.ts → email.d.ts} +22 -11
  25. package/dist/email.js +2 -0
  26. package/dist/fmt.d.ts +32 -0
  27. package/dist/fmt.js +174 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +2 -0
  30. package/dist/{render-vAv_4r9v.js → ir-CLhlklKV.js} +96 -279
  31. package/dist/jsx-runtime-CBlexoeR.js +98 -0
  32. package/dist/{elements-BzjjnoCS.d.ts → jsx-runtime-ylIOnaSf.d.ts} +76 -76
  33. package/dist/profile-pWGtWZXF.js +3803 -0
  34. package/dist/render-BPonDpCl.d.ts +539 -0
  35. package/dist/render-BQmfh94k.js +257 -0
  36. package/dist/render.d.ts +35 -0
  37. package/dist/render.js +49 -0
  38. package/dist/sms.d.ts +16 -0
  39. package/dist/sms.js +12 -0
  40. package/dist/source-locations-C8wCsiAy.d.ts +23 -0
  41. package/dist/template-DMPwxMSq.d.ts +65 -0
  42. package/dist/template-zLUjcwfF.js +42 -0
  43. package/dist/{theme-css-hFqcPSar.js → theme-css-JtRCm1fb.js} +2 -2
  44. package/dist/whatsapp.d.ts +57 -0
  45. package/dist/whatsapp.js +22 -0
  46. package/generated/contract.json +1788 -0
  47. package/generated/reference.md +360 -0
  48. package/package.json +46 -239
  49. package/samva-brand.d.ts +4 -4
  50. package/skills/sml/SKILL.md +123 -0
  51. package/dist/ast-BrDtApwS.d.ts +0 -107
  52. package/dist/ast.d.ts +0 -2
  53. package/dist/ast.js +0 -124
  54. package/dist/candidate.d.ts +0 -29
  55. package/dist/candidate.js +0 -88
  56. package/dist/catalog-CKuOooB8.d.ts +0 -354
  57. package/dist/checks-D73Xizbw.js +0 -59366
  58. package/dist/checks.d.ts +0 -33
  59. package/dist/checks.js +0 -2
  60. package/dist/classes.d.ts +0 -26
  61. package/dist/classes.js +0 -184
  62. package/dist/compile-sms.d.ts +0 -28
  63. package/dist/compile-sms.js +0 -44
  64. package/dist/compile-whatsapp.d.ts +0 -56
  65. package/dist/compile-whatsapp.js +0 -186
  66. package/dist/compile.d.ts +0 -18
  67. package/dist/compile.js +0 -29
  68. package/dist/components.d.ts +0 -289
  69. package/dist/components.js +0 -106
  70. package/dist/conditionals.d.ts +0 -10
  71. package/dist/conditionals.js +0 -74
  72. package/dist/corpus-CPPfgAHN.d.ts +0 -106
  73. package/dist/css-CS2jLID6.d.ts +0 -109
  74. package/dist/email/assets.d.ts +0 -40
  75. package/dist/email/assets.js +0 -63
  76. package/dist/email/brand-footer.d.ts +0 -34
  77. package/dist/email/brand-footer.js +0 -62
  78. package/dist/email/brand.d.ts +0 -2
  79. package/dist/email/components.js +0 -240
  80. package/dist/email/diagnostics.d.ts +0 -2
  81. package/dist/email/elements.d.ts +0 -2
  82. package/dist/email/elements.js +0 -299
  83. package/dist/email/html.d.ts +0 -2
  84. package/dist/email/html.js +0 -278
  85. package/dist/email/render.d.ts +0 -50
  86. package/dist/email/render.js +0 -3
  87. package/dist/email/selection.d.ts +0 -2
  88. package/dist/email/selection.js +0 -91
  89. package/dist/email/source-edit.d.ts +0 -67
  90. package/dist/email/source-edit.js +0 -185
  91. package/dist/email/stylesheets.d.ts +0 -15
  92. package/dist/email/stylesheets.js +0 -48
  93. package/dist/email/text.d.ts +0 -7
  94. package/dist/email/text.js +0 -100
  95. package/dist/emit.d.ts +0 -51
  96. package/dist/emit.js +0 -151
  97. package/dist/html-Bl9BAdqc.d.ts +0 -42
  98. package/dist/interpolate.d.ts +0 -24
  99. package/dist/interpolate.js +0 -43
  100. package/dist/jsx-dev-runtime.d.ts +0 -11
  101. package/dist/jsx-dev-runtime.js +0 -15
  102. package/dist/jsx-runtime-I-1nCtl2.d.ts +0 -57
  103. package/dist/jsx-runtime.d.ts +0 -2
  104. package/dist/jsx-runtime.js +0 -121
  105. package/dist/lexer.d.ts +0 -60
  106. package/dist/lexer.js +0 -281
  107. package/dist/model.d.ts +0 -24
  108. package/dist/model.js +0 -77
  109. package/dist/ops-BvwqHl7V.js +0 -630
  110. package/dist/ops.d.ts +0 -114
  111. package/dist/ops.js +0 -2
  112. package/dist/parser-t29cHLVL.d.ts +0 -396
  113. package/dist/parser.d.ts +0 -2
  114. package/dist/parser.js +0 -859
  115. package/dist/predicate-BI6uRRM-.js +0 -549
  116. package/dist/predicate-M-05Nd7R.d.ts +0 -56
  117. package/dist/predicate.d.ts +0 -2
  118. package/dist/predicate.js +0 -2
  119. package/dist/printer.d.ts +0 -6
  120. package/dist/printer.js +0 -122
  121. package/dist/registry-Dcp6QEK_.d.ts +0 -73
  122. package/dist/registry.d.ts +0 -2
  123. package/dist/registry.js +0 -646
  124. package/dist/render-template.d.ts +0 -45
  125. package/dist/render-template.js +0 -76
  126. package/dist/selection-CCArx9lU.d.ts +0 -91
  127. package/dist/showcase-egkusQBf.d.ts +0 -415
  128. package/dist/sms-segments-y4FpPilJ.d.ts +0 -13
  129. package/dist/sms-segments.d.ts +0 -2
  130. package/dist/sms-segments.js +0 -46
  131. package/dist/source-locations-DrCM82Ym.d.ts +0 -67
  132. package/dist/source-locations.d.ts +0 -2
  133. package/dist/source-locations.js +0 -102
  134. package/dist/starters/category-mapping.d.ts +0 -13
  135. package/dist/starters/category-mapping.js +0 -49
  136. package/dist/starters/corpus.d.ts +0 -2
  137. package/dist/starters/corpus.js +0 -194
  138. package/dist/starters/library.d.ts +0 -18
  139. package/dist/starters/library.js +0 -692
  140. package/dist/starters/public-gallery.d.ts +0 -266
  141. package/dist/starters/public-gallery.js +0 -916
  142. package/dist/starters/public-handoff.d.ts +0 -164
  143. package/dist/starters/public-handoff.js +0 -361
  144. package/dist/starters/public-preview-artifacts.d.ts +0 -43
  145. package/dist/starters/public-preview-artifacts.js +0 -104
  146. package/dist/starters/public-preview.d.ts +0 -61
  147. package/dist/starters/public-preview.js +0 -83
  148. package/dist/starters/showcase.d.ts +0 -2
  149. package/dist/starters/showcase.js +0 -335
  150. package/dist/starters/sources.d.ts +0 -45
  151. package/dist/starters/sources.js +0 -7762
  152. package/dist/starters/theme.d.ts +0 -45
  153. package/dist/starters/theme.js +0 -77
  154. package/dist/starters.d.ts +0 -2
  155. package/dist/starters.js +0 -874
  156. package/dist/tailwind-BYq7Lo6u.js +0 -727
  157. package/dist/template-DWftqh4x.d.ts +0 -60
  158. package/dist/template.d.ts +0 -2
  159. package/dist/template.js +0 -37
  160. package/dist/theme-css.js +0 -2
  161. package/dist/tsx-source-instrumentation.d.ts +0 -30
  162. package/dist/tsx-source-instrumentation.js +0 -115
  163. package/dist/tsx-tracer.d.ts +0 -112
  164. package/dist/tsx-tracer.js +0 -985
  165. package/dist/variables-J_L3qxRa.d.ts +0 -77
  166. package/dist/variables.d.ts +0 -2
  167. package/dist/variables.js +0 -141
  168. package/dist/version-BITr58kk.js +0 -96
  169. package/generated/sml-agent-contract.json +0 -709
  170. package/generated/sml-email-agent-contract.json +0 -615
  171. package/generated/sml-email-language-reference.md +0 -94
  172. package/generated/sml-language-reference.md +0 -195
package/dist/parser.js DELETED
@@ -1,859 +0,0 @@
1
- import { SmlParseError } from "./diagnostics.js";
2
- import { a as printPredicate, i as parsePredicate, o as SML_PARSER_LIMITS } from "./predicate-BI6uRRM-.js";
3
- import { CHANNEL_BY_ROOT_TAG, ROOT_TAGS, SML_TAGS, isRootTag } from "./ast.js";
4
- import { TokenKind, createLexer } from "./lexer.js";
5
- import { Result } from "better-result";
6
- //#region src/parser.ts
7
- const parserLimitFailure = (code, message, sourceLength) => Result.err(new SmlParseError({ diagnostics: [{
8
- code,
9
- message,
10
- span: {
11
- start: 0,
12
- end: sourceLength
13
- }
14
- }] }));
15
- const SMS_CATEGORIES = [
16
- "transactional",
17
- "promotional",
18
- "service_implicit"
19
- ];
20
- const WHATSAPP_CATEGORIES = [
21
- "marketing",
22
- "utility",
23
- "authentication"
24
- ];
25
- const WHATSAPP_HEADER_TYPES = [
26
- "text",
27
- "image",
28
- "video",
29
- "document"
30
- ];
31
- const WHATSAPP_BUTTON_TYPES = [
32
- "quick-reply",
33
- "url",
34
- "phone",
35
- "copy-code"
36
- ];
37
- /**
38
- * Background-image attributes on the email block containers (Section/Container).
39
- * A URL is an ATTRIBUTE, not a class token: a URL inside the closed class
40
- * grammar is hostile (escaping, cache eviction, no caniemail slug), so it rides
41
- * the attribute channel like `src`/`href`. The compiler emits email-safe
42
- * background CSS from these (escaped `url(...)` + an Outlook `background` cell
43
- * attribute); Outlook for Windows renders no CSS background image, so
44
- * `checks` warns via `css-background-image`. Position/size are ignored by the
45
- * compiler when no `backgroundImage` is set.
46
- */
47
- const BACKGROUND_ATTRIBUTES = {
48
- backgroundImage: { type: "string" },
49
- backgroundPosition: {
50
- type: "string",
51
- values: [
52
- "center",
53
- "top",
54
- "bottom"
55
- ]
56
- },
57
- backgroundSize: {
58
- type: "string",
59
- values: [
60
- "cover",
61
- "contain",
62
- "auto"
63
- ]
64
- }
65
- };
66
- /**
67
- * The email language surface as data: per-tag attributes
68
- * (type/required/bounds) and content model. Public API — typed TSX components
69
- * and the reference-docs generator derive from this table, so it cannot drift
70
- * from the parser.
71
- */
72
- const EMAIL_TAG_SPECS = {
73
- Email: {
74
- attributes: {
75
- lang: { type: "string" },
76
- dir: { type: "string" },
77
- width: {
78
- type: "number",
79
- min: 1,
80
- max: 1e4
81
- },
82
- class: { type: "string" }
83
- },
84
- content: "email"
85
- },
86
- Subject: {
87
- attributes: {},
88
- content: "preview"
89
- },
90
- Preview: {
91
- attributes: {},
92
- content: "preview"
93
- },
94
- Container: {
95
- attributes: {
96
- class: { type: "string" },
97
- ...BACKGROUND_ATTRIBUTES
98
- },
99
- content: "block"
100
- },
101
- Section: {
102
- attributes: {
103
- class: { type: "string" },
104
- ...BACKGROUND_ATTRIBUTES
105
- },
106
- content: "block"
107
- },
108
- Row: {
109
- attributes: { class: { type: "string" } },
110
- content: "columns"
111
- },
112
- Column: {
113
- attributes: {
114
- class: { type: "string" },
115
- width: { type: "string" },
116
- valign: {
117
- type: "string",
118
- values: [
119
- "top",
120
- "middle",
121
- "bottom"
122
- ]
123
- }
124
- },
125
- content: "block"
126
- },
127
- Heading: {
128
- attributes: {
129
- level: {
130
- type: "number",
131
- min: 1,
132
- max: 6
133
- },
134
- class: { type: "string" }
135
- },
136
- content: "inline"
137
- },
138
- Text: {
139
- attributes: { class: { type: "string" } },
140
- content: "inline"
141
- },
142
- Button: {
143
- attributes: {
144
- href: {
145
- type: "string",
146
- required: true
147
- },
148
- class: { type: "string" }
149
- },
150
- content: "inline-nolink"
151
- },
152
- Image: {
153
- attributes: {
154
- src: {
155
- type: "string",
156
- required: true
157
- },
158
- alt: { type: "string" },
159
- width: {
160
- type: "number",
161
- min: 0,
162
- max: 1e4
163
- },
164
- height: {
165
- type: "number",
166
- min: 0,
167
- max: 1e4
168
- },
169
- class: { type: "string" }
170
- },
171
- content: "image"
172
- },
173
- Source: {
174
- attributes: {
175
- scheme: {
176
- type: "string",
177
- required: true,
178
- values: ["dark"]
179
- },
180
- src: {
181
- type: "string",
182
- required: true
183
- }
184
- },
185
- content: "void"
186
- },
187
- Divider: {
188
- attributes: { class: { type: "string" } },
189
- content: "void"
190
- },
191
- Spacer: {
192
- attributes: { height: {
193
- type: "number",
194
- required: true,
195
- min: 1,
196
- max: 2e3
197
- } },
198
- content: "void"
199
- },
200
- List: {
201
- attributes: {
202
- type: {
203
- type: "string",
204
- values: ["unordered", "ordered"]
205
- },
206
- class: { type: "string" }
207
- },
208
- content: "list"
209
- },
210
- ListItem: {
211
- attributes: { class: { type: "string" } },
212
- content: "inline"
213
- },
214
- If: {
215
- attributes: { test: {
216
- type: "string",
217
- required: true
218
- } },
219
- content: "if"
220
- },
221
- Else: {
222
- attributes: {},
223
- content: "block"
224
- },
225
- a: {
226
- attributes: {
227
- href: {
228
- type: "string",
229
- required: true
230
- },
231
- class: { type: "string" }
232
- },
233
- content: "inline-nolink"
234
- },
235
- strong: {
236
- attributes: {},
237
- content: "inline-nolink"
238
- },
239
- em: {
240
- attributes: {},
241
- content: "inline-nolink"
242
- }
243
- };
244
- /**
245
- * The sms language surface: a raw verbatim text body with `{{vars}}` and
246
- * send-time conditionals — no layout tags, no classes, no theming.
247
- */
248
- const SMS_TAG_SPECS = {
249
- Sms: {
250
- attributes: { category: {
251
- type: "string",
252
- values: SMS_CATEGORIES
253
- } },
254
- content: "verbatim"
255
- },
256
- If: {
257
- attributes: { test: {
258
- type: "string",
259
- required: true
260
- } },
261
- content: "verbatim-if"
262
- },
263
- Else: {
264
- attributes: {},
265
- content: "verbatim"
266
- }
267
- };
268
- /**
269
- * The whatsapp language surface: Meta's template anatomy IS the closed
270
- * grammar — Header? Body Footer? Buttons?, verbatim text slots, no classes.
271
- * Root attributes carry authoring intent (name defaults from the template
272
- * slug, language from the variant locale — defaulting happens at the API
273
- * layer; the language just carries the attributes).
274
- */
275
- const WHATSAPP_TAG_SPECS = {
276
- WhatsApp: {
277
- attributes: {
278
- name: { type: "string" },
279
- language: { type: "string" },
280
- category: {
281
- type: "string",
282
- required: true,
283
- values: WHATSAPP_CATEGORIES
284
- }
285
- },
286
- content: "whatsapp"
287
- },
288
- Header: {
289
- attributes: { type: {
290
- type: "string",
291
- values: WHATSAPP_HEADER_TYPES
292
- } },
293
- content: "verbatim"
294
- },
295
- Body: {
296
- attributes: {},
297
- content: "verbatim"
298
- },
299
- Footer: {
300
- attributes: {},
301
- content: "verbatim"
302
- },
303
- Buttons: {
304
- attributes: {},
305
- content: "buttons"
306
- },
307
- Button: {
308
- attributes: {
309
- type: {
310
- type: "string",
311
- required: true,
312
- values: WHATSAPP_BUTTON_TYPES
313
- },
314
- url: { type: "string" },
315
- phone: { type: "string" }
316
- },
317
- content: "verbatim"
318
- },
319
- If: {
320
- attributes: { test: {
321
- type: "string",
322
- required: true
323
- } },
324
- content: "verbatim-if"
325
- },
326
- Else: {
327
- attributes: {},
328
- content: "verbatim"
329
- }
330
- };
331
- /** Per-channel tag tables — the root tag picks one for the whole document. */
332
- const CHANNEL_TAG_SPECS = {
333
- email: EMAIL_TAG_SPECS,
334
- sms: SMS_TAG_SPECS,
335
- whatsapp: WHATSAPP_TAG_SPECS
336
- };
337
- /**
338
- * Editor-only attributes accepted on every email block-level tag on top of the
339
- * tag's own spec. The compiler ignores both, so neither ever reaches HTML:
340
- * `label` renames a node in the layout navigator; `hidden="true"` drops it from
341
- * the compiled artifact while it stays in the source. Email-only — sms/whatsapp
342
- * compile has no strip pass, so their tags keep their own specs verbatim.
343
- */
344
- const UNIVERSAL_ATTRIBUTES = {
345
- label: { type: "string" },
346
- hidden: {
347
- type: "string",
348
- values: ["true"]
349
- }
350
- };
351
- /** Email tags a user manipulates as layers — the ones the universal attributes attach to. */
352
- const UNIVERSAL_ATTRIBUTE_TAGS = /* @__PURE__ */ new Set([
353
- "Container",
354
- "Section",
355
- "Row",
356
- "Column",
357
- "Heading",
358
- "Text",
359
- "Button",
360
- "Image",
361
- "Divider",
362
- "Spacer",
363
- "List",
364
- "ListItem",
365
- "If",
366
- "Else"
367
- ]);
368
- const universalAttributeSpec = (name) => Object.hasOwn(UNIVERSAL_ATTRIBUTES, name) ? UNIVERSAL_ATTRIBUTES[name] : void 0;
369
- const BLOCK_CHILDREN = [
370
- "Container",
371
- "Section",
372
- "Row",
373
- "Heading",
374
- "Text",
375
- "Button",
376
- "Image",
377
- "Divider",
378
- "Spacer",
379
- "List",
380
- "If"
381
- ];
382
- const EMAIL_ENVELOPE_CHILDREN = ["Subject", "Preview"];
383
- /** Tags a content model admits as direct children (public alongside the spec tables). */
384
- const allowedChildren = (model) => {
385
- switch (model) {
386
- case "email": return [...EMAIL_ENVELOPE_CHILDREN, ...BLOCK_CHILDREN];
387
- case "block": return BLOCK_CHILDREN;
388
- case "columns": return ["Column"];
389
- case "list": return ["ListItem"];
390
- case "image": return ["Source"];
391
- case "if": return [...BLOCK_CHILDREN, "Else"];
392
- case "inline": return [
393
- "strong",
394
- "em",
395
- "a"
396
- ];
397
- case "inline-nolink": return ["strong", "em"];
398
- case "whatsapp": return [
399
- "Header",
400
- "Body",
401
- "Footer",
402
- "Buttons"
403
- ];
404
- case "buttons": return ["Button"];
405
- case "verbatim": return ["If"];
406
- case "verbatim-if": return ["If", "Else"];
407
- case "preview":
408
- case "void": return [];
409
- }
410
- };
411
- const isInlineModel = (model) => model === "inline" || model === "inline-nolink" || model === "preview";
412
- const isVerbatimModel = (model) => model === "verbatim" || model === "verbatim-if";
413
- /** Models whose direct children may be text / interpolations. */
414
- const admitsText = (model) => isInlineModel(model) || isVerbatimModel(model);
415
- const listTags = (tags) => tags.map((tag) => `<${tag}>`).join(", ");
416
- /** Wrapper tag suggested when a loose child isn't allowed directly under its model. */
417
- const WRAPPER_TAG_BY_MODEL = {
418
- list: "ListItem",
419
- whatsapp: "Body",
420
- buttons: "Button"
421
- };
422
- const parseSml = (source) => {
423
- if (source.length > SML_PARSER_LIMITS.maxCharacters) return parserLimitFailure("source-character-limit", `SML source exceeds the ${SML_PARSER_LIMITS.maxCharacters} character limit (${source.length} characters)`, source.length);
424
- const sourceBytes = new TextEncoder().encode(source).byteLength;
425
- if (sourceBytes > SML_PARSER_LIMITS.maxUtf8Bytes) return parserLimitFailure("source-byte-limit", `SML source exceeds the ${SML_PARSER_LIMITS.maxUtf8Bytes} byte UTF-8 limit (${sourceBytes} bytes)`, source.length);
426
- const lexer = createLexer(source);
427
- let diagnostic;
428
- let nodeCount = 0;
429
- let channel = null;
430
- let specs = EMAIL_TAG_SPECS;
431
- const fail = (code, message, span) => {
432
- diagnostic ??= {
433
- code,
434
- message,
435
- span
436
- };
437
- return null;
438
- };
439
- const failToken = (token) => {
440
- const error = token.error ?? {
441
- code: "unexpected-token",
442
- message: `unexpected ${JSON.stringify(lexer.getText(token))}`
443
- };
444
- return fail(error.code, error.message, {
445
- start: token.start,
446
- end: token.end
447
- });
448
- };
449
- const countNode = (span) => {
450
- const observed = nodeCount + 1;
451
- if (observed > SML_PARSER_LIMITS.maxNodes) {
452
- fail("node-limit", `SML document exceeds the ${SML_PARSER_LIMITS.maxNodes} node limit (${observed} nodes)`, span);
453
- return false;
454
- }
455
- nodeCount = observed;
456
- return true;
457
- };
458
- const parseAttributeValue = (tag, name, spec, nameToken) => {
459
- const nameSpan = {
460
- start: nameToken.start,
461
- end: nameToken.end
462
- };
463
- if (lexer.scanTagToken().kind !== TokenKind.Equals) return fail("missing-attribute-value", `attribute "${name}" needs a value — write ${name}=${spec.type === "number" ? `{${spec.min ?? 1}}` : "\"...\""}`, nameSpan);
464
- const valueToken = lexer.scanTagToken();
465
- if (valueToken.kind === TokenKind.Unknown) return failToken(valueToken);
466
- if (valueToken.kind === TokenKind.Identifier || valueToken.kind === TokenKind.Number) {
467
- const raw = lexer.getText(valueToken);
468
- const suggestion = spec.type === "number" ? `${name}={${raw}}` : `${name}="${raw}"`;
469
- return fail("unquoted-attribute", `attribute values must be quoted — write ${suggestion}`, {
470
- start: valueToken.start,
471
- end: valueToken.end
472
- });
473
- }
474
- if (valueToken.kind === TokenKind.OpenBrace) {
475
- const numberToken = lexer.scanTagToken();
476
- if (numberToken.kind !== TokenKind.Number) return fail("invalid-attribute-value", `expected a number inside braces — write ${name}={16}`, {
477
- start: numberToken.start,
478
- end: numberToken.end
479
- });
480
- const closeToken = lexer.scanTagToken();
481
- if (closeToken.kind !== TokenKind.CloseBrace) return fail("invalid-attribute-value", `missing } after ${name}={${lexer.getText(numberToken)}`, {
482
- start: closeToken.start,
483
- end: closeToken.end
484
- });
485
- if (spec.type !== "number") return fail("invalid-attribute-value", `"${name}" on <${tag}> is a string attribute — write ${name}="..."`, {
486
- start: valueToken.start,
487
- end: closeToken.end
488
- });
489
- return {
490
- value: {
491
- kind: "number",
492
- value: Number.parseInt(lexer.getText(numberToken), 10)
493
- },
494
- valueSpan: {
495
- start: numberToken.start,
496
- end: numberToken.end
497
- }
498
- };
499
- }
500
- if (valueToken.kind !== TokenKind.String) return fail("missing-attribute-value", `attribute "${name}" needs a value — write ${name}=${spec.type === "number" ? "{16}" : "\"...\""}`, {
501
- start: valueToken.start,
502
- end: valueToken.end
503
- });
504
- const raw = lexer.getStringValue(valueToken);
505
- const valueSpan = {
506
- start: valueToken.start + 1,
507
- end: valueToken.end - 1
508
- };
509
- if (spec.type === "number") {
510
- if (!/^\d+$/.test(raw)) return fail("invalid-attribute-value", `"${name}" on <${tag}> must be a number — write ${name}={16}`, valueSpan);
511
- return {
512
- value: {
513
- kind: "number",
514
- value: Number.parseInt(raw, 10)
515
- },
516
- valueSpan
517
- };
518
- }
519
- if (spec.values !== void 0 && !spec.values.includes(raw)) return fail("invalid-attribute-value", `"${name}" on <${tag}> must be one of ${spec.values.map((value) => `"${value}"`).join(", ")}`, valueSpan);
520
- return {
521
- value: {
522
- kind: "string",
523
- value: raw
524
- },
525
- valueSpan
526
- };
527
- };
528
- const validateNumberBounds = (tag, name, spec, attribute) => {
529
- if (attribute.value.kind !== "number") return true;
530
- const { value } = attribute.value;
531
- if (spec.min !== void 0 && value < spec.min) {
532
- fail("invalid-attribute-value", `"${name}" on <${tag}> must be at least ${spec.min}`, attribute.valueSpan);
533
- return false;
534
- }
535
- if (spec.max !== void 0 && value > spec.max) {
536
- fail("invalid-attribute-value", `"${name}" on <${tag}> must be at most ${spec.max}`, attribute.valueSpan);
537
- return false;
538
- }
539
- return true;
540
- };
541
- /** Called with the `<` already consumed; `lessThanToken` is that token. */
542
- const parseElement = (lessThanToken, depth) => {
543
- if (depth > SML_PARSER_LIMITS.maxDepth) return fail("depth-limit", `SML nesting exceeds the maximum depth of ${SML_PARSER_LIMITS.maxDepth} (observed ${depth})`, {
544
- start: lessThanToken.start,
545
- end: lessThanToken.end
546
- });
547
- const tagToken = lexer.scanTagToken();
548
- if (tagToken.kind !== TokenKind.Identifier) {
549
- if (tagToken.kind === TokenKind.Unknown) return failToken(tagToken);
550
- return fail("unknown-tag", "expected a tag name after <", {
551
- start: tagToken.start,
552
- end: tagToken.end
553
- });
554
- }
555
- const tagName = lexer.getText(tagToken);
556
- if (channel === null) {
557
- if (!isRootTag(tagName)) return fail("invalid-root", `the document root must be ${ROOT_TAGS.map((root) => `<${root}>`).join(", ")} — the root tag declares the channel`, {
558
- start: tagToken.start,
559
- end: tagToken.end
560
- });
561
- channel = CHANNEL_BY_ROOT_TAG[tagName];
562
- specs = CHANNEL_TAG_SPECS[channel];
563
- }
564
- const tag = SML_TAGS.find((candidate) => candidate === tagName);
565
- const spec = tag === void 0 ? void 0 : specs[tag];
566
- if (tag === void 0 || spec === void 0) {
567
- const channelTags = Object.keys(specs);
568
- const suggestion = channelTags.find((candidate) => candidate.toLowerCase() === tagName.toLowerCase());
569
- const hint = suggestion !== void 0 ? `did you mean <${suggestion}>?` : tag !== void 0 ? `<${tag}> is not part of ${channel} documents; ${channel} tags are ${listTags(channelTags)}` : `${channel} tags are ${listTags(channelTags)}`;
570
- return fail("unknown-tag", `unknown tag <${tagName}> — ${hint}`, {
571
- start: tagToken.start,
572
- end: tagToken.end
573
- });
574
- }
575
- if (!countNode({
576
- start: lessThanToken.start,
577
- end: tagToken.end
578
- })) return null;
579
- const element = {
580
- kind: "element",
581
- id: "",
582
- tag,
583
- attributes: [],
584
- children: [],
585
- span: {
586
- start: lessThanToken.start,
587
- end: lessThanToken.end
588
- }
589
- };
590
- let selfClosing = false;
591
- for (;;) {
592
- const token = lexer.scanTagToken();
593
- if (token.kind === TokenKind.Identifier) {
594
- const name = lexer.getText(token);
595
- const universal = channel === "email" && UNIVERSAL_ATTRIBUTE_TAGS.has(tag);
596
- const attributeSpec = spec.attributes[name] ?? (universal ? universalAttributeSpec(name) : void 0);
597
- if (attributeSpec === void 0) {
598
- const known = [...Object.keys(spec.attributes), ...universal ? Object.keys(UNIVERSAL_ATTRIBUTES) : []];
599
- const hint = known.length === 0 ? `<${tag}> takes no attributes` : `<${tag}> accepts ${known.map((k) => `"${k}"`).join(", ")}`;
600
- return fail("unknown-attribute", `unknown attribute "${name}" on <${tag}> — ${hint}`, {
601
- start: token.start,
602
- end: token.end
603
- });
604
- }
605
- if (element.attributes.some((attribute) => attribute.name === name)) return fail("duplicate-attribute", `duplicate attribute "${name}" on <${tag}> — remove one`, {
606
- start: token.start,
607
- end: token.end
608
- });
609
- const parsed = parseAttributeValue(tag, name, attributeSpec, token);
610
- if (parsed === null) return null;
611
- const attribute = {
612
- name,
613
- value: parsed.value,
614
- span: {
615
- start: token.start,
616
- end: parsed.valueSpan.end + 1
617
- },
618
- valueSpan: parsed.valueSpan
619
- };
620
- if (!validateNumberBounds(tag, name, attributeSpec, attribute)) return null;
621
- element.attributes.push(attribute);
622
- continue;
623
- }
624
- if (token.kind === TokenKind.SlashGreaterThan) {
625
- selfClosing = true;
626
- element.span.end = token.end;
627
- break;
628
- }
629
- if (token.kind === TokenKind.GreaterThan) {
630
- element.span.end = token.end;
631
- break;
632
- }
633
- if (token.kind === TokenKind.EndOfFile) return fail("unclosed-tag", `unclosed <${tag}> — add </${tag}>`, {
634
- start: lessThanToken.start,
635
- end: tagToken.end
636
- });
637
- return failToken(token);
638
- }
639
- for (const [name, attributeSpec] of Object.entries(spec.attributes)) if (attributeSpec.required === true && !element.attributes.some((a) => a.name === name)) {
640
- const example = attributeSpec.type === "number" ? `${name}={16}` : tag === "If" ? `test="plan == 'pro'"` : `${name}="..."`;
641
- return fail("missing-attribute", `<${tag}> requires the "${name}" attribute — add ${example}`, {
642
- start: lessThanToken.start,
643
- end: element.span.end
644
- });
645
- }
646
- if (tag === "If") {
647
- const testIndex = element.attributes.findIndex((attribute) => attribute.name === "test");
648
- const testAttribute = element.attributes[testIndex];
649
- if (testAttribute.value.kind === "string") {
650
- const parsed = parsePredicate(testAttribute.value.value, testAttribute.valueSpan.start);
651
- if (Result.isError(parsed)) {
652
- const first = parsed.error[0];
653
- return fail(first.code, first.message, first.span);
654
- }
655
- element.attributes[testIndex] = {
656
- ...testAttribute,
657
- value: {
658
- kind: "string",
659
- value: printPredicate(parsed.value)
660
- }
661
- };
662
- }
663
- }
664
- if (spec.content === "void" && !selfClosing) return fail("void-needs-self-close", `<${tag}> is self-closing — write <${tag} ... />`, {
665
- start: lessThanToken.start,
666
- end: element.span.end
667
- });
668
- if (selfClosing) {
669
- if (spec.content === "whatsapp") return fail("whatsapp-missing-body", "<WhatsApp> requires a <Body> component — add <Body>...</Body>", element.span);
670
- return element;
671
- }
672
- const model = spec.content;
673
- for (;;) {
674
- const token = lexer.scanTextToken();
675
- if (token.kind === TokenKind.EndOfFile) return fail("unclosed-tag", `unclosed <${tag}> — add </${tag}>`, {
676
- start: lessThanToken.start,
677
- end: tagToken.end
678
- });
679
- if (token.kind === TokenKind.Unknown) return failToken(token);
680
- const looseChildMessage = (label) => {
681
- if (model === "image") return `${label} is not allowed directly inside <${tag}> — <${tag}> may contain only ${listTags(allowedChildren(model))}`;
682
- const wrapper = WRAPPER_TAG_BY_MODEL[model] ?? "Text";
683
- return `${label} is not allowed directly inside <${tag}> — wrap it in <${wrapper}>`;
684
- };
685
- const afterElse = model === "verbatim-if" && element.children.some((existing) => existing.kind === "element" && existing.tag === "Else");
686
- if (token.kind === TokenKind.Text) {
687
- const value = lexer.getTextValue(token);
688
- if (admitsText(model)) {
689
- if (afterElse) return fail("else-not-last", "content after <Else> — move it inside <Else> or above it", {
690
- start: token.start,
691
- end: token.end
692
- });
693
- if (!countNode({
694
- start: token.start,
695
- end: token.end
696
- })) return null;
697
- element.children.push({
698
- kind: "text",
699
- id: "",
700
- value,
701
- span: {
702
- start: token.start,
703
- end: token.end
704
- }
705
- });
706
- } else if (value.trim().length > 0) return fail("invalid-child", looseChildMessage("text"), {
707
- start: token.start,
708
- end: token.end
709
- });
710
- continue;
711
- }
712
- if (token.kind === TokenKind.Interpolation) {
713
- if (!admitsText(model)) return fail("invalid-child", looseChildMessage(`{{${token.name}}}`), {
714
- start: token.start,
715
- end: token.end
716
- });
717
- if (afterElse) return fail("else-not-last", "content after <Else> — move it inside <Else> or above it", {
718
- start: token.start,
719
- end: token.end
720
- });
721
- if (!countNode({
722
- start: token.start,
723
- end: token.end
724
- })) return null;
725
- element.children.push({
726
- kind: "interpolation",
727
- id: "",
728
- name: token.name,
729
- syntax: token.syntax,
730
- span: {
731
- start: token.start,
732
- end: token.end
733
- }
734
- });
735
- continue;
736
- }
737
- if (token.kind === TokenKind.LessThanSlash) {
738
- const closeName = lexer.scanTagToken();
739
- if (closeName.kind !== TokenKind.Identifier || lexer.getText(closeName) !== tag) {
740
- const found = closeName.kind === TokenKind.Identifier ? `</${lexer.getText(closeName)}>` : "</>";
741
- return fail("mismatched-close-tag", `expected </${tag}> but found ${found} — close <${tag}> first`, {
742
- start: token.start,
743
- end: closeName.end
744
- });
745
- }
746
- const closeEnd = lexer.scanTagToken();
747
- if (closeEnd.kind !== TokenKind.GreaterThan) return fail("mismatched-close-tag", `expected > to finish </${tag}>`, {
748
- start: closeEnd.start,
749
- end: closeEnd.end
750
- });
751
- element.span.end = closeEnd.end;
752
- break;
753
- }
754
- const allowed = allowedChildren(model);
755
- const child = parseElement(token, depth + 1);
756
- if (child === null) return null;
757
- if (!allowed.includes(child.tag)) {
758
- const message = child.tag === "Else" ? `<Else> is only valid as the last child of <If> — wrap this branch in <If>` : isInlineModel(model) ? child.tag === "a" ? `<a> is not allowed inside <${tag}> — links cannot nest inside links or buttons` : `<${child.tag}> is not allowed inside <${tag}> — only text, {{variables}}${allowed.length > 0 ? `, and ${listTags(allowed)}` : ""} may appear here` : `<${child.tag}> is not allowed inside <${tag}> — <${tag}> may contain only ${listTags(allowed)}`;
759
- return fail("invalid-child", message, child.span);
760
- }
761
- if (model === "email" && EMAIL_ENVELOPE_CHILDREN.includes(child.tag)) {
762
- if (element.children.some((existing) => existing.kind === "element" && existing.tag === child.tag)) return fail(`${child.tag.toLowerCase()}-duplicate`, `<Email> allows at most one <${child.tag}> — merge them`, child.span);
763
- if (element.children.some((existing) => existing.kind === "element" && !EMAIL_ENVELOPE_CHILDREN.includes(existing.tag))) return fail(`${child.tag.toLowerCase()}-position`, `<${child.tag}> must appear before the body blocks in <Email> — move it above the other blocks`, child.span);
764
- }
765
- if ((model === "if" || model === "verbatim-if") && element.children.some((existing) => existing.kind === "element" && existing.tag === "Else")) return fail(child.tag === "Else" ? "duplicate-else" : "else-not-last", child.tag === "Else" ? "only one <Else> is allowed inside <If> — merge the branches" : `<Else> must be the last child of <If> — move <${child.tag}> above it`, child.span);
766
- if (model === "whatsapp") {
767
- const rank = {
768
- Header: 0,
769
- Body: 1,
770
- Footer: 2,
771
- Buttons: 3
772
- };
773
- const childRank = rank[child.tag];
774
- for (const existing of element.children) {
775
- if (existing.kind !== "element") continue;
776
- if (existing.tag === child.tag) return fail("whatsapp-duplicate-component", `<WhatsApp> allows at most one <${child.tag}> — merge them`, child.span);
777
- if (rank[existing.tag] > childRank) return fail("whatsapp-component-order", `<${child.tag}> must come before <${existing.tag}> — WhatsApp components are Header?, Body, Footer?, Buttons? in that order`, child.span);
778
- }
779
- }
780
- if (model === "image") {
781
- const scheme = child.attributes.find((attribute) => attribute.name === "scheme");
782
- if (scheme?.value.kind === "string" && element.children.some((existing) => existing.kind === "element" && existing.tag === "Source" && existing.attributes.some((attribute) => attribute.name === "scheme" && attribute.value.kind === "string" && attribute.value.value === scheme.value.value))) return fail("duplicate-source", `<Image> allows one <Source scheme="${scheme.value.value}">; remove the duplicate`, child.span);
783
- }
784
- element.children.push(child);
785
- }
786
- if (model === "whatsapp") {
787
- if (!element.children.some((child) => child.kind === "element" && child.tag === "Body")) return fail("whatsapp-missing-body", "<WhatsApp> requires a <Body> component — add <Body>...</Body>", element.span);
788
- }
789
- if (isInlineModel(model)) normalizeInlineChildren(element);
790
- return element;
791
- };
792
- const skipBlankText = () => {
793
- for (;;) {
794
- const token = lexer.scanTextToken();
795
- if (token.kind === TokenKind.Text && lexer.getTextValue(token).trim().length === 0) continue;
796
- return token;
797
- }
798
- };
799
- const first = skipBlankText();
800
- if (first.kind === TokenKind.Unknown) failToken(first);
801
- else if (first.kind !== TokenKind.LessThan) fail("invalid-root", `the document must start with a channel root element: ${ROOT_TAGS.map((root) => `<${root}>`).join(", ")}`, {
802
- start: first.start,
803
- end: first.end
804
- });
805
- let root = null;
806
- if (diagnostic === void 0) root = parseElement(first, 1);
807
- if (root !== null) {
808
- const trailing = skipBlankText();
809
- if (trailing.kind !== TokenKind.EndOfFile) {
810
- fail("multiple-roots", `content after </${root.tag}> — a document has exactly one root element`, {
811
- start: trailing.start,
812
- end: trailing.end
813
- });
814
- root = null;
815
- }
816
- }
817
- if (root === null || diagnostic !== void 0) return Result.err(new SmlParseError({ diagnostics: [diagnostic ?? {
818
- code: "parse-failed",
819
- message: "parse failed",
820
- span: {
821
- start: 0,
822
- end: 0
823
- }
824
- }] }));
825
- assignNodeIds(root);
826
- return Result.ok({
827
- kind: "document",
828
- root
829
- });
830
- };
831
- /**
832
- * Canonical inline whitespace: collapse runs to a single space, trim at the
833
- * element boundary, drop text nodes that end up empty. Makes parse ∘ print a
834
- * fixpoint — the printer emits exactly these values.
835
- */
836
- const normalizeInlineChildren = (element) => {
837
- for (const child of element.children) if (child.kind === "text") child.value = child.value.replace(/\s+/g, " ");
838
- const firstChild = element.children[0];
839
- if (firstChild?.kind === "text") firstChild.value = firstChild.value.replace(/^ /, "");
840
- const lastChild = element.children[element.children.length - 1];
841
- if (lastChild?.kind === "text") lastChild.value = lastChild.value.replace(/ $/, "");
842
- element.children = element.children.filter((child) => child.kind !== "text" || child.value.length > 0);
843
- };
844
- /** Stable ids in document order — identical source ⇒ identical ids. */
845
- const assignNodeIds = (root) => {
846
- let next = 0;
847
- const pending = [root];
848
- while (pending.length > 0) {
849
- const node = pending.pop();
850
- node.id = `n${next}`;
851
- next++;
852
- if (node.kind !== "element") continue;
853
- for (let index = node.children.length - 1; index >= 0; index--) pending.push(node.children[index]);
854
- }
855
- };
856
- //#endregion
857
- export { CHANNEL_TAG_SPECS, EMAIL_TAG_SPECS, SML_PARSER_LIMITS, SMS_TAG_SPECS, UNIVERSAL_ATTRIBUTES, UNIVERSAL_ATTRIBUTE_TAGS, WHATSAPP_TAG_SPECS, allowedChildren, parseSml };
858
-
859
- //# sourceMappingURL=parser.js.map