@broadpaper/core 0.1.1

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/dist/index.js ADDED
@@ -0,0 +1,4343 @@
1
+ // src/types/template.ts
2
+ var CURRENT_SCHEMA_VERSION = 1;
3
+
4
+ // src/richtext/model.ts
5
+ function createParagraph(text = "", marks) {
6
+ return { type: "paragraph", content: text ? [{ type: "text", text, ...marks ? { marks } : {} }] : [] };
7
+ }
8
+ function createRichText(text = "") {
9
+ return { type: "doc", content: [createParagraph(text)] };
10
+ }
11
+ function richTextFromTemplate(template) {
12
+ const paragraphs = template.split(/\n/);
13
+ return {
14
+ type: "doc",
15
+ content: paragraphs.map((p) => ({ type: "paragraph", content: inlineFromTemplate(p) }))
16
+ };
17
+ }
18
+ function inlineFromTemplate(text) {
19
+ const out = [];
20
+ const re = /\{\{\s*([^}]+?)\s*\}\}/g;
21
+ let last = 0;
22
+ let m;
23
+ while (m = re.exec(text)) {
24
+ if (m.index > last) out.push({ type: "text", text: text.slice(last, m.index) });
25
+ out.push({ type: "token", expr: m[1] });
26
+ last = m.index + m[0].length;
27
+ }
28
+ if (last < text.length) out.push({ type: "text", text: text.slice(last) });
29
+ return out;
30
+ }
31
+ function richTextToPlain(doc, tokenText = (e) => `{{${e}}}`) {
32
+ if (!doc) return "";
33
+ const lines = [];
34
+ for (const block of doc.content) {
35
+ if (block.type === "paragraph") lines.push(inlineToPlain(block.content, tokenText));
36
+ else for (const item of block.content) for (const p of item.content) lines.push(inlineToPlain(p.content, tokenText));
37
+ }
38
+ return lines.join("\n");
39
+ }
40
+ function inlineToPlain(content, tokenText) {
41
+ return content.map((n) => n.type === "text" ? n.text : n.type === "token" ? tokenText(n.expr) : "\n").join("");
42
+ }
43
+ function richTextTokens(doc) {
44
+ const out = [];
45
+ if (!doc) return out;
46
+ const visitInline = (content) => {
47
+ for (const n of content) {
48
+ if (n.type === "token") out.push(n.expr);
49
+ if (n.type === "text" && n.marks?.link) {
50
+ for (const e of extractTemplateTokens(n.marks.link)) out.push(e);
51
+ }
52
+ }
53
+ };
54
+ for (const block of doc.content) {
55
+ if (block.type === "paragraph") visitInline(block.content);
56
+ else for (const item of block.content) for (const p of item.content) visitInline(p.content);
57
+ }
58
+ return out;
59
+ }
60
+ function extractTemplateTokens(text) {
61
+ if (!text) return [];
62
+ const out = [];
63
+ const re = /\{\{\s*([^}]+?)\s*\}\}/g;
64
+ let m;
65
+ while (m = re.exec(text)) out.push(m[1]);
66
+ return out;
67
+ }
68
+ function isRichTextDoc(value) {
69
+ return !!value && typeof value === "object" && value.type === "doc" && Array.isArray(value.content);
70
+ }
71
+
72
+ // src/expr/ast.ts
73
+ function pathToString(segments) {
74
+ let s = "";
75
+ for (const seg of segments) {
76
+ if (seg.kind === "prop") s += s ? `.${seg.name}` : seg.name;
77
+ else s += `[${seg.index}]`;
78
+ }
79
+ return s;
80
+ }
81
+ function exprToString(e) {
82
+ switch (e.kind) {
83
+ case "literal":
84
+ return typeof e.value === "string" ? JSON.stringify(e.value) : String(e.value);
85
+ case "path":
86
+ return pathToString(e.segments);
87
+ case "unary":
88
+ return e.op === "not" ? `not ${wrap(e.arg)}` : `-${wrap(e.arg)}`;
89
+ case "binary":
90
+ return `${wrap(e.left)} ${e.op} ${wrap(e.right)}`;
91
+ case "conditional":
92
+ return `${wrap(e.test)} ? ${wrap(e.then)} : ${wrap(e.else)}`;
93
+ case "call":
94
+ return `${e.name}(${e.args.map(exprToString).join(", ")})`;
95
+ case "pipe":
96
+ return `${exprToString(e.input)} | ${e.name}${e.args.length ? ":" + e.args.map(exprToString).join(":") : ""}`;
97
+ case "array":
98
+ return `[${e.items.map(exprToString).join(", ")}]`;
99
+ }
100
+ }
101
+ function wrap(e) {
102
+ const s = exprToString(e);
103
+ return e.kind === "binary" || e.kind === "conditional" || e.kind === "pipe" ? `(${s})` : s;
104
+ }
105
+ function collectPaths(e, out = []) {
106
+ switch (e.kind) {
107
+ case "path":
108
+ out.push(e.segments);
109
+ break;
110
+ case "unary":
111
+ collectPaths(e.arg, out);
112
+ break;
113
+ case "binary":
114
+ collectPaths(e.left, out);
115
+ collectPaths(e.right, out);
116
+ break;
117
+ case "conditional":
118
+ collectPaths(e.test, out);
119
+ collectPaths(e.then, out);
120
+ collectPaths(e.else, out);
121
+ break;
122
+ case "call":
123
+ case "array":
124
+ for (const a of e.kind === "call" ? e.args : e.items) collectPaths(a, out);
125
+ break;
126
+ case "pipe":
127
+ collectPaths(e.input, out);
128
+ for (const a of e.args) collectPaths(a, out);
129
+ break;
130
+ }
131
+ return out;
132
+ }
133
+
134
+ // src/expr/parser.ts
135
+ var ExpressionError = class extends Error {
136
+ constructor(message, position, source) {
137
+ super(message);
138
+ this.position = position;
139
+ this.source = source;
140
+ this.name = "ExpressionError";
141
+ }
142
+ position;
143
+ source;
144
+ };
145
+ var MAX_LENGTH = 2e3;
146
+ var MAX_DEPTH = 32;
147
+ var KEYWORDS = /* @__PURE__ */ new Set(["and", "or", "not", "in", "true", "false", "null"]);
148
+ var MULTI_OPS = ["==", "!=", "<=", ">="];
149
+ var SINGLE_OPS = new Set("+-*/%<>()[]|:?,.".split(""));
150
+ function tokenize(src) {
151
+ if (src.length > MAX_LENGTH) throw new ExpressionError(`Expression is too long (max ${MAX_LENGTH} characters)`, 0, src);
152
+ const tokens = [];
153
+ let i = 0;
154
+ while (i < src.length) {
155
+ const ch = src[i];
156
+ if (/\s/.test(ch)) {
157
+ i++;
158
+ continue;
159
+ }
160
+ const pos = i;
161
+ if (/[0-9]/.test(ch) || ch === "." && /[0-9]/.test(src[i + 1] ?? "")) {
162
+ let j = i;
163
+ while (j < src.length && /[0-9_]/.test(src[j])) j++;
164
+ if (src[j] === "." && /[0-9]/.test(src[j + 1] ?? "")) {
165
+ j++;
166
+ while (j < src.length && /[0-9]/.test(src[j])) j++;
167
+ }
168
+ if ((src[j] === "e" || src[j] === "E") && /[0-9+-]/.test(src[j + 1] ?? "")) {
169
+ j += 2;
170
+ while (j < src.length && /[0-9]/.test(src[j])) j++;
171
+ }
172
+ tokens.push({ type: "num", value: Number(src.slice(i, j).replace(/_/g, "")), pos });
173
+ i = j;
174
+ continue;
175
+ }
176
+ if (ch === '"' || ch === "'") {
177
+ let j = i + 1;
178
+ let out = "";
179
+ while (j < src.length && src[j] !== ch) {
180
+ if (src[j] === "\\" && j + 1 < src.length) {
181
+ const n = src[j + 1];
182
+ out += n === "n" ? "\n" : n === "t" ? " " : n;
183
+ j += 2;
184
+ } else {
185
+ out += src[j];
186
+ j++;
187
+ }
188
+ }
189
+ if (j >= src.length) throw new ExpressionError("Unterminated string", pos, src);
190
+ tokens.push({ type: "str", value: out, pos });
191
+ i = j + 1;
192
+ continue;
193
+ }
194
+ if (/[A-Za-z_$]/.test(ch)) {
195
+ let j = i;
196
+ while (j < src.length && /[A-Za-z0-9_$]/.test(src[j])) j++;
197
+ tokens.push({ type: "ident", value: src.slice(i, j), pos });
198
+ i = j;
199
+ continue;
200
+ }
201
+ const two = src.slice(i, i + 2);
202
+ if (MULTI_OPS.includes(two)) {
203
+ tokens.push({ type: "op", value: two, pos });
204
+ i += 2;
205
+ continue;
206
+ }
207
+ if (ch === "=" && src[i + 1] !== "=") {
208
+ tokens.push({ type: "op", value: "==", pos });
209
+ i++;
210
+ continue;
211
+ }
212
+ if (ch === "&" && src[i + 1] === "&") {
213
+ tokens.push({ type: "ident", value: "and", pos });
214
+ i += 2;
215
+ continue;
216
+ }
217
+ if (ch === "|" && src[i + 1] === "|") {
218
+ tokens.push({ type: "ident", value: "or", pos });
219
+ i += 2;
220
+ continue;
221
+ }
222
+ if (ch === "!") {
223
+ tokens.push({ type: "ident", value: "not", pos });
224
+ i++;
225
+ continue;
226
+ }
227
+ if (SINGLE_OPS.has(ch)) {
228
+ tokens.push({ type: "op", value: ch, pos });
229
+ i++;
230
+ continue;
231
+ }
232
+ throw new ExpressionError(`Unexpected character "${ch}"`, pos, src);
233
+ }
234
+ tokens.push({ type: "eof", pos: src.length });
235
+ return tokens;
236
+ }
237
+ var Parser = class {
238
+ constructor(tokens, src) {
239
+ this.tokens = tokens;
240
+ this.src = src;
241
+ }
242
+ tokens;
243
+ src;
244
+ i = 0;
245
+ depth = 0;
246
+ parse() {
247
+ const e = this.parsePipe();
248
+ if (this.peek().type !== "eof") this.fail(`Unexpected "${this.describe(this.peek())}"`);
249
+ return e;
250
+ }
251
+ peek() {
252
+ return this.tokens[this.i];
253
+ }
254
+ next() {
255
+ return this.tokens[this.i++];
256
+ }
257
+ isOp(v) {
258
+ const t = this.peek();
259
+ return t.type === "op" && t.value === v;
260
+ }
261
+ isKw(v) {
262
+ const t = this.peek();
263
+ return t.type === "ident" && t.value === v;
264
+ }
265
+ expectOp(v) {
266
+ if (!this.isOp(v)) this.fail(`Expected "${v}" but found "${this.describe(this.peek())}"`);
267
+ this.next();
268
+ }
269
+ describe(t) {
270
+ return t.type === "eof" ? "end of expression" : t.type === "str" ? `"${t.value}"` : String(t.value);
271
+ }
272
+ fail(msg) {
273
+ throw new ExpressionError(msg, this.peek().pos, this.src);
274
+ }
275
+ enter() {
276
+ if (++this.depth > MAX_DEPTH) this.fail("Expression is nested too deeply");
277
+ }
278
+ leave() {
279
+ this.depth--;
280
+ }
281
+ parsePipe() {
282
+ let e = this.parseConditional();
283
+ while (this.isOp("|")) {
284
+ this.next();
285
+ const nameTok = this.next();
286
+ if (nameTok.type !== "ident") this.fail("Expected a formatter name after |");
287
+ const args = [];
288
+ while (this.isOp(":")) {
289
+ this.next();
290
+ args.push(this.parseConditional());
291
+ }
292
+ e = { kind: "pipe", input: e, name: nameTok.value, args };
293
+ }
294
+ return e;
295
+ }
296
+ parseConditional() {
297
+ const test = this.parseOr();
298
+ if (this.isOp("?")) {
299
+ this.enter();
300
+ this.next();
301
+ const then = this.parseConditional();
302
+ this.expectOp(":");
303
+ const els = this.parseConditional();
304
+ this.leave();
305
+ return { kind: "conditional", test, then, else: els };
306
+ }
307
+ return test;
308
+ }
309
+ parseOr() {
310
+ let left = this.parseAnd();
311
+ while (this.isKw("or")) {
312
+ this.next();
313
+ left = { kind: "binary", op: "or", left, right: this.parseAnd() };
314
+ }
315
+ return left;
316
+ }
317
+ parseAnd() {
318
+ let left = this.parseNot();
319
+ while (this.isKw("and")) {
320
+ this.next();
321
+ left = { kind: "binary", op: "and", left, right: this.parseNot() };
322
+ }
323
+ return left;
324
+ }
325
+ parseNot() {
326
+ if (this.isKw("not")) {
327
+ this.enter();
328
+ this.next();
329
+ const arg2 = this.parseNot();
330
+ this.leave();
331
+ return { kind: "unary", op: "not", arg: arg2 };
332
+ }
333
+ return this.parseEquality();
334
+ }
335
+ parseEquality() {
336
+ let left = this.parseComparison();
337
+ for (; ; ) {
338
+ if (this.isOp("==") || this.isOp("!=")) {
339
+ const op = this.next();
340
+ left = { kind: "binary", op: op.value, left, right: this.parseComparison() };
341
+ } else if (this.isKw("in")) {
342
+ this.next();
343
+ left = { kind: "binary", op: "in", left, right: this.parseComparison() };
344
+ } else return left;
345
+ }
346
+ }
347
+ parseComparison() {
348
+ let left = this.parseAdditive();
349
+ while (this.isOp("<") || this.isOp("<=") || this.isOp(">") || this.isOp(">=")) {
350
+ const op = this.next();
351
+ left = { kind: "binary", op: op.value, left, right: this.parseAdditive() };
352
+ }
353
+ return left;
354
+ }
355
+ parseAdditive() {
356
+ let left = this.parseMultiplicative();
357
+ while (this.isOp("+") || this.isOp("-")) {
358
+ const op = this.next();
359
+ left = { kind: "binary", op: op.value, left, right: this.parseMultiplicative() };
360
+ }
361
+ return left;
362
+ }
363
+ parseMultiplicative() {
364
+ let left = this.parseUnary();
365
+ while (this.isOp("*") || this.isOp("/") || this.isOp("%")) {
366
+ const op = this.next();
367
+ left = { kind: "binary", op: op.value, left, right: this.parseUnary() };
368
+ }
369
+ return left;
370
+ }
371
+ parseUnary() {
372
+ if (this.isOp("-")) {
373
+ this.enter();
374
+ this.next();
375
+ const arg2 = this.parseUnary();
376
+ this.leave();
377
+ if (arg2.kind === "literal" && typeof arg2.value === "number") return { kind: "literal", value: -arg2.value };
378
+ return { kind: "unary", op: "-", arg: arg2 };
379
+ }
380
+ return this.parsePostfix();
381
+ }
382
+ parsePostfix() {
383
+ let e = this.parsePrimary();
384
+ for (; ; ) {
385
+ if (this.isOp(".")) {
386
+ this.next();
387
+ const t = this.next();
388
+ if (t.type !== "ident") this.fail("Expected a property name after .");
389
+ e = appendSegment(e, { kind: "prop", name: t.value }, () => this.fail("Property access is only allowed on data paths"));
390
+ } else if (this.isOp("[")) {
391
+ this.next();
392
+ const t = this.next();
393
+ if (t.type !== "num" || !Number.isInteger(t.value) || t.value < 0) this.fail("Index must be a non-negative integer");
394
+ this.expectOp("]");
395
+ e = appendSegment(e, { kind: "index", index: t.value }, () => this.fail("Indexing is only allowed on data paths"));
396
+ } else return e;
397
+ }
398
+ }
399
+ parsePrimary() {
400
+ const t = this.next();
401
+ switch (t.type) {
402
+ case "num":
403
+ return { kind: "literal", value: t.value };
404
+ case "str":
405
+ return { kind: "literal", value: t.value };
406
+ case "ident": {
407
+ if (t.value === "true") return { kind: "literal", value: true };
408
+ if (t.value === "false") return { kind: "literal", value: false };
409
+ if (t.value === "null") return { kind: "literal", value: null };
410
+ if (KEYWORDS.has(t.value)) this.fail(`Unexpected keyword "${t.value}"`);
411
+ if (this.isOp("(")) {
412
+ this.enter();
413
+ this.next();
414
+ const args = [];
415
+ if (!this.isOp(")")) {
416
+ args.push(this.parsePipe());
417
+ while (this.isOp(",")) {
418
+ this.next();
419
+ args.push(this.parsePipe());
420
+ }
421
+ }
422
+ this.expectOp(")");
423
+ this.leave();
424
+ return { kind: "call", name: t.value, args };
425
+ }
426
+ return { kind: "path", segments: [{ kind: "prop", name: t.value }] };
427
+ }
428
+ case "op":
429
+ if (t.value === "(") {
430
+ this.enter();
431
+ const e = this.parsePipe();
432
+ this.expectOp(")");
433
+ this.leave();
434
+ return e;
435
+ }
436
+ if (t.value === "[") {
437
+ this.enter();
438
+ const items = [];
439
+ if (!this.isOp("]")) {
440
+ items.push(this.parsePipe());
441
+ while (this.isOp(",")) {
442
+ this.next();
443
+ items.push(this.parsePipe());
444
+ }
445
+ }
446
+ this.expectOp("]");
447
+ this.leave();
448
+ return { kind: "array", items };
449
+ }
450
+ this.fail(`Unexpected "${t.value}"`);
451
+ // eslint-disable-next-line no-fallthrough
452
+ case "eof":
453
+ this.fail("Unexpected end of expression");
454
+ }
455
+ }
456
+ };
457
+ function appendSegment(e, seg, fail) {
458
+ if (e.kind !== "path") return fail();
459
+ return { kind: "path", segments: [...e.segments, seg] };
460
+ }
461
+ var cache = /* @__PURE__ */ new Map();
462
+ function parseExpression(src) {
463
+ const key = src.trim();
464
+ const hit = cache.get(key);
465
+ if (hit) return hit;
466
+ if (key.length === 0) throw new ExpressionError("Expression is empty", 0, src);
467
+ const ast = new Parser(tokenize(key), key).parse();
468
+ if (cache.size > 5e3) cache.clear();
469
+ cache.set(key, ast);
470
+ return ast;
471
+ }
472
+ function checkExpressionSyntax(src) {
473
+ try {
474
+ parseExpression(src);
475
+ return null;
476
+ } catch (e) {
477
+ return e instanceof ExpressionError ? e : new ExpressionError(String(e), 0, src);
478
+ }
479
+ }
480
+
481
+ // src/expr/format.ts
482
+ var intlNumberCache = /* @__PURE__ */ new Map();
483
+ var intlDateCache = /* @__PURE__ */ new Map();
484
+ function numberFormat(locale, opts) {
485
+ const key = locale + JSON.stringify(opts);
486
+ let f = intlNumberCache.get(key);
487
+ if (!f) {
488
+ try {
489
+ f = new Intl.NumberFormat(locale, opts);
490
+ } catch {
491
+ f = new Intl.NumberFormat("en-GB", { ...opts, currency: opts.currency ?? "GBP" });
492
+ }
493
+ intlNumberCache.set(key, f);
494
+ }
495
+ return f;
496
+ }
497
+ function dateFormat(locale, opts) {
498
+ const key = locale + JSON.stringify(opts);
499
+ let f = intlDateCache.get(key);
500
+ if (!f) {
501
+ try {
502
+ f = new Intl.DateTimeFormat(locale, opts);
503
+ } catch {
504
+ f = new Intl.DateTimeFormat("en-GB", opts);
505
+ }
506
+ intlDateCache.set(key, f);
507
+ }
508
+ return f;
509
+ }
510
+ function toNumber(value) {
511
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
512
+ if (typeof value === "string" && value.trim() !== "") {
513
+ const n = Number(value.replace(/[,\s]/g, ""));
514
+ return Number.isFinite(n) ? n : null;
515
+ }
516
+ if (typeof value === "boolean") return value ? 1 : 0;
517
+ return null;
518
+ }
519
+ function toDate(value) {
520
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
521
+ if (typeof value === "number") return new Date(value);
522
+ if (typeof value === "string" && value.trim()) {
523
+ const d = new Date(value);
524
+ return Number.isNaN(d.getTime()) ? null : d;
525
+ }
526
+ return null;
527
+ }
528
+ function isEmptyValue(value) {
529
+ if (value === null || value === void 0) return true;
530
+ if (typeof value === "string") return value.trim() === "";
531
+ if (Array.isArray(value)) return value.length === 0;
532
+ if (typeof value === "object") return Object.keys(value).length === 0;
533
+ return false;
534
+ }
535
+ function toDisplayString(value) {
536
+ if (value === null || value === void 0) return "";
537
+ if (typeof value === "string") return value;
538
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : "";
539
+ if (typeof value === "boolean") return value ? "Yes" : "No";
540
+ if (value instanceof Date) return value.toISOString();
541
+ if (Array.isArray(value)) return value.map(toDisplayString).join(", ");
542
+ return "";
543
+ }
544
+ function formatDatePattern(date, pattern, ctx) {
545
+ const parts = (opts) => {
546
+ const o = { ...opts };
547
+ if (ctx.timeZone) o.timeZone = ctx.timeZone;
548
+ return dateFormat(ctx.locale, o).formatToParts(date);
549
+ };
550
+ const get = (opts, type) => parts(opts).find((p) => p.type === type)?.value ?? "";
551
+ const pad = (s, n) => s.padStart(n, "0");
552
+ const numeric = parts({ year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "2-digit", second: "2-digit", hour12: false });
553
+ const num = (type) => numeric.find((p) => p.type === type)?.value ?? "";
554
+ const hour24 = Number(num("hour")) % 24;
555
+ const hour12 = hour24 % 12 === 0 ? 12 : hour24 % 12;
556
+ return pattern.replace(/yyyy|yy|MMMM|MMM|MM|M|dd|d|EEEE|EEE|HH|H|hh|h|mm|ss|a|'[^']*'/g, (tok) => {
557
+ switch (tok) {
558
+ case "yyyy":
559
+ return num("year");
560
+ case "yy":
561
+ return num("year").slice(-2);
562
+ case "MMMM":
563
+ return get({ month: "long" }, "month");
564
+ case "MMM":
565
+ return get({ month: "short" }, "month");
566
+ case "MM":
567
+ return pad(num("month"), 2);
568
+ case "M":
569
+ return num("month");
570
+ case "dd":
571
+ return pad(num("day"), 2);
572
+ case "d":
573
+ return num("day");
574
+ case "EEEE":
575
+ return get({ weekday: "long" }, "weekday");
576
+ case "EEE":
577
+ return get({ weekday: "short" }, "weekday");
578
+ case "HH":
579
+ return pad(String(hour24), 2);
580
+ case "H":
581
+ return String(hour24);
582
+ case "hh":
583
+ return pad(String(hour12), 2);
584
+ case "h":
585
+ return String(hour12);
586
+ case "mm":
587
+ return pad(num("minute"), 2);
588
+ case "ss":
589
+ return pad(num("second"), 2);
590
+ case "a":
591
+ return hour24 < 12 ? "AM" : "PM";
592
+ default:
593
+ return tok.startsWith("'") ? tok.slice(1, -1) : tok;
594
+ }
595
+ });
596
+ }
597
+ var NAMED_DATE_STYLES = {
598
+ short: { dateStyle: "short" },
599
+ medium: { dateStyle: "medium" },
600
+ long: { dateStyle: "long" },
601
+ full: { dateStyle: "full" }
602
+ };
603
+ function formatDate(value, pattern, ctx, withTime = false) {
604
+ const d = toDate(value);
605
+ if (!d) return toDisplayString(value);
606
+ if (pattern && NAMED_DATE_STYLES[pattern]) {
607
+ const opts2 = { ...NAMED_DATE_STYLES[pattern] };
608
+ if (withTime) opts2.timeStyle = "short";
609
+ if (ctx.timeZone) opts2.timeZone = ctx.timeZone;
610
+ return dateFormat(ctx.locale, opts2).format(d);
611
+ }
612
+ if (pattern) return formatDatePattern(d, pattern, ctx);
613
+ const opts = withTime ? { dateStyle: "medium", timeStyle: "short" } : { dateStyle: "medium" };
614
+ if (ctx.timeZone) opts.timeZone = ctx.timeZone;
615
+ return dateFormat(ctx.locale, opts).format(d);
616
+ }
617
+ function formatCurrency(value, currency, decimals, ctx) {
618
+ const n = toNumber(value);
619
+ if (n === null) return toDisplayString(value);
620
+ const code = currency && String(currency).toUpperCase() || ctx.currency;
621
+ const opts = { style: "currency", currency: code };
622
+ if (decimals !== void 0) {
623
+ opts.minimumFractionDigits = decimals;
624
+ opts.maximumFractionDigits = decimals;
625
+ }
626
+ return numberFormat(ctx.locale, opts).format(n);
627
+ }
628
+ function formatNumber(value, decimals, ctx) {
629
+ const n = toNumber(value);
630
+ if (n === null) return toDisplayString(value);
631
+ const opts = {};
632
+ if (decimals !== void 0) {
633
+ opts.minimumFractionDigits = decimals;
634
+ opts.maximumFractionDigits = decimals;
635
+ } else opts.maximumFractionDigits = 2;
636
+ return numberFormat(ctx.locale, opts).format(n);
637
+ }
638
+ function formatPercent(value, decimals, ctx, alreadyScaled = false) {
639
+ const n = toNumber(value);
640
+ if (n === null) return toDisplayString(value);
641
+ const opts = { style: "percent" };
642
+ const d = decimals ?? 1;
643
+ opts.minimumFractionDigits = d;
644
+ opts.maximumFractionDigits = d;
645
+ return numberFormat(ctx.locale, opts).format(alreadyScaled ? n / 100 : n);
646
+ }
647
+ function arg(args, i) {
648
+ return args[i];
649
+ }
650
+ var PIPES = {
651
+ currency: {
652
+ name: "currency",
653
+ description: "Format as money",
654
+ returns: "string",
655
+ accepts: ["number", "currency"],
656
+ args: [
657
+ { name: "code", type: "string", optional: true, description: "ISO currency code, e.g. GBP" },
658
+ { name: "decimals", type: "number", optional: true }
659
+ ],
660
+ fn: (v, a, ctx) => formatCurrency(v, arg(a, 0), toNumber(a[1]) ?? void 0, ctx)
661
+ },
662
+ number: {
663
+ name: "number",
664
+ description: "Format with thousands separators",
665
+ returns: "string",
666
+ accepts: ["number"],
667
+ args: [{ name: "decimals", type: "number", optional: true }],
668
+ fn: (v, a, ctx) => formatNumber(v, toNumber(a[0]) ?? void 0, ctx)
669
+ },
670
+ percent: {
671
+ name: "percent",
672
+ description: "Format a fraction (0.25) as a percentage (25%)",
673
+ returns: "string",
674
+ accepts: ["number", "percent"],
675
+ args: [
676
+ { name: "decimals", type: "number", optional: true },
677
+ { name: "scaled", type: "boolean", optional: true, description: "Set true when the value is already 0\u2013100" }
678
+ ],
679
+ fn: (v, a, ctx) => formatPercent(v, toNumber(a[0]) ?? void 0, ctx, a[1] === true)
680
+ },
681
+ date: {
682
+ name: "date",
683
+ description: "Format a date",
684
+ returns: "string",
685
+ accepts: ["date", "datetime"],
686
+ args: [{ name: "pattern", type: "string", optional: true, description: "e.g. dd MMM yyyy, or short/medium/long/full" }],
687
+ fn: (v, a, ctx) => formatDate(v, arg(a, 0), ctx, false)
688
+ },
689
+ datetime: {
690
+ name: "datetime",
691
+ description: "Format a date with time",
692
+ returns: "string",
693
+ accepts: ["date", "datetime"],
694
+ args: [{ name: "pattern", type: "string", optional: true }],
695
+ fn: (v, a, ctx) => formatDate(v, arg(a, 0), ctx, true)
696
+ },
697
+ boolean: {
698
+ name: "boolean",
699
+ description: "Show a boolean as text",
700
+ returns: "string",
701
+ accepts: ["boolean"],
702
+ args: [
703
+ { name: "yes", type: "string", optional: true },
704
+ { name: "no", type: "string", optional: true }
705
+ ],
706
+ fn: (v, a) => v === true || v === "true" || v === 1 ? arg(a, 0) ?? "Yes" : arg(a, 1) ?? "No"
707
+ },
708
+ upper: { name: "upper", description: "UPPERCASE", returns: "string", fn: (v) => toDisplayString(v).toUpperCase() },
709
+ lower: { name: "lower", description: "lowercase", returns: "string", fn: (v) => toDisplayString(v).toLowerCase() },
710
+ title: {
711
+ name: "title",
712
+ description: "Title Case",
713
+ returns: "string",
714
+ fn: (v) => toDisplayString(v).replace(/\b\p{L}/gu, (c) => c.toUpperCase())
715
+ },
716
+ default: {
717
+ name: "default",
718
+ description: "Fallback when the value is empty",
719
+ returns: "same",
720
+ args: [{ name: "fallback", type: "any" }],
721
+ fn: (v, a) => isEmptyValue(v) ? a[0] : v
722
+ },
723
+ join: {
724
+ name: "join",
725
+ description: "Join a list into text",
726
+ returns: "string",
727
+ accepts: ["array"],
728
+ args: [{ name: "separator", type: "string", optional: true }],
729
+ fn: (v, a) => Array.isArray(v) ? v.map(toDisplayString).join(arg(a, 0) ?? ", ") : toDisplayString(v)
730
+ },
731
+ truncate: {
732
+ name: "truncate",
733
+ description: "Limit length with an ellipsis",
734
+ returns: "string",
735
+ args: [{ name: "length", type: "number" }],
736
+ fn: (v, a) => {
737
+ const s = toDisplayString(v);
738
+ const n = toNumber(a[0]) ?? 50;
739
+ return s.length > n ? s.slice(0, Math.max(0, n - 1)).trimEnd() + "\u2026" : s;
740
+ }
741
+ },
742
+ round: {
743
+ name: "round",
744
+ description: "Round to N decimals",
745
+ returns: "number",
746
+ accepts: ["number"],
747
+ args: [{ name: "decimals", type: "number", optional: true }],
748
+ fn: (v, a) => {
749
+ const n = toNumber(v);
750
+ if (n === null) return v;
751
+ const d = toNumber(a[0]) ?? 0;
752
+ const m = 10 ** d;
753
+ return Math.round(n * m) / m;
754
+ }
755
+ },
756
+ abs: { name: "abs", description: "Absolute value", returns: "number", accepts: ["number"], fn: (v) => toNumber(v) === null ? v : Math.abs(toNumber(v)) },
757
+ pluralize: {
758
+ name: "pluralize",
759
+ description: "Choose singular/plural word by count",
760
+ returns: "string",
761
+ args: [
762
+ { name: "singular", type: "string" },
763
+ { name: "plural", type: "string" }
764
+ ],
765
+ fn: (v, a) => toNumber(v) === 1 ? arg(a, 0) ?? "" : arg(a, 1) ?? `${arg(a, 0) ?? ""}s`
766
+ },
767
+ length: { name: "length", description: "Number of items or characters", returns: "number", fn: (v) => Array.isArray(v) ? v.length : toDisplayString(v).length },
768
+ first: { name: "first", description: "First item", returns: "same", accepts: ["array"], fn: (v) => Array.isArray(v) ? v[0] : v },
769
+ last: { name: "last", description: "Last item", returns: "same", accepts: ["array"], fn: (v) => Array.isArray(v) ? v[v.length - 1] : v },
770
+ sortBy: {
771
+ name: "sortBy",
772
+ description: "Sort a list by a field",
773
+ returns: "array",
774
+ accepts: ["array"],
775
+ args: [
776
+ { name: "field", type: "string" },
777
+ { name: "direction", type: "string", optional: true, description: "asc or desc" }
778
+ ],
779
+ fn: (v, a) => {
780
+ if (!Array.isArray(v)) return v;
781
+ const field = arg(a, 0) ?? "";
782
+ const dir = (arg(a, 1) ?? "asc").toLowerCase() === "desc" ? -1 : 1;
783
+ return [...v].sort((x, y) => compareValues(getField(x, field), getField(y, field)) * dir);
784
+ }
785
+ },
786
+ take: {
787
+ name: "take",
788
+ description: "First N items",
789
+ returns: "array",
790
+ accepts: ["array"],
791
+ args: [{ name: "count", type: "number" }],
792
+ fn: (v, a) => Array.isArray(v) ? v.slice(0, Math.max(0, toNumber(a[0]) ?? 0)) : v
793
+ },
794
+ filter: {
795
+ name: "filter",
796
+ description: "Keep items whose field equals a value",
797
+ returns: "array",
798
+ accepts: ["array"],
799
+ args: [
800
+ { name: "field", type: "string" },
801
+ { name: "value", type: "any" }
802
+ ],
803
+ fn: (v, a) => Array.isArray(v) ? v.filter((x) => looseEquals(getField(x, arg(a, 0) ?? ""), a[1])) : v
804
+ },
805
+ pluck: {
806
+ name: "pluck",
807
+ description: "Extract one field from each item",
808
+ returns: "array",
809
+ accepts: ["array"],
810
+ args: [{ name: "field", type: "string" }],
811
+ fn: (v, a) => Array.isArray(v) ? v.map((x) => getField(x, arg(a, 0) ?? "")) : getField(v, arg(a, 0) ?? "")
812
+ },
813
+ initials: {
814
+ name: "initials",
815
+ description: "Initials of a name",
816
+ returns: "string",
817
+ fn: (v) => toDisplayString(v).split(/\s+/).filter(Boolean).map((w) => w[0].toUpperCase()).join("")
818
+ }
819
+ };
820
+ function getField(obj, path) {
821
+ if (obj === null || obj === void 0) return void 0;
822
+ let cur = obj;
823
+ for (const part of path.split(".")) {
824
+ if (!part) continue;
825
+ if (cur === null || cur === void 0 || typeof cur !== "object") return void 0;
826
+ if (!Object.prototype.hasOwnProperty.call(cur, part)) return void 0;
827
+ cur = cur[part];
828
+ }
829
+ return cur;
830
+ }
831
+ function compareValues(a, b) {
832
+ const na = toNumber(a);
833
+ const nb = toNumber(b);
834
+ if (na !== null && nb !== null && typeof a !== "string" && typeof b !== "string") return na - nb;
835
+ const da = toDate(a);
836
+ const db = toDate(b);
837
+ if (da && db && (a instanceof Date || typeof a === "string") && (b instanceof Date || typeof b === "string") && /\d{4}-\d{2}/.test(String(a))) return da.getTime() - db.getTime();
838
+ const sa = toDisplayString(a);
839
+ const sb = toDisplayString(b);
840
+ return sa < sb ? -1 : sa > sb ? 1 : 0;
841
+ }
842
+ function looseEquals(a, b) {
843
+ if (a === b) return true;
844
+ if (a === null || a === void 0 || b === null || b === void 0) return (a ?? null) === (b ?? null);
845
+ if (typeof a === "number" || typeof b === "number") {
846
+ const na = toNumber(a);
847
+ const nb = toNumber(b);
848
+ if (na !== null && nb !== null) return na === nb;
849
+ }
850
+ if (typeof a === "boolean" || typeof b === "boolean") return String(a) === String(b);
851
+ return toDisplayString(a) === toDisplayString(b);
852
+ }
853
+
854
+ // src/expr/evaluate.ts
855
+ var Scope = class _Scope {
856
+ constructor(vars, parent) {
857
+ this.vars = vars;
858
+ this.parent = parent;
859
+ }
860
+ vars;
861
+ parent;
862
+ lookup(name) {
863
+ if (Object.prototype.hasOwnProperty.call(this.vars, name)) return { found: true, value: this.vars[name] };
864
+ if (this.parent) return this.parent.lookup(name);
865
+ return { found: false, value: void 0 };
866
+ }
867
+ child(vars) {
868
+ return new _Scope(vars, this);
869
+ }
870
+ /** Names visible in this scope (innermost first, deduplicated). */
871
+ names() {
872
+ const out = [];
873
+ let s = this;
874
+ while (s) {
875
+ for (const k of Object.keys(s.vars)) if (!out.includes(k)) out.push(k);
876
+ s = s.parent;
877
+ }
878
+ return out;
879
+ }
880
+ };
881
+ var FUNCTIONS = {
882
+ sum: ([list]) => numbers(list).reduce((a, b) => a + b, 0),
883
+ count: ([list]) => Array.isArray(list) ? list.length : isEmptyValue(list) ? 0 : 1,
884
+ avg: ([list]) => {
885
+ const ns = numbers(list);
886
+ return ns.length ? ns.reduce((a, b) => a + b, 0) / ns.length : null;
887
+ },
888
+ min: ([list, ...rest]) => {
889
+ const ns = rest.length ? numbers([list, ...rest]) : numbers(list);
890
+ return ns.length ? Math.min(...ns) : null;
891
+ },
892
+ max: ([list, ...rest]) => {
893
+ const ns = rest.length ? numbers([list, ...rest]) : numbers(list);
894
+ return ns.length ? Math.max(...ns) : null;
895
+ },
896
+ first: ([list]) => Array.isArray(list) ? list[0] : list,
897
+ last: ([list]) => Array.isArray(list) ? list[list.length - 1] : list,
898
+ length: ([v]) => Array.isArray(v) ? v.length : toDisplayString(v).length,
899
+ coalesce: (args) => args.find((a) => !isEmptyValue(a)) ?? null,
900
+ isEmpty: ([v]) => isEmptyValue(v),
901
+ isNotEmpty: ([v]) => !isEmptyValue(v),
902
+ contains: ([hay, needle]) => Array.isArray(hay) ? hay.some((x) => looseEquals(x, needle)) : toDisplayString(hay).toLowerCase().includes(toDisplayString(needle).toLowerCase()),
903
+ startsWith: ([s, p]) => toDisplayString(s).toLowerCase().startsWith(toDisplayString(p).toLowerCase()),
904
+ endsWith: ([s, p]) => toDisplayString(s).toLowerCase().endsWith(toDisplayString(p).toLowerCase()),
905
+ concat: (args) => args.map(toDisplayString).join(""),
906
+ round: ([v, d]) => {
907
+ const n = toNumber(v);
908
+ if (n === null) return null;
909
+ const m = 10 ** (toNumber(d) ?? 0);
910
+ return Math.round(n * m) / m;
911
+ },
912
+ abs: ([v]) => {
913
+ const n = toNumber(v);
914
+ return n === null ? null : Math.abs(n);
915
+ },
916
+ number: ([v]) => toNumber(v),
917
+ string: ([v]) => toDisplayString(v),
918
+ if: ([c, a, b]) => truthy(c) ? a : b,
919
+ now: (_args, ctx) => ctx.format.now ?? /* @__PURE__ */ new Date(),
920
+ year: ([v]) => dateOf(v)?.getFullYear() ?? null,
921
+ daysBetween: ([a, b]) => {
922
+ const da = dateOf(a);
923
+ const db = dateOf(b);
924
+ return da && db ? Math.round((db.getTime() - da.getTime()) / 864e5) : null;
925
+ }
926
+ };
927
+ var FUNCTION_NAMES = Object.keys(FUNCTIONS);
928
+ function dateOf(v) {
929
+ if (v instanceof Date) return v;
930
+ if (typeof v === "string" || typeof v === "number") {
931
+ const d = new Date(v);
932
+ return Number.isNaN(d.getTime()) ? null : d;
933
+ }
934
+ return null;
935
+ }
936
+ function numbers(list) {
937
+ if (!Array.isArray(list)) {
938
+ const n = toNumber(list);
939
+ return n === null ? [] : [n];
940
+ }
941
+ const out = [];
942
+ for (const v of list) {
943
+ const n = toNumber(v);
944
+ if (n !== null) out.push(n);
945
+ }
946
+ return out;
947
+ }
948
+ function truthy(v) {
949
+ if (v === null || v === void 0 || v === false || v === 0 || v === "") return false;
950
+ if (typeof v === "string") return !["false", "no", "0", "off"].includes(v.trim().toLowerCase());
951
+ if (Array.isArray(v)) return v.length > 0;
952
+ return true;
953
+ }
954
+ function readProperty(obj, name) {
955
+ if (obj === null || obj === void 0) return void 0;
956
+ if (typeof obj === "string" && name === "length") return obj.length;
957
+ if (typeof obj !== "object") return void 0;
958
+ if (Array.isArray(obj)) {
959
+ if (name === "length") return obj.length;
960
+ const projected = obj.map((x) => readProperty(x, name));
961
+ return projected.every((v2) => v2 === void 0) ? void 0 : projected;
962
+ }
963
+ if (!Object.prototype.hasOwnProperty.call(obj, name)) return void 0;
964
+ const v = obj[name];
965
+ return typeof v === "function" ? void 0 : v;
966
+ }
967
+ function resolvePath(segments, scope) {
968
+ const first = segments[0];
969
+ if (!first || first.kind !== "prop") return { found: false, value: void 0 };
970
+ const root = scope.lookup(first.name);
971
+ if (!root.found) return { found: false, value: void 0 };
972
+ let cur = root.value;
973
+ for (let i = 1; i < segments.length; i++) {
974
+ const seg = segments[i];
975
+ if (seg.kind === "prop") cur = readProperty(cur, seg.name);
976
+ else cur = Array.isArray(cur) ? cur[seg.index] : void 0;
977
+ if (cur === void 0) return { found: true, value: void 0 };
978
+ }
979
+ return { found: true, value: cur };
980
+ }
981
+ function evaluateAst(e, ctx) {
982
+ switch (e.kind) {
983
+ case "literal":
984
+ return e.value;
985
+ case "path": {
986
+ const r = resolvePath(e.segments, ctx.scope);
987
+ return r.value;
988
+ }
989
+ case "unary": {
990
+ const v = evaluateAst(e.arg, ctx);
991
+ if (e.op === "not") return !truthy(v);
992
+ const n = toNumber(v);
993
+ return n === null ? null : -n;
994
+ }
995
+ case "binary":
996
+ return evaluateBinary(e.op, e.left, e.right, ctx);
997
+ case "conditional":
998
+ return truthy(evaluateAst(e.test, ctx)) ? evaluateAst(e.then, ctx) : evaluateAst(e.else, ctx);
999
+ case "call": {
1000
+ const fn = FUNCTIONS[e.name];
1001
+ if (!fn) {
1002
+ ctx.onWarning?.(`Unknown function "${e.name}"`);
1003
+ return null;
1004
+ }
1005
+ return fn(
1006
+ e.args.map((a) => evaluateAst(a, ctx)),
1007
+ ctx
1008
+ );
1009
+ }
1010
+ case "pipe": {
1011
+ const input = evaluateAst(e.input, ctx);
1012
+ const pipe = PIPES[e.name];
1013
+ if (!pipe) {
1014
+ ctx.onWarning?.(`Unknown formatter "${e.name}"`);
1015
+ return input;
1016
+ }
1017
+ return pipe.fn(
1018
+ input,
1019
+ e.args.map((a) => evaluateAst(a, ctx)),
1020
+ ctx.format
1021
+ );
1022
+ }
1023
+ case "array":
1024
+ return e.items.map((i) => evaluateAst(i, ctx));
1025
+ }
1026
+ }
1027
+ function evaluateBinary(op, leftE, rightE, ctx) {
1028
+ if (op === "and") return truthy(evaluateAst(leftE, ctx)) ? truthy(evaluateAst(rightE, ctx)) : false;
1029
+ if (op === "or") return truthy(evaluateAst(leftE, ctx)) ? true : truthy(evaluateAst(rightE, ctx));
1030
+ const l = evaluateAst(leftE, ctx);
1031
+ const r = evaluateAst(rightE, ctx);
1032
+ switch (op) {
1033
+ case "==":
1034
+ return looseEquals(l, r);
1035
+ case "!=":
1036
+ return !looseEquals(l, r);
1037
+ case "<":
1038
+ return compareOrNull(l, r, (c) => c < 0);
1039
+ case "<=":
1040
+ return compareOrNull(l, r, (c) => c <= 0);
1041
+ case ">":
1042
+ return compareOrNull(l, r, (c) => c > 0);
1043
+ case ">=":
1044
+ return compareOrNull(l, r, (c) => c >= 0);
1045
+ case "in":
1046
+ return Array.isArray(r) ? r.some((x) => looseEquals(x, l)) : toDisplayString(r).includes(toDisplayString(l));
1047
+ case "+": {
1048
+ const nl = toNumber(l);
1049
+ const nr = toNumber(r);
1050
+ if (nl !== null && nr !== null && typeof l !== "string" && typeof r !== "string") return nl + nr;
1051
+ if (typeof l === "string" || typeof r === "string") return toDisplayString(l) + toDisplayString(r);
1052
+ return nl !== null && nr !== null ? nl + nr : null;
1053
+ }
1054
+ case "-":
1055
+ case "*":
1056
+ case "/":
1057
+ case "%": {
1058
+ const nl = toNumber(l);
1059
+ const nr = toNumber(r);
1060
+ if (nl === null || nr === null) return null;
1061
+ if (op === "-") return nl - nr;
1062
+ if (op === "*") return nl * nr;
1063
+ if (op === "/") return nr === 0 ? null : nl / nr;
1064
+ return nr === 0 ? null : nl % nr;
1065
+ }
1066
+ }
1067
+ return null;
1068
+ }
1069
+ function compareOrNull(l, r, test) {
1070
+ if (l === null || l === void 0 || r === null || r === void 0) return false;
1071
+ return test(compareValues(l, r));
1072
+ }
1073
+ function evaluate(src, ctx) {
1074
+ return evaluateAst(parseExpression(src), ctx);
1075
+ }
1076
+ function evaluateCondition(src, ctx) {
1077
+ return truthy(evaluate(src, ctx));
1078
+ }
1079
+ function evaluateText(src, ctx) {
1080
+ return toDisplayString(evaluate(src, ctx));
1081
+ }
1082
+ function splitTemplate(template) {
1083
+ const out = [];
1084
+ const re = /\{\{\s*([^}]+?)\s*\}\}/g;
1085
+ let last = 0;
1086
+ let m;
1087
+ while (m = re.exec(template)) {
1088
+ if (m.index > last) out.push({ kind: "text", text: template.slice(last, m.index) });
1089
+ out.push({ kind: "token", text: m[0], expr: m[1] });
1090
+ last = m.index + m[0].length;
1091
+ }
1092
+ if (last < template.length) out.push({ kind: "text", text: template.slice(last) });
1093
+ return out;
1094
+ }
1095
+ function interpolate(template, ctx) {
1096
+ if (!template.includes("{{")) return template;
1097
+ return splitTemplate(template).map((s) => {
1098
+ if (s.kind === "text") return s.text;
1099
+ try {
1100
+ return evaluateText(s.expr, ctx);
1101
+ } catch {
1102
+ ctx.onWarning?.(`Invalid expression "${s.expr}"`, s.expr);
1103
+ return "";
1104
+ }
1105
+ }).join("");
1106
+ }
1107
+
1108
+ // src/expr/typecheck.ts
1109
+ var SchemaScope = class _SchemaScope {
1110
+ constructor(vars, parent) {
1111
+ this.vars = vars;
1112
+ this.parent = parent;
1113
+ }
1114
+ vars;
1115
+ parent;
1116
+ lookup(name) {
1117
+ if (Object.prototype.hasOwnProperty.call(this.vars, name)) return this.vars[name];
1118
+ return this.parent?.lookup(name);
1119
+ }
1120
+ child(vars) {
1121
+ return new _SchemaScope(vars, this);
1122
+ }
1123
+ entries() {
1124
+ const out = [];
1125
+ let s = this;
1126
+ let depth = 0;
1127
+ while (s) {
1128
+ for (const [name, schema] of Object.entries(s.vars)) if (!out.some((e) => e.name === name)) out.push({ name, schema, depth });
1129
+ s = s.parent;
1130
+ depth++;
1131
+ }
1132
+ return out;
1133
+ }
1134
+ };
1135
+ var GLOBAL_SCHEMAS = {
1136
+ page: {
1137
+ type: "object",
1138
+ label: "Page",
1139
+ fields: {
1140
+ number: { type: "number", label: "Page number" },
1141
+ total: { type: "number", label: "Total pages" }
1142
+ }
1143
+ },
1144
+ now: { type: "datetime", label: "Generated at" },
1145
+ theme: {
1146
+ type: "object",
1147
+ label: "Theme",
1148
+ fields: {
1149
+ name: { type: "string", label: "Theme name" },
1150
+ logo: { type: "image", label: "Logo" },
1151
+ colors: { type: "object", label: "Colours", fields: {} },
1152
+ typography: { type: "object", label: "Typography", fields: {} },
1153
+ meta: { type: "object", label: "Brand details", fields: {} }
1154
+ }
1155
+ }
1156
+ };
1157
+ function dataSourceSchemas(sources) {
1158
+ const out = {};
1159
+ for (const s of sources) out[s.id] = { type: "object", label: s.label, description: s.description, fields: s.schema };
1160
+ return out;
1161
+ }
1162
+ function createSchemaScope(sources, extraGlobals = {}) {
1163
+ return new SchemaScope({ ...GLOBAL_SCHEMAS, ...extraGlobals }).child(dataSourceSchemas(sources));
1164
+ }
1165
+ function resolvePathSchema(segments, scope) {
1166
+ const first = segments[0];
1167
+ if (!first || first.kind !== "prop") return { issue: { code: "unknown-root", message: "Expression must start with a name" } };
1168
+ const root = scope.lookup(first.name);
1169
+ if (!root) return { issue: { code: "unknown-root", message: `Unknown data source or variable "${first.name}"` } };
1170
+ let cur = root;
1171
+ let projected = false;
1172
+ for (let i = 1; i < segments.length; i++) {
1173
+ const seg = segments[i];
1174
+ const soFar = pathToString(segments.slice(0, i));
1175
+ if (seg.kind === "index") {
1176
+ if (cur.type !== "array") return { issue: { code: "not-a-list", message: `"${soFar}" is not a list and cannot be indexed` } };
1177
+ cur = cur.itemSchema ?? { type: "any" };
1178
+ continue;
1179
+ }
1180
+ if (cur.type === "array") {
1181
+ if (!cur.itemSchema) return { schema: { type: "array" } };
1182
+ if (cur.itemSchema.type !== "object" || !cur.itemSchema.fields) return { issue: { code: "not-an-object", message: `Items of "${soFar}" have no field "${seg.name}"` } };
1183
+ const f2 = cur.itemSchema.fields[seg.name];
1184
+ if (!f2) return { issue: { code: "unknown-field", message: `Items of "${soFar}" have no field "${seg.name}"` } };
1185
+ projected = true;
1186
+ cur = { type: "array", itemSchema: f2, label: f2.label };
1187
+ continue;
1188
+ }
1189
+ if (cur.type === "any") return { schema: { type: "any" } };
1190
+ if (cur.type !== "object") return { issue: { code: "not-an-object", message: `"${soFar}" is a ${cur.type} and has no field "${seg.name}"` } };
1191
+ if (cur.fields && Object.keys(cur.fields).length === 0 && ["colors", "typography", "meta"].includes(segments[i - 1].kind === "prop" ? segments[i - 1].name : "")) {
1192
+ return { schema: { type: "string" } };
1193
+ }
1194
+ const f = cur.fields?.[seg.name];
1195
+ if (!f) return { issue: { code: "unknown-field", message: `"${soFar}" has no field "${seg.name}"` } };
1196
+ cur = f;
1197
+ }
1198
+ void projected;
1199
+ return { schema: cur };
1200
+ }
1201
+ function typeOfSchema(s) {
1202
+ return s?.type ?? "unknown";
1203
+ }
1204
+ function typeCheckAst(e, scope) {
1205
+ const issues = [];
1206
+ const type = check(e, scope, issues);
1207
+ return { type: type.type, schema: type.schema, issues };
1208
+ }
1209
+ function check(e, scope, issues) {
1210
+ switch (e.kind) {
1211
+ case "literal":
1212
+ return { type: e.value === null ? "null" : typeof e.value };
1213
+ case "path": {
1214
+ const r = resolvePathSchema(e.segments, scope);
1215
+ if (r.issue) {
1216
+ issues.push(r.issue);
1217
+ return { type: "unknown" };
1218
+ }
1219
+ return { type: typeOfSchema(r.schema), schema: r.schema };
1220
+ }
1221
+ case "unary": {
1222
+ const t = check(e.arg, scope, issues);
1223
+ return { type: e.op === "not" ? "boolean" : t.type === "unknown" ? "unknown" : "number" };
1224
+ }
1225
+ case "binary": {
1226
+ const l = check(e.left, scope, issues);
1227
+ const r = check(e.right, scope, issues);
1228
+ switch (e.op) {
1229
+ case "and":
1230
+ case "or":
1231
+ case "==":
1232
+ case "!=":
1233
+ case "<":
1234
+ case "<=":
1235
+ case ">":
1236
+ case ">=":
1237
+ case "in":
1238
+ return { type: "boolean" };
1239
+ case "+":
1240
+ return { type: l.type === "string" || r.type === "string" ? "string" : "number" };
1241
+ default:
1242
+ return { type: "number" };
1243
+ }
1244
+ }
1245
+ case "conditional": {
1246
+ check(e.test, scope, issues);
1247
+ const a = check(e.then, scope, issues);
1248
+ check(e.else, scope, issues);
1249
+ return a;
1250
+ }
1251
+ case "call": {
1252
+ if (!FUNCTION_NAMES.includes(e.name)) issues.push({ code: "unknown-function", message: `Unknown function "${e.name}"` });
1253
+ for (const a of e.args) check(a, scope, issues);
1254
+ const numeric = ["sum", "count", "avg", "min", "max", "length", "round", "abs", "number", "year", "daysBetween"];
1255
+ if (numeric.includes(e.name)) return { type: "number" };
1256
+ if (["isEmpty", "isNotEmpty", "contains", "startsWith", "endsWith"].includes(e.name)) return { type: "boolean" };
1257
+ if (["concat", "string"].includes(e.name)) return { type: "string" };
1258
+ if (e.name === "now") return { type: "datetime" };
1259
+ if (["first", "last"].includes(e.name) && e.args[0]) {
1260
+ const t = check(e.args[0], scope, issues);
1261
+ return t.schema?.type === "array" ? { type: typeOfSchema(t.schema.itemSchema), schema: t.schema.itemSchema } : t;
1262
+ }
1263
+ return { type: "any" };
1264
+ }
1265
+ case "pipe": {
1266
+ const input = check(e.input, scope, issues);
1267
+ for (const a of e.args) check(a, scope, issues);
1268
+ const def = PIPES[e.name];
1269
+ if (!def) {
1270
+ issues.push({ code: "unknown-pipe", message: `Unknown formatter "${e.name}"` });
1271
+ return input;
1272
+ }
1273
+ if (def.returns === "same") return input;
1274
+ if (def.returns === "array") return { type: "array", schema: input.schema?.type === "array" ? input.schema : void 0 };
1275
+ if (def.accepts && input.type !== "unknown" && input.type !== "any" && input.type !== "null" && !def.accepts.includes(input.type)) {
1276
+ const compat = def.accepts.includes("number") && (input.type === "currency" || input.type === "percent") || def.accepts.includes("array") && input.type === "array";
1277
+ if (!compat) issues.push({ code: "type-mismatch", message: `Formatter "${e.name}" expects ${def.accepts.join(" or ")} but received ${input.type}` });
1278
+ }
1279
+ return { type: def.returns };
1280
+ }
1281
+ case "array":
1282
+ for (const i of e.items) check(i, scope, issues);
1283
+ return { type: "array" };
1284
+ }
1285
+ }
1286
+ function typeCheck(src, scope) {
1287
+ try {
1288
+ return typeCheckAst(parseExpression(src), scope);
1289
+ } catch (err) {
1290
+ const pos = err instanceof ExpressionError ? err.position : void 0;
1291
+ return { type: "unknown", issues: [{ code: "syntax", message: err.message, position: pos }] };
1292
+ }
1293
+ }
1294
+
1295
+ // src/expr/condition.ts
1296
+ function compileCondition(condition) {
1297
+ if (condition === void 0 || condition === null) return null;
1298
+ if (typeof condition === "string") return condition.trim() || null;
1299
+ const src = compileGroup(condition);
1300
+ return src || null;
1301
+ }
1302
+ function compileGroup(group) {
1303
+ const parts = group.rules.map((r) => "op" in r ? compileGroup(r) : compileRule(r)).filter((s) => !!s);
1304
+ if (parts.length === 0) return "";
1305
+ if (parts.length === 1) return parts[0];
1306
+ return parts.map((p) => `(${p})`).join(group.op === "and" ? " and " : " or ");
1307
+ }
1308
+ function literal(v) {
1309
+ if (v === null || v === void 0) return "null";
1310
+ if (typeof v === "string") {
1311
+ const m = /^\{\{\s*(.+?)\s*\}\}$/.exec(v);
1312
+ if (m) return m[1];
1313
+ return JSON.stringify(v);
1314
+ }
1315
+ return String(v);
1316
+ }
1317
+ function compileRule(rule) {
1318
+ const left = rule.left.trim();
1319
+ if (!left) return "";
1320
+ switch (rule.operator) {
1321
+ case "eq":
1322
+ return `${left} == ${literal(rule.right)}`;
1323
+ case "neq":
1324
+ return `${left} != ${literal(rule.right)}`;
1325
+ case "gt":
1326
+ return `${left} > ${literal(rule.right)}`;
1327
+ case "gte":
1328
+ return `${left} >= ${literal(rule.right)}`;
1329
+ case "lt":
1330
+ return `${left} < ${literal(rule.right)}`;
1331
+ case "lte":
1332
+ return `${left} <= ${literal(rule.right)}`;
1333
+ case "contains":
1334
+ return `contains(${left}, ${literal(rule.right)})`;
1335
+ case "startsWith":
1336
+ return `startsWith(${left}, ${literal(rule.right)})`;
1337
+ case "endsWith":
1338
+ return `endsWith(${left}, ${literal(rule.right)})`;
1339
+ case "in":
1340
+ return `${left} in ${Array.isArray(rule.right) ? JSON.stringify(rule.right) : literal(rule.right)}`;
1341
+ case "isEmpty":
1342
+ return `isEmpty(${left})`;
1343
+ case "isNotEmpty":
1344
+ return `isNotEmpty(${left})`;
1345
+ case "isTrue":
1346
+ return `${left} == true`;
1347
+ case "isFalse":
1348
+ return `not ${left}`;
1349
+ }
1350
+ }
1351
+ var CONDITION_OPERATORS = [
1352
+ { value: "eq", label: "equals", needsValue: true },
1353
+ { value: "neq", label: "does not equal", needsValue: true },
1354
+ { value: "gt", label: "is greater than", needsValue: true, types: ["number", "currency", "percent", "date", "datetime"] },
1355
+ { value: "gte", label: "is at least", needsValue: true, types: ["number", "currency", "percent", "date", "datetime"] },
1356
+ { value: "lt", label: "is less than", needsValue: true, types: ["number", "currency", "percent", "date", "datetime"] },
1357
+ { value: "lte", label: "is at most", needsValue: true, types: ["number", "currency", "percent", "date", "datetime"] },
1358
+ { value: "contains", label: "contains", needsValue: true, types: ["string", "array"] },
1359
+ { value: "startsWith", label: "starts with", needsValue: true, types: ["string"] },
1360
+ { value: "endsWith", label: "ends with", needsValue: true, types: ["string"] },
1361
+ { value: "isEmpty", label: "is empty", needsValue: false },
1362
+ { value: "isNotEmpty", label: "is not empty", needsValue: false },
1363
+ { value: "isTrue", label: "is true", needsValue: false, types: ["boolean"] },
1364
+ { value: "isFalse", label: "is false", needsValue: false, types: ["boolean"] }
1365
+ ];
1366
+
1367
+ // src/theme/default-theme.ts
1368
+ var DEFAULT_THEME = {
1369
+ id: "default",
1370
+ name: "Default",
1371
+ typography: {
1372
+ headingFont: "Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
1373
+ bodyFont: "Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
1374
+ monoFont: "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
1375
+ baseFontSize: 11,
1376
+ headingScale: 1.25,
1377
+ headingWeight: 600,
1378
+ bodyWeight: 400,
1379
+ lineHeight: 1.5,
1380
+ headingLetterSpacing: -0.01
1381
+ },
1382
+ colors: {
1383
+ primary: "#1F3A5F",
1384
+ secondary: "#4A6FA5",
1385
+ accent: "#E0B04B",
1386
+ text: "#1A1D21",
1387
+ mutedText: "#6B7280",
1388
+ surface: "#FFFFFF",
1389
+ background: "#F5F6F8",
1390
+ border: "#E2E5EA",
1391
+ success: "#1E8E5A",
1392
+ warning: "#C77D0A",
1393
+ danger: "#C0392B",
1394
+ chart: ["#1F3A5F", "#4A6FA5", "#E0B04B", "#7FB3D5", "#A9CCE3", "#F4D03F", "#6C7A89", "#B0BEC5"]
1395
+ },
1396
+ radius: 6,
1397
+ table: {
1398
+ headerBackground: "#F3F4F6",
1399
+ headerColor: "#1A1D21",
1400
+ zebraBackground: "#FAFAFB",
1401
+ borderColor: "#E2E5EA"
1402
+ }
1403
+ };
1404
+ function defaultDocumentStyles() {
1405
+ return {
1406
+ text: {
1407
+ h1: { fontFamily: "$typography.headingFont", fontSize: 26, fontWeight: 600, lineHeight: 1.2, letterSpacing: -0.02, color: "$colors.primary", marginTop: 0, marginBottom: 8, keepWithNext: true },
1408
+ h2: { fontFamily: "$typography.headingFont", fontSize: 18, fontWeight: 600, lineHeight: 1.25, letterSpacing: -0.01, color: "$colors.primary", marginTop: 12, marginBottom: 6, keepWithNext: true },
1409
+ h3: { fontFamily: "$typography.headingFont", fontSize: 14, fontWeight: 600, lineHeight: 1.3, color: "$colors.text", marginTop: 8, marginBottom: 4, keepWithNext: true },
1410
+ h4: { fontFamily: "$typography.headingFont", fontSize: 12, fontWeight: 600, lineHeight: 1.35, color: "$colors.text", textTransform: "uppercase", letterSpacing: 0.04, marginTop: 8, marginBottom: 4, keepWithNext: true },
1411
+ body: { fontFamily: "$typography.bodyFont", fontSize: 11, fontWeight: 400, lineHeight: 1.5, color: "$colors.text", marginTop: 0, marginBottom: 0 },
1412
+ lead: { fontFamily: "$typography.bodyFont", fontSize: 13, fontWeight: 400, lineHeight: 1.55, color: "$colors.text", marginTop: 0, marginBottom: 0 },
1413
+ caption: { fontFamily: "$typography.bodyFont", fontSize: 9, fontWeight: 400, lineHeight: 1.4, color: "$colors.mutedText", marginTop: 0, marginBottom: 0 },
1414
+ small: { fontFamily: "$typography.bodyFont", fontSize: 8, fontWeight: 400, lineHeight: 1.4, color: "$colors.mutedText", marginTop: 0, marginBottom: 0 }
1415
+ },
1416
+ table: {
1417
+ fontSize: 10,
1418
+ headerBackground: "$table.headerBackground",
1419
+ headerColor: "$table.headerColor",
1420
+ headerFontWeight: 600,
1421
+ borderColor: "$table.borderColor",
1422
+ borderWidth: 1,
1423
+ borders: "horizontal",
1424
+ zebraBackground: "$table.zebraBackground",
1425
+ cellPaddingX: 8,
1426
+ cellPaddingY: 6
1427
+ },
1428
+ card: {
1429
+ background: "$colors.surface",
1430
+ borderColor: "$colors.border",
1431
+ borderWidth: 1,
1432
+ borderRadius: 6,
1433
+ padding: 16,
1434
+ shadow: false
1435
+ },
1436
+ link: { color: "$colors.secondary", underline: true },
1437
+ spacing: { blockGap: 8, sectionPadding: 0, columnGap: 16 }
1438
+ };
1439
+ }
1440
+
1441
+ // src/theme/tokens.ts
1442
+ function isTokenRef(value) {
1443
+ return typeof value === "string" && value.startsWith("$") && value.length > 1;
1444
+ }
1445
+ function resolveToken(token, theme) {
1446
+ const path = token.slice(1).split(".");
1447
+ const fromTheme = walk(theme, path);
1448
+ if (typeof fromTheme === "string") return fromTheme;
1449
+ const fromDefault = walk(DEFAULT_THEME, path);
1450
+ return typeof fromDefault === "string" ? fromDefault : void 0;
1451
+ }
1452
+ function walk(obj, path) {
1453
+ let cur = obj;
1454
+ for (const p of path) {
1455
+ if (cur === null || cur === void 0 || typeof cur !== "object") return void 0;
1456
+ if (!Object.prototype.hasOwnProperty.call(cur, p)) return void 0;
1457
+ cur = cur[p];
1458
+ }
1459
+ return cur;
1460
+ }
1461
+ function resolveValue(value, theme, fallback) {
1462
+ if (value === void 0 || value === null || value === "") return fallback;
1463
+ if (isTokenRef(value)) return resolveToken(value, theme) ?? fallback;
1464
+ return value;
1465
+ }
1466
+ function listThemeTokens(theme) {
1467
+ const merged = mergeTheme(theme);
1468
+ const out = [];
1469
+ for (const [k, v] of Object.entries(merged.colors)) if (typeof v === "string") out.push({ token: `$colors.${k}`, label: humanize(k), value: v, group: "colors" });
1470
+ for (const [k, v] of Object.entries(merged.typography)) if (typeof v === "string") out.push({ token: `$typography.${k}`, label: humanize(k), value: v, group: "typography" });
1471
+ for (const [k, v] of Object.entries(merged.table ?? {})) if (typeof v === "string") out.push({ token: `$table.${k}`, label: humanize(k), value: v, group: "table" });
1472
+ if (merged.logo) out.push({ token: "$logo", label: "Logo", value: merged.logo, group: "brand" });
1473
+ if (merged.logoDark) out.push({ token: "$logoDark", label: "Logo (dark)", value: merged.logoDark, group: "brand" });
1474
+ return out;
1475
+ }
1476
+ function humanize(key) {
1477
+ return key.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
1478
+ }
1479
+ function mergeTheme(theme) {
1480
+ if (!theme) return DEFAULT_THEME;
1481
+ return {
1482
+ ...DEFAULT_THEME,
1483
+ ...theme,
1484
+ typography: { ...DEFAULT_THEME.typography, ...theme.typography ?? {} },
1485
+ colors: { ...DEFAULT_THEME.colors, ...theme.colors ?? {}, chart: theme.colors?.chart?.length ? theme.colors.chart : DEFAULT_THEME.colors.chart },
1486
+ table: { ...DEFAULT_THEME.table, ...theme.table ?? {} },
1487
+ fonts: theme.fonts ?? [],
1488
+ meta: theme.meta ?? {}
1489
+ };
1490
+ }
1491
+ function missingThemeValues(theme) {
1492
+ const missing = [];
1493
+ const required = ["primary", "secondary", "accent", "text", "mutedText", "surface", "background", "border"];
1494
+ for (const k of required) if (!theme.colors?.[k]) missing.push(`colors.${k}`);
1495
+ if (!theme.typography?.headingFont) missing.push("typography.headingFont");
1496
+ if (!theme.typography?.bodyFont) missing.push("typography.bodyFont");
1497
+ return missing;
1498
+ }
1499
+ function isDarkColor(hex) {
1500
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
1501
+ if (!m) return false;
1502
+ const n = parseInt(m[1], 16);
1503
+ const r = n >> 16 & 255;
1504
+ const g2 = n >> 8 & 255;
1505
+ const b = n & 255;
1506
+ const lum = 0.2126 * srgb(r) + 0.7152 * srgb(g2) + 0.0722 * srgb(b);
1507
+ return lum < 0.4;
1508
+ }
1509
+ function srgb(c) {
1510
+ const v = c / 255;
1511
+ return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
1512
+ }
1513
+ var TEXT_STYLE_NAMES = {
1514
+ h1: "Heading 1",
1515
+ h2: "Heading 2",
1516
+ h3: "Heading 3",
1517
+ h4: "Heading 4",
1518
+ h5: "Heading 5",
1519
+ h6: "Heading 6",
1520
+ body: "Body text",
1521
+ lead: "Lead paragraph",
1522
+ caption: "Caption",
1523
+ small: "Small print"
1524
+ };
1525
+ function textStyleLabel(id) {
1526
+ return TEXT_STYLE_NAMES[id] ?? humanize(id);
1527
+ }
1528
+
1529
+ // src/theme/resolve-style.ts
1530
+ function spacing(base, ...layers) {
1531
+ const out = { ...base };
1532
+ for (const l of layers) {
1533
+ if (!l) continue;
1534
+ if (l.top !== void 0) out.top = l.top;
1535
+ if (l.right !== void 0) out.right = l.right;
1536
+ if (l.bottom !== void 0) out.bottom = l.bottom;
1537
+ if (l.left !== void 0) out.left = l.left;
1538
+ }
1539
+ return out;
1540
+ }
1541
+ function resolveStyle(input) {
1542
+ const { theme, styles } = input;
1543
+ const text = styles.text[input.textStyle ?? "body"] ?? styles.text.body ?? {};
1544
+ const d = input.defaults ?? {};
1545
+ const o = input.allowOverrides === false ? {} : input.overrides ?? {};
1546
+ const typo = theme.typography;
1547
+ const fontFamily = resolveValue(o.fontFamily ?? d.fontFamily ?? text.fontFamily, theme, typo.bodyFont);
1548
+ const fontSize = o.fontSize ?? d.fontSize ?? text.fontSize ?? typo.baseFontSize ?? 11;
1549
+ const fontWeight = o.fontWeight ?? d.fontWeight ?? text.fontWeight ?? typo.bodyWeight ?? 400;
1550
+ const lineHeight = o.lineHeight ?? d.lineHeight ?? text.lineHeight ?? typo.lineHeight ?? 1.5;
1551
+ const border = o.border || d.border ? {
1552
+ width: o.border?.width ?? d.border?.width ?? 1,
1553
+ style: o.border?.style ?? d.border?.style ?? "solid",
1554
+ color: resolveValue(o.border?.color ?? d.border?.color, theme, theme.colors.border),
1555
+ radius: o.border?.radius ?? d.border?.radius ?? 0,
1556
+ sides: o.border?.sides ?? d.border?.sides ?? ["top", "right", "bottom", "left"]
1557
+ } : void 0;
1558
+ const background = resolveOptional(o.background ?? d.background, theme);
1559
+ return {
1560
+ fontFamily,
1561
+ fontSize,
1562
+ fontWeight,
1563
+ fontStyle: o.fontStyle ?? d.fontStyle ?? text.fontStyle ?? "normal",
1564
+ lineHeight,
1565
+ letterSpacing: o.letterSpacing ?? d.letterSpacing ?? text.letterSpacing ?? 0,
1566
+ textTransform: o.textTransform ?? d.textTransform ?? text.textTransform ?? "none",
1567
+ textAlign: o.textAlign ?? d.textAlign ?? "left",
1568
+ verticalAlign: o.verticalAlign ?? d.verticalAlign ?? "top",
1569
+ color: resolveValue(o.color ?? d.color ?? text.color, theme, theme.colors.text),
1570
+ background,
1571
+ padding: spacing({ top: 0, right: 0, bottom: 0, left: 0 }, d.padding, o.padding),
1572
+ margin: spacing({ top: text.marginTop ?? 0, right: 0, bottom: text.marginBottom ?? 0, left: 0 }, d.margin, o.margin),
1573
+ border,
1574
+ width: o.width ?? d.width,
1575
+ height: o.height ?? d.height,
1576
+ align: o.align ?? d.align,
1577
+ opacity: o.opacity ?? d.opacity
1578
+ };
1579
+ }
1580
+ function resolveOptional(value, theme) {
1581
+ if (!value || value === "transparent" || value === "none") return void 0;
1582
+ return resolveValue(value, theme, "");
1583
+ }
1584
+ function styleToCss(s, opts = { text: true, box: true }) {
1585
+ const css = {};
1586
+ if (opts.text !== false) {
1587
+ css["font-family"] = s.fontFamily;
1588
+ css["font-size"] = `${s.fontSize}px`;
1589
+ css["font-weight"] = String(s.fontWeight);
1590
+ css["font-style"] = s.fontStyle;
1591
+ css["line-height"] = String(s.lineHeight);
1592
+ if (s.letterSpacing) css["letter-spacing"] = `${s.letterSpacing}em`;
1593
+ if (s.textTransform !== "none") css["text-transform"] = s.textTransform;
1594
+ css["text-align"] = s.textAlign;
1595
+ css["color"] = s.color;
1596
+ }
1597
+ if (opts.box !== false) {
1598
+ if (s.background) css["background"] = s.background;
1599
+ const p = s.padding;
1600
+ if (p.top || p.right || p.bottom || p.left) css["padding"] = `${p.top}px ${p.right}px ${p.bottom}px ${p.left}px`;
1601
+ const m = s.margin;
1602
+ if (m.top || m.right || m.bottom || m.left) css["margin"] = `${m.top}px ${m.right}px ${m.bottom}px ${m.left}px`;
1603
+ if (s.border && s.border.width > 0 && s.border.style !== "none") {
1604
+ const decl = `${s.border.width}px ${s.border.style} ${s.border.color}`;
1605
+ if (s.border.sides.length === 4) css["border"] = decl;
1606
+ else for (const side of s.border.sides) css[`border-${side}`] = decl;
1607
+ }
1608
+ if (s.border?.radius) css["border-radius"] = `${s.border.radius}px`;
1609
+ if (s.width !== void 0) css["width"] = typeof s.width === "number" ? `${s.width}px` : s.width;
1610
+ if (s.height !== void 0) css["height"] = `${s.height}px`;
1611
+ if (s.opacity !== void 0 && s.opacity < 1) css["opacity"] = String(s.opacity);
1612
+ }
1613
+ return css;
1614
+ }
1615
+ function cssText(css) {
1616
+ return Object.entries(css).filter(([, v]) => v !== void 0 && v !== "").map(([k, v]) => `${k}:${v}`).join(";");
1617
+ }
1618
+
1619
+ // src/vnode/h.ts
1620
+ var SVG_TAGS = /* @__PURE__ */ new Set([
1621
+ "svg",
1622
+ "g",
1623
+ "path",
1624
+ "rect",
1625
+ "circle",
1626
+ "ellipse",
1627
+ "line",
1628
+ "polyline",
1629
+ "polygon",
1630
+ "text",
1631
+ "tspan",
1632
+ "defs",
1633
+ "clipPath",
1634
+ "linearGradient",
1635
+ "radialGradient",
1636
+ "stop",
1637
+ "use",
1638
+ "symbol",
1639
+ "mask",
1640
+ "pattern",
1641
+ "title",
1642
+ "desc",
1643
+ "foreignObject"
1644
+ ]);
1645
+ function h(tag, props, ...children) {
1646
+ const node = { tag, props: props ?? {}, children: [] };
1647
+ if (SVG_TAGS.has(tag)) node.ns = "svg";
1648
+ appendChildren(node.children, children);
1649
+ return node;
1650
+ }
1651
+ function appendChildren(out, children) {
1652
+ for (const c of children) {
1653
+ if (c === null || c === void 0 || c === false || c === "") continue;
1654
+ if (Array.isArray(c)) appendChildren(out, c);
1655
+ else if (typeof c === "number") out.push(String(c));
1656
+ else out.push(c);
1657
+ }
1658
+ }
1659
+ function isVNode(v) {
1660
+ return !!v && typeof v === "object" && typeof v.tag === "string" && Array.isArray(v.children);
1661
+ }
1662
+ function classNames(value) {
1663
+ if (!value) return "";
1664
+ if (typeof value === "string") return value;
1665
+ return value.filter(Boolean).join(" ");
1666
+ }
1667
+ function styleString(value) {
1668
+ if (!value) return "";
1669
+ if (typeof value === "string") return value;
1670
+ return Object.entries(value).filter(([, v]) => v !== void 0 && v !== "" && v !== null).map(([k, v]) => `${k}:${v}`).join(";");
1671
+ }
1672
+ var ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
1673
+ function escapeHtml(s) {
1674
+ return s.replace(/[&<>"']/g, (c) => ESC[c]);
1675
+ }
1676
+ var VOID = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"]);
1677
+ function toHtml(node) {
1678
+ if (Array.isArray(node)) return node.map(toHtml).join("");
1679
+ if (typeof node === "string") return escapeHtml(node);
1680
+ const attrs = [];
1681
+ for (const [k, v] of Object.entries(node.props)) {
1682
+ if (v === void 0 || v === null || v === false) continue;
1683
+ if (k === "class") {
1684
+ const c = classNames(v);
1685
+ if (c) attrs.push(`class="${escapeHtml(c)}"`);
1686
+ } else if (k === "style") {
1687
+ const s = styleString(v);
1688
+ if (s) attrs.push(`style="${escapeHtml(s)}"`);
1689
+ } else if (v === true) attrs.push(escapeHtml(k));
1690
+ else attrs.push(`${escapeHtml(k)}="${escapeHtml(String(v))}"`);
1691
+ }
1692
+ const open = `<${node.tag}${attrs.length ? " " + attrs.join(" ") : ""}>`;
1693
+ if (VOID.has(node.tag)) return open;
1694
+ return `${open}${node.children.map(toHtml).join("")}</${node.tag}>`;
1695
+ }
1696
+ function walkVNodes(node, visit) {
1697
+ if (visit(node) === false) return;
1698
+ for (const c of node.children) if (typeof c !== "string") walkVNodes(c, visit);
1699
+ }
1700
+
1701
+ // src/registry/block-definition.ts
1702
+ function defineBlock(def) {
1703
+ if (!def.type || !/^[a-z0-9][a-z0-9._-]*$/i.test(def.type)) {
1704
+ throw new Error(`Invalid block type "${def.type}". Use letters, digits, dots, dashes and underscores.`);
1705
+ }
1706
+ return def;
1707
+ }
1708
+ function resolveDefaultProps(def) {
1709
+ const d = def.defaultProps;
1710
+ return typeof d === "function" ? structuredClone(d()) : structuredClone(d);
1711
+ }
1712
+
1713
+ // src/registry/registry.ts
1714
+ var TIER_RANK = { community: 0, pro: 1, enterprise: 2 };
1715
+ var BlockRegistry = class {
1716
+ defs = /* @__PURE__ */ new Map();
1717
+ listeners = /* @__PURE__ */ new Set();
1718
+ enabledTiers;
1719
+ constructor(defs = [], opts = {}) {
1720
+ this.enabledTiers = new Set(opts.enabledTiers ?? ["community", "pro", "enterprise"]);
1721
+ for (const d of defs) this.register(d);
1722
+ }
1723
+ register(def) {
1724
+ if (this.defs.has(def.type)) throw new Error(`Block type "${def.type}" is already registered`);
1725
+ this.defs.set(def.type, def);
1726
+ this.emit();
1727
+ return this;
1728
+ }
1729
+ /** Replaces an existing definition (used for hot-swapping custom blocks in dev). */
1730
+ replace(def) {
1731
+ this.defs.set(def.type, def);
1732
+ this.emit();
1733
+ return this;
1734
+ }
1735
+ unregister(type) {
1736
+ this.defs.delete(type);
1737
+ this.emit();
1738
+ return this;
1739
+ }
1740
+ get(type) {
1741
+ return this.defs.get(type);
1742
+ }
1743
+ has(type) {
1744
+ return this.defs.has(type);
1745
+ }
1746
+ require(type) {
1747
+ const d = this.defs.get(type);
1748
+ if (!d) throw new Error(`Unknown block type "${type}"`);
1749
+ return d;
1750
+ }
1751
+ list() {
1752
+ return [...this.defs.values()];
1753
+ }
1754
+ /** Blocks shown in the palette, grouped by category, honouring `hidden`. */
1755
+ palette() {
1756
+ return this.list().filter((d) => !d.hidden && d.category !== "structure");
1757
+ }
1758
+ isTierEnabled(tier) {
1759
+ return this.enabledTiers.has(tier ?? "community");
1760
+ }
1761
+ /** The highest tier required by a set of block types. */
1762
+ requiredTier(types) {
1763
+ let best = "community";
1764
+ for (const t of types) {
1765
+ const tier = this.defs.get(t)?.license ?? "community";
1766
+ if (TIER_RANK[tier] > TIER_RANK[best]) best = tier;
1767
+ }
1768
+ return best;
1769
+ }
1770
+ /** Effective binding specs for a block: explicit `bindings`, else derived from the inspector. */
1771
+ bindingsOf(type) {
1772
+ const def = this.defs.get(type);
1773
+ if (!def) return [];
1774
+ return bindingSpecsFor(def);
1775
+ }
1776
+ /** Whether `childType` may be placed inside `parentType`. */
1777
+ accepts(parentType, childType) {
1778
+ const parent = this.defs.get(parentType);
1779
+ const child = this.defs.get(childType);
1780
+ if (!parent?.container) return false;
1781
+ for (const rule of parent.container.accepts) {
1782
+ if (rule === childType) return true;
1783
+ if (rule === "@blocks" && child && child.category !== "structure") return true;
1784
+ if (rule === "@rows" && childType === "row") return true;
1785
+ if (rule === "@columns" && childType === "column") return true;
1786
+ if (rule === "@sections" && childType === "section") return true;
1787
+ }
1788
+ return false;
1789
+ }
1790
+ subscribe(fn) {
1791
+ this.listeners.add(fn);
1792
+ return () => this.listeners.delete(fn);
1793
+ }
1794
+ emit() {
1795
+ for (const l of this.listeners) l();
1796
+ }
1797
+ };
1798
+ var bindingCache = /* @__PURE__ */ new WeakMap();
1799
+ function bindingSpecsFor(def) {
1800
+ const hit = bindingCache.get(def);
1801
+ if (hit) return hit;
1802
+ let specs;
1803
+ if (def.bindings) specs = def.bindings;
1804
+ else {
1805
+ specs = [];
1806
+ const visit = (fields) => {
1807
+ for (const f of fields) {
1808
+ switch (f.kind) {
1809
+ case "binding":
1810
+ specs.push({ prop: f.prop, kind: "value", valueType: f.valueType });
1811
+ break;
1812
+ case "collection":
1813
+ specs.push({ prop: f.prop, kind: "collection" });
1814
+ break;
1815
+ case "expression":
1816
+ specs.push({ prop: f.prop, kind: "value" });
1817
+ break;
1818
+ case "richtext":
1819
+ specs.push({ prop: f.prop, kind: "richtext" });
1820
+ break;
1821
+ case "condition":
1822
+ specs.push({ prop: f.prop, kind: "condition" });
1823
+ break;
1824
+ case "text":
1825
+ case "textarea":
1826
+ if (f.allowTokens !== false) specs.push({ prop: f.prop, kind: "template" });
1827
+ break;
1828
+ case "image":
1829
+ if (f.allowBinding !== false) specs.push({ prop: f.prop, kind: "template" });
1830
+ break;
1831
+ case "list":
1832
+ visit(f.fields);
1833
+ break;
1834
+ default:
1835
+ break;
1836
+ }
1837
+ }
1838
+ };
1839
+ for (const s of def.inspector ?? []) visit(s.fields);
1840
+ }
1841
+ bindingCache.set(def, specs);
1842
+ return specs;
1843
+ }
1844
+
1845
+ // src/schema/template.schema.ts
1846
+ import { z } from "zod";
1847
+ var color = z.string().min(1);
1848
+ var spacing2 = z.object({ top: z.number().optional(), right: z.number().optional(), bottom: z.number().optional(), left: z.number().optional() }).partial();
1849
+ var borderSpec = z.object({
1850
+ width: z.number().min(0).optional(),
1851
+ style: z.enum(["solid", "dashed", "dotted", "none"]).optional(),
1852
+ color: color.optional(),
1853
+ radius: z.number().min(0).optional(),
1854
+ sides: z.array(z.enum(["top", "right", "bottom", "left"])).optional()
1855
+ }).partial();
1856
+ var blockStyleSchema = z.object({
1857
+ textStyle: z.string().optional(),
1858
+ fontFamily: z.string().optional(),
1859
+ fontSize: z.number().positive().optional(),
1860
+ fontWeight: z.union([z.number(), z.enum(["normal", "bold"])]).optional(),
1861
+ fontStyle: z.enum(["normal", "italic"]).optional(),
1862
+ lineHeight: z.number().positive().optional(),
1863
+ letterSpacing: z.number().optional(),
1864
+ textTransform: z.enum(["none", "uppercase", "lowercase", "capitalize"]).optional(),
1865
+ textAlign: z.enum(["left", "center", "right", "justify"]).optional(),
1866
+ verticalAlign: z.enum(["top", "middle", "bottom"]).optional(),
1867
+ color: color.optional(),
1868
+ background: color.optional(),
1869
+ padding: spacing2.optional(),
1870
+ margin: spacing2.optional(),
1871
+ border: borderSpec.optional(),
1872
+ width: z.union([z.number(), z.string()]).optional(),
1873
+ height: z.number().optional(),
1874
+ align: z.enum(["left", "center", "right"]).optional(),
1875
+ opacity: z.number().min(0).max(1).optional()
1876
+ }).partial();
1877
+ var conditionRule = z.object({
1878
+ left: z.string(),
1879
+ operator: z.enum(["eq", "neq", "gt", "gte", "lt", "lte", "contains", "startsWith", "endsWith", "in", "isEmpty", "isNotEmpty", "isTrue", "isFalse"]),
1880
+ right: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional()
1881
+ });
1882
+ var conditionGroupSchema = z.lazy(
1883
+ () => z.object({ op: z.enum(["and", "or"]), rules: z.array(z.union([conditionRule, conditionGroupSchema])) })
1884
+ );
1885
+ var conditionSchema = z.union([z.string(), conditionGroupSchema]);
1886
+ var flowSchema = z.object({
1887
+ breakBefore: z.enum(["auto", "page"]).optional(),
1888
+ breakAfter: z.enum(["auto", "page"]).optional(),
1889
+ keepTogether: z.boolean().optional(),
1890
+ keepWithNext: z.boolean().optional(),
1891
+ minOrphans: z.number().int().min(0).optional(),
1892
+ minWidows: z.number().int().min(0).optional()
1893
+ }).partial();
1894
+ var nodeSchema = z.lazy(
1895
+ () => z.object({
1896
+ id: z.string().min(1),
1897
+ type: z.string().min(1),
1898
+ props: z.record(z.unknown()),
1899
+ style: blockStyleSchema.optional(),
1900
+ children: z.array(nodeSchema).optional(),
1901
+ visibleWhen: conditionSchema.optional(),
1902
+ flow: flowSchema.optional(),
1903
+ name: z.string().optional(),
1904
+ locked: z.boolean().optional()
1905
+ })
1906
+ );
1907
+ var textStyleSchema = z.object({
1908
+ fontFamily: z.string().optional(),
1909
+ fontSize: z.number().positive().optional(),
1910
+ fontWeight: z.union([z.number(), z.enum(["normal", "bold"])]).optional(),
1911
+ fontStyle: z.enum(["normal", "italic"]).optional(),
1912
+ lineHeight: z.number().positive().optional(),
1913
+ letterSpacing: z.number().optional(),
1914
+ textTransform: z.enum(["none", "uppercase", "lowercase", "capitalize"]).optional(),
1915
+ color: color.optional(),
1916
+ marginTop: z.number().optional(),
1917
+ marginBottom: z.number().optional(),
1918
+ keepWithNext: z.boolean().optional()
1919
+ }).partial();
1920
+ var pageRegion = z.object({ height: z.number().min(0), differentFirstPage: z.boolean() });
1921
+ var pageSettingsSchema = z.object({
1922
+ size: z.union([z.enum(["A4", "A3", "A5", "Letter", "Legal", "Tabloid"]), z.object({ width: z.number().positive(), height: z.number().positive() })]),
1923
+ orientation: z.enum(["portrait", "landscape"]),
1924
+ margins: z.object({ top: z.number().min(0), right: z.number().min(0), bottom: z.number().min(0), left: z.number().min(0) }),
1925
+ header: pageRegion,
1926
+ footer: pageRegion,
1927
+ background: color.optional(),
1928
+ minPages: z.number().int().min(1).optional()
1929
+ });
1930
+ var documentStylesSchema = z.object({
1931
+ text: z.record(textStyleSchema),
1932
+ table: z.object({
1933
+ fontSize: z.number().optional(),
1934
+ headerBackground: color.optional(),
1935
+ headerColor: color.optional(),
1936
+ headerFontWeight: z.union([z.number(), z.enum(["normal", "bold"])]).optional(),
1937
+ borderColor: color.optional(),
1938
+ borderWidth: z.number().optional(),
1939
+ borders: z.enum(["all", "horizontal", "outer", "none"]).optional(),
1940
+ zebraBackground: color.optional(),
1941
+ cellPaddingX: z.number().optional(),
1942
+ cellPaddingY: z.number().optional()
1943
+ }).partial(),
1944
+ card: z.object({
1945
+ background: color.optional(),
1946
+ borderColor: color.optional(),
1947
+ borderWidth: z.number().optional(),
1948
+ borderRadius: z.number().optional(),
1949
+ padding: z.number().optional(),
1950
+ shadow: z.boolean().optional()
1951
+ }).partial(),
1952
+ link: z.object({ color: color.optional(), underline: z.boolean().optional() }).partial(),
1953
+ spacing: z.object({ blockGap: z.number(), sectionPadding: z.number(), columnGap: z.number() })
1954
+ });
1955
+ var regionContent = z.object({ default: z.array(nodeSchema), firstPage: z.array(nodeSchema).optional() });
1956
+ var templateSchema = z.object({
1957
+ schemaVersion: z.literal(CURRENT_SCHEMA_VERSION),
1958
+ id: z.string().min(1),
1959
+ name: z.string(),
1960
+ meta: z.object({
1961
+ createdAt: z.string().optional(),
1962
+ updatedAt: z.string().optional(),
1963
+ author: z.string().optional(),
1964
+ description: z.string().optional(),
1965
+ tags: z.array(z.string()).optional(),
1966
+ custom: z.record(z.unknown()).optional()
1967
+ }).partial().optional(),
1968
+ page: pageSettingsSchema,
1969
+ styles: documentStylesSchema,
1970
+ header: regionContent,
1971
+ footer: regionContent,
1972
+ body: z.array(nodeSchema),
1973
+ library: z.array(
1974
+ z.object({
1975
+ id: z.string(),
1976
+ name: z.string(),
1977
+ description: z.string().optional(),
1978
+ category: z.string().optional(),
1979
+ node: nodeSchema,
1980
+ thumbnail: z.string().optional()
1981
+ })
1982
+ ).optional(),
1983
+ settings: z.object({
1984
+ locale: z.string().optional(),
1985
+ currency: z.string().optional(),
1986
+ timeZone: z.string().optional(),
1987
+ dataSources: z.array(z.string()).optional()
1988
+ }).partial().optional()
1989
+ });
1990
+ function parseTemplate(json) {
1991
+ const r = templateSchema.safeParse(json);
1992
+ if (r.success) return { ok: true, template: r.data, errors: [] };
1993
+ return { ok: false, errors: r.error.issues.map((i) => ({ path: i.path.join("."), message: i.message })) };
1994
+ }
1995
+
1996
+ // src/schema/ids.ts
1997
+ var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
1998
+ function randomBytes(n) {
1999
+ const out = new Uint8Array(n);
2000
+ const c = globalThis.crypto;
2001
+ if (c?.getRandomValues) c.getRandomValues(out);
2002
+ else for (let i = 0; i < n; i++) out[i] = Math.floor(Math.random() * 256);
2003
+ return out;
2004
+ }
2005
+ var counter = 0;
2006
+ var deterministic = null;
2007
+ function createId(prefix = "n") {
2008
+ if (deterministic !== null) return `${prefix}_${deterministic}${(++counter).toString(36)}`;
2009
+ const bytes = randomBytes(10);
2010
+ let s = "";
2011
+ for (const b of bytes) s += ALPHABET[b % ALPHABET.length];
2012
+ return `${prefix}_${s}`;
2013
+ }
2014
+ function setDeterministicIds(seed) {
2015
+ deterministic = seed;
2016
+ counter = 0;
2017
+ }
2018
+
2019
+ // src/schema/migrate.ts
2020
+ var MIGRATIONS = [
2021
+ {
2022
+ from: 0,
2023
+ to: 1,
2024
+ description: "Pre-release drafts: normalise `content` \u2192 `body`, add page regions and styles",
2025
+ migrate(t) {
2026
+ const out = { ...t, schemaVersion: 1 };
2027
+ if (Array.isArray(t.content) && !t.body) {
2028
+ out.body = t.content;
2029
+ delete out.content;
2030
+ }
2031
+ out.header ??= { default: [] };
2032
+ out.footer ??= { default: [] };
2033
+ return out;
2034
+ }
2035
+ }
2036
+ ];
2037
+ var MigrationError = class extends Error {
2038
+ constructor(message) {
2039
+ super(message);
2040
+ this.name = "MigrationError";
2041
+ }
2042
+ };
2043
+ function migrateTemplate(input) {
2044
+ const fromVersion = typeof input.schemaVersion === "number" ? input.schemaVersion : 0;
2045
+ if (fromVersion > CURRENT_SCHEMA_VERSION) {
2046
+ throw new MigrationError(`Template schemaVersion ${fromVersion} is newer than this version of BroadPaper supports (${CURRENT_SCHEMA_VERSION}). Upgrade the SDK.`);
2047
+ }
2048
+ let current2 = structuredClone(input);
2049
+ const applied = [];
2050
+ let v = fromVersion;
2051
+ while (v < CURRENT_SCHEMA_VERSION) {
2052
+ const step = MIGRATIONS.find((m) => m.from === v);
2053
+ if (!step) throw new MigrationError(`No migration from schemaVersion ${v} to ${v + 1}`);
2054
+ current2 = step.migrate(current2);
2055
+ current2.schemaVersion = step.to;
2056
+ applied.push(step);
2057
+ v = step.to;
2058
+ }
2059
+ return { template: current2, applied, fromVersion };
2060
+ }
2061
+ function needsMigration(input) {
2062
+ return (input.schemaVersion ?? 0) < CURRENT_SCHEMA_VERSION;
2063
+ }
2064
+
2065
+ // src/schema/create.ts
2066
+ function defaultPageSettings(overrides = {}) {
2067
+ return {
2068
+ size: "A4",
2069
+ orientation: "portrait",
2070
+ margins: { top: 18, right: 18, bottom: 18, left: 18 },
2071
+ header: { height: 14, differentFirstPage: false },
2072
+ footer: { height: 12, differentFirstPage: false },
2073
+ ...overrides
2074
+ };
2075
+ }
2076
+ function createTemplate(opts = {}) {
2077
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2078
+ return {
2079
+ schemaVersion: CURRENT_SCHEMA_VERSION,
2080
+ id: opts.id ?? createId("tpl"),
2081
+ name: opts.name ?? "Untitled report",
2082
+ meta: { createdAt: now, updatedAt: now },
2083
+ page: defaultPageSettings(opts.page),
2084
+ styles: defaultDocumentStyles(),
2085
+ header: { default: [] },
2086
+ footer: { default: [] },
2087
+ body: opts.withEmptySection === false ? [] : [createSection()],
2088
+ settings: { locale: opts.locale ?? "en-GB", currency: opts.currency ?? "GBP" }
2089
+ };
2090
+ }
2091
+ function createNode(registry, type, opts = {}) {
2092
+ const def = registry.get(type);
2093
+ const props = { ...def ? resolveDefaultProps(def) : {}, ...opts.props ?? {} };
2094
+ const node = { id: opts.id ?? createId(), type, props };
2095
+ if (opts.style) node.style = opts.style;
2096
+ if (def?.container || opts.children) node.children = opts.children ?? [];
2097
+ if (opts.flow) node.flow = opts.flow;
2098
+ if (opts.visibleWhen !== void 0) node.visibleWhen = opts.visibleWhen;
2099
+ if (opts.name) node.name = opts.name;
2100
+ return node;
2101
+ }
2102
+ function createSection(opts = {}) {
2103
+ return { id: opts.id ?? createId("sec"), type: "section", props: { padding: 0, ...opts.props ?? {} }, children: opts.children ?? [], ...opts.style ? { style: opts.style } : {}, ...opts.flow ? { flow: opts.flow } : {}, ...opts.name ? { name: opts.name } : {} };
2104
+ }
2105
+ function createColumn(span, children = [], id) {
2106
+ return { id: id ?? createId("col"), type: "column", props: { span }, children };
2107
+ }
2108
+ function createRow(layout, columns, opts = {}) {
2109
+ const total = layout.reduce((a, b) => a + b, 0);
2110
+ if (total !== 12) throw new Error(`Row layout must sum to 12, got ${total} from [${layout.join(", ")}]`);
2111
+ return {
2112
+ id: opts.id ?? createId("row"),
2113
+ type: "row",
2114
+ props: { layout, ...opts.props ?? {} },
2115
+ children: layout.map((span, i) => createColumn(span, columns?.[i] ?? [])),
2116
+ ...opts.style ? { style: opts.style } : {},
2117
+ ...opts.flow ? { flow: opts.flow } : {},
2118
+ ...opts.name ? { name: opts.name } : {}
2119
+ };
2120
+ }
2121
+ var ROW_LAYOUTS = [
2122
+ { id: "12", label: "Full width", layout: [12] },
2123
+ { id: "6-6", label: "Two columns", layout: [6, 6] },
2124
+ { id: "4-4-4", label: "Three columns", layout: [4, 4, 4] },
2125
+ { id: "3-3-3-3", label: "Four columns", layout: [3, 3, 3, 3] },
2126
+ { id: "8-4", label: "Wide left", layout: [8, 4] },
2127
+ { id: "4-8", label: "Wide right", layout: [4, 8] },
2128
+ { id: "9-3", label: "Sidebar right", layout: [9, 3] },
2129
+ { id: "3-9", label: "Sidebar left", layout: [3, 9] }
2130
+ ];
2131
+ function cloneWithNewIds(node) {
2132
+ const prefix = node.type === "section" ? "sec" : node.type === "row" ? "row" : node.type === "column" ? "col" : "n";
2133
+ return {
2134
+ ...structuredClone(node),
2135
+ id: createId(prefix),
2136
+ children: node.children?.map(cloneWithNewIds)
2137
+ };
2138
+ }
2139
+
2140
+ // src/data/sample.ts
2141
+ function synthesizeSampleData(sources, opts = {}) {
2142
+ const out = {};
2143
+ for (const s of sources) out[s.id] = synthesizeObject(s.schema, opts, 0);
2144
+ return out;
2145
+ }
2146
+ function synthesizeValue(schema, opts = {}, depth = 0, index = 0) {
2147
+ if (schema.example !== void 0) return schema.example;
2148
+ if (schema.enum?.length) return schema.enum[index % schema.enum.length];
2149
+ const now = opts.now ?? /* @__PURE__ */ new Date();
2150
+ switch (schema.type) {
2151
+ case "string":
2152
+ return schema.label ?? "Sample text";
2153
+ case "number":
2154
+ return [1240, 875, 2310, 1580, 990, 1870][index % 6];
2155
+ case "currency":
2156
+ return [125e3, 48200.5, 231e3, 15800, 99e3, 187500][index % 6];
2157
+ case "percent":
2158
+ return [0.125, 0.082, 0.31, 0.058, 0.196, 0.229][index % 6];
2159
+ case "date":
2160
+ case "datetime": {
2161
+ const d = new Date(now);
2162
+ d.setMonth(d.getMonth() - index);
2163
+ return d.toISOString();
2164
+ }
2165
+ case "boolean":
2166
+ return index % 2 === 0;
2167
+ case "image":
2168
+ return void 0;
2169
+ case "url":
2170
+ return "https://example.com";
2171
+ case "richtext":
2172
+ return "Sample rich text";
2173
+ case "object":
2174
+ return depth > 6 ? {} : synthesizeObject(schema.fields ?? {}, opts, depth + 1);
2175
+ case "array": {
2176
+ const n = opts.arrayLength ?? 4;
2177
+ const item = schema.itemSchema ?? { type: "string" };
2178
+ return depth > 6 ? [] : Array.from({ length: n }, (_, i) => synthesizeValue(item, opts, depth + 1, i));
2179
+ }
2180
+ default:
2181
+ return void 0;
2182
+ }
2183
+ }
2184
+ function synthesizeObject(fields, opts, depth) {
2185
+ const o = {};
2186
+ let i = 0;
2187
+ for (const [k, f] of Object.entries(fields)) o[k] = synthesizeValue(f, opts, depth, i++);
2188
+ return o;
2189
+ }
2190
+ function labelForPath(segments, scope) {
2191
+ const parts = [];
2192
+ for (let i = 0; i < segments.length; i++) {
2193
+ const seg = segments[i];
2194
+ if (seg.kind === "index") {
2195
+ parts[parts.length - 1] = `${parts[parts.length - 1] ?? ""} #${seg.index + 1}`;
2196
+ continue;
2197
+ }
2198
+ const r = resolvePathSchema(segments.slice(0, i + 1), scope);
2199
+ parts.push(r.schema?.label ?? humanizeKey(seg.name));
2200
+ }
2201
+ return parts.join(" \u203A ");
2202
+ }
2203
+ function humanizeKey(key) {
2204
+ return key.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
2205
+ }
2206
+
2207
+ // src/resolve/describe.ts
2208
+ function describeExpression(expr, scope) {
2209
+ try {
2210
+ const ast = parseExpression(expr);
2211
+ const tc = typeCheckAst(ast, scope);
2212
+ const type = tc.issues.length ? "unknown" : tc.type;
2213
+ const paths = collectPaths(ast);
2214
+ if (ast.kind === "path") return { label: labelForPath(ast.segments, scope), type };
2215
+ if (ast.kind === "pipe" && ast.input.kind === "path") return { label: `${labelForPath(ast.input.segments, scope)} \xB7 ${ast.name}`, type };
2216
+ const names = [...new Set(paths.map((p) => labelForPath(p, scope)))];
2217
+ if (names.length === 1) return { label: `\u0192 ${names[0]}`, type };
2218
+ if (names.length > 1) return { label: `\u0192 ${names.slice(0, 2).join(", ")}${names.length > 2 ? "\u2026" : ""}`, type };
2219
+ return { label: expr, type };
2220
+ } catch {
2221
+ return { label: expr, type: "unknown" };
2222
+ }
2223
+ }
2224
+ function humaniseLabel(text, scope) {
2225
+ const described = (expr) => {
2226
+ const { label } = describeExpression(expr.trim(), scope);
2227
+ return label;
2228
+ };
2229
+ return text.replace(/\{\{([^}]+)\}\}/g, (_, expr) => described(expr)).replace(/\b[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+\b/g, (path) => {
2230
+ const label = described(path);
2231
+ return label === path ? path : label;
2232
+ });
2233
+ }
2234
+
2235
+ // src/resolve/resolve.ts
2236
+ var display = toDisplayString;
2237
+ function resolveTemplate(opts) {
2238
+ const mode = opts.mode ?? "preview";
2239
+ const theme = mergeTheme(opts.theme);
2240
+ const template = opts.template;
2241
+ const dataSources = opts.dataSources ?? [];
2242
+ const now = opts.now ?? /* @__PURE__ */ new Date();
2243
+ const format = {
2244
+ locale: opts.locale ?? template.settings?.locale ?? "en-GB",
2245
+ currency: opts.currency ?? template.settings?.currency ?? "GBP",
2246
+ timeZone: opts.timeZone ?? template.settings?.timeZone,
2247
+ now
2248
+ };
2249
+ const warnings = [];
2250
+ let data;
2251
+ if (mode === "preview") data = opts.data ?? opts.sampleData ?? {};
2252
+ else data = opts.data ?? opts.sampleData ?? synthesizeSampleData(dataSources, { now });
2253
+ if (mode === "design") {
2254
+ const synth = synthesizeSampleData(dataSources, { now });
2255
+ data = { ...synth, ...data };
2256
+ }
2257
+ const globals = {
2258
+ page: { number: 1, total: 1 },
2259
+ now,
2260
+ theme: { name: theme.name, logo: theme.logo, logoDark: theme.logoDark, colors: theme.colors, typography: theme.typography, meta: theme.meta ?? {} }
2261
+ };
2262
+ const scope = new Scope(globals).child(data);
2263
+ const schemaScope = createSchemaScope(dataSources);
2264
+ const ctx = {
2265
+ registry: opts.registry,
2266
+ theme,
2267
+ template,
2268
+ mode,
2269
+ format,
2270
+ warnings,
2271
+ designIterations: Math.max(1, opts.designIterations ?? 1),
2272
+ maxIterations: opts.maxIterations ?? 5e3,
2273
+ allowStyleOverrides: opts.allowStyleOverrides ?? true,
2274
+ designShowHidden: opts.designShowHidden ?? true,
2275
+ dataSources
2276
+ };
2277
+ const resolveList = (nodes) => (nodes ?? []).flatMap((n) => resolveNode(ctx, n, scope, schemaScope, "", "section"));
2278
+ const header = { default: resolveList(template.header?.default) };
2279
+ if (template.page.header.differentFirstPage) header.firstPage = resolveList(template.header?.firstPage);
2280
+ const footer = { default: resolveList(template.footer?.default) };
2281
+ if (template.page.footer.differentFirstPage) footer.firstPage = resolveList(template.footer?.firstPage);
2282
+ return { body: resolveList(template.body), header, footer, warnings, mode };
2283
+ }
2284
+ function isPageDependent(expr) {
2285
+ try {
2286
+ return collectPaths(parseExpression(expr)).some((p) => p[0]?.kind === "prop" && p[0].name === "page");
2287
+ } catch {
2288
+ return false;
2289
+ }
2290
+ }
2291
+ function evalCtx(ctx, scope, nodeId) {
2292
+ return {
2293
+ scope,
2294
+ format: ctx.format,
2295
+ onWarning: (message, expr) => ctx.warnings.push({ message, nodeId, expr })
2296
+ };
2297
+ }
2298
+ function safeEvaluate(ctx, scope, nodeId, expr) {
2299
+ try {
2300
+ return evaluate(expr, evalCtx(ctx, scope, nodeId));
2301
+ } catch (e) {
2302
+ ctx.warnings.push({ message: `Invalid expression: ${e.message}`, nodeId, expr });
2303
+ return void 0;
2304
+ }
2305
+ }
2306
+ var labelFor = describeExpression;
2307
+ function resolveSegments(ctx, scope, schemaScope, nodeId, value) {
2308
+ const segments = [];
2309
+ let pageDependent = false;
2310
+ for (const s of splitTemplate(value)) {
2311
+ if (s.kind === "text") {
2312
+ segments.push(s);
2313
+ continue;
2314
+ }
2315
+ const expr = s.expr;
2316
+ const { label } = labelFor(expr, schemaScope);
2317
+ if (isPageDependent(expr)) {
2318
+ pageDependent = true;
2319
+ segments.push({ ...s, label, deferred: true });
2320
+ continue;
2321
+ }
2322
+ const v = safeEvaluate(ctx, scope, nodeId, expr);
2323
+ segments.push({ ...s, label, value: display(v) });
2324
+ }
2325
+ const text = segments.map((s) => s.kind === "text" ? s.text : s.value ?? "").join("");
2326
+ return { segments, text, pageDependent };
2327
+ }
2328
+ function resolveRichText(ctx, scope, schemaScope, nodeId, doc) {
2329
+ let pageDependent = false;
2330
+ const inline = (content) => content.map((n) => {
2331
+ if (n.type !== "token") {
2332
+ if (n.type === "text" && n.marks?.link?.includes("{{")) {
2333
+ const r = resolveSegments(ctx, scope, schemaScope, nodeId, n.marks.link);
2334
+ return { ...n, marks: { ...n.marks, link: r.text } };
2335
+ }
2336
+ return n;
2337
+ }
2338
+ const { label } = labelFor(n.expr, schemaScope);
2339
+ if (isPageDependent(n.expr)) {
2340
+ pageDependent = true;
2341
+ return { ...n, label, deferred: true };
2342
+ }
2343
+ return { ...n, label, value: display(safeEvaluate(ctx, scope, nodeId, n.expr)) };
2344
+ });
2345
+ const para = (p) => ({ ...p, content: inline(p.content) });
2346
+ const out = {
2347
+ type: "doc",
2348
+ content: doc.content.map((b) => b.type === "paragraph" ? para(b) : { ...b, content: b.content.map((li) => ({ ...li, content: li.content.map(para) })) })
2349
+ };
2350
+ return { doc: out, pageDependent };
2351
+ }
2352
+ function getPath(obj, path) {
2353
+ const parts = path.split(".");
2354
+ let cur = obj;
2355
+ for (const p of parts) {
2356
+ if (cur === null || cur === void 0 || typeof cur !== "object") return void 0;
2357
+ cur = cur[p];
2358
+ }
2359
+ return cur;
2360
+ }
2361
+ function setPath(obj, path, value) {
2362
+ const parts = path.split(".");
2363
+ let cur = obj;
2364
+ for (let i = 0; i < parts.length - 1; i++) {
2365
+ const p = parts[i];
2366
+ if (typeof cur[p] !== "object" || cur[p] === null) cur[p] = {};
2367
+ cur = cur[p];
2368
+ }
2369
+ cur[parts[parts.length - 1]] = value;
2370
+ }
2371
+ function resolveFlow(def, node, textStyleKeepWithNext) {
2372
+ const d = def?.defaultFlow ?? {};
2373
+ const n = node.flow ?? {};
2374
+ const isContainer = !!def?.container;
2375
+ const split = def?.split ?? (isContainer ? "children" : "none");
2376
+ return {
2377
+ breakBefore: n.breakBefore ?? d.breakBefore ?? "auto",
2378
+ breakAfter: n.breakAfter ?? d.breakAfter ?? "auto",
2379
+ keepTogether: n.keepTogether ?? d.keepTogether ?? false,
2380
+ keepWithNext: n.keepWithNext ?? d.keepWithNext ?? textStyleKeepWithNext ?? false,
2381
+ split,
2382
+ minOrphans: n.minOrphans ?? d.minOrphans ?? 2,
2383
+ minWidows: n.minWidows ?? d.minWidows ?? 2
2384
+ };
2385
+ }
2386
+ function roleOf(type) {
2387
+ return type === "section" || type === "row" || type === "column" ? type : "block";
2388
+ }
2389
+ function resolveNode(ctx, node, scope, schemaScope, keyPrefix, _parentRole) {
2390
+ const def = ctx.registry.get(node.type);
2391
+ const key = keyPrefix + node.id;
2392
+ const nodeId = node.id;
2393
+ let hiddenByCondition = false;
2394
+ const cond = compileCondition(node.visibleWhen);
2395
+ if (cond) {
2396
+ const syntax = checkExpressionSyntax(cond);
2397
+ if (syntax) ctx.warnings.push({ message: `Invalid condition: ${syntax.message}`, nodeId, expr: cond });
2398
+ else {
2399
+ let visible = true;
2400
+ try {
2401
+ visible = evaluateCondition(cond, evalCtx(ctx, scope, nodeId));
2402
+ } catch (e) {
2403
+ ctx.warnings.push({ message: `Condition failed: ${e.message}`, nodeId, expr: cond });
2404
+ }
2405
+ if (!visible) {
2406
+ if (ctx.mode === "preview" || !ctx.designShowHidden) return [];
2407
+ hiddenByCondition = true;
2408
+ }
2409
+ }
2410
+ }
2411
+ if (!def) {
2412
+ ctx.warnings.push({ message: `Unknown block type "${node.type}"`, nodeId });
2413
+ return [
2414
+ {
2415
+ key,
2416
+ nodeId,
2417
+ type: node.type,
2418
+ role: roleOf(node.type),
2419
+ props: { ...node.props, __unknown: true },
2420
+ raw: node.props,
2421
+ style: resolveStyle({ theme: ctx.theme, styles: ctx.template.styles }),
2422
+ flow: resolveFlow(void 0, node, void 0),
2423
+ children: [],
2424
+ bindings: {},
2425
+ textSegments: {},
2426
+ richText: {},
2427
+ label: node.name ?? node.type,
2428
+ locked: node.locked
2429
+ }
2430
+ ];
2431
+ }
2432
+ const defaults = resolveDefaultProps(def);
2433
+ const raw = { ...defaults, ...node.props };
2434
+ const props = structuredClone(raw);
2435
+ const bindings = {};
2436
+ const textSegments = {};
2437
+ const richText = {};
2438
+ let pageDependent = false;
2439
+ const applySpec = (spec, target, propPrefix) => {
2440
+ const propPath = propPrefix + spec.prop;
2441
+ const value = getPath(target, spec.prop);
2442
+ switch (spec.kind) {
2443
+ case "value": {
2444
+ if (typeof value !== "string" || value.trim() === "") return;
2445
+ const { label, type } = labelFor(value, schemaScope);
2446
+ bindings[propPath] = { prop: propPath, kind: "value", expr: value, label, type };
2447
+ if (isPageDependent(value)) {
2448
+ pageDependent = true;
2449
+ setPath(target, spec.prop, void 0);
2450
+ return;
2451
+ }
2452
+ setPath(target, spec.prop, safeEvaluate(ctx, scope, nodeId, value));
2453
+ return;
2454
+ }
2455
+ case "collection": {
2456
+ if (typeof value !== "string" || value.trim() === "") {
2457
+ setPath(target, spec.prop, []);
2458
+ return;
2459
+ }
2460
+ const { label, type } = labelFor(value, schemaScope);
2461
+ bindings[propPath] = { prop: propPath, kind: "collection", expr: value, label, type };
2462
+ const v = safeEvaluate(ctx, scope, nodeId, value);
2463
+ setPath(target, spec.prop, Array.isArray(v) ? v : v === void 0 || v === null ? [] : [v]);
2464
+ return;
2465
+ }
2466
+ case "template": {
2467
+ if (typeof value !== "string") return;
2468
+ if (!value.includes("{{")) {
2469
+ textSegments[propPath] = [{ kind: "text", text: value }];
2470
+ return;
2471
+ }
2472
+ const r = resolveSegments(ctx, scope, schemaScope, nodeId, value);
2473
+ if (r.pageDependent) pageDependent = true;
2474
+ textSegments[propPath] = r.segments;
2475
+ const tokens = r.segments.filter((s) => s.kind === "token");
2476
+ if (tokens.length === 1 && r.segments.length === 1) {
2477
+ const { label, type } = labelFor(tokens[0].expr, schemaScope);
2478
+ bindings[propPath] = { prop: propPath, kind: "template", expr: tokens[0].expr, label, type };
2479
+ }
2480
+ setPath(target, spec.prop, r.text);
2481
+ return;
2482
+ }
2483
+ case "richtext": {
2484
+ if (!isRichTextDoc(value)) return;
2485
+ const r = resolveRichText(ctx, scope, schemaScope, nodeId, value);
2486
+ if (r.pageDependent) pageDependent = true;
2487
+ richText[propPath] = r.doc;
2488
+ return;
2489
+ }
2490
+ case "condition":
2491
+ case "list":
2492
+ return;
2493
+ }
2494
+ };
2495
+ for (const spec of bindingSpecsFor(def)) {
2496
+ if (spec.perItem) continue;
2497
+ if (spec.items) {
2498
+ const list = getPath(props, spec.prop);
2499
+ if (Array.isArray(list)) list.forEach((item, i) => item && typeof item === "object" && spec.items.forEach((s) => !s.perItem && applySpec(s, item, `${spec.prop}.${i}.`)));
2500
+ continue;
2501
+ }
2502
+ applySpec(spec, props, "");
2503
+ }
2504
+ if (def.resolve) {
2505
+ const hookCtx = {
2506
+ props,
2507
+ node,
2508
+ scope,
2509
+ mode: ctx.mode,
2510
+ theme: ctx.theme,
2511
+ format: ctx.format,
2512
+ evaluate: (expr) => safeEvaluate(ctx, scope, nodeId, expr),
2513
+ evaluateIn: (expr, vars) => safeEvaluate(ctx, scope.child(vars), nodeId, expr),
2514
+ warn: (message) => ctx.warnings.push({ message, nodeId })
2515
+ };
2516
+ try {
2517
+ const out = def.resolve(hookCtx);
2518
+ if (out && typeof out === "object") Object.assign(props, out);
2519
+ } catch (e) {
2520
+ ctx.warnings.push({ message: `Block "${def.name}" failed to resolve: ${e.message}`, nodeId });
2521
+ }
2522
+ }
2523
+ const textStyleId = node.style?.textStyle ?? def.defaultTextStyle;
2524
+ const style = resolveStyle({
2525
+ theme: ctx.theme,
2526
+ styles: ctx.template.styles,
2527
+ textStyle: textStyleId,
2528
+ defaults: def.defaultStyle,
2529
+ overrides: node.style,
2530
+ allowOverrides: ctx.allowStyleOverrides
2531
+ });
2532
+ const textStyleDef = textStyleId ? ctx.template.styles.text[textStyleId] : void 0;
2533
+ const flow = resolveFlow(def, node, textStyleDef?.keepWithNext);
2534
+ let children = [];
2535
+ let iterationInfo;
2536
+ let placeholder = false;
2537
+ const c = def.container;
2538
+ if (c?.repeat) {
2539
+ const listValue = props[c.repeat.collectionProp];
2540
+ const alias = props[c.repeat.aliasProp ?? "alias"] || "item";
2541
+ const collectionExpr = raw[c.repeat.collectionProp];
2542
+ let items = Array.isArray(listValue) ? listValue : [];
2543
+ const limit = c.repeat.limitProp ? Number(props[c.repeat.limitProp]) || void 0 : void 0;
2544
+ if (limit) items = items.slice(0, limit);
2545
+ if (items.length > ctx.maxIterations) {
2546
+ ctx.warnings.push({ message: `Repeater truncated to ${ctx.maxIterations} items`, nodeId });
2547
+ items = items.slice(0, ctx.maxIterations);
2548
+ }
2549
+ let itemSchema;
2550
+ if (typeof collectionExpr === "string" && collectionExpr.trim()) {
2551
+ try {
2552
+ let ast = parseExpression(collectionExpr);
2553
+ while (ast.kind === "pipe") ast = ast.input;
2554
+ const r = ast.kind === "path" ? resolvePathSchema(ast.segments, schemaScope) : {};
2555
+ itemSchema = r.schema?.type === "array" ? r.schema.itemSchema : void 0;
2556
+ } catch {
2557
+ itemSchema = void 0;
2558
+ }
2559
+ }
2560
+ if (ctx.mode === "design") {
2561
+ if (items.length === 0) {
2562
+ items = itemSchema ? [synthesizeValue(itemSchema, { now: ctx.format.now }, 0, 0)] : [{}];
2563
+ placeholder = true;
2564
+ }
2565
+ items = items.slice(0, ctx.designIterations);
2566
+ }
2567
+ const keepItem = c.repeat.keepItemTogetherProp ? !!props[c.repeat.keepItemTogetherProp] : false;
2568
+ const count = items.length;
2569
+ const childSchemaScope = schemaScope.child({
2570
+ [alias]: itemSchema ?? { type: "object", label: alias, fields: {} },
2571
+ index: { type: "number", label: "Index" },
2572
+ first: { type: "boolean", label: "Is first" },
2573
+ last: { type: "boolean", label: "Is last" },
2574
+ count: { type: "number", label: "Count" }
2575
+ });
2576
+ children = items.map((item, index) => {
2577
+ const iterScope = scope.child({ [alias]: item, item, index: index + 1, first: index === 0, last: index === count - 1, count });
2578
+ const iterKey = `${key}@${index}`;
2579
+ const iterChildren = (node.children ?? []).flatMap((ch) => resolveNode(ctx, ch, iterScope, childSchemaScope, `${iterKey}/`, "block"));
2580
+ const iteration = {
2581
+ key: iterKey,
2582
+ nodeId,
2583
+ type: "repeater-item",
2584
+ role: "block",
2585
+ props: { index, count },
2586
+ raw: {},
2587
+ style: resolveStyle({ theme: ctx.theme, styles: ctx.template.styles }),
2588
+ flow: { breakBefore: "auto", breakAfter: "auto", keepTogether: keepItem, keepWithNext: false, split: "children", minOrphans: 1, minWidows: 1 },
2589
+ children: iterChildren,
2590
+ bindings: {},
2591
+ textSegments: {},
2592
+ richText: {},
2593
+ iteration: { index, count, alias },
2594
+ label: `${alias} ${index + 1}`
2595
+ };
2596
+ return iteration;
2597
+ });
2598
+ iterationInfo = void 0;
2599
+ props.__itemCount = Array.isArray(listValue) ? listValue.length : 0;
2600
+ } else if (c?.conditional) {
2601
+ const condValue = compileCondition(raw[c.conditional.conditionProp]);
2602
+ let show = true;
2603
+ if (condValue) {
2604
+ const syntax = checkExpressionSyntax(condValue);
2605
+ if (syntax) ctx.warnings.push({ message: `Invalid condition: ${syntax.message}`, nodeId, expr: condValue });
2606
+ else {
2607
+ try {
2608
+ show = evaluateCondition(condValue, evalCtx(ctx, scope, nodeId));
2609
+ } catch (e) {
2610
+ ctx.warnings.push({ message: `Condition failed: ${e.message}`, nodeId, expr: condValue });
2611
+ }
2612
+ }
2613
+ }
2614
+ props.__conditionResult = show;
2615
+ if (show || ctx.mode === "design") {
2616
+ children = (node.children ?? []).flatMap((ch) => resolveNode(ctx, ch, scope, schemaScope, keyPrefix, "block"));
2617
+ }
2618
+ } else if (c) {
2619
+ children = (node.children ?? []).flatMap((ch) => resolveNode(ctx, ch, scope, schemaScope, keyPrefix, roleOf(node.type)));
2620
+ }
2621
+ const resolved = {
2622
+ key,
2623
+ nodeId,
2624
+ type: node.type,
2625
+ role: roleOf(node.type),
2626
+ props,
2627
+ raw,
2628
+ style,
2629
+ flow,
2630
+ children,
2631
+ bindings,
2632
+ textSegments,
2633
+ richText,
2634
+ label: node.name ?? def.canvas?.label?.(props) ?? def.name,
2635
+ locked: node.locked,
2636
+ pageDependent: pageDependent || void 0,
2637
+ placeholder: placeholder || void 0,
2638
+ iteration: iterationInfo
2639
+ };
2640
+ if (hiddenByCondition) resolved.props.__hiddenByCondition = true;
2641
+ if (node.type === "row") {
2642
+ const layout = Array.isArray(props.layout) ? props.layout : children.map(() => 12 / Math.max(1, children.length));
2643
+ resolved.layout = { columns: layout };
2644
+ children.forEach((col, i) => {
2645
+ col.span = layout[i] ?? 12 / Math.max(1, children.length);
2646
+ });
2647
+ }
2648
+ return [resolved];
2649
+ }
2650
+ function evaluateToText(expr, data, format) {
2651
+ const scope = new Scope({ now: format?.now ?? /* @__PURE__ */ new Date() }).child(data);
2652
+ return toDisplayString(
2653
+ evaluate(expr, {
2654
+ scope,
2655
+ format: { locale: "en-GB", currency: "GBP", ...format }
2656
+ })
2657
+ );
2658
+ }
2659
+
2660
+ // src/resolve/resolved.ts
2661
+ var DEFAULT_PAGE_CONTEXT = { number: 1, total: 1 };
2662
+
2663
+ // src/layout/page-geometry.ts
2664
+ var PX_PER_MM = 96 / 25.4;
2665
+ var PAGE_SIZES_MM = {
2666
+ A3: { width: 297, height: 420 },
2667
+ A4: { width: 210, height: 297 },
2668
+ A5: { width: 148, height: 210 },
2669
+ Letter: { width: 215.9, height: 279.4 },
2670
+ Legal: { width: 215.9, height: 355.6 },
2671
+ Tabloid: { width: 279.4, height: 431.8 }
2672
+ };
2673
+ function mmToPx(mm) {
2674
+ return Math.round(mm * PX_PER_MM * 100) / 100;
2675
+ }
2676
+ function pxToMm(px) {
2677
+ return Math.round(px / PX_PER_MM * 100) / 100;
2678
+ }
2679
+ function pageSizeMm(size) {
2680
+ return typeof size === "string" ? PAGE_SIZES_MM[size] ?? PAGE_SIZES_MM.A4 : size;
2681
+ }
2682
+ function computePageGeometry(page) {
2683
+ const size = pageSizeMm(page.size);
2684
+ const portrait = page.orientation !== "landscape";
2685
+ const width = mmToPx(portrait ? size.width : size.height);
2686
+ const height = mmToPx(portrait ? size.height : size.width);
2687
+ const margins = {
2688
+ top: mmToPx(page.margins.top),
2689
+ right: mmToPx(page.margins.right),
2690
+ bottom: mmToPx(page.margins.bottom),
2691
+ left: mmToPx(page.margins.left)
2692
+ };
2693
+ const headerHeight = mmToPx(Math.max(0, page.header.height));
2694
+ const footerHeight = mmToPx(Math.max(0, page.footer.height));
2695
+ const contentTop = margins.top + headerHeight;
2696
+ const footerTop = height - margins.bottom - footerHeight;
2697
+ return {
2698
+ width,
2699
+ height,
2700
+ margins,
2701
+ headerHeight,
2702
+ footerHeight,
2703
+ contentWidth: Math.max(0, width - margins.left - margins.right),
2704
+ contentHeight: Math.max(0, footerTop - contentTop),
2705
+ contentTop,
2706
+ headerTop: margins.top,
2707
+ footerTop
2708
+ };
2709
+ }
2710
+
2711
+ // src/layout/paginate.ts
2712
+ function g(ctx, node) {
2713
+ return ctx.geom.nodes[node.key] ?? { top: 0, height: 0, left: 0, width: 0 };
2714
+ }
2715
+ var EPS = 0.01;
2716
+ function bestBreak(ctx, list, limit, minY) {
2717
+ const visible = (from, step) => {
2718
+ for (let j = from; j >= 0 && j < list.length; j += step) {
2719
+ const s = list[j];
2720
+ if (g(ctx, s).height > EPS) return s;
2721
+ }
2722
+ return void 0;
2723
+ };
2724
+ for (let i = list.length - 1; i >= 0; i--) {
2725
+ const child = list[i];
2726
+ const cg = g(ctx, child);
2727
+ const top = cg.top;
2728
+ const bottom = cg.top + cg.height;
2729
+ if (top > limit + EPS) continue;
2730
+ if (cg.height <= EPS) continue;
2731
+ const next = visible(i + 1, 1);
2732
+ if (bottom > limit + EPS) {
2733
+ const inner = breakInside(ctx, child, limit, minY);
2734
+ if (inner !== null && inner > minY + EPS && inner > top + EPS) return inner;
2735
+ } else {
2736
+ const candidate = next ? Math.min(g(ctx, next).top, limit) : Math.min(bottom, limit);
2737
+ const blocked = child.flow.keepWithNext && !!next;
2738
+ if (!blocked && candidate > minY + EPS) return candidate;
2739
+ if (!blocked) return null;
2740
+ }
2741
+ const prev = visible(i - 1, -1);
2742
+ const prevBlocks = !!prev && prev.flow.keepWithNext;
2743
+ if (!prevBlocks && top > minY + EPS) return top;
2744
+ if (!prevBlocks) return null;
2745
+ }
2746
+ return null;
2747
+ }
2748
+ function breakInside(ctx, node, limit, minY) {
2749
+ const ng = g(ctx, node);
2750
+ const fitsOnFreshPage = ng.height <= ctx.pageHeight + EPS;
2751
+ if (node.flow.keepTogether && fitsOnFreshPage) return null;
2752
+ if (node.role === "row" && node.children.length > 1) {
2753
+ const allowSplit = node.props.allowSplit === true;
2754
+ if (!allowSplit && fitsOnFreshPage) return null;
2755
+ return columnsAgree(ctx, node.children, limit, minY);
2756
+ }
2757
+ switch (node.flow.split) {
2758
+ case "none":
2759
+ return null;
2760
+ case "lines": {
2761
+ const lines = ng.lines;
2762
+ if (!lines || lines.length < 2) return null;
2763
+ const minOrphans = Math.max(1, node.flow.minOrphans);
2764
+ const minWidows = Math.max(1, node.flow.minWidows);
2765
+ for (let i = lines.length - 2; i >= 0; i--) {
2766
+ const y = lines[i];
2767
+ if (y > limit + EPS) continue;
2768
+ const before = i + 1;
2769
+ const after = lines.length - before;
2770
+ if (before < minOrphans) return null;
2771
+ if (after < minWidows) continue;
2772
+ return y > minY + EPS ? y : null;
2773
+ }
2774
+ return null;
2775
+ }
2776
+ case "rows": {
2777
+ const rows = ng.rows;
2778
+ if (!rows || rows.length === 0) return null;
2779
+ const body = rows.filter((r) => r.kind === "body");
2780
+ if (body.length < 2) return null;
2781
+ const minOrphans = Math.max(1, node.flow.minOrphans);
2782
+ const minWidows = Math.max(1, node.flow.minWidows);
2783
+ for (let i = body.length - 2; i >= 0; i--) {
2784
+ const r = body[i];
2785
+ const y = r.top + r.height;
2786
+ if (y > limit + EPS) continue;
2787
+ const before = i + 1;
2788
+ const after = body.length - before;
2789
+ if (before < minOrphans) return null;
2790
+ if (after < minWidows) continue;
2791
+ return y > minY + EPS ? y : null;
2792
+ }
2793
+ return null;
2794
+ }
2795
+ case "children":
2796
+ return bestBreak(ctx, node.children, limit, minY);
2797
+ }
2798
+ }
2799
+ function columnsAgree(ctx, columns, limit, minY) {
2800
+ let y = limit;
2801
+ for (let iter = 0; iter < 64; iter++) {
2802
+ let yNext = y;
2803
+ for (const col of columns) {
2804
+ const kids = col.children;
2805
+ if (kids.length === 0) continue;
2806
+ const first = g(ctx, kids[0]).top;
2807
+ const lastG = g(ctx, kids[kids.length - 1]);
2808
+ const last = lastG.top + lastG.height;
2809
+ if (y <= first + EPS || y >= last - EPS) continue;
2810
+ const b = bestBreak(ctx, kids, y, minY);
2811
+ if (b === null) return null;
2812
+ if (b < yNext) yNext = b;
2813
+ }
2814
+ if (Math.abs(yNext - y) < EPS) return y > minY + EPS ? y : null;
2815
+ y = yNext;
2816
+ }
2817
+ return null;
2818
+ }
2819
+ function collectTables(node, out = []) {
2820
+ if (node.flow.split === "rows") out.push(node);
2821
+ for (const c of node.children) collectTables(c, out);
2822
+ return out;
2823
+ }
2824
+ function repeatedHeadersAt(geom, tables, from) {
2825
+ const out = [];
2826
+ for (const t of tables) {
2827
+ const tg = geom.nodes[t.key];
2828
+ if (!tg?.rows) continue;
2829
+ const headerRows = tg.rows.filter((r) => r.kind === "header");
2830
+ if (headerRows.length === 0) continue;
2831
+ const bottom = tg.top + tg.height;
2832
+ if (tg.top < from - EPS && from < bottom - EPS) {
2833
+ const height = headerRows.reduce((a, r) => a + r.height, 0);
2834
+ out.push({ tableKey: t.key, height, left: tg.left, width: tg.width });
2835
+ }
2836
+ }
2837
+ return out;
2838
+ }
2839
+ function forcedBreaks(section, geom) {
2840
+ const offsets = /* @__PURE__ */ new Set();
2841
+ let contentTop = Number.POSITIVE_INFINITY;
2842
+ let contentBottom = 0;
2843
+ const visit = (n) => {
2844
+ const g2 = geom.nodes[n.key];
2845
+ if (g2) {
2846
+ if (n.flow.breakBefore === "page") offsets.add(g2.top);
2847
+ if (n.flow.breakAfter === "page") offsets.add(g2.top + g2.height);
2848
+ }
2849
+ n.children.forEach(visit);
2850
+ };
2851
+ for (const child of section.children) {
2852
+ const g2 = geom.nodes[child.key];
2853
+ if (g2) {
2854
+ contentTop = Math.min(contentTop, g2.top);
2855
+ contentBottom = Math.max(contentBottom, g2.top + g2.height);
2856
+ }
2857
+ visit(child);
2858
+ }
2859
+ if (!Number.isFinite(contentTop)) contentTop = 0;
2860
+ return {
2861
+ inside: [...offsets].filter((y) => y > contentTop + EPS && y < contentBottom - EPS).sort((a, b) => a - b),
2862
+ atStart: [...offsets].some((y) => y <= contentTop + EPS),
2863
+ atEnd: [...offsets].some((y) => y >= contentBottom - EPS)
2864
+ };
2865
+ }
2866
+ function paginate(opts) {
2867
+ const { document, geometry, page } = opts;
2868
+ const pageHeight = page.contentHeight;
2869
+ const warnings = [];
2870
+ const pages = [];
2871
+ let current2 = { index: 0, number: 1, fragments: [], first: true };
2872
+ let y = 0;
2873
+ let pendingBreak = false;
2874
+ const newPage = () => {
2875
+ pages.push(current2);
2876
+ current2 = { index: pages.length, number: pages.length + 1, fragments: [], first: false };
2877
+ y = 0;
2878
+ pendingBreak = false;
2879
+ };
2880
+ for (const section of document.body) {
2881
+ const geom = geometry[section.key];
2882
+ if (!geom) {
2883
+ warnings.push({ code: "forced-split", message: "Section was not measured", sectionKey: section.key, nodeId: section.nodeId });
2884
+ continue;
2885
+ }
2886
+ const H = geom.height;
2887
+ const ctx = { geom, pageHeight };
2888
+ const tables = collectTables(section);
2889
+ const forced = forcedBreaks(section, geom);
2890
+ const contentBottom = section.children.reduce((m, c) => {
2891
+ const cg = geom.nodes[c.key];
2892
+ return cg ? Math.max(m, cg.top + cg.height) : m;
2893
+ }, 0);
2894
+ if ((pendingBreak || section.flow.breakBefore === "page" || forced.atStart) && y > 0) newPage();
2895
+ if (section.flow.keepTogether && H > pageHeight - y + EPS && H <= pageHeight + EPS && y > 0) newPage();
2896
+ if (H <= EPS) {
2897
+ continue;
2898
+ }
2899
+ let from = 0;
2900
+ let guard = 0;
2901
+ while (from < H - EPS) {
2902
+ if (++guard > 1e4) {
2903
+ warnings.push({ code: "forced-split", message: "Pagination aborted after too many fragments", sectionKey: section.key, nodeId: section.nodeId });
2904
+ break;
2905
+ }
2906
+ const avail = pageHeight - y;
2907
+ const headers = repeatedHeadersAt(geom, tables, from);
2908
+ const extra = headers.reduce((a, h2) => a + h2.height, 0);
2909
+ const stop = forced.inside.find((f) => f > from + EPS) ?? H;
2910
+ const lastPass = stop >= H - EPS;
2911
+ const remaining = stop - from;
2912
+ if (remaining + extra <= avail + EPS) {
2913
+ current2.fragments.push({ sectionKey: section.key, from, to: stop, repeatHeaders: headers, y, height: remaining + extra, continued: from > 0, continues: !lastPass });
2914
+ y += remaining + extra;
2915
+ from = stop;
2916
+ if (lastPass) break;
2917
+ newPage();
2918
+ continue;
2919
+ }
2920
+ const limit = Math.min(from + avail - extra, stop);
2921
+ let to = null;
2922
+ if (limit > from + EPS) {
2923
+ to = section.flow.split === "children" ? bestBreak(ctx, section.children, limit, from) : null;
2924
+ if (to !== null && to <= from + EPS) to = null;
2925
+ if (to !== null && to > limit + EPS) to = null;
2926
+ if (lastPass && to !== null && to >= contentBottom - EPS) {
2927
+ current2.fragments.push({ sectionKey: section.key, from, to: H, repeatHeaders: headers, y, height: Math.min(H - from + extra, avail), continued: from > 0, continues: false });
2928
+ y += Math.min(H - from + extra, avail);
2929
+ from = H;
2930
+ break;
2931
+ }
2932
+ }
2933
+ if (to === null) {
2934
+ if (y > 0 || current2.fragments.length > 0) {
2935
+ newPage();
2936
+ continue;
2937
+ }
2938
+ if (limit <= from + EPS) {
2939
+ warnings.push({ code: "oversized-header", message: "A repeated table header is taller than the page", sectionKey: section.key, nodeId: section.nodeId, page: current2.number });
2940
+ to = Math.min(H, from + Math.max(1, pageHeight));
2941
+ } else {
2942
+ to = Math.min(H, limit);
2943
+ warnings.push({ code: "forced-split", message: "Content taller than a page had to be cut at an arbitrary position", sectionKey: section.key, nodeId: section.nodeId, page: current2.number });
2944
+ }
2945
+ }
2946
+ current2.fragments.push({ sectionKey: section.key, from, to, repeatHeaders: headers, y, height: to - from + extra, continued: from > 0, continues: true });
2947
+ from = to;
2948
+ newPage();
2949
+ }
2950
+ if (section.flow.breakAfter === "page" || forced.atEnd) pendingBreak = true;
2951
+ }
2952
+ pages.push(current2);
2953
+ while (pages.length > 1 && pages[pages.length - 1].fragments.length === 0) pages.pop();
2954
+ if (pendingBreak) {
2955
+ warnings.push({ code: "no-op-break", message: "A page break after the last content has nothing to move to a new page", page: pages.length });
2956
+ }
2957
+ const minPages = Math.max(1, opts.minPages ?? 1);
2958
+ while (pages.length < minPages) pages.push({ index: pages.length, number: pages.length + 1, fragments: [], first: pages.length === 0 });
2959
+ if (document.body.length === 0) warnings.push({ code: "empty-document", message: "The document has no content" });
2960
+ if (opts.regionGeometry) {
2961
+ const check2 = (sections, limit, region) => {
2962
+ if (!sections) return;
2963
+ const total = sections.reduce((a, s) => a + (opts.regionGeometry[s.key]?.height ?? 0), 0);
2964
+ if (total > limit + 0.5) warnings.push({ code: "region-overflow", message: `${region} content (${Math.round(total)}px) is taller than the ${region.toLowerCase()} region (${Math.round(limit)}px) and will be clipped` });
2965
+ };
2966
+ check2(document.header.default, page.headerHeight, "Header");
2967
+ check2(document.header.firstPage, page.headerHeight, "First-page header");
2968
+ check2(document.footer.default, page.footerHeight, "Footer");
2969
+ check2(document.footer.firstPage, page.footerHeight, "First-page footer");
2970
+ }
2971
+ return { pages, totalPages: pages.length, geometry: page, warnings };
2972
+ }
2973
+
2974
+ // src/validate/issues.ts
2975
+ function issue(severity, code, message, extra = {}) {
2976
+ return { severity, code, message, ...extra };
2977
+ }
2978
+
2979
+ // src/validate/validate.ts
2980
+ function validateTemplate(opts) {
2981
+ const issues = [];
2982
+ const { template, registry } = opts;
2983
+ const dataSources = opts.dataSources ?? [];
2984
+ const theme = mergeTheme(opts.theme);
2985
+ if (!opts.skipSchema) {
2986
+ const parsed = parseTemplate(template);
2987
+ for (const e of parsed.errors) issues.push(issue("error", "schema-invalid", `${e.path}: ${e.message}`, { region: "document" }));
2988
+ }
2989
+ if (opts.theme) for (const m of missingThemeValues(opts.theme)) issues.push(issue("warning", "missing-theme-value", `Theme "${opts.theme.name ?? opts.theme.id ?? ""}" has no value for ${m}; the default is used`, { region: "document" }));
2990
+ const rootScope = createSchemaScope(dataSources);
2991
+ const sourceIds = new Set(dataSources.map((s) => s.id));
2992
+ for (const id of template.settings?.dataSources ?? []) if (!sourceIds.has(id)) issues.push(issue("warning", "unknown-binding", `Template was designed against data source "${id}" which is not provided`, { region: "document" }));
2993
+ const fonts = /* @__PURE__ */ new Set([...opts.availableFonts ?? [], ...(theme.fonts ?? []).map((f) => f.family), ...splitFonts(theme.typography.headingFont), ...splitFonts(theme.typography.bodyFont), ...splitFonts(theme.typography.monoFont ?? "")]);
2994
+ const checkFont = (value, where) => {
2995
+ if (!value || isTokenRef(value) || !opts.availableFonts) return;
2996
+ const families = splitFonts(value);
2997
+ if (!families.some((f) => fonts.has(f) || GENERIC_FONTS.has(f.toLowerCase()))) issues.push(issue("warning", "unsupported-font", `Font "${value}" is not available; the theme body font will be used`, where));
2998
+ };
2999
+ const checkColor = (value, where) => {
3000
+ if (!value || !isTokenRef(value)) return;
3001
+ if (resolveToken(value, theme) === void 0) issues.push(issue("warning", "missing-theme-token", `Theme token "${value}" does not exist`, where));
3002
+ };
3003
+ for (const [id, ts] of Object.entries(template.styles.text)) {
3004
+ checkFont(ts.fontFamily, { region: "document", prop: `styles.text.${id}.fontFamily` });
3005
+ checkColor(ts.color, { region: "document", prop: `styles.text.${id}.color` });
3006
+ }
3007
+ const geometry = computePageGeometry(template.page);
3008
+ if (geometry.contentHeight < 100) issues.push(issue("error", "layout-invalid", "Page margins, header and footer leave less than 100px for content", { region: "document" }));
3009
+ if (geometry.contentWidth < 100) issues.push(issue("error", "layout-invalid", "Page margins leave less than 100px of width for content", { region: "document" }));
3010
+ const regionOf = (r) => r === "body" ? "body" : r.startsWith("header") ? "header" : "footer";
3011
+ const visitNode = (node, region, parent, scope) => {
3012
+ const where = { nodeId: node.id, region: regionOf(region) };
3013
+ const def = registry.get(node.type);
3014
+ if (!def) {
3015
+ issues.push(issue("error", "unknown-block", `Block type "${node.type}" is not registered${node.type.includes(".") ? " (custom block unavailable)" : ""}`, where));
3016
+ return;
3017
+ }
3018
+ if (!registry.isTierEnabled(def.license)) issues.push(issue("warning", "license-required", `"${def.name}" requires the ${def.license} tier`, where));
3019
+ if (parent) {
3020
+ if (!registry.accepts(parent.type, node.type)) issues.push(issue("error", "layout-invalid", `"${def.name}" cannot be placed inside "${registry.get(parent.type)?.name ?? parent.type}"`, where));
3021
+ } else if (node.type !== "section") issues.push(issue("error", "layout-invalid", `Top-level nodes must be sections, found "${node.type}"`, where));
3022
+ if (node.type === "row") {
3023
+ const layout = node.props.layout;
3024
+ const total = (layout ?? []).reduce((a, b) => a + b, 0);
3025
+ if (!layout || total !== 12) issues.push(issue("error", "layout-invalid", `Row layout must sum to 12 (got ${total})`, where));
3026
+ else if ((node.children?.length ?? 0) !== layout.length) issues.push(issue("error", "layout-invalid", `Row has ${node.children?.length ?? 0} columns but a ${layout.length}-column layout`, where));
3027
+ }
3028
+ if (node.style) {
3029
+ checkFont(node.style.fontFamily, { ...where, prop: "style.fontFamily" });
3030
+ checkColor(node.style.color, { ...where, prop: "style.color" });
3031
+ checkColor(node.style.background, { ...where, prop: "style.background" });
3032
+ checkColor(node.style.border?.color, { ...where, prop: "style.border.color" });
3033
+ if (node.style.textStyle && !template.styles.text[node.style.textStyle]) issues.push(issue("warning", "missing-theme-token", `Text style "${node.style.textStyle}" is not defined in the document styles`, { ...where, prop: "style.textStyle" }));
3034
+ }
3035
+ const cond = compileCondition(node.visibleWhen);
3036
+ if (cond) checkExpr(cond, "visibleWhen", "condition");
3037
+ const props = { ...typeof def.defaultProps === "function" ? def.defaultProps() : def.defaultProps, ...node.props };
3038
+ function checkExpr(expr, prop, kind, s = scope, expected) {
3039
+ const out = [];
3040
+ const r = typeCheck(expr, s);
3041
+ for (const i of r.issues) {
3042
+ const code = i.code === "syntax" ? "invalid-expression" : i.code === "type-mismatch" ? "type-mismatch" : "unknown-binding";
3043
+ out.push(issue(i.code === "syntax" ? "error" : "warning", kind === "condition" && code !== "unknown-binding" ? "invalid-condition" : code, `${i.message} in "${expr}"`, { ...where, prop }));
3044
+ }
3045
+ if (kind === "collection" && r.issues.length === 0 && r.type !== "array" && r.type !== "any" && r.type !== "unknown") out.push(issue("error", "missing-collection", `"${expr}" is a ${r.type}, not a list`, { ...where, prop }));
3046
+ if (expected && r.issues.length === 0 && r.type !== "unknown" && r.type !== "any" && r.type !== "null") {
3047
+ const list = Array.isArray(expected) ? expected : [expected];
3048
+ const numeric = ["number", "currency", "percent"];
3049
+ const compatible = list.includes(r.type) || list.some((t) => numeric.includes(t)) && numeric.includes(r.type) || list.includes("string") && r.type !== "array" && r.type !== "object";
3050
+ if (!compatible) out.push(issue("warning", "type-mismatch", `"${expr}" is a ${r.type}; expected ${list.join(" or ")}`, { ...where, prop }));
3051
+ }
3052
+ issues.push(...out);
3053
+ return out;
3054
+ }
3055
+ const runSpec = (spec, target, prefix, s) => {
3056
+ const value = getPath2(target, spec.prop);
3057
+ const prop = prefix + spec.prop;
3058
+ switch (spec.kind) {
3059
+ case "value":
3060
+ if (typeof value === "string" && value.trim()) checkExpr(value, prop, "value", s, spec.valueType);
3061
+ break;
3062
+ case "collection":
3063
+ if (typeof value === "string" && value.trim()) checkExpr(value, prop, "collection", s);
3064
+ else if (def.container?.repeat?.collectionProp === spec.prop) issues.push(issue("warning", "missing-collection", `"${def.name}" has no list to repeat over`, { ...where, prop }));
3065
+ else if (def.type === "table" || def.type === "chart") issues.push(issue("warning", "missing-collection", `"${def.name}" is not bound to a list`, { ...where, prop }));
3066
+ break;
3067
+ case "template":
3068
+ if (typeof value === "string") for (const e of extractTemplateTokens(value)) checkExpr(e, prop, "value", s);
3069
+ break;
3070
+ case "richtext":
3071
+ if (isRichTextDoc(value)) for (const e of richTextTokens(value)) checkExpr(e, prop, "value", s);
3072
+ break;
3073
+ case "condition": {
3074
+ const c = compileCondition(value);
3075
+ if (c) checkExpr(c, prop, "condition", s);
3076
+ break;
3077
+ }
3078
+ case "list":
3079
+ break;
3080
+ }
3081
+ };
3082
+ let itemScope = scope;
3083
+ let childScope = scope;
3084
+ const specs = bindingSpecsFor(def);
3085
+ for (const spec of specs) {
3086
+ if (spec.kind !== "collection") continue;
3087
+ const collectionExpr = props[spec.prop];
3088
+ const itemSchema = typeof collectionExpr === "string" && collectionExpr.trim() ? itemSchemaOf(collectionExpr, scope) : void 0;
3089
+ const alias = spec.alias && props[spec.alias] || "item";
3090
+ if (dataSources.some((d) => d.id === alias)) issues.push(issue("warning", "scope-shadowing", `Repeater alias "${alias}" hides the data source with the same name`, where));
3091
+ const vars = {
3092
+ [alias]: itemSchema ?? { type: "object", label: alias, fields: {} },
3093
+ item: itemSchema ?? { type: "object", label: "Item", fields: {} },
3094
+ row: itemSchema ?? { type: "object", label: "Row", fields: {} },
3095
+ index: { type: "number" },
3096
+ first: { type: "boolean" },
3097
+ last: { type: "boolean" },
3098
+ count: { type: "number" }
3099
+ };
3100
+ itemScope = scope.child(vars);
3101
+ if (def.container?.repeat?.collectionProp === spec.prop) childScope = itemScope;
3102
+ }
3103
+ for (const spec of specs) {
3104
+ if (spec.perItem) {
3105
+ runSpec(spec, props, "", itemScope);
3106
+ continue;
3107
+ }
3108
+ if (spec.items) {
3109
+ const list = getPath2(props, spec.prop);
3110
+ if (Array.isArray(list)) list.forEach((item, i) => item && typeof item === "object" && spec.items.forEach((sub) => runSpec(sub, item, `${spec.prop}.${i}.`, sub.perItem ? itemScope : scope)));
3111
+ continue;
3112
+ }
3113
+ runSpec(spec, props, "", scope);
3114
+ if (spec.kind === "collection") for (const sub of spec.scoped ?? []) runSpec(sub, props, "", itemScope);
3115
+ }
3116
+ if (def.validate) {
3117
+ const vctx = {
3118
+ props,
3119
+ node,
3120
+ schemaScope: itemScope,
3121
+ dataSources,
3122
+ theme,
3123
+ checkExpression: (expr, prop) => checkExpr(expr, prop, "value", itemScope)
3124
+ };
3125
+ try {
3126
+ const extra = def.validate(vctx);
3127
+ for (const i of extra) issues.push({ ...i, nodeId: i.nodeId ?? node.id, region: i.region ?? where.region });
3128
+ } catch (e) {
3129
+ issues.push(issue("warning", "custom", `Validation for "${def.name}" failed: ${e.message}`, where));
3130
+ }
3131
+ }
3132
+ for (const child of node.children ?? []) visitNode(child, region, node, childScope);
3133
+ };
3134
+ const visitRegion = (nodes, region) => nodes.forEach((n) => visitNode(n, region, null, rootScope));
3135
+ visitRegion(template.body, "body");
3136
+ visitRegion(template.header.default, "header");
3137
+ visitRegion(template.header.firstPage ?? [], "header:first");
3138
+ visitRegion(template.footer.default, "footer");
3139
+ visitRegion(template.footer.firstPage ?? [], "footer:first");
3140
+ if (template.body.every((s) => (s.children?.length ?? 0) === 0)) issues.push(issue("info", "empty-content", "The document has no content yet", { region: "body" }));
3141
+ const errors = issues.filter((i) => i.severity === "error");
3142
+ const warnings = issues.filter((i) => i.severity === "warning");
3143
+ return { ok: errors.length === 0, issues, errors, warnings };
3144
+ }
3145
+ var GENERIC_FONTS = /* @__PURE__ */ new Set(["serif", "sans-serif", "monospace", "system-ui", "ui-sans-serif", "ui-serif", "ui-monospace", "cursive", "fantasy", "inherit"]);
3146
+ function splitFonts(value) {
3147
+ return value.split(",").map((f) => f.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
3148
+ }
3149
+ function getPath2(obj, path) {
3150
+ let cur = obj;
3151
+ for (const p of path.split(".")) {
3152
+ if (cur === null || cur === void 0 || typeof cur !== "object") return void 0;
3153
+ cur = cur[p];
3154
+ }
3155
+ return cur;
3156
+ }
3157
+ function itemSchemaOf(collectionExpr, scope) {
3158
+ try {
3159
+ const ast = parseExpression(collectionExpr);
3160
+ const base = ast.kind === "pipe" ? unwrapPipes(ast) : ast;
3161
+ if (base.kind !== "path") return void 0;
3162
+ const r = resolvePathSchema(base.segments, scope);
3163
+ return r.schema?.type === "array" ? r.schema.itemSchema : void 0;
3164
+ } catch {
3165
+ return void 0;
3166
+ }
3167
+ }
3168
+ function unwrapPipes(e) {
3169
+ let cur = e;
3170
+ while (cur.kind === "pipe") cur = cur.input;
3171
+ return cur;
3172
+ }
3173
+ function isSafeUrl(url, opts = {}) {
3174
+ if (!url) return false;
3175
+ const u = url.trim();
3176
+ if (/^https?:\/\//i.test(u)) return true;
3177
+ if (/^mailto:/i.test(u)) return true;
3178
+ if (u.startsWith("/") && !u.startsWith("//")) return true;
3179
+ if (u.startsWith("./") || u.startsWith("../")) return true;
3180
+ if (opts.allowData && /^data:image\/(png|jpe?g|gif|webp|svg\+xml)[;,]/i.test(u)) {
3181
+ if (!/^data:image\/svg\+xml/i.test(u)) return true;
3182
+ const comma = u.indexOf(",");
3183
+ const payload = comma === -1 ? "" : u.slice(comma + 1);
3184
+ let decoded;
3185
+ if (/;base64/i.test(u.slice(0, comma === -1 ? u.length : comma))) {
3186
+ decoded = decodeBase64(payload);
3187
+ } else {
3188
+ try {
3189
+ decoded = decodeURIComponent(payload);
3190
+ } catch {
3191
+ decoded = payload;
3192
+ }
3193
+ }
3194
+ return !/<script|javascript:|on[a-z]+\s*=|<foreignObject/i.test(decoded);
3195
+ }
3196
+ return false;
3197
+ }
3198
+ function decodeBase64(value) {
3199
+ const b64 = value.trim();
3200
+ try {
3201
+ const g2 = globalThis;
3202
+ if (typeof g2.atob === "function") return g2.atob(b64);
3203
+ if (g2.Buffer) return g2.Buffer.from(b64, "base64").toString("utf8");
3204
+ } catch {
3205
+ }
3206
+ return "<script";
3207
+ }
3208
+
3209
+ // src/validate/output.ts
3210
+ function checkOutput(opts) {
3211
+ const { resolved, registry } = opts;
3212
+ const limit = opts.limit ?? 60;
3213
+ const out = [];
3214
+ const seen = /* @__PURE__ */ new Set();
3215
+ const suffix = opts.dataLabel ? ` in \u201C${opts.dataLabel}\u201D` : " in this data set";
3216
+ const push = (i) => {
3217
+ const key = `${i.nodeId ?? ""}|${i.prop ?? ""}|${i.message}`;
3218
+ if (seen.has(key)) return;
3219
+ seen.add(key);
3220
+ out.push(i);
3221
+ };
3222
+ const blankDocument = resolved.body.every((s) => s.children.length === 0);
3223
+ const regions = [
3224
+ [resolved.body, "body"],
3225
+ [resolved.header.default, "header"],
3226
+ [resolved.header.firstPage ?? [], "header"],
3227
+ [resolved.footer.default, "footer"],
3228
+ [resolved.footer.firstPage ?? [], "footer"]
3229
+ ];
3230
+ for (const [nodes, region] of regions) for (const n of nodes) visit(n, region);
3231
+ return out.slice(0, limit);
3232
+ function visit(node, region) {
3233
+ if (!node.placeholder) inspect(node, region);
3234
+ for (const c of node.children) visit(c, region);
3235
+ }
3236
+ function inspect(node, region) {
3237
+ const def = registry.get(node.type);
3238
+ const name = node.label || def?.name || node.type;
3239
+ const where = { nodeId: node.nodeId, region, source: "output" };
3240
+ for (const [prop, b] of Object.entries(node.bindings)) {
3241
+ if (b.kind !== "value" && b.kind !== "template") continue;
3242
+ if (node.textSegments[prop]?.some((s) => s.deferred)) continue;
3243
+ if (handlesEmpty(b.expr)) continue;
3244
+ const value = getPath3(node.props, prop);
3245
+ if (!isBlank(value)) continue;
3246
+ const blank = !drawsWithout(node, prop);
3247
+ push(
3248
+ issue(
3249
+ "warning",
3250
+ "empty-value",
3251
+ blank ? `\u201C${b.label}\u201D has no value${suffix}, so ${name} renders blank` : `\u201C${b.label}\u201D has no value${suffix} and is left out of ${name}`,
3252
+ { ...where, prop }
3253
+ )
3254
+ );
3255
+ }
3256
+ for (const [prop, segments] of Object.entries(node.textSegments)) {
3257
+ if (node.bindings[prop]) continue;
3258
+ for (const s of segments) {
3259
+ if (s.kind !== "token" || s.deferred || !s.expr) continue;
3260
+ if (s.value === void 0 || !isBlank(s.value)) continue;
3261
+ if (handlesEmpty(s.expr)) continue;
3262
+ push(issue("warning", "empty-value", `\u201C${s.label ?? short(s.expr)}\u201D is empty${suffix}, leaving a gap in ${name}`, { ...where, prop }));
3263
+ }
3264
+ }
3265
+ for (const [prop, doc] of Object.entries(node.richText)) {
3266
+ for (const inline of inlinesOf(doc)) {
3267
+ if (inline.type !== "token" || inline.deferred) continue;
3268
+ if (inline.value === void 0 || !isBlank(inline.value)) continue;
3269
+ if (handlesEmpty(inline.expr)) continue;
3270
+ push(issue("warning", "empty-value", `\u201C${inline.label ?? short(inline.expr)}\u201D is empty${suffix}, leaving a gap in ${name}`, { ...where, prop }));
3271
+ }
3272
+ }
3273
+ if (node.children.length === 0 && !hasOwnContent(node) && carriesText(node)) {
3274
+ push(issue("warning", "empty-content", `${name} renders nothing${suffix}`, where));
3275
+ }
3276
+ if (node.role === "section" && !blankDocument && !subtreeHasContent(node)) {
3277
+ push(issue("warning", "empty-content", `Section \u201C${name}\u201D has no visible content${suffix}`, where));
3278
+ }
3279
+ }
3280
+ function ownText(node, skip) {
3281
+ let text = "";
3282
+ for (const [prop, segments] of Object.entries(node.textSegments)) {
3283
+ if (prop === skip) continue;
3284
+ for (const s of segments) text += s.kind === "text" ? s.text : s.deferred ? "1" : s.value ?? "";
3285
+ }
3286
+ for (const [prop, doc] of Object.entries(node.richText)) {
3287
+ if (prop === skip) continue;
3288
+ for (const inline of inlinesOf(doc)) text += inline.type === "token" ? inline.deferred ? "1" : inline.value ?? "" : inline.type === "text" ? inline.text : "";
3289
+ }
3290
+ for (const [prop, b] of Object.entries(node.bindings)) {
3291
+ if (prop === skip) continue;
3292
+ if (b.kind === "value") text += String(getPath3(node.props, prop) ?? "");
3293
+ else if (b.kind === "collection") {
3294
+ const v = getPath3(node.props, prop);
3295
+ if (Array.isArray(v) && v.length) text += "1";
3296
+ }
3297
+ }
3298
+ return text.trim();
3299
+ }
3300
+ function drawsWithout(node, prop) {
3301
+ if (ownText(node, prop) !== "") return true;
3302
+ return node.children.some(subtreeHasContent) || staticProps(node, prop);
3303
+ }
3304
+ function staticProps(node, skip) {
3305
+ for (const [prop, value] of Object.entries(node.props)) {
3306
+ if (prop === skip || prop.startsWith("__")) continue;
3307
+ if (node.bindings[prop]) continue;
3308
+ if (typeof value === "string" && value.trim() !== "") return true;
3309
+ }
3310
+ return false;
3311
+ }
3312
+ function hasOwnContent(node) {
3313
+ return ownText(node) !== "";
3314
+ }
3315
+ function carriesText(node) {
3316
+ return Object.keys(node.textSegments).length > 0 || Object.keys(node.richText).length > 0;
3317
+ }
3318
+ function subtreeHasContent(node) {
3319
+ if (DECORATION.has(node.type)) return false;
3320
+ const def = registry.get(node.type);
3321
+ if (def?.category === "visualisation" && !DECORATION.has(node.type)) return true;
3322
+ if (node.type === "image" || node.type === "logo" || node.type === "signature") return true;
3323
+ if (hasOwnContent(node)) return true;
3324
+ return node.children.some(subtreeHasContent);
3325
+ }
3326
+ }
3327
+ function inlinesOf(doc) {
3328
+ const out = [];
3329
+ for (const block of doc.content) {
3330
+ const paras = block.type === "paragraph" ? [block] : block.content.flatMap((li) => li.content);
3331
+ for (const p of paras) out.push(...p.content);
3332
+ }
3333
+ return out;
3334
+ }
3335
+ var DECORATION = /* @__PURE__ */ new Set(["spacer", "divider", "pageBreak"]);
3336
+ function handlesEmpty(expr) {
3337
+ try {
3338
+ return guards(parseExpression(expr));
3339
+ } catch {
3340
+ return false;
3341
+ }
3342
+ }
3343
+ function guards(node) {
3344
+ switch (node.kind) {
3345
+ case "conditional":
3346
+ return true;
3347
+ case "pipe":
3348
+ return node.name === "default" || guards(node.input);
3349
+ case "binary":
3350
+ return node.op === "or" || node.op === "and" || guards(node.left) || guards(node.right);
3351
+ case "call":
3352
+ return node.name === "coalesce" || node.name === "default";
3353
+ default:
3354
+ return false;
3355
+ }
3356
+ }
3357
+ function short(expr) {
3358
+ return expr.length > 48 ? `${expr.slice(0, 47)}\u2026` : expr;
3359
+ }
3360
+ function isBlank(value) {
3361
+ if (value === null || value === void 0) return true;
3362
+ if (typeof value === "string") return value.trim() === "";
3363
+ return false;
3364
+ }
3365
+ function getPath3(obj, path) {
3366
+ let cur = obj;
3367
+ for (const p of path.split(".")) {
3368
+ if (cur === null || cur === void 0 || typeof cur !== "object") return void 0;
3369
+ cur = cur[p];
3370
+ }
3371
+ return cur;
3372
+ }
3373
+
3374
+ // src/editor/tree.ts
3375
+ var REGION_IDS = ["body", "header", "header:first", "footer", "footer:first"];
3376
+ function getRegion(template, region) {
3377
+ switch (region) {
3378
+ case "body":
3379
+ return template.body;
3380
+ case "header":
3381
+ return template.header.default;
3382
+ case "header:first":
3383
+ return template.header.firstPage ??= [];
3384
+ case "footer":
3385
+ return template.footer.default;
3386
+ case "footer:first":
3387
+ return template.footer.firstPage ??= [];
3388
+ }
3389
+ }
3390
+ function readRegion(template, region) {
3391
+ switch (region) {
3392
+ case "body":
3393
+ return template.body;
3394
+ case "header":
3395
+ return template.header.default;
3396
+ case "header:first":
3397
+ return template.header.firstPage ?? [];
3398
+ case "footer":
3399
+ return template.footer.default;
3400
+ case "footer:first":
3401
+ return template.footer.firstPage ?? [];
3402
+ }
3403
+ }
3404
+ function indexTemplate(template) {
3405
+ const index = /* @__PURE__ */ new Map();
3406
+ const visit = (nodes, parent, region, path) => {
3407
+ nodes.forEach((node, i) => {
3408
+ const p = [...path, i];
3409
+ index.set(node.id, { node, parent, region, path: p, depth: p.length - 1, index: i });
3410
+ if (node.children) visit(node.children, node, region, p);
3411
+ });
3412
+ };
3413
+ for (const r of REGION_IDS) visit(readRegion(template, r), null, r, []);
3414
+ return index;
3415
+ }
3416
+ function findNode(template, id) {
3417
+ return indexTemplate(template).get(id);
3418
+ }
3419
+ function listAt(template, location) {
3420
+ let list = getRegion(template, location.region);
3421
+ for (let i = 0; i < location.path.length - 1; i++) {
3422
+ const n = list[location.path[i]];
3423
+ if (!n) throw new Error("Invalid node path");
3424
+ n.children ??= [];
3425
+ list = n.children;
3426
+ }
3427
+ return { list, index: location.path[location.path.length - 1] ?? 0 };
3428
+ }
3429
+ function nodeAt(template, location) {
3430
+ const { list, index } = listAt(template, location);
3431
+ return list[index];
3432
+ }
3433
+ function ancestors(index, id) {
3434
+ const chain = [];
3435
+ let cur = index.get(id);
3436
+ while (cur) {
3437
+ chain.unshift(cur);
3438
+ cur = cur.parent ? index.get(cur.parent.id) : void 0;
3439
+ }
3440
+ return chain;
3441
+ }
3442
+ function isDescendant(index, ancestorId, id) {
3443
+ let cur = index.get(id);
3444
+ while (cur?.parent) {
3445
+ if (cur.parent.id === ancestorId) return true;
3446
+ cur = index.get(cur.parent.id);
3447
+ }
3448
+ return false;
3449
+ }
3450
+ function walkTemplate(template, visit) {
3451
+ const rec = (nodes, region, parent) => {
3452
+ for (const n of nodes) {
3453
+ visit(n, region, parent);
3454
+ if (n.children) rec(n.children, region, n);
3455
+ }
3456
+ };
3457
+ for (const r of REGION_IDS) rec(readRegion(template, r), r, null);
3458
+ }
3459
+ function countNodes(template) {
3460
+ let n = 0;
3461
+ walkTemplate(template, () => n++);
3462
+ return n;
3463
+ }
3464
+ function usedBlockTypes(template) {
3465
+ const s = /* @__PURE__ */ new Set();
3466
+ walkTemplate(template, (n) => s.add(n.type));
3467
+ return s;
3468
+ }
3469
+
3470
+ // src/editor/commands.ts
3471
+ import { current, isDraft } from "immer";
3472
+ var CommandError = class extends Error {
3473
+ constructor(message) {
3474
+ super(message);
3475
+ this.name = "CommandError";
3476
+ }
3477
+ };
3478
+ function setDeep(obj, path, value) {
3479
+ const parts = path.split(".");
3480
+ let cur = obj;
3481
+ for (let i = 0; i < parts.length - 1; i++) {
3482
+ const p = parts[i];
3483
+ const next = cur[p];
3484
+ if (Array.isArray(next)) cur = next;
3485
+ else if (next && typeof next === "object") cur = next;
3486
+ else {
3487
+ cur[p] = {};
3488
+ cur = cur[p];
3489
+ }
3490
+ }
3491
+ const last = parts[parts.length - 1];
3492
+ if (value === void 0) delete cur[last];
3493
+ else cur[last] = value;
3494
+ }
3495
+ function applyCommand(draft, cmd) {
3496
+ const t = draft;
3497
+ switch (cmd.type) {
3498
+ case "insertNode": {
3499
+ const { list, index } = listAt(t, cmd.at);
3500
+ list.splice(Math.min(index, list.length), 0, cmd.node);
3501
+ return;
3502
+ }
3503
+ case "insertNodes": {
3504
+ const { list, index } = listAt(t, cmd.at);
3505
+ list.splice(Math.min(index, list.length), 0, ...cmd.nodes);
3506
+ return;
3507
+ }
3508
+ case "removeNodes": {
3509
+ const index = indexTemplate(t);
3510
+ const entries = cmd.ids.map((id) => index.get(id)).filter((e) => !!e);
3511
+ entries.sort((a, b) => b.depth - a.depth || b.index - a.index);
3512
+ for (const e of entries) {
3513
+ const list = e.parent ? e.parent.children ??= [] : getRegion(t, e.region);
3514
+ const i = list.findIndex((n) => n.id === e.node.id);
3515
+ if (i >= 0) list.splice(i, 1);
3516
+ }
3517
+ return;
3518
+ }
3519
+ case "moveNode": {
3520
+ const index = indexTemplate(t);
3521
+ const entry = index.get(cmd.id);
3522
+ if (!entry) throw new CommandError(`Node ${cmd.id} not found`);
3523
+ const target = listAt(t, cmd.to);
3524
+ let probe = target.list === entry.node.children ? entry.node : void 0;
3525
+ if (probe) throw new CommandError("Cannot move a node inside itself");
3526
+ const containerPath = cmd.to.path.slice(0, -1);
3527
+ let cur = getRegion(t, cmd.to.region);
3528
+ for (const i of containerPath) {
3529
+ const n = cur[i];
3530
+ if (!n) break;
3531
+ if (n.id === cmd.id) throw new CommandError("Cannot move a node inside itself");
3532
+ cur = n.children ?? [];
3533
+ }
3534
+ void probe;
3535
+ const sourceList = entry.parent ? entry.parent.children ??= [] : getRegion(t, entry.region);
3536
+ const sourceIndex = sourceList.indexOf(entry.node);
3537
+ const [node] = sourceList.splice(sourceIndex, 1);
3538
+ const dest = listAt(t, cmd.to);
3539
+ let destIndex = dest.index;
3540
+ if (dest.list === sourceList && sourceIndex < destIndex) destIndex--;
3541
+ dest.list.splice(Math.min(destIndex, dest.list.length), 0, node);
3542
+ return;
3543
+ }
3544
+ case "duplicateNode": {
3545
+ const index = indexTemplate(t);
3546
+ const entry = index.get(cmd.id);
3547
+ if (!entry) throw new CommandError(`Node ${cmd.id} not found`);
3548
+ const list = entry.parent ? entry.parent.children ??= [] : getRegion(t, entry.region);
3549
+ const copy = cloneWithNewIds(isDraft(entry.node) ? current(entry.node) : entry.node);
3550
+ if (cmd.newId) copy.id = cmd.newId;
3551
+ list.splice(entry.index + 1, 0, copy);
3552
+ return;
3553
+ }
3554
+ case "setProps": {
3555
+ const n = requireNode(t, cmd.id);
3556
+ for (const [k, v] of Object.entries(cmd.patch)) {
3557
+ if (v === void 0) delete n.props[k];
3558
+ else n.props[k] = v;
3559
+ }
3560
+ return;
3561
+ }
3562
+ case "setProp": {
3563
+ const n = requireNode(t, cmd.id);
3564
+ setDeep(n.props, cmd.path, cmd.value);
3565
+ return;
3566
+ }
3567
+ case "setStyle": {
3568
+ const n = requireNode(t, cmd.id);
3569
+ if (cmd.patch === null) {
3570
+ delete n.style;
3571
+ return;
3572
+ }
3573
+ n.style ??= {};
3574
+ mergeStyle(n.style, cmd.patch);
3575
+ if (Object.keys(n.style).length === 0) delete n.style;
3576
+ return;
3577
+ }
3578
+ case "setFlow": {
3579
+ const n = requireNode(t, cmd.id);
3580
+ n.flow = { ...n.flow ?? {}, ...cmd.patch };
3581
+ for (const k of Object.keys(n.flow)) if (n.flow[k] === void 0) delete n.flow[k];
3582
+ if (Object.keys(n.flow).length === 0) delete n.flow;
3583
+ return;
3584
+ }
3585
+ case "setVisibleWhen": {
3586
+ const n = requireNode(t, cmd.id);
3587
+ if (cmd.condition === void 0 || cmd.condition === "") delete n.visibleWhen;
3588
+ else n.visibleWhen = cmd.condition;
3589
+ return;
3590
+ }
3591
+ case "setName": {
3592
+ const n = requireNode(t, cmd.id);
3593
+ if (cmd.name) n.name = cmd.name;
3594
+ else delete n.name;
3595
+ return;
3596
+ }
3597
+ case "setLocked": {
3598
+ const n = requireNode(t, cmd.id);
3599
+ if (cmd.locked) n.locked = true;
3600
+ else delete n.locked;
3601
+ return;
3602
+ }
3603
+ case "setRowLayout": {
3604
+ const row = requireNode(t, cmd.id);
3605
+ if (row.type !== "row") throw new CommandError("setRowLayout requires a row");
3606
+ const total = cmd.layout.reduce((a, b) => a + b, 0);
3607
+ if (total !== 12) throw new CommandError("Row layout must sum to 12");
3608
+ row.children ??= [];
3609
+ const columns = row.children;
3610
+ while (columns.length > cmd.layout.length) {
3611
+ const removed = columns.pop();
3612
+ const target = columns[columns.length - 1];
3613
+ target.children ??= [];
3614
+ target.children.push(...removed.children ?? []);
3615
+ }
3616
+ while (columns.length < cmd.layout.length) columns.push(createColumn(cmd.layout[columns.length]));
3617
+ columns.forEach((c, i) => {
3618
+ c.props.span = cmd.layout[i];
3619
+ });
3620
+ row.props.layout = [...cmd.layout];
3621
+ return;
3622
+ }
3623
+ case "wrapNodes": {
3624
+ const index = indexTemplate(t);
3625
+ const entries = cmd.ids.map((id) => index.get(id)).filter((e) => !!e);
3626
+ if (entries.length === 0) return;
3627
+ const first = entries[0];
3628
+ const list = first.parent ? first.parent.children ??= [] : getRegion(t, first.region);
3629
+ const nodes = entries.map((e) => e.node);
3630
+ const startIndex = list.indexOf(first.node);
3631
+ for (const n of nodes) {
3632
+ const i = list.indexOf(n);
3633
+ if (i >= 0) list.splice(i, 1);
3634
+ }
3635
+ const wrapper = { ...cmd.wrapper, children: [...cmd.wrapper.children ?? [], ...nodes.map((n) => isDraft(n) ? current(n) : n)] };
3636
+ list.splice(Math.min(startIndex, list.length), 0, wrapper);
3637
+ return;
3638
+ }
3639
+ case "replaceNode": {
3640
+ const index = indexTemplate(t);
3641
+ const entry = index.get(cmd.id);
3642
+ if (!entry) throw new CommandError(`Node ${cmd.id} not found`);
3643
+ const list = entry.parent ? entry.parent.children ??= [] : getRegion(t, entry.region);
3644
+ list[entry.index] = cmd.node;
3645
+ return;
3646
+ }
3647
+ case "setPage": {
3648
+ Object.assign(t.page, structuredClone(cmd.patch));
3649
+ return;
3650
+ }
3651
+ case "setDocumentStyles": {
3652
+ const p = cmd.patch;
3653
+ if (p.text) for (const [id, s] of Object.entries(p.text)) t.styles.text[id] = { ...t.styles.text[id] ?? {}, ...s };
3654
+ if (p.table) Object.assign(t.styles.table, p.table);
3655
+ if (p.card) Object.assign(t.styles.card, p.card);
3656
+ if (p.link) Object.assign(t.styles.link, p.link);
3657
+ if (p.spacing) Object.assign(t.styles.spacing, p.spacing);
3658
+ return;
3659
+ }
3660
+ case "setTextStyle": {
3661
+ if (cmd.patch === null) {
3662
+ delete t.styles.text[cmd.id];
3663
+ return;
3664
+ }
3665
+ const existing = t.styles.text[cmd.id] ?? {};
3666
+ for (const [k, v] of Object.entries(cmd.patch)) {
3667
+ if (v === void 0) delete existing[k];
3668
+ else existing[k] = v;
3669
+ }
3670
+ t.styles.text[cmd.id] = existing;
3671
+ return;
3672
+ }
3673
+ case "setTemplateName": {
3674
+ t.name = cmd.name;
3675
+ return;
3676
+ }
3677
+ case "setSettings": {
3678
+ t.settings = { ...t.settings ?? {}, ...cmd.patch };
3679
+ return;
3680
+ }
3681
+ case "setRegionFirstPage": {
3682
+ t.page[cmd.region].differentFirstPage = cmd.enabled;
3683
+ if (cmd.enabled && !t[cmd.region].firstPage) t[cmd.region].firstPage = (isDraft(t[cmd.region].default) ? current(t[cmd.region].default) : t[cmd.region].default).map(cloneWithNewIds);
3684
+ return;
3685
+ }
3686
+ case "addLibraryItem": {
3687
+ t.library ??= [];
3688
+ t.library.push({ ...cmd.item, id: cmd.item.id || createId("lib") });
3689
+ return;
3690
+ }
3691
+ case "removeLibraryItem": {
3692
+ t.library = (t.library ?? []).filter((l) => l.id !== cmd.id);
3693
+ return;
3694
+ }
3695
+ case "replaceTemplate": {
3696
+ const fresh = structuredClone(cmd.template);
3697
+ for (const k of Object.keys(t)) delete t[k];
3698
+ Object.assign(t, fresh);
3699
+ return;
3700
+ }
3701
+ }
3702
+ }
3703
+ function requireNode(t, id) {
3704
+ const e = indexTemplate(t).get(id);
3705
+ if (!e) throw new CommandError(`Node ${id} not found`);
3706
+ return e.node;
3707
+ }
3708
+ function mergeStyle(target, patch) {
3709
+ for (const [k, v] of Object.entries(patch)) {
3710
+ if (v === void 0 || v === null) {
3711
+ delete target[k];
3712
+ continue;
3713
+ }
3714
+ if ((k === "padding" || k === "margin" || k === "border") && typeof v === "object") {
3715
+ const merged = { ...target[k] ?? {}, ...v };
3716
+ for (const key of Object.keys(merged)) if (merged[key] === void 0) delete merged[key];
3717
+ if (Object.keys(merged).length === 0) delete target[k];
3718
+ else target[k] = merged;
3719
+ continue;
3720
+ }
3721
+ target[k] = v;
3722
+ }
3723
+ }
3724
+
3725
+ // src/editor/store.ts
3726
+ import { produceWithPatches, applyPatches, enablePatches } from "immer";
3727
+ enablePatches();
3728
+ var EditorStore = class {
3729
+ state;
3730
+ undoStack = [];
3731
+ redoStack = [];
3732
+ listeners = /* @__PURE__ */ new Set();
3733
+ txDepth = 0;
3734
+ txPatches = [];
3735
+ txInverse = [];
3736
+ txLabel = "";
3737
+ txSelectionBefore = [];
3738
+ txSkipHistory = false;
3739
+ indexCache = null;
3740
+ /**
3741
+ * The history entry that was on top of the undo stack when the document was
3742
+ * last saved, so that undoing back to it reports clean again. Identity is the
3743
+ * whole test: undo pops, redo pushes, and a new edit pushes something else.
3744
+ */
3745
+ savedEntry = null;
3746
+ /** A change that bypassed history, which no amount of undoing can take back. */
3747
+ unhistoried = false;
3748
+ maxHistory;
3749
+ mergeWindowMs;
3750
+ onChange;
3751
+ constructor(opts) {
3752
+ this.state = {
3753
+ template: opts.template,
3754
+ selection: [],
3755
+ hoverId: null,
3756
+ editingId: null,
3757
+ mode: "design",
3758
+ zoom: 1,
3759
+ activeRegion: "body",
3760
+ dirty: false,
3761
+ version: 0
3762
+ };
3763
+ this.maxHistory = opts.maxHistory ?? 200;
3764
+ this.mergeWindowMs = opts.mergeWindowMs ?? 800;
3765
+ this.onChange = opts.onChange;
3766
+ }
3767
+ /* ----------------------------- reading ----------------------------- */
3768
+ getState() {
3769
+ return this.state;
3770
+ }
3771
+ get template() {
3772
+ return this.state.template;
3773
+ }
3774
+ /** Cached id → node index for the current document version. */
3775
+ index() {
3776
+ if (!this.indexCache || this.indexCache.version !== this.state.version) {
3777
+ this.indexCache = { version: this.state.version, index: indexTemplate(this.state.template) };
3778
+ }
3779
+ return this.indexCache.index;
3780
+ }
3781
+ getNode(id) {
3782
+ return this.index().get(id)?.node;
3783
+ }
3784
+ locationOf(id) {
3785
+ const e = this.index().get(id);
3786
+ return e ? { region: e.region, path: e.path } : void 0;
3787
+ }
3788
+ subscribe(listener) {
3789
+ this.listeners.add(listener);
3790
+ return () => {
3791
+ this.listeners.delete(listener);
3792
+ };
3793
+ }
3794
+ setState(next) {
3795
+ const prev = this.state;
3796
+ this.state = { ...prev, ...next };
3797
+ for (const l of this.listeners) l(this.state, prev);
3798
+ }
3799
+ /* ------------------------------ UI state --------------------------- */
3800
+ select(ids) {
3801
+ const selection = ids === null ? [] : Array.isArray(ids) ? ids : [ids];
3802
+ if (sameIds(selection, this.state.selection)) return;
3803
+ this.setState({ selection, editingId: selection.length === 1 && this.state.editingId === selection[0] ? this.state.editingId : null });
3804
+ }
3805
+ toggleSelect(id) {
3806
+ const has = this.state.selection.includes(id);
3807
+ this.select(has ? this.state.selection.filter((s) => s !== id) : [...this.state.selection, id]);
3808
+ }
3809
+ hover(id) {
3810
+ if (id !== this.state.hoverId) this.setState({ hoverId: id });
3811
+ }
3812
+ setEditing(id) {
3813
+ if (id !== this.state.editingId) this.setState({ editingId: id, selection: id ? [id] : this.state.selection });
3814
+ }
3815
+ setMode(mode) {
3816
+ if (mode !== this.state.mode) this.setState({ mode, editingId: null });
3817
+ }
3818
+ setZoom(zoom) {
3819
+ const z2 = Math.min(3, Math.max(0.25, Math.round(zoom * 100) / 100));
3820
+ if (z2 !== this.state.zoom) this.setState({ zoom: z2 });
3821
+ }
3822
+ setActiveRegion(region) {
3823
+ if (region !== this.state.activeRegion) this.setState({ activeRegion: region });
3824
+ }
3825
+ markClean() {
3826
+ this.savedEntry = this.undoStack[this.undoStack.length - 1] ?? null;
3827
+ this.unhistoried = false;
3828
+ if (this.state.dirty) this.setState({ dirty: false });
3829
+ }
3830
+ /** Whether the document differs from the last saved state. */
3831
+ isDirty() {
3832
+ if (this.unhistoried) return true;
3833
+ return (this.undoStack[this.undoStack.length - 1] ?? null) !== this.savedEntry;
3834
+ }
3835
+ /* ------------------------------ commands --------------------------- */
3836
+ /** Applies one command. Inside a transaction, patches accumulate into one history entry. */
3837
+ dispatch(command, opts = {}) {
3838
+ const [next, patches, inverse] = produceWithPatches(this.state.template, (draft) => {
3839
+ applyCommand(draft, command);
3840
+ });
3841
+ if (patches.length === 0) return;
3842
+ if (this.txDepth > 0) {
3843
+ this.txPatches.push(...patches);
3844
+ this.txInverse.unshift(...inverse);
3845
+ if (opts.skipHistory) this.txSkipHistory = true;
3846
+ this.commitTemplate(next, null);
3847
+ return;
3848
+ }
3849
+ const selectionBefore = this.state.selection;
3850
+ const entry = {
3851
+ label: opts.label ?? labelFor2(command),
3852
+ patches,
3853
+ inversePatches: inverse,
3854
+ selectionBefore,
3855
+ selectionAfter: selectionBefore,
3856
+ at: Date.now(),
3857
+ mergeKey: opts.mergeKey
3858
+ };
3859
+ if (opts.skipHistory) this.unhistoried = true;
3860
+ else this.pushHistory(entry);
3861
+ this.commitTemplate(next, opts.skipHistory ? null : entry);
3862
+ }
3863
+ /**
3864
+ * Groups every dispatch inside `fn` into a single undo step. Nested
3865
+ * transactions join the outermost one. If `fn` throws, all changes roll back.
3866
+ */
3867
+ transaction(label, fn) {
3868
+ if (this.txDepth === 0) {
3869
+ this.txPatches = [];
3870
+ this.txInverse = [];
3871
+ this.txLabel = label;
3872
+ this.txSelectionBefore = this.state.selection;
3873
+ this.txSkipHistory = false;
3874
+ }
3875
+ this.txDepth++;
3876
+ const before = this.state.template;
3877
+ let result;
3878
+ try {
3879
+ result = fn();
3880
+ } catch (e) {
3881
+ this.txDepth--;
3882
+ if (this.txDepth === 0) {
3883
+ this.commitTemplate(before, null);
3884
+ this.txPatches = [];
3885
+ this.txInverse = [];
3886
+ }
3887
+ throw e;
3888
+ }
3889
+ this.txDepth--;
3890
+ if (this.txDepth === 0 && this.txPatches.length > 0) {
3891
+ const entry = {
3892
+ label: this.txLabel,
3893
+ patches: this.txPatches,
3894
+ inversePatches: this.txInverse,
3895
+ selectionBefore: this.txSelectionBefore,
3896
+ selectionAfter: this.state.selection,
3897
+ at: Date.now()
3898
+ };
3899
+ if (this.txSkipHistory) this.unhistoried = true;
3900
+ else this.pushHistory(entry);
3901
+ this.setState({ dirty: this.isDirty() });
3902
+ this.onChange?.(this.state.template, entry);
3903
+ this.txPatches = [];
3904
+ this.txInverse = [];
3905
+ }
3906
+ return result;
3907
+ }
3908
+ commitTemplate(template, entry) {
3909
+ if (template === this.state.template) return;
3910
+ this.setState({ template, dirty: this.txDepth > 0 ? true : this.isDirty(), version: this.state.version + 1 });
3911
+ if (this.txDepth === 0) this.onChange?.(template, entry);
3912
+ }
3913
+ pushHistory(entry) {
3914
+ const last = this.undoStack[this.undoStack.length - 1];
3915
+ if (entry.mergeKey && last && last.mergeKey === entry.mergeKey && entry.at - last.at < this.mergeWindowMs) {
3916
+ if (last === this.savedEntry) this.savedEntry = null;
3917
+ last.patches.push(...entry.patches);
3918
+ last.inversePatches.unshift(...entry.inversePatches);
3919
+ last.at = entry.at;
3920
+ last.selectionAfter = entry.selectionAfter;
3921
+ } else {
3922
+ this.undoStack.push(entry);
3923
+ if (this.undoStack.length > this.maxHistory) this.undoStack.shift();
3924
+ }
3925
+ this.redoStack = [];
3926
+ }
3927
+ /* ------------------------------ history ---------------------------- */
3928
+ canUndo() {
3929
+ return this.undoStack.length > 0;
3930
+ }
3931
+ canRedo() {
3932
+ return this.redoStack.length > 0;
3933
+ }
3934
+ undoLabel() {
3935
+ return this.undoStack[this.undoStack.length - 1]?.label;
3936
+ }
3937
+ redoLabel() {
3938
+ return this.redoStack[this.redoStack.length - 1]?.label;
3939
+ }
3940
+ undo() {
3941
+ const entry = this.undoStack.pop();
3942
+ if (!entry) return false;
3943
+ const template = applyPatches(this.state.template, entry.inversePatches);
3944
+ this.redoStack.push(entry);
3945
+ this.setState({ template, selection: entry.selectionBefore.filter((id) => !!findIn(template, id)), editingId: null, dirty: this.isDirty(), version: this.state.version + 1 });
3946
+ this.onChange?.(template, null);
3947
+ return true;
3948
+ }
3949
+ redo() {
3950
+ const entry = this.redoStack.pop();
3951
+ if (!entry) return false;
3952
+ const template = applyPatches(this.state.template, entry.patches);
3953
+ this.undoStack.push(entry);
3954
+ this.setState({ template, selection: entry.selectionAfter.filter((id) => !!findIn(template, id)), editingId: null, dirty: this.isDirty(), version: this.state.version + 1 });
3955
+ this.onChange?.(template, null);
3956
+ return true;
3957
+ }
3958
+ /** Replaces the document and clears history (loading a template). */
3959
+ load(template) {
3960
+ this.undoStack = [];
3961
+ this.redoStack = [];
3962
+ this.savedEntry = null;
3963
+ this.unhistoried = false;
3964
+ this.setState({ template, selection: [], hoverId: null, editingId: null, dirty: false, version: this.state.version + 1 });
3965
+ }
3966
+ history() {
3967
+ return { undo: this.undoStack, redo: this.redoStack };
3968
+ }
3969
+ };
3970
+ function findIn(template, id) {
3971
+ return indexTemplate(template).get(id)?.node;
3972
+ }
3973
+ function sameIds(a, b) {
3974
+ return a.length === b.length && a.every((x, i) => x === b[i]);
3975
+ }
3976
+ function labelFor2(cmd) {
3977
+ switch (cmd.type) {
3978
+ case "insertNode":
3979
+ case "insertNodes":
3980
+ return "Insert";
3981
+ case "removeNodes":
3982
+ return "Delete";
3983
+ case "moveNode":
3984
+ return "Move";
3985
+ case "duplicateNode":
3986
+ return "Duplicate";
3987
+ case "setProps":
3988
+ case "setProp":
3989
+ return "Edit properties";
3990
+ case "setStyle":
3991
+ return "Edit style";
3992
+ case "setFlow":
3993
+ return "Edit page flow";
3994
+ case "setVisibleWhen":
3995
+ return "Edit visibility";
3996
+ case "setName":
3997
+ return "Rename";
3998
+ case "setLocked":
3999
+ return cmd.locked ? "Lock" : "Unlock";
4000
+ case "setRowLayout":
4001
+ return "Change columns";
4002
+ case "wrapNodes":
4003
+ return "Wrap";
4004
+ case "replaceNode":
4005
+ return "Replace";
4006
+ case "setPage":
4007
+ return "Page settings";
4008
+ case "setDocumentStyles":
4009
+ case "setTextStyle":
4010
+ return "Document styles";
4011
+ case "setTemplateName":
4012
+ return "Rename report";
4013
+ case "setSettings":
4014
+ return "Settings";
4015
+ case "setRegionFirstPage":
4016
+ return "First page header/footer";
4017
+ case "addLibraryItem":
4018
+ return "Save to library";
4019
+ case "removeLibraryItem":
4020
+ return "Remove from library";
4021
+ case "replaceTemplate":
4022
+ return "Load";
4023
+ }
4024
+ }
4025
+
4026
+ // src/render/render-node.ts
4027
+ function deferredScope(opts) {
4028
+ return new Scope({
4029
+ page: opts.page,
4030
+ now: opts.format.now ?? /* @__PURE__ */ new Date(),
4031
+ theme: { name: opts.theme.name, logo: opts.theme.logo, colors: opts.theme.colors, typography: opts.theme.typography, meta: opts.theme.meta ?? {} }
4032
+ });
4033
+ }
4034
+ function evalDeferred(expr, opts) {
4035
+ try {
4036
+ return toDisplayString(evaluate(expr, { scope: deferredScope(opts), format: opts.format }));
4037
+ } catch {
4038
+ return "";
4039
+ }
4040
+ }
4041
+ function tokenChip(expr, label, value) {
4042
+ return h("span", { class: "bp-token", "data-expr": expr, title: expr, contenteditable: "false" }, label ?? expr, value !== void 0 && value !== "" ? h("span", { class: "bp-token__value" }, value) : null);
4043
+ }
4044
+ function renderSegments(segments, fallback, opts) {
4045
+ if (!segments) return fallback !== void 0 ? [fallback] : [];
4046
+ return segments.map((s) => {
4047
+ if (s.kind === "text") return s.text;
4048
+ const value = s.deferred ? evalDeferred(s.expr, opts) : s.value ?? "";
4049
+ if (opts.mode === "design") return tokenChip(s.expr, s.label, s.deferred ? value : void 0);
4050
+ return value;
4051
+ });
4052
+ }
4053
+ function markWrap(children, marks, opts) {
4054
+ if (!marks) return children;
4055
+ let out = children;
4056
+ if (marks.code) out = h("code", { class: "bp-code" }, out);
4057
+ if (marks.bold) out = h("strong", null, out);
4058
+ if (marks.italic) out = h("em", null, out);
4059
+ if (marks.underline) out = h("u", null, out);
4060
+ if (marks.strike) out = h("s", null, out);
4061
+ if (marks.color) out = h("span", { style: { color: resolveValue(marks.color, opts.theme, "inherit") } }, out);
4062
+ if (marks.link) {
4063
+ const href = marks.link;
4064
+ const safe = isSafeUrl(href);
4065
+ const linkColor = resolveValue(opts.styles.link.color, opts.theme, opts.theme.colors.secondary);
4066
+ out = h("a", { class: "bp-link", href: safe ? href : void 0, rel: "noopener", style: { color: linkColor, "text-decoration": opts.styles.link.underline === false ? "none" : "underline" } }, out);
4067
+ }
4068
+ return out;
4069
+ }
4070
+ function renderInline(content, opts) {
4071
+ return content.map((n) => {
4072
+ if (n.type === "break") return h("br");
4073
+ if (n.type === "text") return markWrap([n.text], n.marks, opts);
4074
+ const value = n.deferred ? evalDeferred(n.expr, opts) : n.value ?? "";
4075
+ const inner = opts.mode === "design" ? tokenChip(n.expr, n.label, n.deferred ? value : void 0) : value;
4076
+ return markWrap([inner], n.marks, opts);
4077
+ });
4078
+ }
4079
+ function renderParagraph(p, opts) {
4080
+ const empty = p.content.length === 0;
4081
+ return h("p", { class: ["bp-p", empty && "bp-p--empty"], style: p.align ? { "text-align": p.align } : void 0 }, empty ? h("br") : renderInline(p.content, opts));
4082
+ }
4083
+ function renderRichText(doc, opts) {
4084
+ if (!doc) return [];
4085
+ return doc.content.map((b) => {
4086
+ if (b.type === "paragraph") return renderParagraph(b, opts);
4087
+ return h(
4088
+ b.type === "bulletList" ? "ul" : "ol",
4089
+ { class: "bp-list" },
4090
+ b.content.map((li) => h("li", null, li.content.map((p) => renderParagraph(p, opts))))
4091
+ );
4092
+ });
4093
+ }
4094
+ function nodeAttrs(node, extra = {}) {
4095
+ return {
4096
+ "data-key": node.key,
4097
+ "data-node": node.nodeId,
4098
+ "data-type": node.type,
4099
+ ...extra
4100
+ };
4101
+ }
4102
+ function columnWidth(rowWidth, span, gap, columns) {
4103
+ const inner = rowWidth - gap * Math.max(0, columns - 1);
4104
+ return Math.max(0, inner * span / 12);
4105
+ }
4106
+ function renderNode(node, opts, width = opts.width) {
4107
+ const def = opts.registry.get(node.type);
4108
+ const gap = opts.styles.spacing.blockGap;
4109
+ const columnGap = opts.styles.spacing.columnGap;
4110
+ let childWidth = width;
4111
+ if (def?.container) {
4112
+ const pad = node.style.padding;
4113
+ const bw = node.style.border ? node.style.border.width : 0;
4114
+ childWidth = Math.max(0, width - pad.left - pad.right - 2 * bw);
4115
+ }
4116
+ const children = [];
4117
+ if (node.role === "row") {
4118
+ const count = node.children.length;
4119
+ for (const col of node.children) {
4120
+ const w = columnWidth(childWidth, col.span ?? 12 / Math.max(1, count), columnGap, count);
4121
+ const v = renderNode(col, opts, w);
4122
+ if (v) children.push(v);
4123
+ }
4124
+ } else {
4125
+ for (const c of node.children) {
4126
+ const v = renderNode(c, opts, childWidth);
4127
+ if (v) children.push(v);
4128
+ }
4129
+ }
4130
+ const s = node.style;
4131
+ const css = styleToCss(s);
4132
+ delete css["margin"];
4133
+ delete css["width"];
4134
+ delete css["height"];
4135
+ const wrapperStyle = {};
4136
+ const m = s.margin;
4137
+ if (m.top || m.right || m.bottom || m.left) wrapperStyle["margin"] = `${m.top}px ${m.right}px ${m.bottom}px ${m.left}px`;
4138
+ if (s.width !== void 0 && node.role === "block") {
4139
+ wrapperStyle["width"] = typeof s.width === "number" ? `${s.width}px` : s.width;
4140
+ wrapperStyle["max-width"] = "100%";
4141
+ if (s.align === "center") wrapperStyle["margin-left"] = wrapperStyle["margin-right"] = "auto";
4142
+ else if (s.align === "right") wrapperStyle["margin-left"] = "auto";
4143
+ }
4144
+ if (s.height !== void 0 && node.role === "block") wrapperStyle["height"] = `${s.height}px`;
4145
+ if (node.role === "column") {
4146
+ wrapperStyle["grid-column"] = `span ${node.span ?? 12}`;
4147
+ wrapperStyle["min-width"] = "0";
4148
+ }
4149
+ const classes = ["bp-node", `bp-node--${node.type}`, `bp-${node.role}`];
4150
+ if (node.props.__hiddenByCondition) classes.push("bp-node--hidden");
4151
+ if (node.placeholder) classes.push("bp-node--placeholder");
4152
+ if (node.locked) classes.push("bp-node--locked");
4153
+ if (node.iteration) classes.push("bp-repeater-item");
4154
+ if (node.flow.keepTogether) classes.push("bp-node--keep");
4155
+ let attrs = nodeAttrs(node, { class: classes, style: wrapperStyle });
4156
+ if (opts.decorate) attrs = opts.decorate(node, attrs);
4157
+ if (!def) {
4158
+ return h("div", attrs, h("div", { class: "bp-unknown-block", style: cssText(css) }, `Unavailable block: ${node.type}`));
4159
+ }
4160
+ const ctx = {
4161
+ node,
4162
+ props: node.props,
4163
+ style: s,
4164
+ css,
4165
+ theme: opts.theme,
4166
+ styles: opts.styles,
4167
+ mode: opts.mode,
4168
+ h,
4169
+ children,
4170
+ page: opts.page,
4171
+ width,
4172
+ format: opts.format,
4173
+ registry: opts.registry,
4174
+ text: (prop, fallback) => renderSegments(node.textSegments[prop], fallback ?? (typeof node.props[prop] === "string" ? node.props[prop] : void 0), opts),
4175
+ richText: (prop) => renderRichText(node.richText[prop] ?? node.props[prop], opts),
4176
+ textValue: (value) => value === void 0 ? [] : [value],
4177
+ binding: (prop) => node.bindings[prop],
4178
+ partAttrs: (part, extra = {}) => ({ "data-part": part, "data-node": node.nodeId, ...extra }),
4179
+ placeholder: (text, extra = {}) => h("div", { class: "bp-placeholder", ...extra }, text)
4180
+ };
4181
+ let inner;
4182
+ try {
4183
+ inner = def.render(ctx);
4184
+ } catch (e) {
4185
+ inner = h("div", { class: "bp-block-error" }, `${def.name} failed to render: ${e.message}`);
4186
+ }
4187
+ if (!inner) return null;
4188
+ if (def.container && node.role !== "row" && node.role !== "column" && def.container.direction !== "row") {
4189
+ }
4190
+ void gap;
4191
+ return h("div", attrs, inner);
4192
+ }
4193
+ function renderSections(sections, opts) {
4194
+ const out = [];
4195
+ for (const s of sections) {
4196
+ const v = renderNode(s, opts, opts.width);
4197
+ if (v) out.push(v);
4198
+ }
4199
+ return out;
4200
+ }
4201
+ export {
4202
+ BlockRegistry,
4203
+ CONDITION_OPERATORS,
4204
+ CURRENT_SCHEMA_VERSION,
4205
+ CommandError,
4206
+ DEFAULT_PAGE_CONTEXT,
4207
+ DEFAULT_THEME,
4208
+ EditorStore,
4209
+ ExpressionError,
4210
+ FUNCTION_NAMES,
4211
+ GLOBAL_SCHEMAS,
4212
+ MIGRATIONS,
4213
+ MigrationError,
4214
+ PAGE_SIZES_MM,
4215
+ PIPES,
4216
+ PX_PER_MM,
4217
+ REGION_IDS,
4218
+ ROW_LAYOUTS,
4219
+ SchemaScope,
4220
+ Scope,
4221
+ ancestors,
4222
+ applyCommand,
4223
+ bestBreak,
4224
+ bindingSpecsFor,
4225
+ blockStyleSchema,
4226
+ checkExpressionSyntax,
4227
+ checkOutput,
4228
+ classNames,
4229
+ cloneWithNewIds,
4230
+ collectPaths,
4231
+ columnWidth,
4232
+ compareValues,
4233
+ compileCondition,
4234
+ computePageGeometry,
4235
+ countNodes,
4236
+ createColumn,
4237
+ createId,
4238
+ createNode,
4239
+ createParagraph,
4240
+ createRichText,
4241
+ createRow,
4242
+ createSchemaScope,
4243
+ createSection,
4244
+ createTemplate,
4245
+ cssText,
4246
+ dataSourceSchemas,
4247
+ defaultDocumentStyles,
4248
+ defaultPageSettings,
4249
+ defineBlock,
4250
+ describeExpression,
4251
+ documentStylesSchema,
4252
+ escapeHtml,
4253
+ evaluate,
4254
+ evaluateAst,
4255
+ evaluateCondition,
4256
+ evaluateText,
4257
+ evaluateToText,
4258
+ exprToString,
4259
+ extractTemplateTokens,
4260
+ findNode,
4261
+ formatCurrency,
4262
+ formatDate,
4263
+ formatDatePattern,
4264
+ formatNumber,
4265
+ formatPercent,
4266
+ getField,
4267
+ getRegion,
4268
+ h,
4269
+ humaniseLabel,
4270
+ humanize,
4271
+ humanizeKey,
4272
+ indexTemplate,
4273
+ inlineFromTemplate,
4274
+ inlineToPlain,
4275
+ interpolate,
4276
+ isDarkColor,
4277
+ isDescendant,
4278
+ isEmptyValue,
4279
+ isRichTextDoc,
4280
+ isSafeUrl,
4281
+ isTokenRef,
4282
+ isVNode,
4283
+ issue,
4284
+ itemSchemaOf,
4285
+ labelForPath,
4286
+ listAt,
4287
+ listThemeTokens,
4288
+ looseEquals,
4289
+ mergeTheme,
4290
+ migrateTemplate,
4291
+ missingThemeValues,
4292
+ mmToPx,
4293
+ needsMigration,
4294
+ nodeAt,
4295
+ nodeAttrs,
4296
+ nodeSchema,
4297
+ pageSettingsSchema,
4298
+ pageSizeMm,
4299
+ paginate,
4300
+ parseExpression,
4301
+ parseTemplate,
4302
+ pathToString,
4303
+ pxToMm,
4304
+ readProperty,
4305
+ readRegion,
4306
+ renderInline,
4307
+ renderNode,
4308
+ renderRichText,
4309
+ renderSections,
4310
+ renderSegments,
4311
+ resolveDefaultProps,
4312
+ resolvePath,
4313
+ resolvePathSchema,
4314
+ resolveStyle,
4315
+ resolveTemplate,
4316
+ resolveToken,
4317
+ resolveValue,
4318
+ richTextFromTemplate,
4319
+ richTextToPlain,
4320
+ richTextTokens,
4321
+ setDeterministicIds,
4322
+ splitTemplate,
4323
+ styleString,
4324
+ styleToCss,
4325
+ synthesizeSampleData,
4326
+ synthesizeValue,
4327
+ templateSchema,
4328
+ textStyleLabel,
4329
+ toDate,
4330
+ toDisplayString,
4331
+ toHtml,
4332
+ toNumber,
4333
+ tokenChip,
4334
+ tokenize,
4335
+ truthy,
4336
+ typeCheck,
4337
+ typeCheckAst,
4338
+ usedBlockTypes,
4339
+ validateTemplate,
4340
+ walkTemplate,
4341
+ walkVNodes
4342
+ };
4343
+ //# sourceMappingURL=index.js.map