@templatical/template-tools 0.46.0 → 0.47.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.
@@ -1,1462 +0,0 @@
1
- import Ajv from "ajv";
2
- import { lintTemplate } from "@templatical/quality";
3
- import { safeClone } from "@templatical/types";
4
- //#endregion
5
- //#region src/validate.ts
6
- /** The generated JSON Schema for `TemplateContent`, as a plain object. */
7
- const schema = {
8
- $id: "https://templatical.com/schema/template-content.json",
9
- $schema: "http://json-schema.org/draft-07/schema#",
10
- $ref: "#/definitions/TemplateContent",
11
- definitions: {
12
- "TemplateContent": {
13
- "type": "object",
14
- "properties": {
15
- "blocks": {
16
- "type": "array",
17
- "items": { "$ref": "#/definitions/Block" }
18
- },
19
- "settings": { "$ref": "#/definitions/TemplateSettings" }
20
- },
21
- "required": ["blocks", "settings"],
22
- "additionalProperties": false
23
- },
24
- "Block": { "anyOf": [
25
- { "$ref": "#/definitions/SectionBlock" },
26
- { "$ref": "#/definitions/TitleBlock" },
27
- { "$ref": "#/definitions/ParagraphBlock" },
28
- { "$ref": "#/definitions/ImageBlock" },
29
- { "$ref": "#/definitions/ButtonBlock" },
30
- { "$ref": "#/definitions/DividerBlock" },
31
- { "$ref": "#/definitions/VideoBlock" },
32
- { "$ref": "#/definitions/SocialIconsBlock" },
33
- { "$ref": "#/definitions/SpacerBlock" },
34
- { "$ref": "#/definitions/HtmlBlock" },
35
- { "$ref": "#/definitions/MenuBlock" },
36
- { "$ref": "#/definitions/TableBlock" },
37
- { "$ref": "#/definitions/CountdownBlock" },
38
- { "$ref": "#/definitions/CustomBlock" },
39
- { "$ref": "#/definitions/SlotBlock" },
40
- { "$ref": "#/definitions/WrapperBlock" }
41
- ] },
42
- "SectionBlock": {
43
- "type": "object",
44
- "properties": {
45
- "id": { "type": "string" },
46
- "type": {
47
- "type": "string",
48
- "const": "section"
49
- },
50
- "styles": { "$ref": "#/definitions/BlockStyles" },
51
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
52
- "displayCondition": {
53
- "type": "object",
54
- "properties": {
55
- "label": { "type": "string" },
56
- "before": { "type": "string" },
57
- "after": { "type": "string" },
58
- "group": { "type": "string" },
59
- "description": { "type": "string" }
60
- },
61
- "required": [
62
- "label",
63
- "before",
64
- "after"
65
- ],
66
- "additionalProperties": false
67
- },
68
- "columns": { "$ref": "#/definitions/ColumnLayout" },
69
- "children": {
70
- "type": "array",
71
- "items": {
72
- "type": "array",
73
- "items": { "$ref": "#/definitions/Block" }
74
- }
75
- },
76
- "stackOnMobile": {
77
- "type": "boolean",
78
- "description": "Whether columns stack vertically on mobile. Absent or `true` keeps MJML's default responsive behavior (columns stack below 480px). `false` renders the columns inside an `<mj-group>` so they stay side-by-side on mobile, proportionally shrunk to fit."
79
- },
80
- "borderRadius": {
81
- "$ref": "#/definitions/BorderRadiusValue",
82
- "description": "Corner radius in px — one number, or a radius per corner. Omitted/0 = square corners."
83
- },
84
- "border": {
85
- "$ref": "#/definitions/BorderValue",
86
- "description": "Border around the section box, per side. Omitted = no border; a side with width 0 is not drawn."
87
- },
88
- "wrapper": {
89
- "$ref": "#/definitions/SectionWrapper",
90
- "description": "Optional outer frame (rendered as an `mj-wrapper` around the section)."
91
- }
92
- },
93
- "required": [
94
- "children",
95
- "columns",
96
- "id",
97
- "styles",
98
- "type"
99
- ],
100
- "additionalProperties": false
101
- },
102
- "BlockStyles": {
103
- "type": "object",
104
- "properties": {
105
- "padding": { "$ref": "#/definitions/SpacingValue" },
106
- "backgroundColor": { "type": "string" }
107
- },
108
- "required": ["padding"],
109
- "additionalProperties": false
110
- },
111
- "SpacingValue": {
112
- "type": "object",
113
- "properties": {
114
- "top": { "type": "number" },
115
- "right": { "type": "number" },
116
- "bottom": { "type": "number" },
117
- "left": { "type": "number" }
118
- },
119
- "required": [
120
- "top",
121
- "right",
122
- "bottom",
123
- "left"
124
- ],
125
- "additionalProperties": false
126
- },
127
- "BlockVisibility": {
128
- "type": "object",
129
- "properties": {
130
- "desktop": { "type": "boolean" },
131
- "mobile": { "type": "boolean" }
132
- },
133
- "required": ["desktop", "mobile"],
134
- "additionalProperties": false
135
- },
136
- "ColumnLayout": {
137
- "type": "string",
138
- "enum": [
139
- "1",
140
- "2",
141
- "3",
142
- "2-1",
143
- "1-2"
144
- ]
145
- },
146
- "BorderRadiusValue": {
147
- "anyOf": [{ "type": "number" }, { "$ref": "#/definitions/CornerRadius" }],
148
- "description": "A corner radius in px: one number for all four corners, or a radius per corner."
149
- },
150
- "CornerRadius": {
151
- "type": "object",
152
- "properties": {
153
- "topLeft": { "type": "number" },
154
- "topRight": { "type": "number" },
155
- "bottomRight": { "type": "number" },
156
- "bottomLeft": { "type": "number" }
157
- },
158
- "required": [
159
- "topLeft",
160
- "topRight",
161
- "bottomRight",
162
- "bottomLeft"
163
- ],
164
- "additionalProperties": false,
165
- "description": "A radius per corner, in px."
166
- },
167
- "BorderValue": {
168
- "type": "object",
169
- "properties": {
170
- "top": { "$ref": "#/definitions/BorderSideValue" },
171
- "right": { "$ref": "#/definitions/BorderSideValue" },
172
- "bottom": { "$ref": "#/definitions/BorderSideValue" },
173
- "left": { "$ref": "#/definitions/BorderSideValue" }
174
- },
175
- "required": [
176
- "top",
177
- "right",
178
- "bottom",
179
- "left"
180
- ],
181
- "additionalProperties": false,
182
- "description": "A border described per side, like `SpacingValue`. Only elements MJML can border natively carry one: sections, images and buttons."
183
- },
184
- "BorderSideValue": {
185
- "type": "object",
186
- "properties": {
187
- "width": {
188
- "type": "number",
189
- "description": "Width in px. `0` means no border on this side."
190
- },
191
- "style": { "$ref": "#/definitions/BorderStyle" },
192
- "color": { "type": "string" }
193
- },
194
- "required": [
195
- "width",
196
- "style",
197
- "color"
198
- ],
199
- "additionalProperties": false,
200
- "description": "One side of a border. A width of `0` leaves that side undrawn."
201
- },
202
- "BorderStyle": {
203
- "type": "string",
204
- "enum": [
205
- "solid",
206
- "dashed",
207
- "dotted"
208
- ]
209
- },
210
- "SectionWrapper": {
211
- "type": "object",
212
- "properties": {
213
- "backgroundColor": { "type": "string" },
214
- "padding": { "$ref": "#/definitions/SpacingValue" },
215
- "borderRadius": {
216
- "$ref": "#/definitions/BorderRadiusValue",
217
- "description": "Corner radius in px for the outer frame — one number, or a radius per corner. Omitted/0 = square corners."
218
- }
219
- },
220
- "additionalProperties": false,
221
- "description": "Optional outer frame for a section. When present, the section is rendered inside an `mj-wrapper` — a band (its own background + padding) that frames the section, e.g. a white card sitting on a colored band. The band is as wide as the email body (`settings.width`), not the recipient's viewport."
222
- },
223
- "TitleBlock": {
224
- "type": "object",
225
- "properties": {
226
- "id": { "type": "string" },
227
- "type": {
228
- "type": "string",
229
- "const": "title"
230
- },
231
- "styles": { "$ref": "#/definitions/BlockStyles" },
232
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
233
- "displayCondition": {
234
- "type": "object",
235
- "properties": {
236
- "label": { "type": "string" },
237
- "before": { "type": "string" },
238
- "after": { "type": "string" },
239
- "group": { "type": "string" },
240
- "description": { "type": "string" }
241
- },
242
- "required": [
243
- "label",
244
- "before",
245
- "after"
246
- ],
247
- "additionalProperties": false
248
- },
249
- "content": { "type": "string" },
250
- "level": { "$ref": "#/definitions/HeadingLevel" },
251
- "color": {
252
- "type": "string",
253
- "description": "Text color. Unset = inherit the document-level `textColor`."
254
- },
255
- "textAlign": {
256
- "type": "string",
257
- "enum": [
258
- "left",
259
- "center",
260
- "right"
261
- ]
262
- },
263
- "fontFamily": { "type": "string" }
264
- },
265
- "required": [
266
- "content",
267
- "id",
268
- "level",
269
- "styles",
270
- "textAlign",
271
- "type"
272
- ],
273
- "additionalProperties": false
274
- },
275
- "HeadingLevel": {
276
- "type": "number",
277
- "enum": [
278
- 1,
279
- 2,
280
- 3,
281
- 4
282
- ]
283
- },
284
- "ParagraphBlock": {
285
- "type": "object",
286
- "properties": {
287
- "id": { "type": "string" },
288
- "type": {
289
- "type": "string",
290
- "const": "paragraph"
291
- },
292
- "styles": { "$ref": "#/definitions/BlockStyles" },
293
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
294
- "displayCondition": {
295
- "type": "object",
296
- "properties": {
297
- "label": { "type": "string" },
298
- "before": { "type": "string" },
299
- "after": { "type": "string" },
300
- "group": { "type": "string" },
301
- "description": { "type": "string" }
302
- },
303
- "required": [
304
- "label",
305
- "before",
306
- "after"
307
- ],
308
- "additionalProperties": false
309
- },
310
- "content": { "type": "string" },
311
- "paragraphSpacing": {
312
- "type": "number",
313
- "description": "Gap in px between this block's paragraphs — the space below every `<p>` except the last. Absent means `RICH_TEXT_SPACING.paragraphGap`.\n\nOnly affects a block holding more than one paragraph; a single `<p>` has no internal gap, and the space around the block is `styles.padding`.\n\n`0` is a valid choice (paragraphs butted together) and is distinct from the field being absent, so readers must test for `undefined` rather than falsiness."
314
- }
315
- },
316
- "required": [
317
- "content",
318
- "id",
319
- "styles",
320
- "type"
321
- ],
322
- "additionalProperties": false
323
- },
324
- "ImageBlock": {
325
- "type": "object",
326
- "properties": {
327
- "id": { "type": "string" },
328
- "type": {
329
- "type": "string",
330
- "const": "image"
331
- },
332
- "styles": { "$ref": "#/definitions/BlockStyles" },
333
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
334
- "displayCondition": {
335
- "type": "object",
336
- "properties": {
337
- "label": { "type": "string" },
338
- "before": { "type": "string" },
339
- "after": { "type": "string" },
340
- "group": { "type": "string" },
341
- "description": { "type": "string" }
342
- },
343
- "required": [
344
- "label",
345
- "before",
346
- "after"
347
- ],
348
- "additionalProperties": false
349
- },
350
- "src": { "type": "string" },
351
- "alt": { "type": "string" },
352
- "width": { "anyOf": [{ "type": "number" }, {
353
- "type": "string",
354
- "const": "full"
355
- }] },
356
- "height": {
357
- "type": "number",
358
- "description": "Height in pixels. Absent means the height is derived from the width, so the image keeps its aspect ratio — setting both stretches it, since email clients don't support `object-fit`."
359
- },
360
- "align": {
361
- "type": "string",
362
- "enum": [
363
- "left",
364
- "center",
365
- "right"
366
- ]
367
- },
368
- "borderRadius": {
369
- "$ref": "#/definitions/BorderRadiusValue",
370
- "description": "Corner radius in px. Omitted/0 = square corners. A radius of at least half the rendered size rounds a square image to a circle, which is how avatar and portrait layouts are built. A radius per corner is also accepted."
371
- },
372
- "border": {
373
- "$ref": "#/definitions/BorderValue",
374
- "description": "Border around the image itself, per side. Omitted = no border; a side with width 0 is not drawn."
375
- },
376
- "linkUrl": { "type": "string" },
377
- "linkOpenInNewTab": { "type": "boolean" },
378
- "placeholderUrl": { "type": "string" },
379
- "decorative": { "type": "boolean" }
380
- },
381
- "required": [
382
- "align",
383
- "alt",
384
- "id",
385
- "src",
386
- "styles",
387
- "type",
388
- "width"
389
- ],
390
- "additionalProperties": false
391
- },
392
- "ButtonBlock": {
393
- "type": "object",
394
- "properties": {
395
- "id": { "type": "string" },
396
- "type": {
397
- "type": "string",
398
- "const": "button"
399
- },
400
- "styles": { "$ref": "#/definitions/BlockStyles" },
401
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
402
- "displayCondition": {
403
- "type": "object",
404
- "properties": {
405
- "label": { "type": "string" },
406
- "before": { "type": "string" },
407
- "after": { "type": "string" },
408
- "group": { "type": "string" },
409
- "description": { "type": "string" }
410
- },
411
- "required": [
412
- "label",
413
- "before",
414
- "after"
415
- ],
416
- "additionalProperties": false
417
- },
418
- "text": { "type": "string" },
419
- "url": { "type": "string" },
420
- "openInNewTab": { "type": "boolean" },
421
- "backgroundColor": { "type": "string" },
422
- "textColor": { "type": "string" },
423
- "borderRadius": {
424
- "$ref": "#/definitions/BorderRadiusValue",
425
- "description": "Corner radius in px — one number, or a radius per corner."
426
- },
427
- "border": {
428
- "$ref": "#/definitions/BorderValue",
429
- "description": "Border around the button itself, per side. Omitted = no border; a side with width 0 is not drawn. For an outline (\"ghost\") button, set `backgroundColor` to the keyword `\"transparent\"` (the background control stores it) and set `textColor` too — a new button is `#333333` with white text. An empty fill exports as `transparent`, because MJML paints `#414141` when the attribute is omitted."
430
- },
431
- "fontSize": { "type": "number" },
432
- "buttonPadding": { "$ref": "#/definitions/SpacingValue" },
433
- "fontFamily": { "type": "string" },
434
- "width": { "anyOf": [{ "type": "number" }, {
435
- "type": "string",
436
- "const": "full"
437
- }] },
438
- "align": {
439
- "type": "string",
440
- "enum": [
441
- "left",
442
- "center",
443
- "right"
444
- ],
445
- "description": "Placement of the button within its column. No visible effect when `width` is `\"full\"`, since the button then spans the column."
446
- }
447
- },
448
- "required": [
449
- "align",
450
- "backgroundColor",
451
- "borderRadius",
452
- "buttonPadding",
453
- "fontSize",
454
- "id",
455
- "styles",
456
- "text",
457
- "textColor",
458
- "type",
459
- "url"
460
- ],
461
- "additionalProperties": false
462
- },
463
- "DividerBlock": {
464
- "type": "object",
465
- "properties": {
466
- "id": { "type": "string" },
467
- "type": {
468
- "type": "string",
469
- "const": "divider"
470
- },
471
- "styles": { "$ref": "#/definitions/BlockStyles" },
472
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
473
- "displayCondition": {
474
- "type": "object",
475
- "properties": {
476
- "label": { "type": "string" },
477
- "before": { "type": "string" },
478
- "after": { "type": "string" },
479
- "group": { "type": "string" },
480
- "description": { "type": "string" }
481
- },
482
- "required": [
483
- "label",
484
- "before",
485
- "after"
486
- ],
487
- "additionalProperties": false
488
- },
489
- "lineStyle": {
490
- "type": "string",
491
- "enum": [
492
- "solid",
493
- "dashed",
494
- "dotted"
495
- ]
496
- },
497
- "color": { "type": "string" },
498
- "thickness": { "type": "number" },
499
- "width": {
500
- "anyOf": [
501
- { "type": "number" },
502
- {
503
- "type": "string",
504
- "const": "full"
505
- },
506
- { "$ref": "#/definitions/DividerPercentWidth" }
507
- ],
508
- "description": "`\"full\"` spans the column and a number is pixels. A percentage is a share of the column: MJML renders it natively, and it shrinks with the column on a phone where a pixel width would overflow."
509
- }
510
- },
511
- "required": [
512
- "color",
513
- "id",
514
- "lineStyle",
515
- "styles",
516
- "thickness",
517
- "type",
518
- "width"
519
- ],
520
- "additionalProperties": false
521
- },
522
- "DividerPercentWidth": {
523
- "type": "string",
524
- "description": "A share of the column, from `\"0%\"` to `\"100%\"`.",
525
- "pattern": "^(?:100(?:\\.0+)?|\\d{1,2}(?:\\.\\d+)?)%$"
526
- },
527
- "VideoBlock": {
528
- "type": "object",
529
- "properties": {
530
- "id": { "type": "string" },
531
- "type": {
532
- "type": "string",
533
- "const": "video"
534
- },
535
- "styles": { "$ref": "#/definitions/BlockStyles" },
536
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
537
- "displayCondition": {
538
- "type": "object",
539
- "properties": {
540
- "label": { "type": "string" },
541
- "before": { "type": "string" },
542
- "after": { "type": "string" },
543
- "group": { "type": "string" },
544
- "description": { "type": "string" }
545
- },
546
- "required": [
547
- "label",
548
- "before",
549
- "after"
550
- ],
551
- "additionalProperties": false
552
- },
553
- "url": { "type": "string" },
554
- "openInNewTab": { "type": "boolean" },
555
- "thumbnailUrl": { "type": "string" },
556
- "alt": { "type": "string" },
557
- "width": { "anyOf": [{ "type": "number" }, {
558
- "type": "string",
559
- "const": "full"
560
- }] },
561
- "height": {
562
- "type": "number",
563
- "description": "Height in pixels for the thumbnail. Absent means the height is derived from the width, so the thumbnail keeps its aspect ratio — setting both stretches it, since email clients don't support `object-fit`."
564
- },
565
- "align": {
566
- "type": "string",
567
- "enum": [
568
- "left",
569
- "center",
570
- "right"
571
- ]
572
- },
573
- "placeholderUrl": { "type": "string" }
574
- },
575
- "required": [
576
- "align",
577
- "alt",
578
- "id",
579
- "styles",
580
- "thumbnailUrl",
581
- "type",
582
- "url",
583
- "width"
584
- ],
585
- "additionalProperties": false
586
- },
587
- "SocialIconsBlock": {
588
- "type": "object",
589
- "properties": {
590
- "id": { "type": "string" },
591
- "type": {
592
- "type": "string",
593
- "const": "social"
594
- },
595
- "styles": { "$ref": "#/definitions/BlockStyles" },
596
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
597
- "displayCondition": {
598
- "type": "object",
599
- "properties": {
600
- "label": { "type": "string" },
601
- "before": { "type": "string" },
602
- "after": { "type": "string" },
603
- "group": { "type": "string" },
604
- "description": { "type": "string" }
605
- },
606
- "required": [
607
- "label",
608
- "before",
609
- "after"
610
- ],
611
- "additionalProperties": false
612
- },
613
- "icons": {
614
- "type": "array",
615
- "items": { "$ref": "#/definitions/SocialIcon" }
616
- },
617
- "iconStyle": { "$ref": "#/definitions/SocialIconStyle" },
618
- "iconTone": {
619
- "$ref": "#/definitions/SocialIconTone",
620
- "description": "Absent means `\"brand\"`."
621
- },
622
- "iconSize": { "$ref": "#/definitions/SocialIconSize" },
623
- "spacing": { "type": "number" },
624
- "align": {
625
- "type": "string",
626
- "enum": [
627
- "left",
628
- "center",
629
- "right"
630
- ]
631
- }
632
- },
633
- "required": [
634
- "align",
635
- "iconSize",
636
- "iconStyle",
637
- "icons",
638
- "id",
639
- "spacing",
640
- "styles",
641
- "type"
642
- ],
643
- "additionalProperties": false
644
- },
645
- "SocialIcon": {
646
- "type": "object",
647
- "properties": {
648
- "id": { "type": "string" },
649
- "platform": { "$ref": "#/definitions/SocialPlatform" },
650
- "url": { "type": "string" }
651
- },
652
- "required": [
653
- "id",
654
- "platform",
655
- "url"
656
- ],
657
- "additionalProperties": false
658
- },
659
- "SocialPlatform": {
660
- "type": "string",
661
- "enum": [
662
- "facebook",
663
- "twitter",
664
- "instagram",
665
- "linkedin",
666
- "youtube",
667
- "tiktok",
668
- "pinterest",
669
- "email",
670
- "whatsapp",
671
- "telegram",
672
- "discord",
673
- "snapchat",
674
- "reddit",
675
- "github",
676
- "dribbble",
677
- "behance",
678
- "website"
679
- ]
680
- },
681
- "SocialIconStyle": {
682
- "type": "string",
683
- "enum": [
684
- "solid",
685
- "outlined",
686
- "rounded",
687
- "square",
688
- "circle",
689
- "plain"
690
- ],
691
- "description": "The icon's shape. `plain` is the glyph alone, with no badge or outline."
692
- },
693
- "SocialIconTone": {
694
- "type": "string",
695
- "enum": [
696
- "brand",
697
- "dark",
698
- "light"
699
- ],
700
- "description": "The colors an icon is drawn in: each platform's own brand color, or one tone (dark or light) for every icon."
701
- },
702
- "SocialIconSize": {
703
- "type": "string",
704
- "enum": [
705
- "small",
706
- "medium",
707
- "large"
708
- ]
709
- },
710
- "SpacerBlock": {
711
- "type": "object",
712
- "properties": {
713
- "id": { "type": "string" },
714
- "type": {
715
- "type": "string",
716
- "const": "spacer"
717
- },
718
- "styles": { "$ref": "#/definitions/BlockStyles" },
719
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
720
- "displayCondition": {
721
- "type": "object",
722
- "properties": {
723
- "label": { "type": "string" },
724
- "before": { "type": "string" },
725
- "after": { "type": "string" },
726
- "group": { "type": "string" },
727
- "description": { "type": "string" }
728
- },
729
- "required": [
730
- "label",
731
- "before",
732
- "after"
733
- ],
734
- "additionalProperties": false
735
- },
736
- "height": { "type": "number" }
737
- },
738
- "required": [
739
- "height",
740
- "id",
741
- "styles",
742
- "type"
743
- ],
744
- "additionalProperties": false
745
- },
746
- "HtmlBlock": {
747
- "type": "object",
748
- "properties": {
749
- "id": { "type": "string" },
750
- "type": {
751
- "type": "string",
752
- "const": "html"
753
- },
754
- "styles": { "$ref": "#/definitions/BlockStyles" },
755
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
756
- "displayCondition": {
757
- "type": "object",
758
- "properties": {
759
- "label": { "type": "string" },
760
- "before": { "type": "string" },
761
- "after": { "type": "string" },
762
- "group": { "type": "string" },
763
- "description": { "type": "string" }
764
- },
765
- "required": [
766
- "label",
767
- "before",
768
- "after"
769
- ],
770
- "additionalProperties": false
771
- },
772
- "content": { "type": "string" }
773
- },
774
- "required": [
775
- "content",
776
- "id",
777
- "styles",
778
- "type"
779
- ],
780
- "additionalProperties": false
781
- },
782
- "MenuBlock": {
783
- "type": "object",
784
- "properties": {
785
- "id": { "type": "string" },
786
- "type": {
787
- "type": "string",
788
- "const": "menu"
789
- },
790
- "styles": { "$ref": "#/definitions/BlockStyles" },
791
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
792
- "displayCondition": {
793
- "type": "object",
794
- "properties": {
795
- "label": { "type": "string" },
796
- "before": { "type": "string" },
797
- "after": { "type": "string" },
798
- "group": { "type": "string" },
799
- "description": { "type": "string" }
800
- },
801
- "required": [
802
- "label",
803
- "before",
804
- "after"
805
- ],
806
- "additionalProperties": false
807
- },
808
- "items": {
809
- "type": "array",
810
- "items": { "$ref": "#/definitions/MenuItemData" }
811
- },
812
- "fontSize": { "type": "number" },
813
- "fontFamily": { "type": "string" },
814
- "color": {
815
- "type": "string",
816
- "description": "Base text/link color. Unset = inherit the document-level `textColor`."
817
- },
818
- "linkColor": { "type": "string" },
819
- "textAlign": {
820
- "type": "string",
821
- "enum": [
822
- "left",
823
- "center",
824
- "right"
825
- ]
826
- },
827
- "separator": { "type": "string" },
828
- "separatorColor": { "type": "string" },
829
- "spacing": { "type": "number" }
830
- },
831
- "required": [
832
- "fontSize",
833
- "id",
834
- "items",
835
- "separator",
836
- "separatorColor",
837
- "spacing",
838
- "styles",
839
- "textAlign",
840
- "type"
841
- ],
842
- "additionalProperties": false
843
- },
844
- "MenuItemData": {
845
- "type": "object",
846
- "properties": {
847
- "id": { "type": "string" },
848
- "text": { "type": "string" },
849
- "url": { "type": "string" },
850
- "openInNewTab": { "type": "boolean" },
851
- "bold": { "type": "boolean" },
852
- "underline": { "type": "boolean" },
853
- "color": { "type": "string" }
854
- },
855
- "required": [
856
- "id",
857
- "text",
858
- "url",
859
- "openInNewTab",
860
- "bold",
861
- "underline"
862
- ],
863
- "additionalProperties": false
864
- },
865
- "TableBlock": {
866
- "type": "object",
867
- "properties": {
868
- "id": { "type": "string" },
869
- "type": {
870
- "type": "string",
871
- "const": "table"
872
- },
873
- "styles": { "$ref": "#/definitions/BlockStyles" },
874
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
875
- "displayCondition": {
876
- "type": "object",
877
- "properties": {
878
- "label": { "type": "string" },
879
- "before": { "type": "string" },
880
- "after": { "type": "string" },
881
- "group": { "type": "string" },
882
- "description": { "type": "string" }
883
- },
884
- "required": [
885
- "label",
886
- "before",
887
- "after"
888
- ],
889
- "additionalProperties": false
890
- },
891
- "rows": {
892
- "type": "array",
893
- "items": { "$ref": "#/definitions/TableRowData" }
894
- },
895
- "hasHeaderRow": { "type": "boolean" },
896
- "headerBackgroundColor": { "type": "string" },
897
- "borderColor": { "type": "string" },
898
- "borderWidth": { "type": "number" },
899
- "cellPadding": { "type": "number" },
900
- "fontSize": { "type": "number" },
901
- "fontFamily": { "type": "string" },
902
- "color": {
903
- "type": "string",
904
- "description": "Text color. Unset = inherit the document-level `textColor`."
905
- },
906
- "textAlign": {
907
- "type": "string",
908
- "enum": [
909
- "left",
910
- "center",
911
- "right"
912
- ]
913
- }
914
- },
915
- "required": [
916
- "borderColor",
917
- "borderWidth",
918
- "cellPadding",
919
- "fontSize",
920
- "hasHeaderRow",
921
- "id",
922
- "rows",
923
- "styles",
924
- "textAlign",
925
- "type"
926
- ],
927
- "additionalProperties": false
928
- },
929
- "TableRowData": {
930
- "type": "object",
931
- "properties": {
932
- "id": { "type": "string" },
933
- "cells": {
934
- "type": "array",
935
- "items": { "$ref": "#/definitions/TableCellData" }
936
- }
937
- },
938
- "required": ["id", "cells"],
939
- "additionalProperties": false
940
- },
941
- "TableCellData": {
942
- "type": "object",
943
- "properties": {
944
- "id": { "type": "string" },
945
- "content": { "type": "string" }
946
- },
947
- "required": ["id", "content"],
948
- "additionalProperties": false
949
- },
950
- "CountdownBlock": {
951
- "type": "object",
952
- "properties": {
953
- "id": { "type": "string" },
954
- "type": {
955
- "type": "string",
956
- "const": "countdown"
957
- },
958
- "styles": { "$ref": "#/definitions/BlockStyles" },
959
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
960
- "displayCondition": {
961
- "type": "object",
962
- "properties": {
963
- "label": { "type": "string" },
964
- "before": { "type": "string" },
965
- "after": { "type": "string" },
966
- "group": { "type": "string" },
967
- "description": { "type": "string" }
968
- },
969
- "required": [
970
- "label",
971
- "before",
972
- "after"
973
- ],
974
- "additionalProperties": false
975
- },
976
- "targetDate": { "type": "string" },
977
- "timezone": { "type": "string" },
978
- "showDays": { "type": "boolean" },
979
- "showHours": { "type": "boolean" },
980
- "showMinutes": { "type": "boolean" },
981
- "showSeconds": { "type": "boolean" },
982
- "separator": {
983
- "type": "string",
984
- "enum": [
985
- ":",
986
- "-",
987
- " "
988
- ]
989
- },
990
- "digitFontSize": { "type": "number" },
991
- "digitColor": { "type": "string" },
992
- "labelColor": { "type": "string" },
993
- "labelFontSize": { "type": "number" },
994
- "backgroundColor": { "type": "string" },
995
- "fontFamily": { "type": "string" },
996
- "labelDays": { "type": "string" },
997
- "labelHours": { "type": "string" },
998
- "labelMinutes": { "type": "string" },
999
- "labelSeconds": { "type": "string" },
1000
- "expiredMessage": { "type": "string" },
1001
- "expiredImageUrl": { "type": "string" },
1002
- "hideOnExpiry": { "type": "boolean" }
1003
- },
1004
- "required": [
1005
- "backgroundColor",
1006
- "digitColor",
1007
- "digitFontSize",
1008
- "expiredImageUrl",
1009
- "expiredMessage",
1010
- "hideOnExpiry",
1011
- "id",
1012
- "labelColor",
1013
- "labelDays",
1014
- "labelFontSize",
1015
- "labelHours",
1016
- "labelMinutes",
1017
- "labelSeconds",
1018
- "separator",
1019
- "showDays",
1020
- "showHours",
1021
- "showMinutes",
1022
- "showSeconds",
1023
- "styles",
1024
- "targetDate",
1025
- "timezone",
1026
- "type"
1027
- ],
1028
- "additionalProperties": false
1029
- },
1030
- "CustomBlock": {
1031
- "type": "object",
1032
- "properties": {
1033
- "id": { "type": "string" },
1034
- "type": {
1035
- "type": "string",
1036
- "const": "custom"
1037
- },
1038
- "styles": { "$ref": "#/definitions/BlockStyles" },
1039
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
1040
- "displayCondition": {
1041
- "type": "object",
1042
- "properties": {
1043
- "label": { "type": "string" },
1044
- "before": { "type": "string" },
1045
- "after": { "type": "string" },
1046
- "group": { "type": "string" },
1047
- "description": { "type": "string" }
1048
- },
1049
- "required": [
1050
- "label",
1051
- "before",
1052
- "after"
1053
- ],
1054
- "additionalProperties": false
1055
- },
1056
- "customType": { "type": "string" },
1057
- "fieldValues": {
1058
- "type": "object",
1059
- "additionalProperties": {}
1060
- },
1061
- "renderedHtml": { "type": "string" },
1062
- "dataSourceFetched": { "type": "boolean" }
1063
- },
1064
- "required": [
1065
- "customType",
1066
- "fieldValues",
1067
- "id",
1068
- "styles",
1069
- "type"
1070
- ],
1071
- "additionalProperties": false
1072
- },
1073
- "SlotBlock": {
1074
- "type": "object",
1075
- "properties": {
1076
- "id": { "type": "string" },
1077
- "type": {
1078
- "type": "string",
1079
- "const": "slot"
1080
- },
1081
- "styles": { "$ref": "#/definitions/BlockStyles" },
1082
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
1083
- "displayCondition": {
1084
- "type": "object",
1085
- "properties": {
1086
- "label": { "type": "string" },
1087
- "before": { "type": "string" },
1088
- "after": { "type": "string" },
1089
- "group": { "type": "string" },
1090
- "description": { "type": "string" }
1091
- },
1092
- "required": [
1093
- "label",
1094
- "before",
1095
- "after"
1096
- ],
1097
- "additionalProperties": false
1098
- }
1099
- },
1100
- "required": [
1101
- "id",
1102
- "styles",
1103
- "type"
1104
- ],
1105
- "additionalProperties": false,
1106
- "description": "Layout-only hole where authored content is spliced in. Extends BaseBlock so Block walkers stay typed (`styles` unused; spliced out before render)."
1107
- },
1108
- "WrapperBlock": {
1109
- "type": "object",
1110
- "properties": {
1111
- "id": { "type": "string" },
1112
- "type": {
1113
- "type": "string",
1114
- "const": "wrapper"
1115
- },
1116
- "styles": { "$ref": "#/definitions/BlockStyles" },
1117
- "visibility": { "$ref": "#/definitions/BlockVisibility" },
1118
- "displayCondition": {
1119
- "type": "object",
1120
- "properties": {
1121
- "label": { "type": "string" },
1122
- "before": { "type": "string" },
1123
- "after": { "type": "string" },
1124
- "group": { "type": "string" },
1125
- "description": { "type": "string" }
1126
- },
1127
- "required": [
1128
- "label",
1129
- "before",
1130
- "after"
1131
- ],
1132
- "additionalProperties": false
1133
- },
1134
- "children": {
1135
- "type": "array",
1136
- "items": { "$ref": "#/definitions/Block" }
1137
- },
1138
- "borderRadius": { "type": "number" }
1139
- },
1140
- "required": [
1141
- "children",
1142
- "id",
1143
- "styles",
1144
- "type"
1145
- ],
1146
- "additionalProperties": false,
1147
- "description": "Layout-only band rendered as `mj-wrapper`. The block is the band: `styles.backgroundColor` / `styles.padding` / `borderRadius` map to the wrapper. Legal in layout only this ship."
1148
- },
1149
- "TemplateSettings": {
1150
- "type": "object",
1151
- "properties": {
1152
- "width": { "type": "number" },
1153
- "backgroundColor": { "type": "string" },
1154
- "textColor": {
1155
- "type": "string",
1156
- "description": "Document-level default text color: the `<mj-text>` default in the rendered MJML, inherited by every text block (Title, Paragraph, Menu, Table) that doesn't set its own `color`. Required, defaulting to `#1a1a1a` (see `DEFAULT_TEMPLATE_DEFAULTS`); customize the default per-consumer via `templateDefaults`. A block's explicit `color` or an inline text-color mark overrides it."
1157
- },
1158
- "linkColor": {
1159
- "type": "string",
1160
- "description": "Document-level link color: emitted as the global `a { color }` rule in the rendered MJML, so it cascades to every link — rich-text and menu alike. Optional: when unset, links inherit the surrounding text color (`color: inherit`), preserving the pre-#352 default. A per-block or per-item color (a Menu item's `color`, `MenuBlock.linkColor`) still overrides it."
1161
- },
1162
- "linkUnderline": {
1163
- "type": "boolean",
1164
- "description": "Whether links are underlined document-wide: drives the global `a { text-decoration }` rule (`underline` when true, `none` when false). Required, defaulting to `true` (see `DEFAULT_TEMPLATE_DEFAULTS`) — the common, more accessible email default. Applies to body (rich-text) links; buttons and menu items carry their own inline `text-decoration` and are unaffected. Set `false` to render links without an underline."
1165
- },
1166
- "fontFamily": { "type": "string" },
1167
- "preheaderText": { "type": "string" },
1168
- "locale": {
1169
- "type": "string",
1170
- "description": "BCP-47 language code for the rendered email's `<html lang>`. Drives screen-reader pronunciation. Default `'en'` via `DEFAULT_TEMPLATE_DEFAULTS`."
1171
- },
1172
- "direction": {
1173
- "$ref": "#/definitions/ContentDirection",
1174
- "description": "Writing direction of the delivered email: the canvas `dir` and the rendered `<mjml dir>`. Independent of the editor chrome (`init({ locale })`). Optional: when unset, `resolveContentDirection` falls through to the content language (`ar` / `he` / `fa` / … → `\"rtl\"`, otherwise `\"ltr\"`). An explicit `\"ltr\"` on an Arabic template is a stated decision, not a missing value."
1175
- }
1176
- },
1177
- "required": [
1178
- "width",
1179
- "backgroundColor",
1180
- "textColor",
1181
- "linkUnderline",
1182
- "fontFamily",
1183
- "locale"
1184
- ],
1185
- "additionalProperties": false
1186
- },
1187
- "ContentDirection": {
1188
- "type": "string",
1189
- "enum": ["ltr", "rtl"],
1190
- "description": "Writing direction of the delivered email — the canvas `dir` and the rendered `<mjml dir>`. Independent of the editor chrome language (`init({ locale })`)."
1191
- }
1192
- }
1193
- };
1194
- const ajv = new Ajv({
1195
- allErrors: true,
1196
- strict: false
1197
- });
1198
- const typeToDef = {};
1199
- for (const [name, def] of Object.entries(schema.definitions)) {
1200
- const constType = def?.properties?.type?.const;
1201
- if (typeof constType === "string") typeToDef[constType] = name;
1202
- }
1203
- const validatorCache = /* @__PURE__ */ new Map();
1204
- function validatorFor(defName) {
1205
- const cached = validatorCache.get(defName);
1206
- if (cached) return cached;
1207
- const definitions = structuredClone(schema.definitions);
1208
- if (defName === "SectionBlock") {
1209
- const section = definitions.SectionBlock;
1210
- if (section?.properties) section.properties.children = { type: "array" };
1211
- }
1212
- const compiled = ajv.compile({
1213
- $ref: `#/definitions/${defName}`,
1214
- definitions
1215
- });
1216
- validatorCache.set(defName, compiled);
1217
- return compiled;
1218
- }
1219
- const settingsValidator = ajv.compile({
1220
- $ref: "#/definitions/TemplateSettings",
1221
- definitions: structuredClone(schema.definitions)
1222
- });
1223
- function flattenBlocks(blocks, basePath, out) {
1224
- if (!Array.isArray(blocks)) return;
1225
- blocks.forEach((block, i) => {
1226
- const path = `${basePath}[${i}]`;
1227
- out.push({
1228
- path,
1229
- block: block ?? null
1230
- });
1231
- if (block?.type === "section" && Array.isArray(block.children)) block.children.forEach((column, ci) => {
1232
- flattenBlocks(column, `${path}.children[${ci}]`, out);
1233
- });
1234
- });
1235
- }
1236
- function formatAjvErrors(errors, prefix) {
1237
- return (errors ?? []).map((e) => {
1238
- const where = `${prefix}${e.instancePath}`;
1239
- const extra = e.keyword === "additionalProperties" ? ` (${e.params.additionalProperty})` : "";
1240
- return `${where} ${e.message}${extra}`;
1241
- });
1242
- }
1243
- /**
1244
- * Structural validation. Synchronous, depends only on ajv + the committed
1245
- * schema.json (no build of the workspace packages required).
1246
- */
1247
- function validateTemplate(data) {
1248
- const errors = [];
1249
- if (data === null || typeof data !== "object" || Array.isArray(data)) return {
1250
- valid: false,
1251
- errors: ["(root) must be an object"]
1252
- };
1253
- const doc = data;
1254
- if (!Array.isArray(doc.blocks)) errors.push("blocks must be an array");
1255
- if (doc.settings === null || typeof doc.settings !== "object") errors.push("settings must be an object");
1256
- else if (!settingsValidator(doc.settings)) errors.push(...formatAjvErrors(settingsValidator.errors, "settings"));
1257
- const flat = [];
1258
- flattenBlocks(doc.blocks, "blocks", flat);
1259
- for (const { path, block } of flat) {
1260
- const type = block?.type;
1261
- const defName = typeof type === "string" ? typeToDef[type] : void 0;
1262
- if (!defName) {
1263
- const known = Object.keys(typeToDef).join(", ");
1264
- errors.push(`${path} has unknown or missing block type ${JSON.stringify(type)} (expected one of: ${known})`);
1265
- continue;
1266
- }
1267
- const validate = validatorFor(defName);
1268
- if (!validate(block)) errors.push(...formatAjvErrors(validate.errors, `${path} (${type})`));
1269
- }
1270
- return {
1271
- valid: errors.length === 0,
1272
- errors
1273
- };
1274
- }
1275
- /**
1276
- * Quality layer — accessibility / structure / link linting.
1277
- *
1278
- * Assumes structurally-valid input, so callers should run `validateTemplate`
1279
- * first; the try/catch is a guard against a malformed template crashing the
1280
- * linter rather than a substitute for that ordering.
1281
- */
1282
- function runQualityLint(data) {
1283
- try {
1284
- return { issues: lintTemplate(data) ?? [] };
1285
- } catch (err) {
1286
- return {
1287
- issues: [],
1288
- error: err.message
1289
- };
1290
- }
1291
- }
1292
- //#endregion
1293
- //#region src/operations.ts
1294
- /** Columns a layout declares. Mirrors the helper in core's editor. */
1295
- function getColumnCount(layout) {
1296
- if (layout === "1") return 1;
1297
- if (layout === "3") return 3;
1298
- return 2;
1299
- }
1300
- function findBlockById(blocks, id) {
1301
- for (const block of blocks) {
1302
- if (block.id === id) return block;
1303
- if (block.type === "section") for (const column of block.children) {
1304
- const found = findBlockById(column, id);
1305
- if (found) return found;
1306
- }
1307
- }
1308
- return null;
1309
- }
1310
- function findBlockParent(blocks, id, parent = { blocks }) {
1311
- for (const block of blocks) {
1312
- if (block.id === id) return parent;
1313
- if (block.type === "section") for (let colIdx = 0; colIdx < block.children.length; colIdx++) {
1314
- const result = findBlockParent(block.children[colIdx], id, {
1315
- blocks: block.children[colIdx],
1316
- sectionId: block.id,
1317
- columnIndex: colIdx
1318
- });
1319
- if (result) return result;
1320
- }
1321
- }
1322
- return null;
1323
- }
1324
- function reject(content, error) {
1325
- return {
1326
- ok: false,
1327
- content,
1328
- error
1329
- };
1330
- }
1331
- /**
1332
- * Resolve the array an operation targets, enforcing the column rules.
1333
- * Returns a string on rejection so callers can surface the reason.
1334
- */
1335
- function resolveTarget(content, targetSectionId, columnIndex) {
1336
- if (!targetSectionId) return content.blocks;
1337
- const section = findBlockById(content.blocks, targetSectionId);
1338
- if (!section) return `No block with id "${targetSectionId}".`;
1339
- if (section.type !== "section") return `Block "${targetSectionId}" is a ${section.type}, not a section — it has no columns.`;
1340
- const count = getColumnCount(section.columns);
1341
- if (columnIndex < 0 || columnIndex >= count) return `Column ${columnIndex} is out of range for a "${section.columns}" section (${count} column(s)).`;
1342
- const children = section;
1343
- children.children[columnIndex] = children.children[columnIndex] || [];
1344
- return children.children[columnIndex];
1345
- }
1346
- function insertAt(target, block, index) {
1347
- if (index !== void 0 && index < target.length) target.splice(index, 0, block);
1348
- else target.push(block);
1349
- }
1350
- /** Apply one operation, returning a new document. Never mutates the input. */
1351
- function applyOperation(content, payload) {
1352
- const data = payload.data ?? {};
1353
- switch (payload.operation) {
1354
- case "setContent": {
1355
- const next = data.content;
1356
- if (!next || typeof next !== "object" || !Array.isArray(next.blocks)) return reject(content, "setContent needs `content` with a blocks array.");
1357
- return {
1358
- ok: true,
1359
- content: safeClone(next)
1360
- };
1361
- }
1362
- case "updateSettings": {
1363
- const updates = data.settings;
1364
- if (!updates || typeof updates !== "object") return reject(content, "updateSettings needs a `settings` object.");
1365
- const draft = safeClone(content);
1366
- draft.settings = {
1367
- ...draft.settings,
1368
- ...updates
1369
- };
1370
- return {
1371
- ok: true,
1372
- content: draft
1373
- };
1374
- }
1375
- case "addBlock": {
1376
- const block = data.block;
1377
- if (!block || typeof block !== "object" || typeof block.id !== "string") return reject(content, "addBlock needs a `block` with an id.");
1378
- const targetSectionId = data.targetSectionId;
1379
- if (targetSectionId && block.type === "section") return reject(content, "A section cannot be nested inside a section column — MJML would drop it on export. Add it at the top level instead.");
1380
- const draft = safeClone(content);
1381
- if (findBlockById(draft.blocks, block.id)) return reject(content, `A block with id "${block.id}" already exists.`);
1382
- const target = resolveTarget(draft, targetSectionId, data.columnIndex ?? 0);
1383
- if (typeof target === "string") return reject(content, target);
1384
- insertAt(target, safeClone(block), data.index);
1385
- return {
1386
- ok: true,
1387
- content: draft
1388
- };
1389
- }
1390
- case "updateBlock": {
1391
- const blockId = data.blockId;
1392
- const updates = data.updates;
1393
- if (!blockId) return reject(content, "updateBlock needs a `blockId`.");
1394
- if (!updates || typeof updates !== "object") return reject(content, "updateBlock needs an `updates` object.");
1395
- const draft = safeClone(content);
1396
- const block = findBlockById(draft.blocks, blockId);
1397
- if (!block) return reject(content, `No block with id "${blockId}".`);
1398
- if ("type" in updates && updates.type !== block.type) return reject(content, `Cannot change a block's type (${block.type} → ${String(updates.type)}). Delete it and add the new block instead.`);
1399
- Object.assign(block, updates);
1400
- return {
1401
- ok: true,
1402
- content: draft
1403
- };
1404
- }
1405
- case "updateBlockStyle": {
1406
- const blockId = data.blockId;
1407
- const styles = data.styles;
1408
- if (!blockId) return reject(content, "updateBlockStyle needs a `blockId`.");
1409
- if (!styles || typeof styles !== "object") return reject(content, "updateBlockStyle needs a `styles` object.");
1410
- const draft = safeClone(content);
1411
- const block = findBlockById(draft.blocks, blockId);
1412
- if (!block) return reject(content, `No block with id "${blockId}".`);
1413
- block.styles = {
1414
- ...block.styles,
1415
- ...styles
1416
- };
1417
- return {
1418
- ok: true,
1419
- content: draft
1420
- };
1421
- }
1422
- case "deleteBlock": {
1423
- const blockId = data.blockId;
1424
- if (!blockId) return reject(content, "deleteBlock needs a `blockId`.");
1425
- const draft = safeClone(content);
1426
- const parent = findBlockParent(draft.blocks, blockId);
1427
- if (!parent) return reject(content, `No block with id "${blockId}".`);
1428
- const index = parent.blocks.findIndex((b) => b.id === blockId);
1429
- parent.blocks.splice(index, 1);
1430
- return {
1431
- ok: true,
1432
- content: draft
1433
- };
1434
- }
1435
- case "moveBlock": {
1436
- const blockId = data.blockId;
1437
- const index = data.index;
1438
- if (!blockId) return reject(content, "moveBlock needs a `blockId`.");
1439
- if (typeof index !== "number" || index < 0) return reject(content, "moveBlock needs a non-negative `index`.");
1440
- const targetSectionId = data.targetSectionId;
1441
- const draft = safeClone(content);
1442
- const parent = findBlockParent(draft.blocks, blockId);
1443
- if (!parent) return reject(content, `No block with id "${blockId}".`);
1444
- const oldIndex = parent.blocks.findIndex((b) => b.id === blockId);
1445
- if (targetSectionId && parent.blocks[oldIndex].type === "section") return reject(content, "A section cannot be moved into a section column — MJML would drop it on export.");
1446
- if (targetSectionId === blockId) return reject(content, "A block cannot be moved into itself.");
1447
- const target = resolveTarget(draft, targetSectionId, data.columnIndex ?? 0);
1448
- if (typeof target === "string") return reject(content, target);
1449
- const [block] = parent.blocks.splice(oldIndex, 1);
1450
- target.splice(index, 0, block);
1451
- return {
1452
- ok: true,
1453
- content: draft
1454
- };
1455
- }
1456
- default: return reject(content, `Unknown operation "${String(payload.operation)}".`);
1457
- }
1458
- }
1459
- //#endregion
1460
- export { validateTemplate as a, schema as i, getColumnCount as n, runQualityLint as r, applyOperation as t };
1461
-
1462
- //# sourceMappingURL=src-De5svMDM.js.map