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