@psrt/sdk 0.1.0 → 0.1.2

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.cjs CHANGED
@@ -18,8 +18,9 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
 
20
20
  // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ CompileStep: () => CompileStep,
23
24
  Transformer: () => Transformer,
24
25
  adaptEntriesForWeb: () => adaptEntriesForWeb,
25
26
  addConst: () => addConst,
@@ -28,6 +29,7 @@ __export(index_exports, {
28
29
  addPage: () => addPage,
29
30
  addText: () => addText,
30
31
  compileToHtml: () => compileToHtml,
32
+ compileToHtmlPure: () => compileToHtmlPure2,
31
33
  compileToSvg: () => compileToSvg,
32
34
  findMaskByIndex: () => findMaskByIndex,
33
35
  findPage: () => findPage,
@@ -39,6 +41,7 @@ __export(index_exports, {
39
41
  mergePageDocumentPSRT: () => mergePageDocumentPSRT,
40
42
  mergeStyle: () => mergeStyle,
41
43
  movePage: () => movePage,
44
+ notifyObservers: () => notifyObservers,
42
45
  nudgeTextPosition: () => nudgeTextPosition,
43
46
  parse: () => parse,
44
47
  parseTextIndex: () => parseTextIndex,
@@ -56,6 +59,7 @@ __export(index_exports, {
56
59
  reorderTextRelative: () => reorderTextRelative,
57
60
  reorderTextTo: () => reorderTextTo,
58
61
  resolveDocument: () => resolveDocument,
62
+ resolveDocumentPure: () => resolveDocumentPure,
59
63
  resolveDocumentStrict: () => resolveDocumentStrict,
60
64
  revertConstReferences: () => revertConstReferences,
61
65
  setMaskPosition: () => setMaskPosition,
@@ -70,10 +74,9 @@ __export(index_exports, {
70
74
  substituteConstReferences: () => substituteConstReferences,
71
75
  transform: () => transform
72
76
  });
73
- module.exports = __toCommonJS(index_exports);
77
+ module.exports = __toCommonJS(src_exports);
74
78
 
75
79
  // src/wasm.ts
76
- var import_meta = {};
77
80
  var decoder = new TextDecoder();
78
81
  var encoder = new TextEncoder();
79
82
  var wasmExports = null;
@@ -100,21 +103,21 @@ function encodeArg(arg) {
100
103
  }
101
104
  function call(name, ...args) {
102
105
  if (!wasmExports) {
103
- throw new Error("PSRT WASM not initialized; call initPsrt() first");
106
+ throw new Error("PSRT is not initialized; call initPsrt() first");
104
107
  }
105
108
  const fn = wasmExports[name];
106
109
  if (!fn) {
107
- throw new Error(`unknown WASM handler: ${name}`);
110
+ throw new Error(`unknown PSRT handler: ${name}`);
108
111
  }
109
112
  const encoded = args.map(encodeArg);
110
113
  return fn(...encoded);
111
114
  }
112
115
  function requireBytes(result) {
113
116
  if (!result.ok) {
114
- throw new Error(result.err ?? "WASM call failed");
117
+ throw new Error(result.err ?? "PSRT call failed");
115
118
  }
116
119
  if (!result.data) {
117
- throw new Error("WASM call returned no data");
120
+ throw new Error("PSRT call returned no data");
118
121
  }
119
122
  return result.data;
120
123
  }
@@ -160,35 +163,45 @@ function invokeCompileToSvg(input, pageName, options) {
160
163
  function wireWasmFromGlobal() {
161
164
  const exp = globalThis.psrtWasm;
162
165
  if (!exp) {
163
- throw new Error("psrtWasm exports not found on globalThis");
166
+ throw new Error("PSRT core exports not found");
164
167
  }
165
168
  wasmExports = exp;
166
169
  }
167
- async function initPsrt(wasmUrl) {
168
- if (wasmExports) {
169
- return;
170
+
171
+ // src/runtime/boot.node.ts
172
+ var import_node_fs = require("fs");
173
+ var import_node_path = require("path");
174
+ var import_node_url = require("url");
175
+ var import_meta = {};
176
+ function wasmDir() {
177
+ let dir = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
178
+ for (; ; ) {
179
+ const candidate = (0, import_node_path.join)(dir, "wasm", "psrt.wasm");
180
+ if ((0, import_node_fs.existsSync)(candidate)) {
181
+ return (0, import_node_path.join)(dir, "wasm");
182
+ }
183
+ const parent = (0, import_node_path.dirname)(dir);
184
+ if (parent === dir) {
185
+ break;
186
+ }
187
+ dir = parent;
170
188
  }
171
- if (globalThis.psrtWasm) {
172
- wireWasmFromGlobal();
189
+ throw new Error("PSRT core binary not found");
190
+ }
191
+ async function loadGoRuntimeNode() {
192
+ if (typeof globalThis.Go !== "undefined") {
173
193
  return;
174
194
  }
175
- await loadWasmExec();
176
- const go = new Go();
177
- const url = wasmUrl ?? new URL("../wasm/psrt.wasm", import_meta.url).href;
178
- const response = await fetch(url);
179
- if (!response.ok) {
180
- throw new Error(`failed to fetch WASM: ${response.status}`);
181
- }
182
- const bytes = await response.arrayBuffer();
183
- const { instance } = await WebAssembly.instantiate(bytes, go.importObject);
184
- void go.run(instance);
185
- await waitForExports();
186
- const exp = globalThis.psrtWasm;
187
- if (!exp) {
188
- throw new Error("psrtWasm exports not found after WASM init");
189
- }
190
- wasmExports = exp;
195
+ const scriptPath = (0, import_node_path.join)(wasmDir(), "wasm_exec.js");
196
+ await import((0, import_node_url.pathToFileURL)(scriptPath).href);
191
197
  }
198
+ async function loadCoreBytesNode() {
199
+ const bytes = (0, import_node_fs.readFileSync)((0, import_node_path.join)(wasmDir(), "psrt.wasm"));
200
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
201
+ }
202
+
203
+ // src/runtime/boot.node-entry.ts
204
+ var bootPromise = null;
192
205
  function waitForExports() {
193
206
  return new Promise((resolve, reject) => {
194
207
  let attempts = 0;
@@ -199,7 +212,7 @@ function waitForExports() {
199
212
  }
200
213
  attempts++;
201
214
  if (attempts > 200) {
202
- reject(new Error("timeout waiting for psrtWasm exports"));
215
+ reject(new Error("timeout loading PSRT core"));
203
216
  return;
204
217
  }
205
218
  setTimeout(tick, 10);
@@ -207,15 +220,32 @@ function waitForExports() {
207
220
  tick();
208
221
  });
209
222
  }
210
- async function loadWasmExec() {
211
- if (typeof globalThis.Go !== "undefined") {
223
+ async function startCore(bytes) {
224
+ await loadGoRuntimeNode();
225
+ const go = new Go();
226
+ const { instance } = await WebAssembly.instantiate(bytes, go.importObject);
227
+ void go.run(instance);
228
+ await waitForExports();
229
+ wireWasmFromGlobal();
230
+ }
231
+ async function bootCore() {
232
+ if (bootPromise) {
233
+ return bootPromise;
234
+ }
235
+ if (globalThis.psrtWasm) {
236
+ wireWasmFromGlobal();
212
237
  return;
213
238
  }
214
- const scriptUrl = new URL("../wasm/wasm_exec.js", import_meta.url).href;
215
- await import(
216
- /* @vite-ignore */
217
- scriptUrl
218
- );
239
+ bootPromise = (async () => {
240
+ const bytes = await loadCoreBytesNode();
241
+ await startCore(bytes);
242
+ })();
243
+ return bootPromise;
244
+ }
245
+
246
+ // src/init.ts
247
+ async function initPsrt() {
248
+ await bootCore();
219
249
  }
220
250
 
221
251
  // src/parse.ts
@@ -229,10 +259,2360 @@ function formatDocument(doc) {
229
259
  return invokeFormatDocument(doc);
230
260
  }
231
261
 
262
+ // src/html/text/expandConsts.ts
263
+ function expandConsts(content, consts) {
264
+ if (!consts || !content) return content;
265
+ const keys = Object.keys(consts).sort((a, b) => {
266
+ if (b.length !== a.length) return b.length - a.length;
267
+ return a.localeCompare(b);
268
+ });
269
+ let out = content;
270
+ for (const k of keys) {
271
+ out = out.split(`@${k}@`).join(consts[k]);
272
+ }
273
+ return out;
274
+ }
275
+ function compactJsonString(s) {
276
+ const trimmed = s.trim();
277
+ if (!trimmed) return trimmed;
278
+ try {
279
+ return JSON.stringify(JSON.parse(trimmed));
280
+ } catch {
281
+ return trimmed.replace(/\n/g, "").replace(/\r/g, "");
282
+ }
283
+ }
284
+ function expandConstsInStyle(style, consts) {
285
+ const raw = typeof style === "string" ? style.trim() : JSON.stringify(style ?? {});
286
+ if (!raw || raw === "{}") return style;
287
+ const compact = compactJsonString(raw);
288
+ const expanded = expandConsts(compact, consts);
289
+ try {
290
+ JSON.parse(expanded);
291
+ return expanded;
292
+ } catch {
293
+ return style;
294
+ }
295
+ }
296
+
297
+ // src/html/assets/dimensions.ts
298
+ var DEFAULT_WIDTH = 1080;
299
+ var DEFAULT_HEIGHT = 1920;
300
+ function dimensionsFromStandard(body) {
301
+ if (body.length < 24) return null;
302
+ if (body[0] === 137 && body[1] === 80 && body[2] === 78 && body[3] === 71) {
303
+ const w = body[16] << 24 | body[17] << 16 | body[18] << 8 | body[19];
304
+ const h = body[20] << 24 | body[21] << 16 | body[22] << 8 | body[23];
305
+ if (w > 0 && h > 0) return { w, h };
306
+ }
307
+ if (body[0] === 255 && body[1] === 216) {
308
+ let i = 2;
309
+ while (i + 9 < body.length) {
310
+ if (body[i] !== 255) {
311
+ i++;
312
+ continue;
313
+ }
314
+ const marker = body[i + 1];
315
+ const len = body[i + 2] << 8 | body[i + 3];
316
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
317
+ const h = body[i + 5] << 8 | body[i + 6];
318
+ const w = body[i + 7] << 8 | body[i + 8];
319
+ if (w > 0 && h > 0) return { w, h };
320
+ return null;
321
+ }
322
+ i += 2 + len;
323
+ }
324
+ }
325
+ if (body[0] === 71 && body[1] === 73 && body[2] === 70) {
326
+ const w = body[6] | body[7] << 8;
327
+ const h = body[8] | body[9] << 8;
328
+ if (w > 0 && h > 0) return { w, h };
329
+ }
330
+ return null;
331
+ }
332
+ function isWebP(body) {
333
+ return body.length >= 12 && body[0] === 82 && body[1] === 73 && body[2] === 70 && body[3] === 70 && body[8] === 87 && body[9] === 69 && body[10] === 66 && body[11] === 80;
334
+ }
335
+ function webpDimensions(body) {
336
+ if (!isWebP(body)) return null;
337
+ let off = 12;
338
+ while (off + 8 <= body.length) {
339
+ const chunk = String.fromCharCode(body[off], body[off + 1], body[off + 2], body[off + 3]);
340
+ const size = body[off + 4] | body[off + 5] << 8 | body[off + 6] << 16 | body[off + 7] << 24;
341
+ const dataStart = off + 8;
342
+ const dataEnd = dataStart + size;
343
+ if (size < 0 || dataEnd > body.length) break;
344
+ if (chunk === "VP8X" && size >= 10) {
345
+ const w = body[dataStart + 4] | body[dataStart + 5] << 8 | body[dataStart + 6] << 16;
346
+ const h = body[dataStart + 7] | body[dataStart + 8] << 8 | body[dataStart + 9] << 16;
347
+ if (w > 0 && h > 0) return { w: w + 1, h: h + 1 };
348
+ }
349
+ if (chunk === "VP8L" && size >= 5) {
350
+ const b0 = body[dataStart];
351
+ const b1 = body[dataStart + 1];
352
+ const b2 = body[dataStart + 2];
353
+ const b3 = body[dataStart + 3];
354
+ const w = 1 + b0 + (b1 & 63) * 256;
355
+ const h = 1 + (b1 >> 6) + (b2 & 15) * 4 + (b3 & 252) * 2;
356
+ if (w > 0 && h > 0) return { w, h };
357
+ }
358
+ off = dataEnd + (size & 1);
359
+ }
360
+ return null;
361
+ }
362
+ function isAVIF(body) {
363
+ if (body.length < 12) return false;
364
+ if (String.fromCharCode(body[4], body[5], body[6], body[7]) !== "ftyp") return false;
365
+ const brand = String.fromCharCode(body[8], body[9], body[10], body[11]);
366
+ return brand === "avif" || brand === "avis" || brand === "mif1" || brand === "MA1A";
367
+ }
368
+ function avifDimensions(body) {
369
+ let found = null;
370
+ const walk = (buf) => {
371
+ for (let off = 0; off + 8 <= buf.length; ) {
372
+ let size = buf[off] << 24 | buf[off + 1] << 16 | buf[off + 2] << 8 | buf[off + 3];
373
+ const boxType = String.fromCharCode(buf[off + 4], buf[off + 5], buf[off + 6], buf[off + 7]);
374
+ let header = 8;
375
+ if (size === 1 && off + 16 <= buf.length) {
376
+ size = Number(
377
+ BigInt(buf[off + 8]) << 56n | BigInt(buf[off + 9]) << 48n | BigInt(buf[off + 10]) << 40n | BigInt(buf[off + 11]) << 32n | BigInt(buf[off + 12]) << 24n | BigInt(buf[off + 13]) << 16n | BigInt(buf[off + 14]) << 8n | BigInt(buf[off + 15])
378
+ );
379
+ header = 16;
380
+ }
381
+ if (size < header || off + size > buf.length) break;
382
+ const payloadStart = off + header;
383
+ const payloadEnd = off + size;
384
+ const payload = buf.slice(payloadStart, payloadEnd);
385
+ if (boxType === "ispe" && payload.length >= 12) {
386
+ const w = payload[4] << 24 | payload[5] << 16 | payload[6] << 8 | payload[7];
387
+ const h = payload[8] << 24 | payload[9] << 16 | payload[10] << 8 | payload[11];
388
+ if (w > 0 && h > 0) {
389
+ found = { w, h };
390
+ return false;
391
+ }
392
+ }
393
+ const containers = ["meta", "iprp", "moov", "trak", "mdia", "minf", "stbl", "stsd", "ipco"];
394
+ if (containers.includes(boxType)) {
395
+ let child = payload;
396
+ if (payload.length >= 4) child = payload.slice(4);
397
+ if (!walk(child)) return false;
398
+ }
399
+ off += size;
400
+ }
401
+ return true;
402
+ };
403
+ walk(body);
404
+ return found;
405
+ }
406
+ function imageDimensions(body, mime) {
407
+ if (!body || body.length === 0) return { w: DEFAULT_WIDTH, h: DEFAULT_HEIGHT };
408
+ const std = dimensionsFromStandard(body);
409
+ if (std) return std;
410
+ const m = mime.trim().toLowerCase();
411
+ if (m === "image/webp" || isWebP(body)) {
412
+ const d = webpDimensions(body);
413
+ if (d) return d;
414
+ }
415
+ if (m === "image/avif" || isAVIF(body)) {
416
+ const d = avifDimensions(body);
417
+ if (d) return d;
418
+ }
419
+ return { w: DEFAULT_WIDTH, h: DEFAULT_HEIGHT };
420
+ }
421
+ function decodeDataUri(uri) {
422
+ const match = /^data:([^;,]+)?(?:;base64)?,(.*)$/s.exec(uri);
423
+ if (!match) return null;
424
+ const mime = match[1] || "application/octet-stream";
425
+ const data = match[2];
426
+ if (/;base64/i.test(uri.slice(0, uri.indexOf(",") + 1))) {
427
+ const binary = atob(data);
428
+ const bytes2 = new Uint8Array(binary.length);
429
+ for (let i = 0; i < binary.length; i++) bytes2[i] = binary.charCodeAt(i);
430
+ return { mime, bytes: bytes2 };
431
+ }
432
+ const decoded = decodeURIComponent(data);
433
+ const bytes = new TextEncoder().encode(decoded);
434
+ return { mime, bytes };
435
+ }
436
+ function encodeDataUri(mime, bytes) {
437
+ let binary = "";
438
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
439
+ return `data:${mime};base64,${btoa(binary)}`;
440
+ }
441
+
442
+ // src/html/assets/refs.ts
443
+ function looksLikeHttpUrl(raw) {
444
+ const u = raw.trim().toLowerCase();
445
+ return u.startsWith("http://") || u.startsWith("https://");
446
+ }
447
+ function isDataUri(raw) {
448
+ return raw.startsWith("data:");
449
+ }
450
+ function hasWindowsDrive(s) {
451
+ if (s.length < 2 || s[1] !== ":") return false;
452
+ const c = s[0];
453
+ return c >= "a" && c <= "z" || c >= "A" && c <= "Z";
454
+ }
455
+ function isLocalAssetRef(raw) {
456
+ const trimmed = raw.trim();
457
+ if (!trimmed) return false;
458
+ if (looksLikeHttpUrl(trimmed)) return false;
459
+ const lower = trimmed.toLowerCase();
460
+ if (lower.startsWith("file:")) return true;
461
+ return trimmed.includes("\\") || trimmed.includes("/") || hasWindowsDrive(trimmed);
462
+ }
463
+ function isAssetReference(raw) {
464
+ return looksLikeHttpUrl(raw) || isLocalAssetRef(raw) || isDataUri(raw);
465
+ }
466
+ function resolveAssetReference(raw, consts) {
467
+ return expandConsts(raw.trim(), consts);
468
+ }
469
+ function collectAssetUrls(doc) {
470
+ const consts = doc.consts ?? {};
471
+ const seen = /* @__PURE__ */ new Set();
472
+ const out = [];
473
+ const add = (raw) => {
474
+ const u = resolveAssetReference(raw ?? "", consts);
475
+ if (!u || !isAssetReference(u) || seen.has(u)) return;
476
+ seen.add(u);
477
+ out.push(u);
478
+ };
479
+ for (const page of doc.pages ?? []) {
480
+ add(page.imageUrl);
481
+ for (const t of page.texts ?? []) add(t.imageRef);
482
+ for (const m of page.masks ?? []) add(m.imageRef);
483
+ }
484
+ for (const f of doc.fonts ?? []) add(f);
485
+ return out;
486
+ }
487
+ function assetRef(resolvedUrl, asset, linksOnly) {
488
+ if (linksOnly) return resolvedUrl;
489
+ if (!asset) return resolvedUrl;
490
+ return encodeDataUri(asset.mime, asset.bytes);
491
+ }
492
+ function fontSrcUrl(fontUrl, asset, linksOnly) {
493
+ if (linksOnly) return `url(${fontUrl})`;
494
+ if (!asset) return `url(${fontUrl})`;
495
+ return `url(${encodeDataUri(asset.mime, asset.bytes)})`;
496
+ }
497
+ function buildAssetMap(docs, linksOnly) {
498
+ const list = Array.isArray(docs) ? docs : [docs];
499
+ const map = /* @__PURE__ */ new Map();
500
+ if (linksOnly) return map;
501
+ for (const doc of list) {
502
+ for (const url of collectAssetUrls(doc)) {
503
+ if (isDataUri(url)) {
504
+ const asset = decodeDataUri(url);
505
+ if (asset) map.set(url, asset);
506
+ }
507
+ }
508
+ for (const font of doc.fonts ?? []) {
509
+ const u = resolveAssetReference(font, doc.consts ?? {});
510
+ if (isDataUri(u)) {
511
+ const asset = decodeDataUri(u);
512
+ if (asset) map.set(u, asset);
513
+ }
514
+ }
515
+ }
516
+ return map;
517
+ }
518
+ function faceFormat(mime) {
519
+ switch (mime.trim().toLowerCase()) {
520
+ case "font/woff2":
521
+ return " format('woff2')";
522
+ case "font/woff":
523
+ return " format('woff')";
524
+ case "font/ttf":
525
+ return " format('truetype')";
526
+ case "font/otf":
527
+ return " format('opentype')";
528
+ default:
529
+ return "";
530
+ }
531
+ }
532
+ function fontFamilyNameForURL(fontURL, index) {
533
+ try {
534
+ const u = new URL(fontURL);
535
+ const family = u.searchParams.get("family");
536
+ if (family) {
537
+ const name = family.split(":")[0]?.replace(/\+/g, " ");
538
+ if (name) return name;
539
+ }
540
+ } catch {
541
+ }
542
+ return `CompiledFont_${index}`;
543
+ }
544
+
545
+ // src/html/text/inlineMarkup.ts
546
+ var DELIMS = [
547
+ { open: "***", close: "***", tagOpen: "<strong><em>", tagClose: "</em></strong>" },
548
+ { open: "**", close: "**", tagOpen: "<strong>", tagClose: "</strong>" },
549
+ { open: "*", close: "*", tagOpen: "<em>", tagClose: "</em>" },
550
+ { open: "_", close: "_", tagOpen: "<u>", tagClose: "</u>" },
551
+ { open: "~", close: "~", tagOpen: "<s>", tagClose: "</s>" }
552
+ ];
553
+ function escapeHtml(s) {
554
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
555
+ }
556
+ function renderSegment(s) {
557
+ let out = "";
558
+ let i = 0;
559
+ while (i < s.length) {
560
+ if (s[i] === "\\" && i + 1 < s.length) {
561
+ out += escapeHtml(s[i + 1]);
562
+ i += 2;
563
+ continue;
564
+ }
565
+ let matched = false;
566
+ for (const d of DELIMS) {
567
+ if (!s.startsWith(d.open, i)) continue;
568
+ const innerStart = i + d.open.length;
569
+ const closeAt = s.indexOf(d.close, innerStart);
570
+ if (closeAt <= innerStart) continue;
571
+ out += d.tagOpen + renderSegment(s.slice(innerStart, closeAt)) + d.tagClose;
572
+ i = closeAt + d.close.length;
573
+ matched = true;
574
+ break;
575
+ }
576
+ if (matched) continue;
577
+ out += escapeHtml(s[i]);
578
+ i += 1;
579
+ }
580
+ return out;
581
+ }
582
+ function plainSegment(s) {
583
+ let out = "";
584
+ let i = 0;
585
+ while (i < s.length) {
586
+ if (s[i] === "\\" && i + 1 < s.length) {
587
+ out += s[i + 1];
588
+ i += 2;
589
+ continue;
590
+ }
591
+ let matched = false;
592
+ for (const d of DELIMS) {
593
+ if (!s.startsWith(d.open, i)) continue;
594
+ const innerStart = i + d.open.length;
595
+ const closeAt = s.indexOf(d.close, innerStart);
596
+ if (closeAt <= innerStart) continue;
597
+ out += plainSegment(s.slice(innerStart, closeAt));
598
+ i = closeAt + d.close.length;
599
+ matched = true;
600
+ break;
601
+ }
602
+ if (matched) continue;
603
+ out += s[i];
604
+ i += 1;
605
+ }
606
+ return out;
607
+ }
608
+ function renderInlineHTML(content) {
609
+ if (!content) return "";
610
+ return content.split("\n").map((line) => renderSegment(line)).join("<br/>");
611
+ }
612
+ function plainTextForLayout(content) {
613
+ if (!content) return "";
614
+ return content.split("\n").map((line) => plainSegment(line)).join("\n");
615
+ }
616
+ function normalizeTextContent(content) {
617
+ return content.replace(/^[\t ]+/, "").replace(/[\t \r\n]+$/, "");
618
+ }
619
+
620
+ // src/html/resolve/resolveDocumentPure.ts
621
+ function resolveDocumentPure(doc) {
622
+ const consts = doc.consts;
623
+ if (!consts || Object.keys(consts).length === 0) return doc;
624
+ return {
625
+ ...doc,
626
+ pages: doc.pages.map((p) => ({
627
+ ...p,
628
+ style: expandConstsInStyle(p.style, consts),
629
+ imageUrl: expandConsts(p.imageUrl.trim(), consts),
630
+ texts: (p.texts ?? []).map((t) => ({
631
+ ...t,
632
+ style: expandConstsInStyle(t.style, consts),
633
+ content: normalizeTextContent(expandConsts(t.content, consts)),
634
+ imageRef: expandConsts((t.imageRef ?? "").trim(), consts)
635
+ })),
636
+ masks: (p.masks ?? []).map((m) => ({
637
+ ...m,
638
+ style: expandConstsInStyle(m.style, consts),
639
+ imageRef: expandConsts((m.imageRef ?? "").trim(), consts)
640
+ }))
641
+ }))
642
+ };
643
+ }
644
+
645
+ // src/html/document/pageBlocks.ts
646
+ var BlockText = "text";
647
+ var BlockMask = "mask";
648
+ function pageBlocksByIndex(page) {
649
+ const out = [];
650
+ for (const t of page.texts ?? []) {
651
+ out.push({ kind: BlockText, text: t });
652
+ }
653
+ for (const m of page.masks ?? []) {
654
+ out.push({ kind: BlockMask, mask: m });
655
+ }
656
+ out.sort((a, b) => blockIndex(a) - blockIndex(b));
657
+ return out;
658
+ }
659
+ function blockIndex(e) {
660
+ if (e.kind === BlockText && e.text) return e.text.index;
661
+ if (e.kind === BlockMask && e.mask) return e.mask.index;
662
+ return 0;
663
+ }
664
+
665
+ // src/html/style/keys.ts
666
+ var KeyPosition = "position";
667
+ var KeyLeft = "left";
668
+ var KeyTop = "top";
669
+ var KeyWidth = "width";
670
+ var KeyHeight = "height";
671
+ var KeyBoxSizing = "boxSizing";
672
+ var KeyPadding = "padding";
673
+ var KeyPaddingTop = "paddingTop";
674
+ var KeyPaddingRight = "paddingRight";
675
+ var KeyPaddingBottom = "paddingBottom";
676
+ var KeyPaddingLeft = "paddingLeft";
677
+ var KeyBorder = "border";
678
+ var KeyBorderTop = "borderTop";
679
+ var KeyBorderRight = "borderRight";
680
+ var KeyBorderBottom = "borderBottom";
681
+ var KeyBorderLeft = "borderLeft";
682
+ var KeyBorderWidth = "borderWidth";
683
+ var KeyBorderStyle = "borderStyle";
684
+ var KeyBorderColor = "borderColor";
685
+ var KeyBorderRadius = "borderRadius";
686
+ var KeyBorderTopLeftRadius = "borderTopLeftRadius";
687
+ var KeyBorderTopRightRadius = "borderTopRightRadius";
688
+ var KeyBorderBottomRightRadius = "borderBottomRightRadius";
689
+ var KeyBorderBottomLeftRadius = "borderBottomLeftRadius";
690
+ var KeyBoxShadow = "boxShadow";
691
+ var KeyTextShadow = "textShadow";
692
+ var KeyGlow = "glow";
693
+ var KeyBevel = "bevel";
694
+ var KeyBlur = "blur";
695
+ var KeyBlurLeft = "blurLeft";
696
+ var KeyBlurRight = "blurRight";
697
+ var KeyBlurTop = "blurTop";
698
+ var KeyBlurBottom = "blurBottom";
699
+ var KeyBackground = "background";
700
+ var KeyColor = "color";
701
+ var KeyTextAlign = "textAlign";
702
+ var KeyAlignItems = "alignItems";
703
+ var KeyTextDecoration = "textDecoration";
704
+ var KeyTextDecorationLine = "textDecorationLine";
705
+ var KeyLetterSpacing = "letterSpacing";
706
+ var KeyLineHeight = "lineHeight";
707
+ var KeyWordSpacing = "wordSpacing";
708
+ var KeyWhiteSpace = "whiteSpace";
709
+ var KeyTextTransform = "textTransform";
710
+ var KeyTextIndent = "textIndent";
711
+ var KeyTextOverflow = "textOverflow";
712
+ var KeyOpacity = "opacity";
713
+ var KeyStroke = "stroke";
714
+ var KeyStrokeWidth = "strokeWidth";
715
+ var KeyStrokeColor = "strokeColor";
716
+ var KeyTransform = "transform";
717
+ var KeyTransformOrigin = "transformOrigin";
718
+ var KeyTranslate = "translate";
719
+ var KeyRotate = "rotate";
720
+ var KeyScale = "scale";
721
+ var KeySkew = "skew";
722
+ var KeyMatrix = "matrix";
723
+ var KeyFontFamily = "fontFamily";
724
+ var KeyFontSize = "fontSize";
725
+ var KeyFontWeight = "fontWeight";
726
+ var KeyFontStyle = "fontStyle";
727
+ var KeyFontVariant = "fontVariant";
728
+ var KeyFontStretch = "fontStretch";
729
+ var boxKeys = /* @__PURE__ */ new Set([
730
+ KeyHeight,
731
+ KeyBackground,
732
+ KeyPadding,
733
+ KeyPaddingTop,
734
+ KeyPaddingRight,
735
+ KeyPaddingBottom,
736
+ KeyPaddingLeft,
737
+ KeyBorder,
738
+ KeyBorderTop,
739
+ KeyBorderRight,
740
+ KeyBorderBottom,
741
+ KeyBorderLeft,
742
+ KeyBorderWidth,
743
+ KeyBorderStyle,
744
+ KeyBorderColor,
745
+ KeyBorderRadius,
746
+ KeyBorderTopLeftRadius,
747
+ KeyBorderTopRightRadius,
748
+ KeyBorderBottomRightRadius,
749
+ KeyBorderBottomLeftRadius,
750
+ KeyBoxShadow,
751
+ KeyGlow,
752
+ KeyBevel,
753
+ KeyBlur,
754
+ KeyBlurLeft,
755
+ KeyBlurRight,
756
+ KeyBlurTop,
757
+ KeyBlurBottom
758
+ ]);
759
+ var textKeys = /* @__PURE__ */ new Set([
760
+ KeyColor,
761
+ KeyTextAlign,
762
+ KeyAlignItems,
763
+ KeyTextDecoration,
764
+ KeyTextDecorationLine,
765
+ KeyLetterSpacing,
766
+ KeyLineHeight,
767
+ KeyWordSpacing,
768
+ KeyWhiteSpace,
769
+ KeyTextTransform,
770
+ KeyTextIndent,
771
+ KeyTextOverflow,
772
+ KeyOpacity,
773
+ KeyTextShadow,
774
+ KeyStroke,
775
+ KeyStrokeWidth,
776
+ KeyStrokeColor,
777
+ KeyFontFamily,
778
+ KeyFontSize,
779
+ KeyFontWeight,
780
+ KeyFontStyle,
781
+ KeyFontVariant,
782
+ KeyFontStretch
783
+ ]);
784
+ var transformKeys = /* @__PURE__ */ new Set([
785
+ KeyTransform,
786
+ KeyTransformOrigin,
787
+ KeyTranslate,
788
+ KeyRotate,
789
+ KeyScale,
790
+ KeySkew,
791
+ KeyMatrix
792
+ ]);
793
+ function isBoxKey(key) {
794
+ return boxKeys.has(key);
795
+ }
796
+ function isTextKey(key) {
797
+ return textKeys.has(key);
798
+ }
799
+ function isTransformKey(key) {
800
+ return transformKeys.has(key);
801
+ }
802
+
803
+ // src/html/style/values.ts
804
+ function stringifyCSSValue(raw) {
805
+ if (raw === null || raw === void 0) return "";
806
+ if (typeof raw === "string") return raw.trim();
807
+ if (typeof raw === "number") return formatJSONNumber(raw);
808
+ if (typeof raw === "boolean") return raw ? "1" : "0";
809
+ return JSON.stringify(raw).trim();
810
+ }
811
+ function formatJSONNumber(value) {
812
+ if (Math.abs(value - Math.trunc(value)) < 1e-9 && Math.abs(value) < 1e12) {
813
+ return String(Math.trunc(value));
814
+ }
815
+ return value.toString();
816
+ }
817
+ function sanitizeCSSValue(value) {
818
+ const trimmed = value.trim();
819
+ if (!trimmed) return "";
820
+ return trimmed.replaceAll(";", "");
821
+ }
822
+ function hasStyleValue(key, raw) {
823
+ if (raw === null || raw === void 0) return false;
824
+ if (typeof raw === "boolean") return raw;
825
+ const value = sanitizeCSSValue(stringifyCSSValue(raw));
826
+ if (!value) return false;
827
+ if (omitZeroForKey(key) && isZeroLikeCSSValue(value)) return false;
828
+ return true;
829
+ }
830
+ function omitZeroForKey(key) {
831
+ switch (key) {
832
+ case KeyHeight:
833
+ case KeyWidth:
834
+ case KeyPadding:
835
+ case KeyPaddingTop:
836
+ case KeyPaddingRight:
837
+ case KeyPaddingBottom:
838
+ case KeyPaddingLeft:
839
+ case KeyBorderWidth:
840
+ case KeyStrokeWidth:
841
+ case KeyBorderRadius:
842
+ case KeyBorderTopLeftRadius:
843
+ case KeyBorderTopRightRadius:
844
+ case KeyBorderBottomRightRadius:
845
+ case KeyBorderBottomLeftRadius:
846
+ case KeyLetterSpacing:
847
+ case KeyWordSpacing:
848
+ case KeyLineHeight:
849
+ case KeyTextIndent:
850
+ case KeyBlur:
851
+ case KeyBlurLeft:
852
+ case KeyBlurRight:
853
+ case KeyBlurTop:
854
+ case KeyBlurBottom:
855
+ return true;
856
+ default:
857
+ return false;
858
+ }
859
+ }
860
+ function isZeroLikeCSSValue(value) {
861
+ const v = value.trim().toLowerCase();
862
+ switch (v) {
863
+ case "0":
864
+ case "0%":
865
+ case "0px":
866
+ case "0em":
867
+ case "0rem":
868
+ case "0pt":
869
+ case "0cqh":
870
+ case "0ch":
871
+ case "0vw":
872
+ case "0vh":
873
+ case "0vmin":
874
+ case "0vmax":
875
+ return true;
876
+ }
877
+ const units = ["px", "%", "em", "rem", "pt", "cqh", "ch"];
878
+ for (const unit of units) {
879
+ if (!v.endsWith(unit)) continue;
880
+ const n2 = Number.parseFloat(v.slice(0, -unit.length));
881
+ if (Number.isFinite(n2) && n2 === 0) return true;
882
+ }
883
+ const n = Number.parseFloat(v);
884
+ return Number.isFinite(n) && n === 0;
885
+ }
886
+ function filterStyleMap(style) {
887
+ if (Object.keys(style).length === 0) return style;
888
+ const out = {};
889
+ for (const [key, raw] of Object.entries(style)) {
890
+ if (hasStyleValue(key, raw)) {
891
+ out[key] = raw;
892
+ }
893
+ }
894
+ return out;
895
+ }
896
+ function pctString(value) {
897
+ return `${value}%`;
898
+ }
899
+ function pxString(value) {
900
+ return `${value}px`;
901
+ }
902
+
903
+ // src/html/style/names.ts
904
+ var aliasToCanonical = {
905
+ "border-radius": KeyBorderRadius,
906
+ "border-width": KeyBorderWidth,
907
+ "border-style": KeyBorderStyle,
908
+ "border-color": KeyBorderColor,
909
+ "box-shadow": KeyBoxShadow,
910
+ "text-shadow": KeyTextShadow,
911
+ "text-align": KeyTextAlign,
912
+ "align-items": KeyAlignItems,
913
+ "vertical-align": KeyAlignItems,
914
+ "font-family": KeyFontFamily,
915
+ "font-weight": KeyFontWeight,
916
+ "font-style": KeyFontStyle,
917
+ "line-height": KeyLineHeight,
918
+ "letter-spacing": KeyLetterSpacing,
919
+ "text-decoration": KeyTextDecoration,
920
+ "background-color": KeyBackground,
921
+ "stroke-width": KeyStrokeWidth,
922
+ "stroke-color": KeyStrokeColor,
923
+ "text-stroke": KeyStroke,
924
+ backGround: KeyBackground,
925
+ backgroundColor: KeyBackground,
926
+ br: KeyBorderRadius,
927
+ bw: KeyBorderWidth,
928
+ bc: KeyBorderColor,
929
+ bs: KeyBorderStyle,
930
+ bg: KeyBackground,
931
+ ta: KeyTextAlign,
932
+ ts: KeyTextShadow,
933
+ bsh: KeyBoxShadow,
934
+ blur: KeyBlur,
935
+ "blur-left": KeyBlurLeft,
936
+ "blur-right": KeyBlurRight,
937
+ "blur-top": KeyBlurTop,
938
+ "blur-bottom": KeyBlurBottom,
939
+ ff: KeyFontFamily,
940
+ fw: KeyFontWeight,
941
+ fs: KeyFontStyle,
942
+ pd: KeyPadding,
943
+ sw: KeyStrokeWidth,
944
+ sc: KeyStrokeColor,
945
+ textStroke: KeyStroke,
946
+ textStrokeWidth: KeyStrokeWidth,
947
+ textStrokeColor: KeyStrokeColor
948
+ };
949
+ function resolveName(raw) {
950
+ const key = raw.trim();
951
+ if (!key) return { canonical: "", ok: false };
952
+ const mapped = aliasToCanonical[key];
953
+ if (mapped) return { canonical: mapped, ok: true };
954
+ if (key.includes("-")) {
955
+ const camel = kebabToCamel(key);
956
+ const mappedCamel = aliasToCanonical[camel];
957
+ if (mappedCamel) return { canonical: mappedCamel, ok: true };
958
+ const mappedRaw = aliasToCanonical[key];
959
+ if (mappedRaw) return { canonical: mappedRaw, ok: true };
960
+ return { canonical: camel, ok: isKnownCanonical(camel) };
961
+ }
962
+ const mappedAgain = aliasToCanonical[key];
963
+ if (mappedAgain) return { canonical: mappedAgain, ok: true };
964
+ return { canonical: key, ok: isKnownCanonical(key) };
965
+ }
966
+ function isKnownCanonical(name) {
967
+ return isBoxKey(name) || isTextKey(name) || isTransformKey(name) || name === KeyLeft || name === KeyTop || name === KeyWidth || name === KeyHeight || name === KeyGlow || name === KeyBevel || name === KeyBlur || name === KeyBlurLeft || name === KeyBlurRight || name === KeyBlurTop || name === KeyBlurBottom;
968
+ }
969
+ function kebabToCamel(value) {
970
+ const parts = value.split("-");
971
+ if (parts.length === 0) return value;
972
+ let out = "";
973
+ for (let i = 0; i < parts.length; i += 1) {
974
+ const part = parts[i]?.trim();
975
+ if (!part) continue;
976
+ if (i === 0) {
977
+ out += part.toLowerCase();
978
+ continue;
979
+ }
980
+ out += part[0].toUpperCase() + part.slice(1).toLowerCase();
981
+ }
982
+ return out;
983
+ }
984
+ function normalizeStyle(style) {
985
+ const rawObject = parseStyleObject(style);
986
+ if (!rawObject) return {};
987
+ const out = {};
988
+ const applyKeys = (vendorOnly) => {
989
+ for (const [rawKey, value] of Object.entries(rawObject)) {
990
+ const vendor = isVendorRawKey(rawKey);
991
+ if (vendorOnly !== vendor) continue;
992
+ const canonical = canonicalKeyForRaw(rawKey);
993
+ if (!canonical || isVendorKey(canonical)) continue;
994
+ if (vendorOnly && Object.prototype.hasOwnProperty.call(out, canonical)) continue;
995
+ out[canonical] = value;
996
+ }
997
+ };
998
+ applyKeys(false);
999
+ applyKeys(true);
1000
+ mergeBackgroundKeys(out);
1001
+ return filterStyleMap(out);
1002
+ }
1003
+ function parseStyleObject(style) {
1004
+ if (typeof style === "string") {
1005
+ const trimmed = style.trim();
1006
+ if (!trimmed || trimmed === "{}") return null;
1007
+ try {
1008
+ const parsed = JSON.parse(trimmed);
1009
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
1010
+ return parsed;
1011
+ } catch {
1012
+ return null;
1013
+ }
1014
+ }
1015
+ if (!style || typeof style !== "object" || Array.isArray(style)) return null;
1016
+ if (Object.keys(style).length === 0) return null;
1017
+ return style;
1018
+ }
1019
+ function canonicalKeyForRaw(rawKey) {
1020
+ for (const candidate of expandRawKey(rawKey)) {
1021
+ const resolved = resolveName(candidate);
1022
+ if (resolved.ok) return resolved.canonical;
1023
+ if (!isVendorKey(candidate)) return candidate;
1024
+ }
1025
+ return "";
1026
+ }
1027
+ function isVendorRawKey(key) {
1028
+ if (isVendorKey(key)) return true;
1029
+ return stripVendorPrefix(key).ok;
1030
+ }
1031
+ function expandRawKey(rawKey) {
1032
+ const key = rawKey.trim();
1033
+ const stripped = stripVendorPrefix(key);
1034
+ if (stripped.ok) return [stripped.value, key];
1035
+ return [key];
1036
+ }
1037
+ function stripVendorPrefix(key) {
1038
+ const trimmed = key.trim();
1039
+ const lower = trimmed.toLowerCase();
1040
+ for (const prefix of ["-webkit-", "webkit-", "webkit"]) {
1041
+ if (!lower.startsWith(prefix)) continue;
1042
+ const rest = trimmed.slice(prefix.length);
1043
+ if (!rest) return { value: "", ok: false };
1044
+ return { value: decapitalize(rest), ok: true };
1045
+ }
1046
+ if (trimmed.startsWith("Webkit")) {
1047
+ return { value: decapitalize(trimmed.slice(6)), ok: true };
1048
+ }
1049
+ if (trimmed.startsWith("webkit")) {
1050
+ return { value: decapitalize(trimmed.slice(6)), ok: true };
1051
+ }
1052
+ return { value: "", ok: false };
1053
+ }
1054
+ function decapitalize(value) {
1055
+ if (!value) return value;
1056
+ return value[0].toLowerCase() + value.slice(1);
1057
+ }
1058
+ function isVendorKey(key) {
1059
+ return key.startsWith("Webkit") || key.startsWith("webkit") || key.toLowerCase().startsWith("-webkit-");
1060
+ }
1061
+ function mergeBackgroundKeys(style) {
1062
+ if (Object.prototype.hasOwnProperty.call(style, KeyBackground)) {
1063
+ style[KeyBackground] = style[KeyBackground];
1064
+ delete style.backgroundColor;
1065
+ }
1066
+ }
1067
+
1068
+ // src/html/style/percent.ts
1069
+ var defaultHandlers = [];
1070
+ function applyPercentHandlers(style, dims) {
1071
+ if (Object.keys(style).length === 0) return style;
1072
+ const resolvedDims = resolveImageDims(dims);
1073
+ const keyHandler = /* @__PURE__ */ new Map();
1074
+ for (const handler of defaultHandlers) {
1075
+ for (const key of handler.keys()) {
1076
+ keyHandler.set(key, handler);
1077
+ }
1078
+ }
1079
+ const out = {};
1080
+ for (const [key, raw] of Object.entries(style)) {
1081
+ const value = stringifyRaw(raw);
1082
+ if (resolvedDims.preservePercent && value.includes("%")) {
1083
+ out[key] = raw;
1084
+ continue;
1085
+ }
1086
+ const handler = keyHandler.get(key);
1087
+ if (handler && value) {
1088
+ const resolved = handler.resolve(key, value, resolvedDims);
1089
+ if (resolved.ok) {
1090
+ out[key] = resolved.resolved;
1091
+ continue;
1092
+ }
1093
+ }
1094
+ out[key] = raw;
1095
+ }
1096
+ return out;
1097
+ }
1098
+ function resolveImageDims(dims) {
1099
+ const w = dims.W ?? dims.w ?? 0;
1100
+ const h = dims.H ?? dims.h ?? 0;
1101
+ const fontSizePx = dims.FontSizePx ?? dims.fontSizePx ?? 0;
1102
+ const zoom = dims.Zoom ?? dims.zoom ?? 1;
1103
+ const preservePercent = dims.PreservePercent ?? dims.preservePercent ?? false;
1104
+ return { w, h, fontSizePx, zoom: zoom > 0 ? zoom : 1, preservePercent };
1105
+ }
1106
+ function stringifyRaw(raw) {
1107
+ if (raw === null || raw === void 0) return "";
1108
+ if (typeof raw === "string") return raw;
1109
+ return String(raw);
1110
+ }
1111
+ var DimensionHandler = class {
1112
+ keys() {
1113
+ return ["height", "width"];
1114
+ }
1115
+ resolve(key, value, dims) {
1116
+ if (!value.includes("%")) return { resolved: value, ok: true };
1117
+ const base = key === "width" ? dims.w : dims.h;
1118
+ return singlePercentToken(value, base, dims);
1119
+ }
1120
+ };
1121
+ var PaddingHandler = class {
1122
+ keys() {
1123
+ return ["padding", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft"];
1124
+ }
1125
+ resolve(key, value, dims) {
1126
+ if (!value.includes("%")) return { resolved: value, ok: true };
1127
+ const tokens = value.trim().split(/\s+/).filter(Boolean);
1128
+ if (tokens.length === 0) return { resolved: value, ok: true };
1129
+ if (tokens.length === 1 || key !== "padding") {
1130
+ const base = paddingAxisBase(key, tokens.length, 0, dims);
1131
+ if (tokens.length === 1) {
1132
+ return singlePercentToken(tokens[0], base, dims);
1133
+ }
1134
+ }
1135
+ const bases = shorthandBases(tokens.length);
1136
+ const out = [];
1137
+ for (let i = 0; i < tokens.length; i += 1) {
1138
+ const token = tokens[i] ?? "";
1139
+ const axis = bases[i] ?? 0;
1140
+ const base = paddingAxisBase(key, tokens.length, axis, dims);
1141
+ const resolved = singlePercentToken(token, base, dims);
1142
+ out.push(resolved.resolved);
1143
+ }
1144
+ return { resolved: out.join(" "), ok: true };
1145
+ }
1146
+ };
1147
+ function shorthandBases(count) {
1148
+ switch (count) {
1149
+ case 1:
1150
+ return [0, 0, 0, 0];
1151
+ case 2:
1152
+ return [0, 1, 0, 1];
1153
+ case 3:
1154
+ return [0, 1, 2, 1];
1155
+ default:
1156
+ return [0, 1, 2, 3];
1157
+ }
1158
+ }
1159
+ function paddingAxisBase(key, count, axis, dims) {
1160
+ switch (key) {
1161
+ case "paddingTop":
1162
+ case "paddingBottom":
1163
+ return dims.h;
1164
+ case "paddingLeft":
1165
+ case "paddingRight":
1166
+ return dims.w;
1167
+ }
1168
+ if (count === 1) return max(dims.w, dims.h);
1169
+ switch (axis) {
1170
+ case 0:
1171
+ case 2:
1172
+ return dims.h;
1173
+ case 1:
1174
+ case 3:
1175
+ return dims.w;
1176
+ default:
1177
+ return dims.w;
1178
+ }
1179
+ }
1180
+ var BorderWidthHandler = class {
1181
+ keys() {
1182
+ return ["borderWidth", "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth"];
1183
+ }
1184
+ resolve(_key, value, dims) {
1185
+ if (!value.includes("%")) return { resolved: value, ok: true };
1186
+ return singlePercentToken(value, dims.w, dims);
1187
+ }
1188
+ };
1189
+ var LineHeightHandler = class {
1190
+ keys() {
1191
+ return ["lineHeight"];
1192
+ }
1193
+ resolve(_key, value, dims) {
1194
+ if (!value.trim().endsWith("%")) return { resolved: value, ok: true };
1195
+ return singlePercentToken(value, dims.h, dims);
1196
+ }
1197
+ };
1198
+ var StrokeWidthHandler = class {
1199
+ keys() {
1200
+ return ["strokeWidth"];
1201
+ }
1202
+ resolve(_key, value, dims) {
1203
+ if (!value.includes("%")) return { resolved: value, ok: true };
1204
+ const base = dims.fontSizePx > 0 ? dims.fontSizePx : 1;
1205
+ return singlePercentToken(value, base, dims);
1206
+ }
1207
+ };
1208
+ var BlurHandler = class {
1209
+ keys() {
1210
+ return ["blur", "blurLeft", "blurRight", "blurTop", "blurBottom"];
1211
+ }
1212
+ resolve(_key, value, dims) {
1213
+ if (!value.includes("%")) return { resolved: value, ok: true };
1214
+ let base = dims.w;
1215
+ if (dims.h > 0 && dims.h < dims.w) base = dims.h;
1216
+ return singlePercentToken(value, base, dims);
1217
+ }
1218
+ };
1219
+ var TextShadowHandler = class {
1220
+ keys() {
1221
+ return ["textShadow"];
1222
+ }
1223
+ resolve(_key, value, dims) {
1224
+ if (!value.includes("%")) return { resolved: value, ok: true };
1225
+ return { resolved: resolveShadowList(value, dims), ok: true };
1226
+ }
1227
+ };
1228
+ var BoxShadowHandler = class {
1229
+ keys() {
1230
+ return ["boxShadow"];
1231
+ }
1232
+ resolve(_key, value, dims) {
1233
+ if (!value.includes("%")) return { resolved: value, ok: true };
1234
+ return { resolved: resolveShadowList(value, dims), ok: true };
1235
+ }
1236
+ };
1237
+ function resolveShadowList(value, dims) {
1238
+ const parts = value.split(",");
1239
+ for (let i = 0; i < parts.length; i += 1) {
1240
+ const shadow = parts[i]?.trim() ?? "";
1241
+ parts[i] = resolveShadowOne(shadow, dims);
1242
+ }
1243
+ return parts.join(", ");
1244
+ }
1245
+ function resolveShadowOne(shadow, dims) {
1246
+ if (!shadow) return shadow;
1247
+ const tokens = shadow.split(/\s+/).filter(Boolean);
1248
+ if (tokens.length === 0) return shadow;
1249
+ const colorIndex = findColorTokenIndex(tokens);
1250
+ const numericEnd = colorIndex < 0 ? tokens.length : colorIndex;
1251
+ for (let i = 0; i < numericEnd && i < 3; i += 1) {
1252
+ const token = tokens[i];
1253
+ if (token) tokens[i] = resolveShadowToken(token, i, dims);
1254
+ }
1255
+ return tokens.join(" ");
1256
+ }
1257
+ function findColorTokenIndex(tokens) {
1258
+ for (let i = 0; i < tokens.length; i += 1) {
1259
+ const token = tokens[i] ?? "";
1260
+ if (token.startsWith("#") || token.toLowerCase().startsWith("rgb")) return i;
1261
+ }
1262
+ return -1;
1263
+ }
1264
+ function resolveShadowToken(token, index, dims) {
1265
+ if (!token.endsWith("%")) return token;
1266
+ const pct = Number.parseFloat(token.slice(0, -1));
1267
+ if (!Number.isFinite(pct)) return token;
1268
+ const z = dims.zoom > 0 ? dims.zoom : 1;
1269
+ let base = 0;
1270
+ switch (index) {
1271
+ case 0:
1272
+ base = dims.w * z;
1273
+ break;
1274
+ case 1:
1275
+ base = dims.h * z;
1276
+ break;
1277
+ case 2:
1278
+ base = max(dims.w, dims.h) * z;
1279
+ break;
1280
+ default:
1281
+ return token;
1282
+ }
1283
+ return `${(pct / 100 * base).toFixed(3)}px`;
1284
+ }
1285
+ function percentToPx(percentString, base, zoom) {
1286
+ const pct = Number.parseFloat(percentString.trim().replace(/%$/, ""));
1287
+ if (!Number.isFinite(pct)) return percentString;
1288
+ const z = zoom > 0 ? zoom : 1;
1289
+ return `${(pct / 100 * base * z).toFixed(3)}px`;
1290
+ }
1291
+ function singlePercentToken(value, base, dims) {
1292
+ const v = value.trim();
1293
+ if (!v.endsWith("%")) return { resolved: v, ok: true };
1294
+ return { resolved: percentToPx(v, base, dims.zoom), ok: true };
1295
+ }
1296
+ function max(a, b) {
1297
+ return a > b ? a : b;
1298
+ }
1299
+ defaultHandlers = [
1300
+ new TextShadowHandler(),
1301
+ new BoxShadowHandler(),
1302
+ new BlurHandler(),
1303
+ new BorderWidthHandler(),
1304
+ new StrokeWidthHandler(),
1305
+ new LineHeightHandler(),
1306
+ new PaddingHandler(),
1307
+ new DimensionHandler()
1308
+ ];
1309
+
1310
+ // src/html/geometry/layoutHelpers.ts
1311
+ function textSizeBasisPx(canvasW, canvasH) {
1312
+ const w = canvasW;
1313
+ const h = canvasH;
1314
+ if (w <= 0 && h <= 0) return 1;
1315
+ if (w <= 0) return h;
1316
+ if (h <= 0) return w;
1317
+ return w < h ? w : h;
1318
+ }
1319
+ function textFontSizePx(textSizePct, canvasW, canvasH) {
1320
+ const px = textSizePct / 100 * textSizeBasisPx(canvasW, canvasH);
1321
+ return px < 1 ? 1 : px;
1322
+ }
1323
+ function paddingHorizontal(p) {
1324
+ return p.left + p.right;
1325
+ }
1326
+ function paddingVertical(p) {
1327
+ return p.top + p.bottom;
1328
+ }
1329
+ function cssLengthToPx(s, refFontPx) {
1330
+ s = s.trim().toLowerCase();
1331
+ if (!s) return 0;
1332
+ if (s.endsWith("px")) {
1333
+ const v2 = parseFloat(s.slice(0, -2));
1334
+ return Number.isFinite(v2) ? Math.max(0, v2) : 0;
1335
+ }
1336
+ if (s.endsWith("em")) {
1337
+ const v2 = parseFloat(s.slice(0, -2));
1338
+ return Number.isFinite(v2) ? Math.max(0, v2 * refFontPx) : 0;
1339
+ }
1340
+ if (s.endsWith("%")) return 0;
1341
+ const v = parseFloat(s);
1342
+ return Number.isFinite(v) ? Math.max(0, v) : 0;
1343
+ }
1344
+ function rawStringProp(m, ...keys) {
1345
+ for (const k of keys) {
1346
+ const v = m[k];
1347
+ if (typeof v === "string" && v.trim()) return v.trim();
1348
+ if (typeof v === "number") return String(v);
1349
+ }
1350
+ return "";
1351
+ }
1352
+ function cssColorFromRaw(v) {
1353
+ if (typeof v === "string" && v.trim()) return v.trim().replace(/;/g, "");
1354
+ return "";
1355
+ }
1356
+ function backgroundColorFromStyle(style) {
1357
+ const m = normalizeStyle(style);
1358
+ if (!m) return "";
1359
+ for (const key of ["backGround", "background", "backgroundColor"]) {
1360
+ const c = cssColorFromRaw(m[key]);
1361
+ if (c) return c;
1362
+ }
1363
+ return "";
1364
+ }
1365
+ function parsePaddingCSS(css, refFontPx) {
1366
+ css = css.trim();
1367
+ if (!css) return { top: 0, right: 0, bottom: 0, left: 0 };
1368
+ const parts = css.split(/\s+/);
1369
+ const vals = parts.map((p) => cssLengthToPx(p, refFontPx));
1370
+ switch (vals.length) {
1371
+ case 1:
1372
+ return { top: vals[0], right: vals[0], bottom: vals[0], left: vals[0] };
1373
+ case 2:
1374
+ return { top: vals[0], right: vals[1], bottom: vals[0], left: vals[1] };
1375
+ case 3:
1376
+ return { top: vals[0], right: vals[1], bottom: vals[2], left: vals[1] };
1377
+ default:
1378
+ return { top: vals[0], right: vals[1], bottom: vals[2], left: vals[3] };
1379
+ }
1380
+ }
1381
+ function styleResolvedForCanvas(style, canvasW, canvasH, fontPx) {
1382
+ const m = normalizeStyle(style);
1383
+ if (!m) return {};
1384
+ return applyPercentHandlers(m, { w: canvasW, h: canvasH, fontSizePx: fontPx, zoom: 1 });
1385
+ }
1386
+ function textBoxInsetsForCanvas(style, refFontPx, canvasW, canvasH) {
1387
+ const m = styleResolvedForCanvas(style, canvasW, canvasH, refFontPx);
1388
+ const padding = parsePaddingCSS(rawStringProp(m, "padding"), refFontPx);
1389
+ let borderW = rawStringProp(m, "borderWidth");
1390
+ if (!borderW && typeof m.border === "string") {
1391
+ borderW = m.border.split(/\s+/)[0] ?? "";
1392
+ }
1393
+ const px = cssLengthToPx(borderW, refFontPx);
1394
+ const border = px > 0 ? { top: px, right: px, bottom: px, left: px } : { top: 0, right: 0, bottom: 0, left: 0 };
1395
+ return {
1396
+ top: padding.top + border.top,
1397
+ right: padding.right + border.right,
1398
+ bottom: padding.bottom + border.bottom,
1399
+ left: padding.left + border.left
1400
+ };
1401
+ }
1402
+ function explicitHeightPx(style, canvasW, canvasH, fontPx) {
1403
+ if (canvasH < 1) return { px: 0, ok: false };
1404
+ const m = styleResolvedForCanvas(style, canvasW, canvasH, fontPx);
1405
+ const val = rawStringProp(m, "height");
1406
+ if (!val) return { px: 0, ok: false };
1407
+ if (val.endsWith("%")) {
1408
+ const pct = parseFloat(val.slice(0, -1));
1409
+ if (!Number.isFinite(pct) || pct <= 0) return { px: 0, ok: false };
1410
+ return { px: Math.round(canvasH * pct / 100), ok: true };
1411
+ }
1412
+ if (val.endsWith("px")) {
1413
+ const px = parseFloat(val.slice(0, -2));
1414
+ if (!Number.isFinite(px) || px < 1) return { px: 0, ok: false };
1415
+ return { px: Math.round(px), ok: true };
1416
+ }
1417
+ if (val.endsWith("em") || val.endsWith("rem")) {
1418
+ const suffix = val.endsWith("rem") ? "rem" : "em";
1419
+ const n2 = parseFloat(val.slice(0, -suffix.length));
1420
+ if (!Number.isFinite(n2) || n2 <= 0 || fontPx <= 0) return { px: 0, ok: false };
1421
+ return { px: Math.round(n2 * fontPx), ok: true };
1422
+ }
1423
+ const n = parseFloat(val);
1424
+ if (Number.isFinite(n) && n > 0) return { px: Math.round(n), ok: true };
1425
+ return { px: 0, ok: false };
1426
+ }
1427
+ function textLayerNeedsComputedHeight(style, canvasW, canvasH, textSize) {
1428
+ const fontPx = textFontSizePx(textSize, canvasW, canvasH);
1429
+ if (explicitHeightPx(style, canvasW, canvasH, fontPx).ok) return true;
1430
+ return paddingVertical(textBoxInsetsForCanvas(style, fontPx, canvasW, canvasH)) > 0;
1431
+ }
1432
+ function fontWeightIsBold(style) {
1433
+ const m = normalizeStyle(style);
1434
+ if (!m) return false;
1435
+ const w = rawStringProp(m, "fontWeight", "font-weight").toLowerCase();
1436
+ if (["bold", "bolder", "600", "700", "800", "900"].includes(w)) return true;
1437
+ const n = parseInt(w, 10);
1438
+ return Number.isFinite(n) && n >= 600;
1439
+ }
1440
+ function lineHeightMultiplier(style, fontSizePx) {
1441
+ const def = 1.2;
1442
+ const m = normalizeStyle(style);
1443
+ if (!m) return def;
1444
+ const raw = m.lineHeight;
1445
+ if (typeof raw === "number" && raw > 0) return raw;
1446
+ const val = typeof raw === "string" ? raw.trim() : "";
1447
+ if (!val) return def;
1448
+ if (val.endsWith("%")) {
1449
+ const pct = parseFloat(val.slice(0, -1));
1450
+ if (Number.isFinite(pct) && pct > 0) return pct / 100;
1451
+ return def;
1452
+ }
1453
+ if (val.endsWith("px")) {
1454
+ const px = parseFloat(val.slice(0, -2));
1455
+ if (Number.isFinite(px) && px > 0 && fontSizePx > 0) return px / fontSizePx;
1456
+ return def;
1457
+ }
1458
+ const v = parseFloat(val);
1459
+ return Number.isFinite(v) && v > 0 ? v : def;
1460
+ }
1461
+
1462
+ // src/html/geometry/textBlockGeometry.ts
1463
+ function textBlockWidthPx(widthPct, canvasW) {
1464
+ const w = Math.round(canvasW * widthPct / 100);
1465
+ return w < 1 ? 1 : w;
1466
+ }
1467
+ function charsPerLineForWidth(widthPx, fontSizePx, bold) {
1468
+ const em = bold ? 0.58 : 0.48;
1469
+ const cpl = Math.floor(widthPx / (fontSizePx * em));
1470
+ return cpl < 1 ? 1 : cpl;
1471
+ }
1472
+ function estimateTextLines(content, widthPx, fontSizePx, bold) {
1473
+ if (widthPx < 1) widthPx = 1;
1474
+ const charsPerLine = charsPerLineForWidth(widthPx, fontSizePx, bold);
1475
+ const parts = content.split("\n");
1476
+ let total = 0;
1477
+ for (const part of parts) {
1478
+ const n = [...part.trim()].length;
1479
+ if (n === 0) continue;
1480
+ total += Math.ceil(n / charsPerLine);
1481
+ }
1482
+ return total < 1 ? 1 : total;
1483
+ }
1484
+ function textBlockGeometry(t, content, canvasW, canvasH) {
1485
+ const x = Math.round(canvasW * t.x / 100);
1486
+ const y = Math.round(canvasH * t.y / 100);
1487
+ const outerW = textBlockWidthPx(t.width, canvasW);
1488
+ const fontPx = textFontSizePx(t.textSize, canvasW, canvasH);
1489
+ const insets = textBoxInsetsForCanvas(t.style, fontPx, canvasW, canvasH);
1490
+ const padW = Math.round(paddingHorizontal(insets));
1491
+ const padH = Math.round(paddingVertical(insets));
1492
+ let contentW = outerW - padW;
1493
+ if (contentW < 1) contentW = 1;
1494
+ const plain = plainTextForLayout(content);
1495
+ const lines = estimateTextLines(plain, contentW, fontPx, fontWeightIsBold(t.style));
1496
+ const lh = lineHeightMultiplier(t.style, fontPx);
1497
+ const linePx = fontPx * lh;
1498
+ let contentH = Math.round(linePx * lines);
1499
+ if (contentH < Math.round(linePx)) contentH = Math.round(linePx);
1500
+ let width = outerW;
1501
+ let height = contentH + padH;
1502
+ const explicit = explicitHeightPx(t.style, canvasW, canvasH, fontPx);
1503
+ if (explicit.ok) {
1504
+ if (content.trim() === "") height = explicit.px;
1505
+ else if (explicit.px > height) height = explicit.px;
1506
+ }
1507
+ if (width < 1) width = 1;
1508
+ if (height < 1) height = 1;
1509
+ return { x, y, width, height };
1510
+ }
1511
+ function appendTextLayerGeometryCSS(boxCSS, t, content, canvasW, canvasH) {
1512
+ if (canvasH < 1 || boxCSS.includes("height:")) return boxCSS;
1513
+ if (!textLayerNeedsComputedHeight(t.style, canvasW, canvasH, t.textSize)) return boxCSS;
1514
+ const { height: geomH } = textBlockGeometry(t, content, canvasW, canvasH);
1515
+ if (geomH < 1) return boxCSS;
1516
+ const heightPct = geomH / canvasH * 100;
1517
+ return `${boxCSS}height:${heightPct}%;`;
1518
+ }
1519
+
1520
+ // src/html/style/fragment.ts
1521
+ var TypeKey = "__type__";
1522
+ var TypeMotionDiv = "div";
1523
+ var TypeSpan = "span";
1524
+ var TypeFilter = "filter";
1525
+ var TypeMask = "mask";
1526
+ function newFragment(type) {
1527
+ return { [TypeKey]: type };
1528
+ }
1529
+ function mergeFragments(fragments) {
1530
+ const byType = /* @__PURE__ */ new Map();
1531
+ const order = [];
1532
+ for (const fragment of fragments) {
1533
+ if (!fragment) continue;
1534
+ const type = getFragmentString(fragment, TypeKey);
1535
+ if (!type) continue;
1536
+ const existing = byType.get(type);
1537
+ if (existing) {
1538
+ for (const [key, value] of Object.entries(fragment)) {
1539
+ if (key !== TypeKey) {
1540
+ existing[key] = value;
1541
+ }
1542
+ }
1543
+ continue;
1544
+ }
1545
+ const copy = { [TypeKey]: type };
1546
+ for (const [key, value] of Object.entries(fragment)) {
1547
+ copy[key] = value;
1548
+ }
1549
+ byType.set(type, copy);
1550
+ order.push(type);
1551
+ }
1552
+ return order.map((type) => byType.get(type)).filter((v) => Boolean(v));
1553
+ }
1554
+ function setFragmentValue(fragment, prop, value) {
1555
+ if (!fragment) return;
1556
+ fragment[prop] = value;
1557
+ }
1558
+ function getFragmentString(fragment, prop) {
1559
+ if (!fragment) return "";
1560
+ const value = fragment[prop];
1561
+ return typeof value === "string" ? value : "";
1562
+ }
1563
+
1564
+ // src/html/style/effects.ts
1565
+ function expandEffects(style, dims, targetSVG, filterID) {
1566
+ if (Object.keys(style).length === 0) return { style, fragments: [] };
1567
+ const out = { ...style };
1568
+ const extra = [];
1569
+ if (Object.prototype.hasOwnProperty.call(out, KeyGlow)) {
1570
+ const raw = out[KeyGlow];
1571
+ const value = stringifyCSSValue(raw);
1572
+ delete out[KeyGlow];
1573
+ if (hasStyleValue(KeyGlow, raw)) {
1574
+ if (targetSVG) {
1575
+ extra.push(glowFilterFragment(filterID, value, dims));
1576
+ } else {
1577
+ mergeShadowKey(out, KeyTextShadow, value);
1578
+ }
1579
+ }
1580
+ }
1581
+ if (Object.prototype.hasOwnProperty.call(out, KeyBevel)) {
1582
+ const raw = out[KeyBevel];
1583
+ const value = stringifyCSSValue(raw);
1584
+ delete out[KeyBevel];
1585
+ if (hasStyleValue(KeyBevel, raw)) {
1586
+ if (targetSVG) {
1587
+ extra.push(bevelFilterFragment(`${filterID}-bevel`, value));
1588
+ } else {
1589
+ mergeBoxShadowKey(out, bevelBoxShadows(value));
1590
+ }
1591
+ }
1592
+ }
1593
+ return { style: out, fragments: extra };
1594
+ }
1595
+ function mergeShadowKey(style, key, add) {
1596
+ const existing = stringifyCSSValue(style[key]);
1597
+ if (!existing) {
1598
+ style[key] = add;
1599
+ return;
1600
+ }
1601
+ style[key] = `${existing}, ${add}`;
1602
+ }
1603
+ function mergeBoxShadowKey(style, add) {
1604
+ const existing = stringifyCSSValue(style[KeyBoxShadow]);
1605
+ if (!existing) {
1606
+ style[KeyBoxShadow] = add;
1607
+ return;
1608
+ }
1609
+ style[KeyBoxShadow] = `${existing}, ${add}`;
1610
+ }
1611
+ function bevelBoxShadows(value) {
1612
+ const light = "inset 1px 1px 0 rgba(255,255,255,0.35)";
1613
+ let dark = "inset -1px -1px 0 rgba(0,0,0,0.35)";
1614
+ if (value.trim()) {
1615
+ dark = `inset -1px -1px 2px ${value.trim()}`;
1616
+ }
1617
+ return `${light}, ${dark}`;
1618
+ }
1619
+ function glowFilterFragment(id, value, dims) {
1620
+ const parsed = parseSimpleShadow(value, dims);
1621
+ const fragment = newFragment(TypeFilter);
1622
+ setFragmentValue(fragment, "id", id);
1623
+ setFragmentValue(fragment, "feDropShadowDx", parsed.dx.toFixed(3));
1624
+ setFragmentValue(fragment, "feDropShadowDy", parsed.dy.toFixed(3));
1625
+ setFragmentValue(fragment, "feGaussianBlurStd", (parsed.blur / 2).toFixed(3));
1626
+ setFragmentValue(fragment, "floodColor", parsed.color);
1627
+ return fragment;
1628
+ }
1629
+ function bevelFilterFragment(id, value) {
1630
+ const fragment = newFragment(TypeFilter);
1631
+ setFragmentValue(fragment, "id", id);
1632
+ setFragmentValue(fragment, "feDropShadowDx", "-1");
1633
+ setFragmentValue(fragment, "feDropShadowDy", "-1");
1634
+ setFragmentValue(fragment, "feGaussianBlurStd", "0.5");
1635
+ const color = value.trim() || "rgba(0,0,0,0.4)";
1636
+ setFragmentValue(fragment, "floodColor", color);
1637
+ setFragmentValue(fragment, "feDropShadowDx2", "1");
1638
+ setFragmentValue(fragment, "floodColor2", "rgba(255,255,255,0.35)");
1639
+ return fragment;
1640
+ }
1641
+ function parseSimpleShadow(value, dims) {
1642
+ const trimmed = value.trim();
1643
+ if (!trimmed) {
1644
+ return { dx: 0, dy: 0, blur: 4, color: "rgba(0,0,0,0.5)" };
1645
+ }
1646
+ const resolved = applyPercentHandlers({ textShadow: trimmed }, dims);
1647
+ const parts = stringifyCSSValue(resolved.textShadow).split(/\s+/).filter(Boolean);
1648
+ let dx = 0;
1649
+ let dy = 0;
1650
+ let blur = 0;
1651
+ let color = "";
1652
+ if (parts.length >= 3) {
1653
+ dx = parsePxNum(parts[0] ?? "");
1654
+ dy = parsePxNum(parts[1] ?? "");
1655
+ blur = parsePxNum(parts[2] ?? "");
1656
+ }
1657
+ if (parts.length >= 4) {
1658
+ color = parts[3] ?? "";
1659
+ }
1660
+ if (!color) color = "rgba(0,0,0,0.5)";
1661
+ return { dx, dy, blur, color };
1662
+ }
1663
+ function parsePxNum(value) {
1664
+ const parsed = Number.parseFloat(value.trim().replace(/px$/, ""));
1665
+ return Number.isFinite(parsed) ? parsed : 0;
1666
+ }
1667
+
1668
+ // src/html/style/blur.ts
1669
+ var blurFilterKind = "blur";
1670
+ var blurSideKeys = [
1671
+ { key: KeyBlurLeft, side: "left" },
1672
+ { key: KeyBlurRight, side: "right" },
1673
+ { key: KeyBlurTop, side: "top" },
1674
+ { key: KeyBlurBottom, side: "bottom" }
1675
+ ];
1676
+ function expandBlur(style, dims, html, filterID) {
1677
+ if (Object.keys(style).length === 0) {
1678
+ return { style, meta: { filterID: "", maskID: "" }, fragments: [] };
1679
+ }
1680
+ const parsed = parseBlurFromStyle(style, dims);
1681
+ if (!parsed.ok) {
1682
+ return { style, meta: { filterID: "", maskID: "" }, fragments: [] };
1683
+ }
1684
+ const spec = parsed.spec;
1685
+ const out = { ...style };
1686
+ for (const sideKey of blurSideKeys) {
1687
+ delete out[sideKey.key];
1688
+ }
1689
+ delete out[KeyBlur];
1690
+ const meta = { filterID: `${filterID}-blur`, maskID: "" };
1691
+ if (html) {
1692
+ const patch = newFragment(TypeMotionDiv);
1693
+ applyBlurHTML(patch, spec);
1694
+ return { style: out, meta, fragments: [patch] };
1695
+ }
1696
+ const fragments = [blurFilterFragment(meta.filterID, spec)];
1697
+ if (spec.side) {
1698
+ meta.maskID = `${meta.filterID}-mask`;
1699
+ fragments.push(blurMaskFragment(meta.maskID, spec.side));
1700
+ }
1701
+ return { style: out, meta, fragments };
1702
+ }
1703
+ function parseBlurFromStyle(style, dims) {
1704
+ for (const side of blurSideKeys) {
1705
+ const raw2 = style[side.key];
1706
+ if (!hasStyleValue(side.key, raw2)) continue;
1707
+ const amount = parseBlurAmount(stringifyCSSValue(raw2), dims);
1708
+ if (!amount.ok || amount.amount <= 0) return { spec: { amountPx: 0, side: "" }, ok: false };
1709
+ return { spec: { amountPx: amount.amount, side: side.side }, ok: true };
1710
+ }
1711
+ const raw = style[KeyBlur];
1712
+ if (!hasStyleValue(KeyBlur, raw)) return { spec: { amountPx: 0, side: "" }, ok: false };
1713
+ return parseBlurCSSValue(stringifyCSSValue(raw), dims);
1714
+ }
1715
+ function parseBlurCSSValue(value, dims) {
1716
+ const trimmed = value.trim();
1717
+ if (!trimmed) return { spec: { amountPx: 0, side: "" }, ok: false };
1718
+ const parts = trimmed.split(/\s+/).filter(Boolean);
1719
+ if (parts.length === 0) return { spec: { amountPx: 0, side: "" }, ok: false };
1720
+ let side = "";
1721
+ const amountParts = [];
1722
+ for (const part of parts) {
1723
+ const lower = part.toLowerCase();
1724
+ if ((lower === "left" || lower === "right" || lower === "top" || lower === "bottom") && !side) {
1725
+ side = lower;
1726
+ continue;
1727
+ }
1728
+ amountParts.push(part);
1729
+ }
1730
+ let amountString = amountParts.join(" ");
1731
+ if (!amountString && side && parts.length >= 2) {
1732
+ for (let i = 0; i < parts.length; i += 1) {
1733
+ const current = parts[i]?.toLowerCase();
1734
+ if (current !== side) continue;
1735
+ amountString = parts.slice(i + 1).join(" ");
1736
+ break;
1737
+ }
1738
+ }
1739
+ if (!amountString) {
1740
+ amountString = trimmed;
1741
+ side = "";
1742
+ }
1743
+ const amount = parseBlurAmount(amountString, dims);
1744
+ if (!amount.ok || amount.amount <= 0) return { spec: { amountPx: 0, side: "" }, ok: false };
1745
+ return { spec: { amountPx: amount.amount, side }, ok: true };
1746
+ }
1747
+ function parseBlurAmount(value, dims) {
1748
+ let trimmed = value.trim();
1749
+ if (!trimmed) return { amount: 0, ok: false };
1750
+ if (trimmed.includes("%")) {
1751
+ const resolved = applyPercentHandlers({ [KeyBlur]: trimmed }, dims);
1752
+ trimmed = stringifyCSSValue(resolved[KeyBlur]);
1753
+ }
1754
+ const px = parsePxNum2(trimmed);
1755
+ return { amount: px, ok: px > 0 };
1756
+ }
1757
+ function applyBlurHTML(box, spec) {
1758
+ if (spec.amountPx <= 0) return;
1759
+ const px = `${spec.amountPx.toFixed(3)}px`;
1760
+ const blurValue = `blur(${px})`;
1761
+ setFragmentValue(box, "backdropFilter", blurValue);
1762
+ setFragmentValue(box, "WebkitBackdropFilter", blurValue);
1763
+ if (!spec.side) return;
1764
+ const mask = blurMaskCSS(spec.side);
1765
+ setFragmentValue(box, "maskImage", mask);
1766
+ setFragmentValue(box, "WebkitMaskImage", mask);
1767
+ setFragmentValue(box, "maskSize", "100% 100%");
1768
+ setFragmentValue(box, "WebkitMaskSize", "100% 100%");
1769
+ }
1770
+ function blurMaskCSS(side) {
1771
+ switch (side) {
1772
+ case "left":
1773
+ return "linear-gradient(to right, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%)";
1774
+ case "right":
1775
+ return "linear-gradient(to left, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%)";
1776
+ case "top":
1777
+ return "linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%)";
1778
+ case "bottom":
1779
+ return "linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%)";
1780
+ default:
1781
+ return "";
1782
+ }
1783
+ }
1784
+ function blurFilterFragment(id, spec) {
1785
+ const fragment = newFragment(TypeFilter);
1786
+ setFragmentValue(fragment, "id", id);
1787
+ setFragmentValue(fragment, "filterKind", blurFilterKind);
1788
+ let std = spec.amountPx / 2;
1789
+ if (std < 0.5) std = 0.5;
1790
+ setFragmentValue(fragment, "feGaussianBlurStd", std.toFixed(3));
1791
+ setFragmentValue(fragment, "feGaussianBlurIn", "SourceGraphic");
1792
+ return fragment;
1793
+ }
1794
+ function blurMaskFragment(id, side) {
1795
+ const fragment = newFragment(TypeMask);
1796
+ setFragmentValue(fragment, "id", id);
1797
+ setFragmentValue(fragment, "maskSide", side);
1798
+ return fragment;
1799
+ }
1800
+ function parsePxNum2(value) {
1801
+ const trimmed = value.trim().replace(/px$/, "");
1802
+ const parsed = Number.parseFloat(trimmed);
1803
+ return Number.isFinite(parsed) ? parsed : 0;
1804
+ }
1805
+
1806
+ // src/html/style/stroke.ts
1807
+ function applyStrokeHTML(span, style) {
1808
+ if (!span) return;
1809
+ const stroke = stringifyCSSValue(style[KeyStroke]);
1810
+ if (stroke) {
1811
+ setFragmentValue(span, "WebkitTextStroke", stroke);
1812
+ }
1813
+ const strokeWidth = stringifyCSSValue(style[KeyStrokeWidth]);
1814
+ if (strokeWidth) {
1815
+ setFragmentValue(span, "WebkitTextStrokeWidth", strokeWidth);
1816
+ }
1817
+ const strokeColor = stringifyCSSValue(style[KeyStrokeColor]);
1818
+ if (strokeColor) {
1819
+ setFragmentValue(span, "WebkitTextStrokeColor", strokeColor);
1820
+ }
1821
+ }
1822
+
1823
+ // src/html/style/context.ts
1824
+ function contextImageDims(ctx) {
1825
+ const zoom = ctx.zoom && ctx.zoom > 0 ? ctx.zoom : 1;
1826
+ return {
1827
+ w: ctx.canvasW,
1828
+ h: ctx.canvasH,
1829
+ fontSizePx: ctx.fontSizePx ?? 0,
1830
+ zoom
1831
+ };
1832
+ }
1833
+ function fontSizePxOrCompute(ctx) {
1834
+ const fontPx = ctx.fontSizePx ?? 0;
1835
+ if (fontPx > 0) return fontPx;
1836
+ return textFontSizePx2(ctx.text.textSize, ctx.canvasW, ctx.canvasH);
1837
+ }
1838
+ function textSizeBasisPx2(canvasW, canvasH) {
1839
+ if (canvasW <= 0 && canvasH <= 0) return 1;
1840
+ if (canvasW <= 0) return canvasH;
1841
+ if (canvasH <= 0) return canvasW;
1842
+ return canvasW < canvasH ? canvasW : canvasH;
1843
+ }
1844
+ function textFontSizePx2(textSizePct, canvasW, canvasH) {
1845
+ const px = textSizePct / 100 * textSizeBasisPx2(canvasW, canvasH);
1846
+ return px < 1 ? 1 : px;
1847
+ }
1848
+
1849
+ // src/html/style/adapter.ts
1850
+ function adaptHTML(ctx) {
1851
+ return adapt(ctx, true);
1852
+ }
1853
+ function adapt(ctx, html) {
1854
+ let style = normalizeStyle(ctx.text.style);
1855
+ if (Object.keys(style).length === 0) {
1856
+ style = {};
1857
+ }
1858
+ const dims = contextImageDims(ctx);
1859
+ dims.fontSizePx = fontSizePxOrCompute(ctx);
1860
+ if (html && ctx.htmlCompile) {
1861
+ dims.preservePercent = true;
1862
+ }
1863
+ style = applyPercentHandlers(style, dims);
1864
+ style = filterStyleMap(style);
1865
+ const filterID = `psrt-filter-${ctx.pageSlug ?? ""}-${ctx.textIndex ?? 0}`;
1866
+ const effects = expandEffects(style, dims, !html, filterID);
1867
+ const blur = expandBlur(effects.style, dims, html, filterID);
1868
+ const fragments = html ? buildHTMLFragments(ctx, blur.style) : [];
1869
+ return mergeFragments([...fragments, ...effects.fragments, ...blur.fragments]);
1870
+ }
1871
+ function buildHTMLFragments(ctx, style) {
1872
+ const box = newFragment(TypeMotionDiv);
1873
+ const text = newFragment(TypeSpan);
1874
+ applyLayout(box, ctx);
1875
+ applyTransform(box, style);
1876
+ for (const [key, raw] of Object.entries(style)) {
1877
+ if (!hasStyleValue(key, raw)) continue;
1878
+ const value = sanitizeCSSValue(stringifyCSSValue(raw));
1879
+ if (isBoxKey(key)) {
1880
+ applyBoxCSS(box, key, value);
1881
+ continue;
1882
+ }
1883
+ if (isTextKey(key) && key !== KeyStroke && key !== KeyStrokeWidth && key !== KeyStrokeColor) {
1884
+ applyTextCSS(text, key, value);
1885
+ continue;
1886
+ }
1887
+ if (isTransformKey(key)) {
1888
+ continue;
1889
+ }
1890
+ }
1891
+ applyStrokeHTML(text, style);
1892
+ applyFontSize(box, ctx, Boolean(ctx.htmlCompile));
1893
+ applyTextAlignHTML(box, text, style);
1894
+ if (!getFragmentString(text, KeyColor)) {
1895
+ const color = textColor(style);
1896
+ if (color) {
1897
+ setFragmentValue(text, KeyColor, color);
1898
+ }
1899
+ }
1900
+ return [box, text];
1901
+ }
1902
+ function applyLayout(box, ctx) {
1903
+ setFragmentValue(box, KeyPosition, "absolute");
1904
+ setFragmentValue(box, KeyBoxSizing, "border-box");
1905
+ setFragmentValue(box, KeyLeft, pctString(ctx.text.x));
1906
+ setFragmentValue(box, KeyTop, pctString(ctx.text.y));
1907
+ setFragmentValue(box, KeyWidth, pctString(ctx.text.width));
1908
+ }
1909
+ function applyTransform(target, style) {
1910
+ const parts = [];
1911
+ const transform2 = stringifyCSSValue(style[KeyTransform]);
1912
+ if (transform2) parts.push(transform2);
1913
+ for (const key of [KeyTranslate, KeyRotate, KeyScale, KeySkew, KeyMatrix]) {
1914
+ const value = stringifyCSSValue(style[key]);
1915
+ if (!value) continue;
1916
+ parts.push(`${key}(${value})`);
1917
+ }
1918
+ if (parts.length === 0) return;
1919
+ setFragmentValue(target, "transform", parts.join(" "));
1920
+ const origin = stringifyCSSValue(style[KeyTransformOrigin]);
1921
+ if (origin) {
1922
+ setFragmentValue(target, KeyTransformOrigin, origin);
1923
+ }
1924
+ }
1925
+ function applyBoxCSS(fragment, key, value) {
1926
+ switch (key) {
1927
+ case KeyBackground:
1928
+ setFragmentValue(fragment, "backgroundColor", value);
1929
+ return;
1930
+ case KeyPadding:
1931
+ case KeyPaddingTop:
1932
+ case KeyPaddingRight:
1933
+ case KeyPaddingBottom:
1934
+ case KeyPaddingLeft:
1935
+ case KeyBorder:
1936
+ case KeyBorderWidth:
1937
+ case KeyBorderStyle:
1938
+ case KeyBorderColor:
1939
+ case KeyBorderTop:
1940
+ case KeyBorderRight:
1941
+ case KeyBorderBottom:
1942
+ case KeyBorderLeft:
1943
+ case KeyBorderRadius:
1944
+ case KeyBorderTopLeftRadius:
1945
+ case KeyBorderTopRightRadius:
1946
+ case KeyBorderBottomRightRadius:
1947
+ case KeyBorderBottomLeftRadius:
1948
+ case KeyBoxShadow:
1949
+ case KeyHeight:
1950
+ setFragmentValue(fragment, key, value);
1951
+ return;
1952
+ default:
1953
+ return;
1954
+ }
1955
+ }
1956
+ function applyTextCSS(fragment, key, value) {
1957
+ const cssKey = key === KeyTextAlign ? "textAlign" : key;
1958
+ setFragmentValue(fragment, cssKey, value);
1959
+ }
1960
+ function applyFontSize(fragment, ctx, htmlCompile) {
1961
+ if (htmlCompile) {
1962
+ setFragmentValue(fragment, KeyFontSize, `${ctx.text.textSize}cqmin`);
1963
+ return;
1964
+ }
1965
+ setFragmentValue(fragment, KeyFontSize, pxString(fontSizePxOrCompute(ctx)));
1966
+ }
1967
+ function applyTextAlignHTML(box, text, style) {
1968
+ let ta = getFragmentString(text, KeyTextAlign).toLowerCase().trim();
1969
+ if (!ta) {
1970
+ ta = readTextAlignFromStyle(style);
1971
+ }
1972
+ const va = verticalAlignFromStyle(style);
1973
+ if (!ta && !va) return;
1974
+ if (!ta) ta = "left";
1975
+ setFragmentValue(text, "display", "block");
1976
+ setFragmentValue(text, KeyWidth, "100%");
1977
+ setFragmentValue(text, KeyTextAlign, ta);
1978
+ switch (ta) {
1979
+ case "justify":
1980
+ setFragmentValue(box, "display", "block");
1981
+ return;
1982
+ case "center":
1983
+ case "right":
1984
+ case "left":
1985
+ case "start": {
1986
+ setFragmentValue(box, "display", "flex");
1987
+ setFragmentValue(box, "flexDirection", "column");
1988
+ const justify = va || "center";
1989
+ setFragmentValue(box, "justifyContent", justify);
1990
+ let align = "stretch";
1991
+ if (ta === "center") align = "center";
1992
+ if (ta === "right") align = "flex-end";
1993
+ if (ta === "left" || ta === "start") align = "flex-start";
1994
+ setFragmentValue(box, "alignItems", align);
1995
+ return;
1996
+ }
1997
+ default:
1998
+ setFragmentValue(box, "display", "block");
1999
+ }
2000
+ }
2001
+ function readTextAlignFromStyle(style) {
2002
+ const direct = style[KeyTextAlign];
2003
+ if (direct !== void 0) {
2004
+ return sanitizeCSSValue(stringifyCSSValue(direct)).toLowerCase();
2005
+ }
2006
+ for (const key of ["text-align", "ta"]) {
2007
+ const value = style[key];
2008
+ if (value === void 0) continue;
2009
+ return sanitizeCSSValue(stringifyCSSValue(value)).toLowerCase();
2010
+ }
2011
+ return "";
2012
+ }
2013
+ function verticalAlignFromStyle(style) {
2014
+ const direct = style[KeyAlignItems];
2015
+ let value = direct !== void 0 ? sanitizeCSSValue(stringifyCSSValue(direct)).toLowerCase() : "";
2016
+ if (!value) {
2017
+ for (const key of ["align-items", "verticalAlign", "vertical-align"]) {
2018
+ const raw = style[key];
2019
+ if (raw === void 0) continue;
2020
+ value = sanitizeCSSValue(stringifyCSSValue(raw)).toLowerCase();
2021
+ break;
2022
+ }
2023
+ }
2024
+ switch (value) {
2025
+ case "flex-start":
2026
+ case "start":
2027
+ case "top":
2028
+ return "flex-start";
2029
+ case "flex-end":
2030
+ case "end":
2031
+ case "bottom":
2032
+ return "flex-end";
2033
+ case "center":
2034
+ case "middle":
2035
+ return "center";
2036
+ default:
2037
+ return "";
2038
+ }
2039
+ }
2040
+ function textColor(style) {
2041
+ const color = style[KeyColor];
2042
+ if (color === void 0) return "";
2043
+ return sanitizeCSSValue(stringifyCSSValue(color));
2044
+ }
2045
+
2046
+ // src/html/style/maskAdapter.ts
2047
+ function adaptMaskHTML(ctx) {
2048
+ const mask = ctx.mask;
2049
+ if (!mask) return [];
2050
+ let style = normalizeStyle(mask.style);
2051
+ if (Object.keys(style).length === 0) {
2052
+ style = {};
2053
+ }
2054
+ const dims = contextImageDims(ctx);
2055
+ if (ctx.htmlCompile) {
2056
+ dims.preservePercent = true;
2057
+ }
2058
+ style = applyPercentHandlers(style, dims);
2059
+ style = filterStyleMap(style);
2060
+ const filterID = maskFilterID(ctx);
2061
+ const effects = expandEffects(style, dims, false, filterID);
2062
+ const blur = expandBlur(effects.style, dims, true, filterID);
2063
+ const fragments = buildMaskHTMLFragments(ctx, blur.style);
2064
+ return mergeFragments([...fragments, ...effects.fragments, ...blur.fragments]);
2065
+ }
2066
+ function maskFilterID(ctx) {
2067
+ const index = ctx.mask?.index ?? ctx.textIndex ?? 0;
2068
+ return `psrt-filter-${ctx.pageSlug ?? ""}-${index}`;
2069
+ }
2070
+ function buildMaskHTMLFragments(ctx, style) {
2071
+ const box = newFragment(TypeMotionDiv);
2072
+ applyMaskLayout(box, ctx);
2073
+ applyTransform2(box, style);
2074
+ for (const [key, raw] of Object.entries(style)) {
2075
+ if (!hasStyleValue(key, raw)) continue;
2076
+ const value = sanitizeCSSValue(stringifyCSSValue(raw));
2077
+ if (isBoxKey(key)) {
2078
+ applyBoxCSS(box, key, value);
2079
+ }
2080
+ }
2081
+ if (!box["background-size"]) {
2082
+ setFragmentValue(box, "background-size", "cover");
2083
+ }
2084
+ return [box];
2085
+ }
2086
+ function applyMaskLayout(box, ctx) {
2087
+ const mask = ctx.mask;
2088
+ if (!mask) return;
2089
+ setFragmentValue(box, KeyPosition, "absolute");
2090
+ setFragmentValue(box, KeyBoxSizing, "border-box");
2091
+ setFragmentValue(box, KeyLeft, pctString(mask.x));
2092
+ setFragmentValue(box, KeyTop, pctString(mask.y));
2093
+ setFragmentValue(box, KeyWidth, pctString(mask.width));
2094
+ setFragmentValue(box, KeyHeight, pctString(mask.height));
2095
+ }
2096
+ function applyTransform2(target, style) {
2097
+ const parts = [];
2098
+ const transform2 = stringifyCSSValue(style[KeyTransform]);
2099
+ if (transform2) parts.push(transform2);
2100
+ for (const key of [KeyTranslate, KeyRotate, KeyScale, KeySkew, KeyMatrix]) {
2101
+ const value = stringifyCSSValue(style[key]);
2102
+ if (!value) continue;
2103
+ parts.push(`${key}(${value})`);
2104
+ }
2105
+ if (parts.length > 0) {
2106
+ setFragmentValue(target, KeyTransform, parts.join(" "));
2107
+ }
2108
+ const origin = stringifyCSSValue(style[KeyTransformOrigin]);
2109
+ if (origin) {
2110
+ setFragmentValue(target, KeyTransformOrigin, origin);
2111
+ }
2112
+ }
2113
+
2114
+ // src/html/style/render.ts
2115
+ function htmlLayerCSS(fragments) {
2116
+ const merged = mergeFragments(fragments);
2117
+ let box = null;
2118
+ let text = null;
2119
+ for (const fragment of merged) {
2120
+ const type = getFragmentString(fragment, TypeKey);
2121
+ if (type === TypeMotionDiv) box = fragment;
2122
+ if (type === TypeSpan) text = fragment;
2123
+ }
2124
+ return { boxCSS: fragmentCSS(box), textCSS: fragmentCSS(text) };
2125
+ }
2126
+ function fragmentCSS(fragment) {
2127
+ if (!fragment) return "";
2128
+ const keys = Object.keys(fragment).filter((key) => key !== TypeKey).sort();
2129
+ let out = "";
2130
+ for (const key of keys) {
2131
+ const raw = fragment[key];
2132
+ const value = raw === void 0 || raw === null ? "" : String(raw);
2133
+ if (!value) continue;
2134
+ out += `${camelToKebab(key)}:${value};`;
2135
+ }
2136
+ return out;
2137
+ }
2138
+ function camelToKebab(value) {
2139
+ if (value.startsWith("-")) return value;
2140
+ let out = "";
2141
+ for (let i = 0; i < value.length; i += 1) {
2142
+ const char = value[i] ?? "";
2143
+ const upper = char >= "A" && char <= "Z";
2144
+ if (upper) {
2145
+ if (i > 0) out += "-";
2146
+ out += char.toLowerCase();
2147
+ continue;
2148
+ }
2149
+ out += char;
2150
+ }
2151
+ return out;
2152
+ }
2153
+
2154
+ // src/html/steps.ts
2155
+ var CompileStep = {
2156
+ RESOLVE: "resolve",
2157
+ BUILD_ASSETS: "buildAssets",
2158
+ ADAPT_STYLE: "adaptStyle",
2159
+ RENDER_FONTS: "renderFonts",
2160
+ RENDER_HEAD: "renderHead",
2161
+ RENDER_PAGE: "renderPage",
2162
+ RENDER_TEXT: "renderText",
2163
+ RENDER_MASK: "renderMask",
2164
+ RENDER_INLINE: "renderInline",
2165
+ FINALIZE: "finalize"
2166
+ };
2167
+ function notifyObservers(observers, ctx) {
2168
+ if (!observers) return;
2169
+ const fns = observers[ctx.step];
2170
+ if (!fns?.length) return;
2171
+ for (const fn of fns) fn(ctx);
2172
+ }
2173
+
2174
+ // src/html/variants.ts
2175
+ function slugPageName(name) {
2176
+ const s = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
2177
+ return s || "page";
2178
+ }
2179
+ function pageByName(doc, pageName) {
2180
+ return doc.pages.find((p) => p.name === pageName);
2181
+ }
2182
+ function variantLabels(variants) {
2183
+ return variants.map((v, i) => {
2184
+ const l = v.label.trim();
2185
+ return l || (i === 0 ? "PSRT" : `variant-${i}`);
2186
+ });
2187
+ }
2188
+ function overlayDomId(pageName, variantIndex) {
2189
+ return `psrt-overlay-${slugPageName(pageName)}-v${variantIndex}`;
2190
+ }
2191
+ function textLayerDomId(pageName, textIndex, variantIndex) {
2192
+ return `psrt-text-${slugPageName(pageName)}-${textIndex}-v${variantIndex}`;
2193
+ }
2194
+ function maskLayerDomId(pageName, maskIndex, variantIndex) {
2195
+ return `psrt-mask-${slugPageName(pageName)}-${maskIndex}-v${variantIndex}`;
2196
+ }
2197
+ function buildHtmlVariants(primary, extra) {
2198
+ const variants = [{ label: "PSRT", doc: primary }];
2199
+ for (const item of extra ?? []) {
2200
+ const label = item.label?.trim() || `variant-${variants.length}`;
2201
+ variants.push({ label, doc: item.doc });
2202
+ }
2203
+ return variants;
2204
+ }
2205
+
2206
+ // src/html/render/script.ts
2207
+ function variantSwitcherCSS() {
2208
+ return `
2209
+ .psrt-hidden{display:none!important}
2210
+ .psrt-variant-hint{position:fixed;z-index:9999;right:12px;bottom:12px;padding:6px 10px;font:12px/1.3 system-ui,sans-serif;color:#e8e8e8;background:rgba(0,0,0,.72);border-radius:6px;pointer-events:none;user-select:none}
2211
+ `;
2212
+ }
2213
+ function writeVariantSwitcher(labels) {
2214
+ if (labels.length < 2) return "";
2215
+ const labelsJSON = JSON.stringify(labels);
2216
+ return `<div id="psrt-variant-hint" class="psrt-variant-hint" aria-live="polite"></div>
2217
+ <script>
2218
+ /**
2219
+ * PSRT HTML \u2014 variant switcher
2220
+ *
2221
+ * Bundled PSRT variants share the same page images; only the text/mask
2222
+ * overlays differ. Press Ctrl+L to cycle:
2223
+ * variant 0 \u2192 variant 1 \u2192 \u2026 \u2192 "Sem PSRT" (hide all overlays) \u2192 variant 0 \u2026
2224
+ *
2225
+ * Each overlay has class psrt-v-N (N = variant index). Inactive overlays
2226
+ * use class psrt-hidden (display:none).
2227
+ */
2228
+ (function () {
2229
+ /** Human-readable labels shown in the bottom-right hint. */
2230
+ var VARIANT_LABELS = ${labelsJSON};
2231
+
2232
+ /** Extra slot at the end: hide all PSRT overlays ("Sem PSRT"). */
2233
+ VARIANT_LABELS.push('');
2234
+
2235
+ /** Index into VARIANT_LABELS for the currently visible variant. */
2236
+ var activeVariantIndex = 0;
2237
+
2238
+ /** Bottom-right hint element (created above this script). */
2239
+ var hintEl = document.getElementById('psrt-variant-hint');
2240
+
2241
+ /**
2242
+ * Show one variant index, or hide every overlay when index is the off-state.
2243
+ */
2244
+ function applyVariant(index) {
2245
+ document.querySelectorAll('.psrt-overlay').forEach(function (overlay) {
2246
+ overlay.classList.add('psrt-hidden');
2247
+ });
2248
+
2249
+ if (index < VARIANT_LABELS.length - 1) {
2250
+ document.querySelectorAll('.psrt-overlay.psrt-v-' + index).forEach(function (overlay) {
2251
+ overlay.classList.remove('psrt-hidden');
2252
+ });
2253
+ }
2254
+
2255
+ if (hintEl) {
2256
+ var label = VARIANT_LABELS[index];
2257
+ hintEl.textContent = label === ''
2258
+ ? 'Sem PSRT (Ctrl+L)'
2259
+ : label + ' (Ctrl+L)';
2260
+ }
2261
+ }
2262
+
2263
+ document.addEventListener('keydown', function (event) {
2264
+ if (!event.ctrlKey || event.altKey) return;
2265
+ if (event.key.toLowerCase() !== 'l') return;
2266
+ event.preventDefault();
2267
+ activeVariantIndex = (activeVariantIndex + 1) % VARIANT_LABELS.length;
2268
+ applyVariant(activeVariantIndex);
2269
+ });
2270
+
2271
+ applyVariant(0);
2272
+ })();
2273
+ </script>
2274
+ `;
2275
+ }
2276
+ function variantClass(v) {
2277
+ return `psrt-v-${v}`;
2278
+ }
2279
+ function baseCSS(fontFaces) {
2280
+ return `${fontFaces.trim()}
2281
+ *{box-sizing:border-box;}
2282
+ .slides-wrap{margin:0;padding:0;display:flex;width:100%;flex-direction:column;align-items:center;}
2283
+ .slide{position:relative;display:block;flex:0 0 auto;line-height:0;margin:0;padding:0;}
2284
+ .slide-img{display:block;width:100%;height:auto;margin:0;padding:0;vertical-align:bottom;}
2285
+ .slide-overlays{position:absolute;left:0;top:0;right:0;bottom:0;}
2286
+ .slide-overlay{position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;container-type:size;container-name:slide;}
2287
+ .text-layer{position:absolute;box-sizing:border-box;margin:0;padding:0;line-height:1.2;overflow:hidden;overflow-wrap:anywhere;word-wrap:break-word;white-space:pre-wrap;}
2288
+ .text-ref-img{display:block;max-width:100%;height:auto;margin:0 0 .25em;padding:0;}
2289
+ `;
2290
+ }
2291
+ function escapeHtmlAttr(s) {
2292
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2293
+ }
2294
+
2295
+ // src/html/render/html.ts
2296
+ function buildFontCSS(fontURLs, assets, linksOnly) {
2297
+ if (fontURLs.length === 0) {
2298
+ return {
2299
+ fontFacesCSS: "",
2300
+ bodyStack: "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif"
2301
+ };
2302
+ }
2303
+ let fb = "";
2304
+ const names = [];
2305
+ for (let i = 0; i < fontURLs.length; i++) {
2306
+ const u = fontURLs[i]?.trim() ?? "";
2307
+ if (!u) continue;
2308
+ const a = assets.get(u);
2309
+ if (!a && !linksOnly) continue;
2310
+ const name = fontFamilyNameForURL(u, i);
2311
+ const src = fontSrcUrl(u, a, linksOnly);
2312
+ const format = faceFormat(a?.mime ?? "");
2313
+ if (linksOnly || a) {
2314
+ fb += `@font-face{font-family:'${name}';src:${src}${format};font-display:swap;}
2315
+ `;
2316
+ }
2317
+ names.push(`'${name}'`);
2318
+ }
2319
+ if (names.length === 0) {
2320
+ return {
2321
+ fontFacesCSS: fb,
2322
+ bodyStack: "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif"
2323
+ };
2324
+ }
2325
+ const stack = `${names.join(",")},-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif`;
2326
+ return { fontFacesCSS: fb, bodyStack: stack };
2327
+ }
2328
+ function pageBackgroundCSS(style) {
2329
+ const bg = backgroundColorFromStyle(style);
2330
+ return bg ? `background:${bg};` : "";
2331
+ }
2332
+ function writeTextLayer(parts, t, assets, canvasW, canvasH, variantIndex, linksOnly, pageName, observers) {
2333
+ const content = normalizeTextContent(t.content);
2334
+ const fontPx = textFontSizePx(t.textSize, canvasW, canvasH);
2335
+ const frags = adaptHTML({
2336
+ text: t,
2337
+ canvasW,
2338
+ canvasH,
2339
+ fontSizePx: fontPx,
2340
+ htmlCompile: true,
2341
+ pageSlug: pageName,
2342
+ textIndex: t.index
2343
+ });
2344
+ notifyObservers(observers, {
2345
+ step: CompileStep.ADAPT_STYLE,
2346
+ pageName,
2347
+ blockIndex: t.index,
2348
+ kind: "text"
2349
+ });
2350
+ let { boxCSS, textCSS } = htmlLayerCSS(frags);
2351
+ boxCSS = appendTextLayerGeometryCSS(boxCSS, t, content, canvasW, canvasH);
2352
+ const layerId = textLayerDomId(pageName, t.index, variantIndex);
2353
+ const classes = `text-layer psrt-text ${variantClass(variantIndex)}`;
2354
+ parts.push(
2355
+ `<div id="${escapeHtmlAttr(layerId)}" class="${classes}" style="${escapeHtmlAttr(boxCSS)}">`
2356
+ );
2357
+ const imgRef = (t.imageRef ?? "").trim();
2358
+ if (imgRef && isAssetReference(imgRef)) {
2359
+ const aa = assets.get(imgRef);
2360
+ if (linksOnly || aa && aa.mime.startsWith("image/")) {
2361
+ const refURI = assetRef(imgRef, aa, linksOnly);
2362
+ parts.push(
2363
+ `<img class="text-ref-img" src="${escapeHtmlAttr(refURI)}" alt="" style="margin:0 0 .25em 0;display:block;max-width:100%;height:auto"/>`
2364
+ );
2365
+ }
2366
+ }
2367
+ const inlineHTML = renderInlineHTML(content);
2368
+ notifyObservers(observers, {
2369
+ step: CompileStep.RENDER_INLINE,
2370
+ pageName,
2371
+ textIndex: t.index,
2372
+ htmlLength: inlineHTML.length
2373
+ });
2374
+ notifyObservers(observers, {
2375
+ step: CompileStep.RENDER_TEXT,
2376
+ pageName,
2377
+ textIndex: t.index,
2378
+ contentPreview: content.slice(0, 80)
2379
+ });
2380
+ if (textCSS) {
2381
+ parts.push(`<span style="${escapeHtmlAttr(textCSS)}">`);
2382
+ } else {
2383
+ parts.push("<span>");
2384
+ }
2385
+ parts.push(inlineHTML);
2386
+ parts.push("</span></div>");
2387
+ }
2388
+ function writeMaskLayer(parts, m, assets, canvasW, canvasH, variantIndex, linksOnly, pageName, observers) {
2389
+ const frags = adaptMaskHTML({
2390
+ text: {
2391
+ x: m.x,
2392
+ y: m.y,
2393
+ width: m.width,
2394
+ textSize: 0,
2395
+ style: m.style,
2396
+ index: m.index,
2397
+ content: ""
2398
+ },
2399
+ mask: m,
2400
+ canvasW,
2401
+ canvasH,
2402
+ htmlCompile: true,
2403
+ pageSlug: pageName,
2404
+ textIndex: m.index
2405
+ });
2406
+ notifyObservers(observers, {
2407
+ step: CompileStep.ADAPT_STYLE,
2408
+ pageName,
2409
+ blockIndex: m.index,
2410
+ kind: "mask"
2411
+ });
2412
+ let { boxCSS } = htmlLayerCSS(frags);
2413
+ const imgRef = (m.imageRef ?? "").trim();
2414
+ if (imgRef && isAssetReference(imgRef)) {
2415
+ const aa = assets.get(imgRef);
2416
+ if (linksOnly || aa && aa.mime.startsWith("image/")) {
2417
+ const refURI = assetRef(imgRef, aa, linksOnly);
2418
+ if (boxCSS && !boxCSS.endsWith(";")) boxCSS += ";";
2419
+ boxCSS += `background-image:url(${refURI});background-size:cover;background-position:center;background-repeat:no-repeat;`;
2420
+ }
2421
+ }
2422
+ const layerId = maskLayerDomId(pageName, m.index, variantIndex);
2423
+ const classes = `text-layer psrt-mask ${variantClass(variantIndex)}`;
2424
+ notifyObservers(observers, {
2425
+ step: CompileStep.RENDER_MASK,
2426
+ pageName,
2427
+ maskIndex: m.index
2428
+ });
2429
+ parts.push(`<div id="${escapeHtmlAttr(layerId)}" class="${classes}" style="${escapeHtmlAttr(boxCSS)}"></div>`);
2430
+ }
2431
+ function writeVariantOverlay(parts, variantPage, variantIndex, multiVariant, assets, canvasW, canvasH, linksOnly, observers) {
2432
+ const overlayId = overlayDomId(variantPage.name, variantIndex);
2433
+ let overlayClasses = `slide-overlay psrt-overlay ${variantClass(variantIndex)}`;
2434
+ if (multiVariant && variantIndex > 0) {
2435
+ overlayClasses += " psrt-hidden";
2436
+ }
2437
+ parts.push(`<div id="${escapeHtmlAttr(overlayId)}" class="${overlayClasses}">`);
2438
+ for (const entry of pageBlocksByIndex(variantPage)) {
2439
+ if (entry.kind === BlockText && entry.text) {
2440
+ writeTextLayer(
2441
+ parts,
2442
+ entry.text,
2443
+ assets,
2444
+ canvasW,
2445
+ canvasH,
2446
+ variantIndex,
2447
+ linksOnly,
2448
+ variantPage.name,
2449
+ observers
2450
+ );
2451
+ } else if (entry.kind === BlockMask && entry.mask) {
2452
+ writeMaskLayer(
2453
+ parts,
2454
+ entry.mask,
2455
+ assets,
2456
+ canvasW,
2457
+ canvasH,
2458
+ variantIndex,
2459
+ linksOnly,
2460
+ variantPage.name,
2461
+ observers
2462
+ );
2463
+ }
2464
+ }
2465
+ parts.push("</div>");
2466
+ }
2467
+ function writeSlide(parts, page, variants, assets, opts, pageIndex, multiVariant, observers) {
2468
+ const bg = pageBackgroundCSS(page.style);
2469
+ const imgURL = page.imageUrl.trim();
2470
+ const a = assets.get(imgURL);
2471
+ if (!opts.linksOnly && !a) {
2472
+ throw new Error(`missing fetched asset for page image "${imgURL}"`);
2473
+ }
2474
+ const src = assetRef(imgURL, a, !!opts.linksOnly);
2475
+ let canvasW;
2476
+ let canvasH;
2477
+ if (a) {
2478
+ const dims = imageDimensions(a.bytes, a.mime);
2479
+ canvasW = dims.w;
2480
+ canvasH = dims.h;
2481
+ } else {
2482
+ const dims = imageDimensions(null, "");
2483
+ canvasW = dims.w;
2484
+ canvasH = dims.h;
2485
+ }
2486
+ notifyObservers(observers, {
2487
+ step: CompileStep.RENDER_PAGE,
2488
+ pageIndex,
2489
+ pageName: page.name,
2490
+ canvasW,
2491
+ canvasH
2492
+ });
2493
+ let slideStyle = `width:${canvasW}px`;
2494
+ if (bg) slideStyle += `;${bg}`;
2495
+ parts.push(`<div class="slide" style="${escapeHtmlAttr(slideStyle)}">`);
2496
+ parts.push(`<img class="slide-img" src="${escapeHtmlAttr(src)}" alt=""/>`);
2497
+ parts.push('<div class="slide-overlays">');
2498
+ for (let vi = 0; vi < variants.length; vi++) {
2499
+ const variantPage = pageByName(variants[vi].doc, page.name);
2500
+ if (!variantPage) continue;
2501
+ writeVariantOverlay(
2502
+ parts,
2503
+ variantPage,
2504
+ vi,
2505
+ multiVariant,
2506
+ assets,
2507
+ canvasW,
2508
+ canvasH,
2509
+ !!opts.linksOnly,
2510
+ observers
2511
+ );
2512
+ }
2513
+ parts.push("</div></div>");
2514
+ }
2515
+ function renderHtmlBundle(variants, assets, opts, observers) {
2516
+ if (variants.length === 0) {
2517
+ throw new Error("no variants");
2518
+ }
2519
+ const primary = variants[0].doc;
2520
+ const labels = variantLabels(variants);
2521
+ const multiVariant = variants.length > 1;
2522
+ const includeVariantUI = multiVariant && !opts.noScript;
2523
+ const { fontFacesCSS, bodyStack } = buildFontCSS(primary.fonts ?? [], assets, !!opts.linksOnly);
2524
+ notifyObservers(observers, {
2525
+ step: CompileStep.RENDER_FONTS,
2526
+ fontCount: primary.fonts?.length ?? 0
2527
+ });
2528
+ let title = "PSRT";
2529
+ if (primary.pages.length > 0 && primary.pages[0]?.name.trim()) {
2530
+ title = primary.pages[0].name.trim();
2531
+ }
2532
+ notifyObservers(observers, {
2533
+ step: CompileStep.RENDER_HEAD,
2534
+ title
2535
+ });
2536
+ const parts = [];
2537
+ parts.push(`<!DOCTYPE html>
2538
+ <html lang="pt-BR">
2539
+ <head>
2540
+ <meta charset="utf-8"/>
2541
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
2542
+ <title>${escapeHtmlAttr(title)}</title>
2543
+ <style>`);
2544
+ parts.push(baseCSS(fontFacesCSS));
2545
+ if (multiVariant) parts.push(variantSwitcherCSS());
2546
+ parts.push(`
2547
+ body{font-family:${bodyStack};margin:0;padding:0;background:#111;overflow-x:auto;}
2548
+ </style>
2549
+ </head>
2550
+ <body>
2551
+ <main class="slides-wrap">
2552
+ `);
2553
+ for (let i = 0; i < primary.pages.length; i++) {
2554
+ writeSlide(parts, primary.pages[i], variants, assets, opts, i, multiVariant, observers);
2555
+ }
2556
+ parts.push(`
2557
+ </main>
2558
+ `);
2559
+ if (includeVariantUI) parts.push(writeVariantSwitcher(labels));
2560
+ parts.push(`
2561
+ </body>
2562
+ </html>`);
2563
+ const html = parts.join("");
2564
+ notifyObservers(observers, {
2565
+ step: CompileStep.FINALIZE,
2566
+ htmlLength: html.length,
2567
+ pageCount: primary.pages.length
2568
+ });
2569
+ return html;
2570
+ }
2571
+
2572
+ // src/html/compilePure.ts
2573
+ function buildAssetMapWithSizes(variants, linksOnly) {
2574
+ const docs = variants.map((v) => v.doc);
2575
+ const assets = buildAssetMap(docs, linksOnly);
2576
+ const canvasSizes = {};
2577
+ for (const doc of docs) {
2578
+ for (const page of doc.pages) {
2579
+ const url = page.imageUrl.trim();
2580
+ if (canvasSizes[url]) continue;
2581
+ const asset = assets.get(url);
2582
+ if (asset) {
2583
+ canvasSizes[url] = imageDimensions(asset.bytes, asset.mime);
2584
+ } else if (linksOnly) {
2585
+ canvasSizes[url] = imageDimensions(null, "");
2586
+ }
2587
+ }
2588
+ }
2589
+ return { assets, canvasSizes };
2590
+ }
2591
+ function compileToHtmlPure(doc, options) {
2592
+ const opts = options ?? {};
2593
+ const primary = resolveDocumentPure(doc);
2594
+ notifyObservers(opts.observers, { step: CompileStep.RESOLVE, doc: primary });
2595
+ const extraResolved = (opts.variants ?? []).map((v) => ({
2596
+ label: v.label,
2597
+ doc: resolveDocumentPure(v.doc)
2598
+ }));
2599
+ const htmlVariants = buildHtmlVariants(primary, extraResolved);
2600
+ const { assets, canvasSizes } = buildAssetMapWithSizes(htmlVariants, !!opts.linksOnly);
2601
+ notifyObservers(opts.observers, {
2602
+ step: CompileStep.BUILD_ASSETS,
2603
+ assetCount: assets.size,
2604
+ canvasSizes
2605
+ });
2606
+ return renderHtmlBundle(htmlVariants, assets, opts, opts.observers);
2607
+ }
2608
+
232
2609
  // src/compile.ts
233
2610
  function compileToHtml(doc, options) {
234
2611
  return invokeCompileToHtml(doc, options);
235
2612
  }
2613
+ function compileToHtmlPure2(doc, options) {
2614
+ return compileToHtmlPure(doc, options);
2615
+ }
236
2616
  function compileToSvg(doc, pageName, options) {
237
2617
  return invokeCompileToSvg(doc, pageName, options);
238
2618
  }
@@ -517,6 +2897,7 @@ function resolveDocumentStrict(doc) {
517
2897
  }
518
2898
  // Annotate the CommonJS export names for ESM import in node:
519
2899
  0 && (module.exports = {
2900
+ CompileStep,
520
2901
  Transformer,
521
2902
  adaptEntriesForWeb,
522
2903
  addConst,
@@ -525,6 +2906,7 @@ function resolveDocumentStrict(doc) {
525
2906
  addPage,
526
2907
  addText,
527
2908
  compileToHtml,
2909
+ compileToHtmlPure,
528
2910
  compileToSvg,
529
2911
  findMaskByIndex,
530
2912
  findPage,
@@ -536,6 +2918,7 @@ function resolveDocumentStrict(doc) {
536
2918
  mergePageDocumentPSRT,
537
2919
  mergeStyle,
538
2920
  movePage,
2921
+ notifyObservers,
539
2922
  nudgeTextPosition,
540
2923
  parse,
541
2924
  parseTextIndex,
@@ -553,6 +2936,7 @@ function resolveDocumentStrict(doc) {
553
2936
  reorderTextRelative,
554
2937
  reorderTextTo,
555
2938
  resolveDocument,
2939
+ resolveDocumentPure,
556
2940
  resolveDocumentStrict,
557
2941
  revertConstReferences,
558
2942
  setMaskPosition,