@psrt/sdk 0.1.1 → 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,12 +74,7 @@ __export(index_exports, {
70
74
  substituteConstReferences: () => substituteConstReferences,
71
75
  transform: () => transform
72
76
  });
73
- module.exports = __toCommonJS(index_exports);
74
-
75
- // src/runtime/boot.ts
76
- var import_node_fs = require("fs");
77
- var import_node_path = require("path");
78
- var import_node_url = require("url");
77
+ module.exports = __toCommonJS(src_exports);
79
78
 
80
79
  // src/wasm.ts
81
80
  var decoder = new TextDecoder();
@@ -169,12 +168,11 @@ function wireWasmFromGlobal() {
169
168
  wasmExports = exp;
170
169
  }
171
170
 
172
- // src/runtime/boot.ts
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");
173
175
  var import_meta = {};
174
- var bootPromise = null;
175
- function isNodeRuntime() {
176
- return typeof process !== "undefined" && Boolean(process.versions?.node);
177
- }
178
176
  function wasmDir() {
179
177
  let dir = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
180
178
  for (; ; ) {
@@ -190,6 +188,20 @@ function wasmDir() {
190
188
  }
191
189
  throw new Error("PSRT core binary not found");
192
190
  }
191
+ async function loadGoRuntimeNode() {
192
+ if (typeof globalThis.Go !== "undefined") {
193
+ return;
194
+ }
195
+ const scriptPath = (0, import_node_path.join)(wasmDir(), "wasm_exec.js");
196
+ await import((0, import_node_url.pathToFileURL)(scriptPath).href);
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;
193
205
  function waitForExports() {
194
206
  return new Promise((resolve, reject) => {
195
207
  let attempts = 0;
@@ -208,27 +220,8 @@ function waitForExports() {
208
220
  tick();
209
221
  });
210
222
  }
211
- async function loadGoRuntime() {
212
- if (typeof globalThis.Go !== "undefined") {
213
- return;
214
- }
215
- const scriptPath = (0, import_node_path.join)(wasmDir(), "wasm_exec.js");
216
- await import((0, import_node_url.pathToFileURL)(scriptPath).href);
217
- }
218
- async function loadCoreBytes() {
219
- if (isNodeRuntime()) {
220
- const bytes = (0, import_node_fs.readFileSync)((0, import_node_path.join)(wasmDir(), "psrt.wasm"));
221
- return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
222
- }
223
- const url = new URL("../../wasm/psrt.wasm", import_meta.url).href;
224
- const response = await fetch(url);
225
- if (!response.ok) {
226
- throw new Error(`failed to load PSRT core (${response.status})`);
227
- }
228
- return response.arrayBuffer();
229
- }
230
223
  async function startCore(bytes) {
231
- await loadGoRuntime();
224
+ await loadGoRuntimeNode();
232
225
  const go = new Go();
233
226
  const { instance } = await WebAssembly.instantiate(bytes, go.importObject);
234
227
  void go.run(instance);
@@ -244,7 +237,7 @@ async function bootCore() {
244
237
  return;
245
238
  }
246
239
  bootPromise = (async () => {
247
- const bytes = await loadCoreBytes();
240
+ const bytes = await loadCoreBytesNode();
248
241
  await startCore(bytes);
249
242
  })();
250
243
  return bootPromise;
@@ -266,10 +259,2360 @@ function formatDocument(doc) {
266
259
  return invokeFormatDocument(doc);
267
260
  }
268
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
+
269
2609
  // src/compile.ts
270
2610
  function compileToHtml(doc, options) {
271
2611
  return invokeCompileToHtml(doc, options);
272
2612
  }
2613
+ function compileToHtmlPure2(doc, options) {
2614
+ return compileToHtmlPure(doc, options);
2615
+ }
273
2616
  function compileToSvg(doc, pageName, options) {
274
2617
  return invokeCompileToSvg(doc, pageName, options);
275
2618
  }
@@ -554,6 +2897,7 @@ function resolveDocumentStrict(doc) {
554
2897
  }
555
2898
  // Annotate the CommonJS export names for ESM import in node:
556
2899
  0 && (module.exports = {
2900
+ CompileStep,
557
2901
  Transformer,
558
2902
  adaptEntriesForWeb,
559
2903
  addConst,
@@ -562,6 +2906,7 @@ function resolveDocumentStrict(doc) {
562
2906
  addPage,
563
2907
  addText,
564
2908
  compileToHtml,
2909
+ compileToHtmlPure,
565
2910
  compileToSvg,
566
2911
  findMaskByIndex,
567
2912
  findPage,
@@ -573,6 +2918,7 @@ function resolveDocumentStrict(doc) {
573
2918
  mergePageDocumentPSRT,
574
2919
  mergeStyle,
575
2920
  movePage,
2921
+ notifyObservers,
576
2922
  nudgeTextPosition,
577
2923
  parse,
578
2924
  parseTextIndex,
@@ -590,6 +2936,7 @@ function resolveDocumentStrict(doc) {
590
2936
  reorderTextRelative,
591
2937
  reorderTextTo,
592
2938
  resolveDocument,
2939
+ resolveDocumentPure,
593
2940
  resolveDocumentStrict,
594
2941
  revertConstReferences,
595
2942
  setMaskPosition,