@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.
- package/AGENTS.md +90 -37
- package/README.md +188 -52
- package/dist/assets-C5duwTya.d.ts +40 -0
- package/dist/{brand-DcoVkh_S.d.ts → brand-B-0bN_Ir.d.ts} +2 -2
- package/dist/{render-vAv_4r9v.js → cascade-t6kxlgEI.js} +93 -280
- package/dist/channel-edit.d.ts +121 -0
- package/dist/channel-edit.js +609 -0
- package/dist/channel-specs-B6M_HlaV.js +29 -0
- package/dist/channel-specs-C2feJwuP.d.ts +13 -0
- package/dist/compatibility-DbISoFIq.js +589 -0
- package/dist/compiler.d.ts +10 -257
- package/dist/compiler.js +10 -730
- package/dist/{css-CS2jLID6.d.ts → css-BVQKICKC.d.ts} +3 -3
- package/dist/diagnostic-codes.d.ts +403 -0
- package/dist/diagnostic-codes.js +399 -0
- package/dist/{diagnostics-BMYtBCQk.d.ts → diagnostics-B-Gym34R.d.ts} +8 -4
- package/dist/diagnostics.d.ts +31 -2
- package/dist/diagnostics.js +1 -19
- package/dist/{elements-BzjjnoCS.d.ts → elements-cqldpaJt.d.ts} +3 -3
- package/dist/email/assets.d.ts +2 -40
- package/dist/email/brand-footer.d.ts +2 -2
- package/dist/email/brand-footer.js +4 -3
- package/dist/email/brand.d.ts +1 -1
- package/dist/email/brand.js +1 -1
- package/dist/email/components.d.ts +21 -10
- package/dist/email/components.js +46 -11
- package/dist/email/diagnostics.d.ts +2 -2
- package/dist/email/diagnostics.js +2 -1
- package/dist/email/elements.d.ts +1 -1
- package/dist/email/html.d.ts +2 -2
- package/dist/email/html.js +45 -2
- package/dist/email/jsx-dev-runtime.d.ts +2 -2
- package/dist/email/jsx-runtime.d.ts +1 -1
- package/dist/email/render.d.ts +8 -18
- package/dist/email/render.js +163 -3
- package/dist/email/text.d.ts +1 -1
- package/dist/fmt.d.ts +32 -0
- package/dist/fmt.js +174 -0
- package/dist/{html-Bl9BAdqc.d.ts → html-BJepOwZ4.d.ts} +19 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/ir-aaqNE987.d.ts +252 -0
- package/dist/ir.d.ts +2 -0
- package/dist/ir.js +7 -0
- package/dist/preview-Ekdg1dXG.d.ts +100 -0
- package/dist/preview.d.ts +2 -0
- package/dist/preview.js +160 -0
- package/dist/profile-BH-OtdDA.js +3670 -0
- package/dist/render-ir.d.ts +19 -0
- package/dist/render-ir.js +826 -0
- package/dist/sms-segments.d.ts +13 -2
- package/dist/sms.d.ts +16 -0
- package/dist/sms.js +12 -0
- package/dist/{email/source-edit.d.ts → source-edit.d.ts} +55 -6
- package/dist/source-edit.js +343 -0
- package/dist/source-locations-C8wCsiAy.d.ts +23 -0
- package/dist/source-locations.d.ts +2 -2
- package/dist/source-locations.js +3 -89
- package/dist/static.d.ts +57 -0
- package/dist/static.js +69 -0
- package/dist/tailwind-CMYpZf1v.js +614 -0
- package/dist/template-_QE_rQND.d.ts +65 -0
- package/dist/template.d.ts +2 -2
- package/dist/template.js +15 -10
- package/dist/theme-css.d.ts +1 -1
- package/dist/theme-imports-lvBw3AD1.js +109 -0
- package/dist/transform-Ct2OMkFN.d.ts +153 -0
- package/dist/whatsapp.d.ts +57 -0
- package/dist/whatsapp.js +22 -0
- package/generated/contract.json +1782 -0
- package/generated/reference.md +353 -0
- package/package.json +41 -151
- package/skills/sml/SKILL.md +123 -0
- package/dist/ast-BrDtApwS.d.ts +0 -107
- package/dist/ast.d.ts +0 -2
- package/dist/ast.js +0 -124
- package/dist/candidate.d.ts +0 -29
- package/dist/candidate.js +0 -88
- package/dist/catalog-CKuOooB8.d.ts +0 -354
- package/dist/checks-D73Xizbw.js +0 -59366
- package/dist/checks.d.ts +0 -33
- package/dist/checks.js +0 -2
- package/dist/classes.d.ts +0 -26
- package/dist/classes.js +0 -184
- package/dist/compile-sms.d.ts +0 -28
- package/dist/compile-sms.js +0 -44
- package/dist/compile-whatsapp.d.ts +0 -56
- package/dist/compile-whatsapp.js +0 -186
- package/dist/compile.d.ts +0 -18
- package/dist/compile.js +0 -29
- package/dist/components.d.ts +0 -289
- package/dist/components.js +0 -106
- package/dist/conditionals.d.ts +0 -10
- package/dist/conditionals.js +0 -74
- package/dist/corpus-CPPfgAHN.d.ts +0 -106
- package/dist/diagnostics-OcQKQYSY.d.ts +0 -49
- package/dist/email/selection.d.ts +0 -2
- package/dist/email/selection.js +0 -91
- package/dist/email/source-edit.js +0 -185
- package/dist/email/stylesheets.d.ts +0 -15
- package/dist/email/stylesheets.js +0 -48
- package/dist/emit.d.ts +0 -51
- package/dist/emit.js +0 -151
- package/dist/interpolate.d.ts +0 -24
- package/dist/interpolate.js +0 -43
- package/dist/jsx-dev-runtime.d.ts +0 -11
- package/dist/jsx-dev-runtime.js +0 -15
- package/dist/jsx-runtime-I-1nCtl2.d.ts +0 -57
- package/dist/jsx-runtime.d.ts +0 -2
- package/dist/jsx-runtime.js +0 -121
- package/dist/lexer.d.ts +0 -60
- package/dist/lexer.js +0 -281
- package/dist/model.d.ts +0 -24
- package/dist/model.js +0 -77
- package/dist/ops-BvwqHl7V.js +0 -630
- package/dist/ops.d.ts +0 -114
- package/dist/ops.js +0 -2
- package/dist/parser-t29cHLVL.d.ts +0 -396
- package/dist/parser.d.ts +0 -2
- package/dist/parser.js +0 -859
- package/dist/predicate-BI6uRRM-.js +0 -549
- package/dist/predicate-M-05Nd7R.d.ts +0 -56
- package/dist/predicate.d.ts +0 -2
- package/dist/predicate.js +0 -2
- package/dist/printer.d.ts +0 -6
- package/dist/printer.js +0 -122
- package/dist/registry-Dcp6QEK_.d.ts +0 -73
- package/dist/registry.d.ts +0 -2
- package/dist/registry.js +0 -646
- package/dist/render-template.d.ts +0 -45
- package/dist/render-template.js +0 -76
- package/dist/selection-CCArx9lU.d.ts +0 -91
- package/dist/showcase-egkusQBf.d.ts +0 -415
- package/dist/sms-segments-y4FpPilJ.d.ts +0 -13
- package/dist/source-locations-DrCM82Ym.d.ts +0 -67
- package/dist/starters/category-mapping.d.ts +0 -13
- package/dist/starters/category-mapping.js +0 -49
- package/dist/starters/corpus.d.ts +0 -2
- package/dist/starters/corpus.js +0 -194
- package/dist/starters/library.d.ts +0 -18
- package/dist/starters/library.js +0 -692
- package/dist/starters/public-gallery.d.ts +0 -266
- package/dist/starters/public-gallery.js +0 -916
- package/dist/starters/public-handoff.d.ts +0 -164
- package/dist/starters/public-handoff.js +0 -361
- package/dist/starters/public-preview-artifacts.d.ts +0 -43
- package/dist/starters/public-preview-artifacts.js +0 -104
- package/dist/starters/public-preview.d.ts +0 -61
- package/dist/starters/public-preview.js +0 -83
- package/dist/starters/showcase.d.ts +0 -2
- package/dist/starters/showcase.js +0 -335
- package/dist/starters/sources.d.ts +0 -45
- package/dist/starters/sources.js +0 -7762
- package/dist/starters/theme.d.ts +0 -45
- package/dist/starters/theme.js +0 -77
- package/dist/starters.d.ts +0 -2
- package/dist/starters.js +0 -874
- package/dist/tailwind-BYq7Lo6u.js +0 -727
- package/dist/template-DWftqh4x.d.ts +0 -60
- package/dist/tsx-source-instrumentation.d.ts +0 -30
- package/dist/tsx-source-instrumentation.js +0 -115
- package/dist/tsx-tracer.d.ts +0 -112
- package/dist/tsx-tracer.js +0 -985
- package/dist/variables-J_L3qxRa.d.ts +0 -77
- package/dist/variables.d.ts +0 -2
- package/dist/variables.js +0 -141
- package/dist/version-BITr58kk.js +0 -96
- package/generated/sml-agent-contract.json +0 -709
- package/generated/sml-email-agent-contract.json +0 -615
- package/generated/sml-email-language-reference.md +0 -94
- 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
|