@hyperframes/studio-server 0.7.60 → 0.7.61
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/chunk-4ETS2LXI.js +192 -0
- package/dist/chunk-4ETS2LXI.js.map +1 -0
- package/dist/chunk-6XMC64FJ.js +584 -0
- package/dist/chunk-6XMC64FJ.js.map +1 -0
- package/dist/chunk-LVXVG4V6.js +282 -0
- package/dist/chunk-LVXVG4V6.js.map +1 -0
- package/dist/chunk-W2SBTCO2.js +32 -0
- package/dist/chunk-W2SBTCO2.js.map +1 -0
- package/dist/chunk-X62ASOGO.js +30 -0
- package/dist/chunk-X62ASOGO.js.map +1 -0
- package/dist/chunk-XVQX2JHE.js +422 -0
- package/dist/chunk-XVQX2JHE.js.map +1 -0
- package/dist/chunk-YBR7MXIO.js +459 -0
- package/dist/chunk-YBR7MXIO.js.map +1 -0
- package/dist/chunk-ZUW4PULZ.js +66 -0
- package/dist/chunk-ZUW4PULZ.js.map +1 -0
- package/dist/helpers/finiteMutation.js +4 -24
- package/dist/helpers/finiteMutation.js.map +1 -1
- package/dist/helpers/manualEditsRenderScript.js +5 -577
- package/dist/helpers/manualEditsRenderScript.js.map +1 -1
- package/dist/helpers/mediaCodecMap.d.ts +100 -0
- package/dist/helpers/mediaCodecMap.js +25 -0
- package/dist/helpers/mediaCodecMap.js.map +1 -0
- package/dist/helpers/mediaProxyPreview.d.ts +4 -0
- package/dist/helpers/mediaProxyPreview.js +17 -0
- package/dist/helpers/mediaProxyPreview.js.map +1 -0
- package/dist/helpers/proxyTranscoder.d.ts +61 -0
- package/dist/helpers/proxyTranscoder.js +30 -0
- package/dist/helpers/proxyTranscoder.js.map +1 -0
- package/dist/helpers/screenshotClip.js +3 -22
- package/dist/helpers/screenshotClip.js.map +1 -1
- package/dist/helpers/sourceMutation.js +9 -419
- package/dist/helpers/sourceMutation.js.map +1 -1
- package/dist/helpers/studioMotionRenderScript.js +4 -186
- package/dist/helpers/studioMotionRenderScript.js.map +1 -1
- package/dist/index.d.ts +6 -202
- package/dist/index.js +172 -1373
- package/dist/index.js.map +1 -1
- package/dist/mediaProxyPreview-CeshDUjE.d.ts +259 -0
- package/package.json +15 -3
package/dist/index.js
CHANGED
|
@@ -1,3 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findUnsafeDomPatchValues,
|
|
3
|
+
findUnsafeMutationValues
|
|
4
|
+
} from "./chunk-X62ASOGO.js";
|
|
5
|
+
import {
|
|
6
|
+
isHTMLElement,
|
|
7
|
+
patchElementInHtml,
|
|
8
|
+
probeElementInSource,
|
|
9
|
+
removeElementFromHtml,
|
|
10
|
+
splitElementInHtml,
|
|
11
|
+
unwrapElementsFromHtml,
|
|
12
|
+
wrapElementsInHtml
|
|
13
|
+
} from "./chunk-XVQX2JHE.js";
|
|
14
|
+
import {
|
|
15
|
+
getElementScreenshotClip
|
|
16
|
+
} from "./chunk-W2SBTCO2.js";
|
|
17
|
+
import {
|
|
18
|
+
injectMediaCodecMap,
|
|
19
|
+
isAutoProxyEnabled,
|
|
20
|
+
proxyEtagSalt,
|
|
21
|
+
resolvePreviewMediaCodecProbeCache
|
|
22
|
+
} from "./chunk-ZUW4PULZ.js";
|
|
23
|
+
import {
|
|
24
|
+
ProxyCapacityError,
|
|
25
|
+
ProxyTranscodeError,
|
|
26
|
+
resolveProxy
|
|
27
|
+
} from "./chunk-YBR7MXIO.js";
|
|
28
|
+
import {
|
|
29
|
+
PROXY_VARIANT_CONFIG,
|
|
30
|
+
decideMediaProxyEligibility,
|
|
31
|
+
isProxyVariantRequest,
|
|
32
|
+
probeAssetCodec,
|
|
33
|
+
probeMediaMetadata,
|
|
34
|
+
resolveProxyVariantRequest
|
|
35
|
+
} from "./chunk-LVXVG4V6.js";
|
|
36
|
+
import {
|
|
37
|
+
STUDIO_MANUAL_EDITS_PATH,
|
|
38
|
+
createStudioManualEditsRenderBodyScript,
|
|
39
|
+
createStudioPositionSeekReapplyScript
|
|
40
|
+
} from "./chunk-6XMC64FJ.js";
|
|
41
|
+
import {
|
|
42
|
+
STUDIO_MOTION_PATH,
|
|
43
|
+
createStudioMotionRenderBodyScript
|
|
44
|
+
} from "./chunk-4ETS2LXI.js";
|
|
45
|
+
|
|
1
46
|
// src/createStudioApi.ts
|
|
2
47
|
import { Hono } from "hono";
|
|
3
48
|
|
|
@@ -302,7 +347,7 @@ import {
|
|
|
302
347
|
renameSync,
|
|
303
348
|
readdirSync as readdirSync4
|
|
304
349
|
} from "fs";
|
|
305
|
-
import { resolve as
|
|
350
|
+
import { resolve as resolve2, dirname, join as join6 } from "path";
|
|
306
351
|
|
|
307
352
|
// src/helpers/mime.ts
|
|
308
353
|
var MIME_TYPES = {
|
|
@@ -319,7 +364,13 @@ var MIME_TYPES = {
|
|
|
319
364
|
".webp": "image/webp",
|
|
320
365
|
".ico": "image/x-icon",
|
|
321
366
|
".mp4": "video/mp4",
|
|
367
|
+
".m4v": "video/mp4",
|
|
322
368
|
".mov": "video/quicktime",
|
|
369
|
+
".mkv": "video/x-matroska",
|
|
370
|
+
".mxf": "video/mxf",
|
|
371
|
+
".mts": "video/mp2t",
|
|
372
|
+
".m2ts": "video/mp2t",
|
|
373
|
+
".ts": "video/mp2t",
|
|
323
374
|
".webm": "video/webm",
|
|
324
375
|
".mp3": "audio/mpeg",
|
|
325
376
|
".wav": "audio/wav",
|
|
@@ -347,7 +398,8 @@ function isAudioFile(name) {
|
|
|
347
398
|
// src/helpers/waveform.ts
|
|
348
399
|
import { spawn } from "child_process";
|
|
349
400
|
import { existsSync as existsSync2, writeFileSync, mkdirSync } from "fs";
|
|
350
|
-
import { join as join3
|
|
401
|
+
import { join as join3 } from "path";
|
|
402
|
+
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
351
403
|
var SAMPLE_RATE = 4e3;
|
|
352
404
|
var PEAK_COUNT = 4e3;
|
|
353
405
|
var WAVEFORM_CACHE_VERSION = "v2";
|
|
@@ -370,15 +422,10 @@ function computePeaks(floats, count) {
|
|
|
370
422
|
const maxPeak = Math.max(...peaks, 1e-3);
|
|
371
423
|
return peaks.map((p) => p / maxPeak);
|
|
372
424
|
}
|
|
373
|
-
function ffmpegBinary() {
|
|
374
|
-
const configured = process.env.HYPERFRAMES_FFMPEG_PATH?.trim();
|
|
375
|
-
if (configured) return resolve2(configured);
|
|
376
|
-
return "ffmpeg";
|
|
377
|
-
}
|
|
378
425
|
function decodeAudioPeaks(audioPath) {
|
|
379
426
|
return new Promise((resolvePromise, reject) => {
|
|
380
427
|
const proc = spawn(
|
|
381
|
-
|
|
428
|
+
findFfBinary("ffmpeg") ?? "ffmpeg",
|
|
382
429
|
[
|
|
383
430
|
"-i",
|
|
384
431
|
audioPath,
|
|
@@ -430,7 +477,7 @@ import { spawnSync } from "child_process";
|
|
|
430
477
|
import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
431
478
|
import { tmpdir } from "os";
|
|
432
479
|
import { basename, join as join4 } from "path";
|
|
433
|
-
var VIDEO_EXT = /\.(mp4|webm|mov)$/i;
|
|
480
|
+
var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
|
|
434
481
|
var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
|
|
435
482
|
function validateUploadedMedia(filePath, runner = spawnSync) {
|
|
436
483
|
const isVideo = VIDEO_EXT.test(filePath);
|
|
@@ -580,31 +627,6 @@ function consumeFileWriteReceipt(absPath) {
|
|
|
580
627
|
return { path, version, writeToken };
|
|
581
628
|
}
|
|
582
629
|
|
|
583
|
-
// src/helpers/finiteMutation.ts
|
|
584
|
-
function findUnsafeMutationValues(value, path = "body", options = {}) {
|
|
585
|
-
if (value === null) {
|
|
586
|
-
return options.allowNullPath?.(path) ? [] : [{ path, reason: "null" }];
|
|
587
|
-
}
|
|
588
|
-
if (typeof value === "number") {
|
|
589
|
-
return Number.isFinite(value) ? [] : [{ path, reason: "non-finite-number" }];
|
|
590
|
-
}
|
|
591
|
-
if (!value || typeof value !== "object") return [];
|
|
592
|
-
if (Array.isArray(value)) {
|
|
593
|
-
return value.flatMap(
|
|
594
|
-
(item, index) => findUnsafeMutationValues(item, `${path}[${index}]`, options)
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
return Object.entries(value).flatMap(
|
|
598
|
-
([key, item]) => findUnsafeMutationValues(item, `${path}.${key}`, options)
|
|
599
|
-
);
|
|
600
|
-
}
|
|
601
|
-
var DOM_PATCH_NULL_VALUE_PATH = /^body\.operations\[\d+\]\.value$/;
|
|
602
|
-
function findUnsafeDomPatchValues(value) {
|
|
603
|
-
return findUnsafeMutationValues(value, "body", {
|
|
604
|
-
allowNullPath: (path) => DOM_PATCH_NULL_VALUE_PATH.test(path)
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
|
|
608
630
|
// src/routes/files.ts
|
|
609
631
|
import { classifyPropertyGroup } from "@hyperframes/parsers/gsap-constants";
|
|
610
632
|
import { parseGsapScriptAcorn } from "@hyperframes/parsers/gsap-parser-acorn";
|
|
@@ -632,429 +654,7 @@ import {
|
|
|
632
654
|
scalePositionsInScript,
|
|
633
655
|
dedupePositionWritesInScript
|
|
634
656
|
} from "@hyperframes/parsers/gsap-writer-acorn";
|
|
635
|
-
|
|
636
|
-
// src/helpers/sourceMutation.ts
|
|
637
657
|
import { parseHTML } from "linkedom";
|
|
638
|
-
import postcss from "postcss";
|
|
639
|
-
import selectorParser from "postcss-selector-parser";
|
|
640
|
-
import { isAllowedHtmlAttribute, isSafeAttributeValue } from "@hyperframes/core/html-attr-safety";
|
|
641
|
-
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
|
642
|
-
|
|
643
|
-
// src/helpers/sourceStyleMutation.ts
|
|
644
|
-
function parseStyleDecls(style) {
|
|
645
|
-
const props = /* @__PURE__ */ new Map();
|
|
646
|
-
const order = [];
|
|
647
|
-
let i = 0;
|
|
648
|
-
while (i < style.length) {
|
|
649
|
-
let depth = 0;
|
|
650
|
-
let inSingle = false;
|
|
651
|
-
let inDouble = false;
|
|
652
|
-
const start = i;
|
|
653
|
-
while (i < style.length) {
|
|
654
|
-
const ch = style[i];
|
|
655
|
-
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
656
|
-
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
657
|
-
else if (!inSingle && !inDouble) {
|
|
658
|
-
if (ch === "(") depth++;
|
|
659
|
-
else if (ch === ")") depth = Math.max(0, depth - 1);
|
|
660
|
-
else if (ch === ";" && depth === 0) break;
|
|
661
|
-
}
|
|
662
|
-
i++;
|
|
663
|
-
}
|
|
664
|
-
const decl = style.slice(start, i).trim();
|
|
665
|
-
i++;
|
|
666
|
-
if (!decl) continue;
|
|
667
|
-
const colon = decl.indexOf(":");
|
|
668
|
-
if (colon < 0) continue;
|
|
669
|
-
const key = decl.slice(0, colon).trim();
|
|
670
|
-
const val = decl.slice(colon + 1).trim();
|
|
671
|
-
if (!key) continue;
|
|
672
|
-
if (!props.has(key)) order.push(key);
|
|
673
|
-
props.set(key, val);
|
|
674
|
-
}
|
|
675
|
-
return { props, order };
|
|
676
|
-
}
|
|
677
|
-
function serializeStyleDecls(props, order) {
|
|
678
|
-
return order.map((k) => `${k}: ${props.get(k) ?? ""}`).filter((d) => d.trim()).join("; ");
|
|
679
|
-
}
|
|
680
|
-
function patchStyleAttrString(style, property, value) {
|
|
681
|
-
const { props, order } = parseStyleDecls(style);
|
|
682
|
-
if (value === null) {
|
|
683
|
-
props.delete(property);
|
|
684
|
-
const idx = order.indexOf(property);
|
|
685
|
-
if (idx >= 0) order.splice(idx, 1);
|
|
686
|
-
} else {
|
|
687
|
-
if (!props.has(property)) order.push(property);
|
|
688
|
-
props.set(property, value);
|
|
689
|
-
}
|
|
690
|
-
return serializeStyleDecls(props, order);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// src/helpers/sourceMutation.ts
|
|
694
|
-
function parseSourceDocument(source) {
|
|
695
|
-
const hasDocumentShell = /<!doctype|<html[\s>]/i.test(source);
|
|
696
|
-
if (hasDocumentShell) {
|
|
697
|
-
return { document: parseHTML(source).document, wrappedFragment: false };
|
|
698
|
-
}
|
|
699
|
-
return {
|
|
700
|
-
document: parseHTML(`<!DOCTYPE html><html><head></head><body>${source}</body></html>`).document,
|
|
701
|
-
wrappedFragment: true
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
function duplicateCssRulesForId(document2, originalId, newId) {
|
|
705
|
-
const idToken = `#${originalId}`;
|
|
706
|
-
const transform = selectorParser((selectors) => {
|
|
707
|
-
selectors.walkIds((node) => {
|
|
708
|
-
if (node.value === originalId) node.value = newId;
|
|
709
|
-
});
|
|
710
|
-
});
|
|
711
|
-
for (const styleEl of document2.querySelectorAll("style")) {
|
|
712
|
-
const css = styleEl.textContent ?? "";
|
|
713
|
-
let root;
|
|
714
|
-
try {
|
|
715
|
-
root = postcss.parse(css);
|
|
716
|
-
} catch {
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
const clones = [];
|
|
720
|
-
root.walkRules((rule) => {
|
|
721
|
-
if (!rule.selector.includes(idToken)) return;
|
|
722
|
-
const newSelector = transform.processSync(rule.selector);
|
|
723
|
-
if (newSelector === rule.selector) return;
|
|
724
|
-
const clone = rule.clone({ selector: newSelector });
|
|
725
|
-
clones.push(clone);
|
|
726
|
-
});
|
|
727
|
-
if (clones.length > 0) {
|
|
728
|
-
for (const c of clones) root.append(c);
|
|
729
|
-
styleEl.textContent = root.toString();
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
function querySelectorAllWithTemplates(root, selector) {
|
|
734
|
-
const matches = Array.from(root.querySelectorAll(selector));
|
|
735
|
-
if (matches.length > 0) return matches;
|
|
736
|
-
const templates = Array.from(root.querySelectorAll("template"));
|
|
737
|
-
for (const tmpl of templates) {
|
|
738
|
-
const inner = querySelectorAllWithTemplates(tmpl, selector);
|
|
739
|
-
if (inner.length > 0) return inner;
|
|
740
|
-
}
|
|
741
|
-
return [];
|
|
742
|
-
}
|
|
743
|
-
function escapeCssAttrValue(value) {
|
|
744
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
745
|
-
}
|
|
746
|
-
function findByHfId(document2, hfId) {
|
|
747
|
-
try {
|
|
748
|
-
const matches = querySelectorAllWithTemplates(
|
|
749
|
-
document2,
|
|
750
|
-
`[data-hf-id="${escapeCssAttrValue(hfId)}"]`
|
|
751
|
-
);
|
|
752
|
-
if (matches.length > 1) {
|
|
753
|
-
console.warn(
|
|
754
|
-
`sourceMutation: data-hf-id "${hfId}" matched ${matches.length} elements; using the first. ids must be unique per document.`
|
|
755
|
-
);
|
|
756
|
-
}
|
|
757
|
-
return matches[0] ?? null;
|
|
758
|
-
} catch {
|
|
759
|
-
return null;
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
function findTargetElement(document2, target) {
|
|
763
|
-
if (target.hfId) {
|
|
764
|
-
const el = findByHfId(document2, target.hfId);
|
|
765
|
-
if (el) return el;
|
|
766
|
-
}
|
|
767
|
-
if (target.id) {
|
|
768
|
-
const byId = document2.getElementById(target.id);
|
|
769
|
-
if (byId) return byId;
|
|
770
|
-
}
|
|
771
|
-
if (!target.selector) return null;
|
|
772
|
-
try {
|
|
773
|
-
const matches = querySelectorAllWithTemplates(document2, target.selector);
|
|
774
|
-
return matches[target.selectorIndex ?? 0] ?? null;
|
|
775
|
-
} catch {
|
|
776
|
-
return null;
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
function removeElementFromHtml(source, target) {
|
|
780
|
-
const { document: document2, wrappedFragment } = parseSourceDocument(source);
|
|
781
|
-
const element = findTargetElement(document2, target);
|
|
782
|
-
if (!element) return source;
|
|
783
|
-
element.remove();
|
|
784
|
-
return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
|
|
785
|
-
}
|
|
786
|
-
function isHTMLElement(el) {
|
|
787
|
-
const HTMLEl = el.ownerDocument?.defaultView?.HTMLElement;
|
|
788
|
-
return HTMLEl ? el instanceof HTMLEl : el.nodeType === 1 && "style" in el;
|
|
789
|
-
}
|
|
790
|
-
function resolveOperationTarget(parent, op) {
|
|
791
|
-
if (op.childSelector === void 0) return parent;
|
|
792
|
-
try {
|
|
793
|
-
const child = parent.querySelectorAll(op.childSelector)[op.childIndex ?? 0] ?? null;
|
|
794
|
-
return child && isHTMLElement(child) ? child : null;
|
|
795
|
-
} catch {
|
|
796
|
-
return null;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
function patchElementInHtml(source, target, operations) {
|
|
800
|
-
const { document: document2, wrappedFragment } = parseSourceDocument(source);
|
|
801
|
-
const el = findTargetElement(document2, target);
|
|
802
|
-
if (!el || !isHTMLElement(el)) return { html: source, matched: false };
|
|
803
|
-
const htmlEl = el;
|
|
804
|
-
const resolved = [];
|
|
805
|
-
for (const op of operations) {
|
|
806
|
-
const opTarget = resolveOperationTarget(htmlEl, op);
|
|
807
|
-
if (!opTarget) return { html: source, matched: false };
|
|
808
|
-
resolved.push({ op, target: opTarget });
|
|
809
|
-
}
|
|
810
|
-
for (const { op, target: opTarget } of resolved) {
|
|
811
|
-
switch (op.type) {
|
|
812
|
-
case "inline-style":
|
|
813
|
-
{
|
|
814
|
-
const raw = opTarget.getAttribute("style") ?? "";
|
|
815
|
-
const patched = patchStyleAttrString(raw, op.property, op.value);
|
|
816
|
-
opTarget.setAttribute("style", patched);
|
|
817
|
-
}
|
|
818
|
-
break;
|
|
819
|
-
case "attribute":
|
|
820
|
-
{
|
|
821
|
-
const fullAttr = op.property.startsWith("data-") ? op.property : `data-${op.property}`;
|
|
822
|
-
if (op.value != null) {
|
|
823
|
-
opTarget.setAttribute(fullAttr, op.value);
|
|
824
|
-
} else {
|
|
825
|
-
opTarget.removeAttribute(fullAttr);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
break;
|
|
829
|
-
case "html-attribute":
|
|
830
|
-
if (!isAllowedHtmlAttribute(op.property)) break;
|
|
831
|
-
if (op.value != null) {
|
|
832
|
-
if (!isSafeAttributeValue(op.property, op.value)) break;
|
|
833
|
-
opTarget.setAttribute(op.property, op.value);
|
|
834
|
-
} else {
|
|
835
|
-
opTarget.removeAttribute(op.property);
|
|
836
|
-
}
|
|
837
|
-
break;
|
|
838
|
-
case "text-content":
|
|
839
|
-
if (op.value != null) {
|
|
840
|
-
const inner = opTarget.children.length === 1 ? opTarget.firstElementChild : null;
|
|
841
|
-
const textTarget = inner && isHTMLElement(inner) ? inner : opTarget;
|
|
842
|
-
textTarget.textContent = op.value;
|
|
843
|
-
}
|
|
844
|
-
break;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
return {
|
|
848
|
-
html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
|
|
849
|
-
matched: true
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
function probeElementInSource(source, target) {
|
|
853
|
-
if (!target.id && !target.hfId && !target.selector) return false;
|
|
854
|
-
const { document: document2 } = parseSourceDocument(source);
|
|
855
|
-
const el = findTargetElement(document2, target);
|
|
856
|
-
return el != null && isHTMLElement(el);
|
|
857
|
-
}
|
|
858
|
-
function resolveElementTiming(el) {
|
|
859
|
-
const start = parseFloat(el.getAttribute("data-start") ?? "0") || 0;
|
|
860
|
-
const usesDataEnd = el.hasAttribute("data-end");
|
|
861
|
-
const duration = usesDataEnd ? parseFloat(el.getAttribute("data-end") ?? "") - start || 0 : parseFloat(el.getAttribute("data-duration") ?? "0") || 0;
|
|
862
|
-
return { start, duration, usesDataEnd };
|
|
863
|
-
}
|
|
864
|
-
function setElementDuration(el, start, duration, usesDataEnd) {
|
|
865
|
-
if (usesDataEnd) {
|
|
866
|
-
const endTime = String(Math.round((start + duration) * 1e3) / 1e3);
|
|
867
|
-
el.setAttribute("data-end", endTime);
|
|
868
|
-
el.removeAttribute("data-duration");
|
|
869
|
-
} else {
|
|
870
|
-
el.setAttribute("data-duration", String(Math.round(duration * 1e3) / 1e3));
|
|
871
|
-
el.removeAttribute("data-end");
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
function splitElementInHtml(source, target, splitTime, newId, fallbackTiming) {
|
|
875
|
-
const { document: document2, wrappedFragment } = parseSourceDocument(source);
|
|
876
|
-
const el = findTargetElement(document2, target);
|
|
877
|
-
if (!el || !isHTMLElement(el)) return { html: source, matched: false, newId: null };
|
|
878
|
-
const timing = resolveElementTiming(el);
|
|
879
|
-
const { usesDataEnd } = timing;
|
|
880
|
-
let { start, duration } = timing;
|
|
881
|
-
if (duration <= 0 && fallbackTiming && fallbackTiming.duration > 0) {
|
|
882
|
-
start = fallbackTiming.start;
|
|
883
|
-
duration = fallbackTiming.duration;
|
|
884
|
-
}
|
|
885
|
-
if (duration <= 0 || splitTime <= start || splitTime >= start + duration) {
|
|
886
|
-
return { html: source, matched: false, newId: null };
|
|
887
|
-
}
|
|
888
|
-
if (document2.getElementById(newId)) {
|
|
889
|
-
let suffix = 2;
|
|
890
|
-
const base = newId;
|
|
891
|
-
while (document2.getElementById(newId)) {
|
|
892
|
-
newId = `${base}-${suffix++}`;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
const firstDuration = splitTime - start;
|
|
896
|
-
const secondDuration = duration - firstDuration;
|
|
897
|
-
const clone = el.cloneNode(true);
|
|
898
|
-
if (!isHTMLElement(clone)) return { html: source, matched: false, newId: null };
|
|
899
|
-
clone.setAttribute("id", newId);
|
|
900
|
-
clone.removeAttribute("data-hf-id");
|
|
901
|
-
for (const node of clone.querySelectorAll("[data-hf-id]")) node.removeAttribute("data-hf-id");
|
|
902
|
-
clone.setAttribute("data-start", String(Math.round(splitTime * 1e3) / 1e3));
|
|
903
|
-
setElementDuration(clone, splitTime, secondDuration, usesDataEnd);
|
|
904
|
-
const playbackStartAttr = el.hasAttribute("data-playback-start") ? "data-playback-start" : el.hasAttribute("data-media-start") ? "data-media-start" : null;
|
|
905
|
-
if (playbackStartAttr) {
|
|
906
|
-
const currentTrim = parseFloat(el.getAttribute(playbackStartAttr) ?? "0") || 0;
|
|
907
|
-
const rateRaw = parseFloat(el.getAttribute("data-playback-rate") ?? "");
|
|
908
|
-
const rate = Number.isFinite(rateRaw) ? rateRaw : 1;
|
|
909
|
-
clone.setAttribute(
|
|
910
|
-
playbackStartAttr,
|
|
911
|
-
String(Math.round((currentTrim + firstDuration * rate) * 1e3) / 1e3)
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
const originalId = el.getAttribute("id");
|
|
915
|
-
if (originalId) {
|
|
916
|
-
duplicateCssRulesForId(document2, originalId, newId);
|
|
917
|
-
}
|
|
918
|
-
el.setAttribute("data-start", String(Math.round(start * 1e3) / 1e3));
|
|
919
|
-
setElementDuration(el, start, firstDuration, usesDataEnd);
|
|
920
|
-
if (el.nextSibling) {
|
|
921
|
-
el.parentElement.insertBefore(clone, el.nextSibling);
|
|
922
|
-
} else {
|
|
923
|
-
el.parentElement.appendChild(clone);
|
|
924
|
-
}
|
|
925
|
-
const html = wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
|
|
926
|
-
return {
|
|
927
|
-
// The split owns its new nodes' stable ids. Leaving the clone unstamped makes
|
|
928
|
-
// the next preview request persist different bytes after history is recorded.
|
|
929
|
-
html: ensureHfIds(html),
|
|
930
|
-
matched: true,
|
|
931
|
-
newId
|
|
932
|
-
};
|
|
933
|
-
}
|
|
934
|
-
function getInlineStylePx(el, property) {
|
|
935
|
-
const style = (isHTMLElement(el) ? el.getAttribute("style") : null) ?? "";
|
|
936
|
-
const { props } = parseStyleDecls(style);
|
|
937
|
-
const raw = props.get(property);
|
|
938
|
-
if (!raw) return 0;
|
|
939
|
-
const n = parseFloat(raw);
|
|
940
|
-
return Number.isFinite(n) ? n : 0;
|
|
941
|
-
}
|
|
942
|
-
function setInlineLeftTop(el, left, top) {
|
|
943
|
-
let style = el.getAttribute("style") ?? "";
|
|
944
|
-
style = patchStyleAttrString(style, "left", `${left}px`);
|
|
945
|
-
style = patchStyleAttrString(style, "top", `${top}px`);
|
|
946
|
-
el.setAttribute("style", style);
|
|
947
|
-
}
|
|
948
|
-
function uniqueGroupDomId(document2, groupId) {
|
|
949
|
-
const base = groupId.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "group";
|
|
950
|
-
let id = base;
|
|
951
|
-
let n = 2;
|
|
952
|
-
while (document2.getElementById(id)) {
|
|
953
|
-
id = `${base}-${n}`;
|
|
954
|
-
n += 1;
|
|
955
|
-
}
|
|
956
|
-
return id;
|
|
957
|
-
}
|
|
958
|
-
function wrapElementsInHtml(source, targets, groupId, bbox, rebases) {
|
|
959
|
-
const { document: document2, wrappedFragment } = parseSourceDocument(source);
|
|
960
|
-
if (targets.length === 0) {
|
|
961
|
-
return { html: source, matched: false, groupId: null, error: "no targets" };
|
|
962
|
-
}
|
|
963
|
-
const els = [];
|
|
964
|
-
const seen = /* @__PURE__ */ new Set();
|
|
965
|
-
for (const target of targets) {
|
|
966
|
-
const el = findTargetElement(document2, target);
|
|
967
|
-
if (!el || !isHTMLElement(el) || seen.has(el)) continue;
|
|
968
|
-
seen.add(el);
|
|
969
|
-
els.push(el);
|
|
970
|
-
}
|
|
971
|
-
if (els.length === 0) {
|
|
972
|
-
return { html: source, matched: false, groupId: null, error: "no targets matched" };
|
|
973
|
-
}
|
|
974
|
-
const parent = els[0]?.parentElement;
|
|
975
|
-
if (!parent || els.some((el) => el.parentElement !== parent)) {
|
|
976
|
-
return {
|
|
977
|
-
html: source,
|
|
978
|
-
matched: false,
|
|
979
|
-
groupId: null,
|
|
980
|
-
error: "grouped elements must share a single parent"
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
const memberSet = new Set(els);
|
|
984
|
-
const ordered = Array.from(parent.children).filter((c) => memberSet.has(c));
|
|
985
|
-
const rebaseByEl = /* @__PURE__ */ new Map();
|
|
986
|
-
for (const rebase of rebases) {
|
|
987
|
-
const el = findTargetElement(document2, rebase.target);
|
|
988
|
-
if (el) rebaseByEl.set(el, { left: rebase.left, top: rebase.top });
|
|
989
|
-
}
|
|
990
|
-
const wrapper = document2.createElement("div");
|
|
991
|
-
wrapper.setAttribute("data-hf-group", groupId);
|
|
992
|
-
wrapper.setAttribute("id", uniqueGroupDomId(document2, groupId));
|
|
993
|
-
const memberZIndexes = ordered.map(
|
|
994
|
-
(el) => Number.parseInt(
|
|
995
|
-
parseStyleDecls(el.getAttribute("style") ?? "").props.get("z-index") ?? "",
|
|
996
|
-
10
|
|
997
|
-
)
|
|
998
|
-
).filter((z) => Number.isFinite(z));
|
|
999
|
-
const maxZ = memberZIndexes.length > 0 ? Math.max(...memberZIndexes) : null;
|
|
1000
|
-
wrapper.setAttribute(
|
|
1001
|
-
"style",
|
|
1002
|
-
`position: absolute; left: ${bbox.left}px; top: ${bbox.top}px; width: ${bbox.width}px; height: ${bbox.height}px` + (maxZ !== null ? `; z-index: ${maxZ}` : "")
|
|
1003
|
-
);
|
|
1004
|
-
parent.insertBefore(wrapper, ordered[ordered.length - 1] ?? null);
|
|
1005
|
-
for (const el of ordered) {
|
|
1006
|
-
const rebase = rebaseByEl.get(el);
|
|
1007
|
-
if (rebase) setInlineLeftTop(el, rebase.left, rebase.top);
|
|
1008
|
-
wrapper.appendChild(el);
|
|
1009
|
-
}
|
|
1010
|
-
return {
|
|
1011
|
-
html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
|
|
1012
|
-
matched: true,
|
|
1013
|
-
groupId
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
function unwrapElementsFromHtml(source, groupTarget) {
|
|
1017
|
-
const { document: document2, wrappedFragment } = parseSourceDocument(source);
|
|
1018
|
-
const group = findTargetElement(document2, groupTarget);
|
|
1019
|
-
if (!group || !isHTMLElement(group)) return { html: source, unwrapped: false };
|
|
1020
|
-
if (!group.hasAttribute("data-hf-group")) return { html: source, unwrapped: false };
|
|
1021
|
-
const parent = group.parentElement;
|
|
1022
|
-
if (!parent) return { html: source, unwrapped: false };
|
|
1023
|
-
const wLeft = getInlineStylePx(group, "left");
|
|
1024
|
-
const wTop = getInlineStylePx(group, "top");
|
|
1025
|
-
const groupCenter = {
|
|
1026
|
-
cx: wLeft + getInlineStylePx(group, "width") / 2,
|
|
1027
|
-
cy: wTop + getInlineStylePx(group, "height") / 2
|
|
1028
|
-
};
|
|
1029
|
-
const members = [];
|
|
1030
|
-
for (const child of Array.from(group.children)) {
|
|
1031
|
-
if (isHTMLElement(child)) {
|
|
1032
|
-
const newLeft = getInlineStylePx(child, "left") + wLeft;
|
|
1033
|
-
const newTop = getInlineStylePx(child, "top") + wTop;
|
|
1034
|
-
setInlineLeftTop(child, newLeft, newTop);
|
|
1035
|
-
if (child.id) {
|
|
1036
|
-
members.push({
|
|
1037
|
-
id: child.id,
|
|
1038
|
-
cx: newLeft + getInlineStylePx(child, "width") / 2,
|
|
1039
|
-
cy: newTop + getInlineStylePx(child, "height") / 2
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
parent.insertBefore(child, group);
|
|
1044
|
-
}
|
|
1045
|
-
const groupId = group.id || void 0;
|
|
1046
|
-
group.remove();
|
|
1047
|
-
return {
|
|
1048
|
-
html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
|
|
1049
|
-
unwrapped: true,
|
|
1050
|
-
unwrappedGroupId: groupId,
|
|
1051
|
-
members,
|
|
1052
|
-
groupCenter
|
|
1053
|
-
};
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
// src/routes/files.ts
|
|
1057
|
-
import { parseHTML as parseHTML2 } from "linkedom";
|
|
1058
658
|
function isAcornGsapWriterEnabled() {
|
|
1059
659
|
const val = process.env["STUDIO_SDK_CUTOVER_ENABLED"];
|
|
1060
660
|
return val === "true" || val === "1";
|
|
@@ -1244,7 +844,7 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
1244
844
|
const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
|
|
1245
845
|
let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
|
|
1246
846
|
let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
|
|
1247
|
-
while (existsSync3(
|
|
847
|
+
while (existsSync3(resolve2(projectDir, candidate))) {
|
|
1248
848
|
num++;
|
|
1249
849
|
candidate = `${cleanBase} (copy ${num})${ext}`;
|
|
1250
850
|
}
|
|
@@ -1255,7 +855,7 @@ function walkFiles(dir, filter) {
|
|
|
1255
855
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
1256
856
|
const full = join6(dir, entry.name);
|
|
1257
857
|
if (entry.isDirectory()) {
|
|
1258
|
-
if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders")
|
|
858
|
+
if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders" || entry.name === ".transcode-cache")
|
|
1259
859
|
continue;
|
|
1260
860
|
results.push(...walkFiles(full, filter));
|
|
1261
861
|
} else if (filter(entry.name)) {
|
|
@@ -1282,10 +882,10 @@ function updateReferences(projectDir, oldPath, newPath) {
|
|
|
1282
882
|
return updatedCount;
|
|
1283
883
|
}
|
|
1284
884
|
function extractGsapScriptBlock(html) {
|
|
1285
|
-
const { document
|
|
885
|
+
const { document } = parseHTML(html);
|
|
1286
886
|
const scripts = [
|
|
1287
|
-
...
|
|
1288
|
-
...Array.from(
|
|
887
|
+
...document.querySelectorAll("script:not([src])"),
|
|
888
|
+
...Array.from(document.querySelectorAll("template")).flatMap(
|
|
1289
889
|
(tmpl) => Array.from(tmpl.querySelectorAll("script:not([src])"))
|
|
1290
890
|
)
|
|
1291
891
|
];
|
|
@@ -1294,10 +894,10 @@ function extractGsapScriptBlock(html) {
|
|
|
1294
894
|
if (content.includes("gsap.timeline") || content.includes(".set(") || content.includes(".to(")) {
|
|
1295
895
|
return {
|
|
1296
896
|
scriptText: content,
|
|
1297
|
-
document
|
|
897
|
+
document,
|
|
1298
898
|
replaceScript(newText) {
|
|
1299
899
|
script.textContent = newText;
|
|
1300
|
-
return
|
|
900
|
+
return document.toString();
|
|
1301
901
|
}
|
|
1302
902
|
};
|
|
1303
903
|
}
|
|
@@ -1395,11 +995,11 @@ function bakeGroupTransformIntoMembers(html, groupId, members, groupCenter) {
|
|
|
1395
995
|
}
|
|
1396
996
|
return block.replaceScript(script);
|
|
1397
997
|
}
|
|
1398
|
-
function stripStudioEditsFromTarget(
|
|
998
|
+
function stripStudioEditsFromTarget(document, selector) {
|
|
1399
999
|
if (!selector) return 0;
|
|
1400
1000
|
let stripped = 0;
|
|
1401
1001
|
try {
|
|
1402
|
-
for (const el of
|
|
1002
|
+
for (const el of document.querySelectorAll(selector)) {
|
|
1403
1003
|
if (!isHTMLElement(el)) continue;
|
|
1404
1004
|
const htmlEl = el;
|
|
1405
1005
|
let touched = false;
|
|
@@ -1469,11 +1069,11 @@ function resolveFinalOpacity(anim) {
|
|
|
1469
1069
|
const num = Number(raw);
|
|
1470
1070
|
return Number.isFinite(num) && num !== 0 ? num : null;
|
|
1471
1071
|
}
|
|
1472
|
-
function bakeVisibilityOnDelete(
|
|
1072
|
+
function bakeVisibilityOnDelete(document, anim) {
|
|
1473
1073
|
const opacity = resolveFinalOpacity(anim);
|
|
1474
1074
|
if (opacity === null) return;
|
|
1475
1075
|
try {
|
|
1476
|
-
for (const el of
|
|
1076
|
+
for (const el of document.querySelectorAll(anim.targetSelector)) {
|
|
1477
1077
|
if (isHTMLElement(el)) el.style.setProperty("opacity", String(opacity));
|
|
1478
1078
|
}
|
|
1479
1079
|
} catch {
|
|
@@ -2226,7 +1826,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2226
1826
|
skipped.push(name);
|
|
2227
1827
|
continue;
|
|
2228
1828
|
}
|
|
2229
|
-
const destPath =
|
|
1829
|
+
const destPath = resolve2(targetDir, name);
|
|
2230
1830
|
if (!isSafePath(projectDir, destPath)) continue;
|
|
2231
1831
|
let finalPath = destPath;
|
|
2232
1832
|
let finalName = name;
|
|
@@ -2236,13 +1836,13 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2236
1836
|
const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
|
|
2237
1837
|
let n = 2;
|
|
2238
1838
|
const MAX_COPY_INDEX = 1e4;
|
|
2239
|
-
while (n < MAX_COPY_INDEX && existsSync3(
|
|
1839
|
+
while (n < MAX_COPY_INDEX && existsSync3(resolve2(targetDir, `${base} (${n})${ext}`))) n++;
|
|
2240
1840
|
if (n >= MAX_COPY_INDEX) {
|
|
2241
1841
|
skipped.push(name);
|
|
2242
1842
|
continue;
|
|
2243
1843
|
}
|
|
2244
1844
|
finalName = `${base} (${n})${ext}`;
|
|
2245
|
-
finalPath =
|
|
1845
|
+
finalPath = resolve2(targetDir, finalName);
|
|
2246
1846
|
}
|
|
2247
1847
|
const buffer = Buffer.from(await value.arrayBuffer());
|
|
2248
1848
|
const validation = validateUploadedMediaBuffer(finalName, buffer);
|
|
@@ -2797,7 +2397,7 @@ import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRunt
|
|
|
2797
2397
|
// src/helpers/subComposition.ts
|
|
2798
2398
|
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
2799
2399
|
import { join as join7 } from "path";
|
|
2800
|
-
import { parseHTML as
|
|
2400
|
+
import { parseHTML as parseHTML2 } from "linkedom";
|
|
2801
2401
|
import {
|
|
2802
2402
|
rewriteAssetPaths,
|
|
2803
2403
|
rewriteCssAssetUrls,
|
|
@@ -2845,7 +2445,7 @@ function fixDigitLeadingIdSelectors(root) {
|
|
|
2845
2445
|
}
|
|
2846
2446
|
}
|
|
2847
2447
|
function extractFullDocumentParts(rawHtml, compPath) {
|
|
2848
|
-
const { document: doc } =
|
|
2448
|
+
const { document: doc } = parseHTML2(rawHtml);
|
|
2849
2449
|
const rewriteTargets = [doc.head, doc.body].filter(Boolean);
|
|
2850
2450
|
for (const target of rewriteTargets) {
|
|
2851
2451
|
rewriteRelativePaths(target, compPath);
|
|
@@ -2859,7 +2459,7 @@ function extractFullDocumentParts(rawHtml, compPath) {
|
|
|
2859
2459
|
return { headContent, bodyContent, htmlAttrs, bodyAttrs };
|
|
2860
2460
|
}
|
|
2861
2461
|
function extractTemplateInnerHtml(rawComp) {
|
|
2862
|
-
const { document: doc } =
|
|
2462
|
+
const { document: doc } = parseHTML2(rawComp);
|
|
2863
2463
|
const template = doc.querySelector("template");
|
|
2864
2464
|
return template ? template.innerHTML : null;
|
|
2865
2465
|
}
|
|
@@ -2911,7 +2511,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
|
|
|
2911
2511
|
let bodyAttrs = "";
|
|
2912
2512
|
const templateInner = extractTemplateInnerHtml(rawComp);
|
|
2913
2513
|
if (templateInner != null) {
|
|
2914
|
-
const { document: contentDoc } =
|
|
2514
|
+
const { document: contentDoc } = parseHTML2(
|
|
2915
2515
|
`<!DOCTYPE html><html><head></head><body>${templateInner}</body></html>`
|
|
2916
2516
|
);
|
|
2917
2517
|
rewriteRelativePaths(contentDoc, compPath);
|
|
@@ -2925,7 +2525,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
|
|
|
2925
2525
|
htmlAttrs = parts.htmlAttrs;
|
|
2926
2526
|
bodyAttrs = parts.bodyAttrs;
|
|
2927
2527
|
} else {
|
|
2928
|
-
const { document: contentDoc } =
|
|
2528
|
+
const { document: contentDoc } = parseHTML2(
|
|
2929
2529
|
`<!DOCTYPE html><html><head></head><body>${rawComp}</body></html>`
|
|
2930
2530
|
);
|
|
2931
2531
|
rewriteRelativePaths(contentDoc, compPath);
|
|
@@ -2970,198 +2570,11 @@ ${rewrittenContent}
|
|
|
2970
2570
|
</html>`;
|
|
2971
2571
|
}
|
|
2972
2572
|
|
|
2973
|
-
// src/helpers/studioMotionRenderScript.ts
|
|
2974
|
-
var STUDIO_MOTION_PATH = ".hyperframes/studio-motion.json";
|
|
2975
|
-
function hasStudioMotionEntries(manifestContent) {
|
|
2976
|
-
try {
|
|
2977
|
-
const parsed = JSON.parse(manifestContent);
|
|
2978
|
-
return Array.isArray(parsed.motions) && parsed.motions.length > 0;
|
|
2979
|
-
} catch {
|
|
2980
|
-
return false;
|
|
2981
|
-
}
|
|
2982
|
-
}
|
|
2983
|
-
function createStudioMotionRenderBodyScript(manifestContent, options = {}) {
|
|
2984
|
-
if (!manifestContent.trim() || !hasStudioMotionEntries(manifestContent)) return null;
|
|
2985
|
-
return `(${studioMotionRenderRuntime.toString()})(${JSON.stringify(manifestContent)}, ${JSON.stringify(options.activeCompositionPath ?? null)});`;
|
|
2986
|
-
}
|
|
2987
|
-
function studioMotionRenderRuntime(manifestContent, activeCompositionPath) {
|
|
2988
|
-
const STUDIO_MOTION_TIMELINE_ID = "studio-motion";
|
|
2989
|
-
const STUDIO_MOTION_ATTR = "data-hf-studio-motion";
|
|
2990
|
-
const ORIGINAL_TRANSFORM_ATTR = "data-hf-studio-motion-original-transform";
|
|
2991
|
-
const ORIGINAL_OPACITY_ATTR = "data-hf-studio-motion-original-opacity";
|
|
2992
|
-
const ORIGINAL_VISIBILITY_ATTR = "data-hf-studio-motion-original-visibility";
|
|
2993
|
-
const objectRecord = (value) => value && typeof value === "object" ? value : null;
|
|
2994
|
-
const finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
2995
|
-
const runtimeWindow = window;
|
|
2996
|
-
const parseMotionValues = (value) => {
|
|
2997
|
-
const record = objectRecord(value);
|
|
2998
|
-
if (!record) return null;
|
|
2999
|
-
const parsed = {};
|
|
3000
|
-
for (const key of ["x", "y", "scale", "rotation", "opacity", "autoAlpha"]) {
|
|
3001
|
-
const next = finiteNumber(record[key]);
|
|
3002
|
-
if (next != null) parsed[key] = next;
|
|
3003
|
-
}
|
|
3004
|
-
return Object.keys(parsed).length > 0 ? parsed : null;
|
|
3005
|
-
};
|
|
3006
|
-
const parseCustomEase = (value) => {
|
|
3007
|
-
const record = objectRecord(value);
|
|
3008
|
-
if (!record) return null;
|
|
3009
|
-
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
3010
|
-
const data = typeof record.data === "string" ? record.data.trim() : "";
|
|
3011
|
-
if (!id || !data) return null;
|
|
3012
|
-
return { id, data };
|
|
3013
|
-
};
|
|
3014
|
-
const parsedManifest = (() => {
|
|
3015
|
-
try {
|
|
3016
|
-
return objectRecord(JSON.parse(manifestContent));
|
|
3017
|
-
} catch {
|
|
3018
|
-
return null;
|
|
3019
|
-
}
|
|
3020
|
-
})();
|
|
3021
|
-
const manifestMotions = Array.isArray(parsedManifest?.motions) ? parsedManifest.motions : [];
|
|
3022
|
-
const sourceFileForElement = (element) => {
|
|
3023
|
-
let current = element;
|
|
3024
|
-
while (current) {
|
|
3025
|
-
const sourceFile = current.getAttribute("data-composition-file") ?? current.getAttribute("data-composition-src");
|
|
3026
|
-
if (sourceFile) return sourceFile;
|
|
3027
|
-
current = current.parentElement;
|
|
3028
|
-
}
|
|
3029
|
-
return activeCompositionPath ?? "index.html";
|
|
3030
|
-
};
|
|
3031
|
-
const elementMatchesSourceFile = (element, sourceFile) => sourceFileForElement(element) === sourceFile;
|
|
3032
|
-
const isHTMLElement2 = (element) => element instanceof HTMLElement;
|
|
3033
|
-
const querySelectorCandidates = (selector) => {
|
|
3034
|
-
const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1];
|
|
3035
|
-
if (className) {
|
|
3036
|
-
return Array.from(document.getElementsByTagName("*")).filter(
|
|
3037
|
-
(element) => isHTMLElement2(element) && element.classList.contains(className)
|
|
3038
|
-
);
|
|
3039
|
-
}
|
|
3040
|
-
if (/^[A-Za-z][A-Za-z0-9-]*$/.test(selector)) {
|
|
3041
|
-
return Array.from(document.getElementsByTagName(selector)).filter(isHTMLElement2);
|
|
3042
|
-
}
|
|
3043
|
-
return Array.from(document.querySelectorAll(selector)).filter(isHTMLElement2);
|
|
3044
|
-
};
|
|
3045
|
-
const resolveTarget = (targetRecord) => {
|
|
3046
|
-
const sourceFile = typeof targetRecord.sourceFile === "string" ? targetRecord.sourceFile : "";
|
|
3047
|
-
if (!sourceFile) return null;
|
|
3048
|
-
const id = typeof targetRecord.id === "string" ? targetRecord.id : "";
|
|
3049
|
-
if (id) {
|
|
3050
|
-
const byId = document.getElementById(id);
|
|
3051
|
-
if (isHTMLElement2(byId) && elementMatchesSourceFile(byId, sourceFile)) return byId;
|
|
3052
|
-
}
|
|
3053
|
-
const selector = typeof targetRecord.selector === "string" ? targetRecord.selector : "";
|
|
3054
|
-
if (!selector) return null;
|
|
3055
|
-
try {
|
|
3056
|
-
const selectorIndex = Math.max(0, Math.floor(finiteNumber(targetRecord.selectorIndex) ?? 0));
|
|
3057
|
-
return querySelectorCandidates(selector).filter(
|
|
3058
|
-
(element) => elementMatchesSourceFile(element, sourceFile)
|
|
3059
|
-
)[selectorIndex] ?? null;
|
|
3060
|
-
} catch {
|
|
3061
|
-
return null;
|
|
3062
|
-
}
|
|
3063
|
-
};
|
|
3064
|
-
const restoreElement = (element) => {
|
|
3065
|
-
runtimeWindow.gsap?.set?.(element, { clearProps: "transform,opacity,visibility" });
|
|
3066
|
-
element.style.transform = element.getAttribute(ORIGINAL_TRANSFORM_ATTR) ?? "";
|
|
3067
|
-
element.style.opacity = element.getAttribute(ORIGINAL_OPACITY_ATTR) ?? "";
|
|
3068
|
-
element.style.visibility = element.getAttribute(ORIGINAL_VISIBILITY_ATTR) ?? "";
|
|
3069
|
-
element.removeAttribute(STUDIO_MOTION_ATTR);
|
|
3070
|
-
element.removeAttribute(ORIGINAL_TRANSFORM_ATTR);
|
|
3071
|
-
element.removeAttribute(ORIGINAL_OPACITY_ATTR);
|
|
3072
|
-
element.removeAttribute(ORIGINAL_VISIBILITY_ATTR);
|
|
3073
|
-
};
|
|
3074
|
-
const restoreStudioMotionElements = () => {
|
|
3075
|
-
for (const element of Array.from(document.querySelectorAll(`[${STUDIO_MOTION_ATTR}]`))) {
|
|
3076
|
-
if (isHTMLElement2(element)) restoreElement(element);
|
|
3077
|
-
}
|
|
3078
|
-
};
|
|
3079
|
-
const readCurrentTime = () => {
|
|
3080
|
-
try {
|
|
3081
|
-
const playerTime = runtimeWindow.__player?.getTime?.();
|
|
3082
|
-
if (typeof playerTime === "number" && Number.isFinite(playerTime)) {
|
|
3083
|
-
return Math.max(0, playerTime);
|
|
3084
|
-
}
|
|
3085
|
-
} catch {
|
|
3086
|
-
}
|
|
3087
|
-
try {
|
|
3088
|
-
const timelineTime = runtimeWindow.__timeline?.time?.();
|
|
3089
|
-
if (typeof timelineTime === "number" && Number.isFinite(timelineTime)) {
|
|
3090
|
-
return Math.max(0, timelineTime);
|
|
3091
|
-
}
|
|
3092
|
-
} catch {
|
|
3093
|
-
}
|
|
3094
|
-
return 0;
|
|
3095
|
-
};
|
|
3096
|
-
const resolveEase = (motion) => {
|
|
3097
|
-
const fallback = typeof motion.ease === "string" && motion.ease.trim() ? motion.ease.trim() : "none";
|
|
3098
|
-
const customEase = parseCustomEase(motion.customEase);
|
|
3099
|
-
const customEasePlugin = runtimeWindow.CustomEase;
|
|
3100
|
-
if (!customEase || typeof customEasePlugin?.create !== "function") return fallback;
|
|
3101
|
-
try {
|
|
3102
|
-
runtimeWindow.gsap?.registerPlugin?.(customEasePlugin);
|
|
3103
|
-
customEasePlugin.create(customEase.id, customEase.data);
|
|
3104
|
-
return customEase.id;
|
|
3105
|
-
} catch {
|
|
3106
|
-
return fallback;
|
|
3107
|
-
}
|
|
3108
|
-
};
|
|
3109
|
-
const applyManifest = () => {
|
|
3110
|
-
runtimeWindow.__timelines = runtimeWindow.__timelines ?? {};
|
|
3111
|
-
runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID]?.kill?.();
|
|
3112
|
-
delete runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID];
|
|
3113
|
-
restoreStudioMotionElements();
|
|
3114
|
-
const gsap = runtimeWindow.gsap;
|
|
3115
|
-
if (!gsap?.timeline || manifestMotions.length === 0) return 0;
|
|
3116
|
-
const timeline = gsap.timeline({ paused: true, defaults: { overwrite: "auto" } });
|
|
3117
|
-
let applied = 0;
|
|
3118
|
-
for (const motionValue of manifestMotions) {
|
|
3119
|
-
const motion = objectRecord(motionValue);
|
|
3120
|
-
if (!motion || motion.kind !== "gsap-motion") continue;
|
|
3121
|
-
const targetRecord = objectRecord(motion.target);
|
|
3122
|
-
if (!targetRecord) continue;
|
|
3123
|
-
const target = resolveTarget(targetRecord);
|
|
3124
|
-
if (!target || typeof timeline.fromTo !== "function") continue;
|
|
3125
|
-
const start = finiteNumber(motion.start);
|
|
3126
|
-
const duration = finiteNumber(motion.duration);
|
|
3127
|
-
if (start == null || duration == null || start < 0 || duration <= 0) continue;
|
|
3128
|
-
const from = parseMotionValues(motion.from);
|
|
3129
|
-
const to = parseMotionValues(motion.to);
|
|
3130
|
-
if (!from || !to) continue;
|
|
3131
|
-
if (!target.hasAttribute(STUDIO_MOTION_ATTR)) {
|
|
3132
|
-
target.setAttribute(ORIGINAL_TRANSFORM_ATTR, target.style.transform);
|
|
3133
|
-
target.setAttribute(ORIGINAL_OPACITY_ATTR, target.style.opacity);
|
|
3134
|
-
target.setAttribute(ORIGINAL_VISIBILITY_ATTR, target.style.visibility);
|
|
3135
|
-
}
|
|
3136
|
-
target.setAttribute(STUDIO_MOTION_ATTR, "true");
|
|
3137
|
-
timeline.fromTo(
|
|
3138
|
-
target,
|
|
3139
|
-
from,
|
|
3140
|
-
{ ...to, duration, ease: resolveEase(motion), overwrite: "auto", immediateRender: false },
|
|
3141
|
-
start
|
|
3142
|
-
);
|
|
3143
|
-
applied += 1;
|
|
3144
|
-
}
|
|
3145
|
-
if (applied === 0) {
|
|
3146
|
-
timeline.kill?.();
|
|
3147
|
-
return 0;
|
|
3148
|
-
}
|
|
3149
|
-
runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID] = timeline;
|
|
3150
|
-
timeline.pause?.();
|
|
3151
|
-
const currentTime = readCurrentTime();
|
|
3152
|
-
if (typeof timeline.totalTime === "function") timeline.totalTime(currentTime, false);
|
|
3153
|
-
else timeline.time?.(currentTime);
|
|
3154
|
-
return applied;
|
|
3155
|
-
};
|
|
3156
|
-
runtimeWindow.__hfStudioMotionApply = applyManifest;
|
|
3157
|
-
applyManifest();
|
|
3158
|
-
}
|
|
3159
|
-
|
|
3160
2573
|
// src/routes/preview.ts
|
|
3161
|
-
import { ensureHfIds as
|
|
2574
|
+
import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
|
|
3162
2575
|
|
|
3163
2576
|
// src/helpers/hfIdPersist.ts
|
|
3164
|
-
import { ensureHfIds
|
|
2577
|
+
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
|
3165
2578
|
import {
|
|
3166
2579
|
closeSync as closeSync2,
|
|
3167
2580
|
constants,
|
|
@@ -3173,7 +2586,7 @@ import {
|
|
|
3173
2586
|
writeSync as writeSync2
|
|
3174
2587
|
} from "fs";
|
|
3175
2588
|
function persistHfIdsIfNeeded(filePath, html) {
|
|
3176
|
-
const normalized =
|
|
2589
|
+
const normalized = ensureHfIds(html);
|
|
3177
2590
|
const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3178
2591
|
const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3179
2592
|
if (idsAfter > idsBefore) {
|
|
@@ -3207,7 +2620,7 @@ function stampFileHfIds(filePath) {
|
|
|
3207
2620
|
try {
|
|
3208
2621
|
if (!fstatSync(fd).isFile()) return null;
|
|
3209
2622
|
const html = readFileSync6(fd, "utf-8");
|
|
3210
|
-
const normalized =
|
|
2623
|
+
const normalized = ensureHfIds(html);
|
|
3211
2624
|
const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3212
2625
|
const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3213
2626
|
if (writable && idsAfter > idsBefore) {
|
|
@@ -3409,11 +2822,17 @@ async function transformPreviewHtml(html, adapter, project, activeCompositionPat
|
|
|
3409
2822
|
function resolveProjectMainHtml(projectDir, projectId) {
|
|
3410
2823
|
const indexPath = join8(projectDir, "index.html");
|
|
3411
2824
|
if (existsSync5(indexPath)) {
|
|
3412
|
-
return {
|
|
2825
|
+
return {
|
|
2826
|
+
html: readFileSync7(indexPath, "utf-8"),
|
|
2827
|
+
compositionPath: "index.html"
|
|
2828
|
+
};
|
|
3413
2829
|
}
|
|
3414
2830
|
const blockHtmlPath = join8(projectDir, `${projectId}.html`);
|
|
3415
2831
|
if (existsSync5(blockHtmlPath)) {
|
|
3416
|
-
return {
|
|
2832
|
+
return {
|
|
2833
|
+
html: readFileSync7(blockHtmlPath, "utf-8"),
|
|
2834
|
+
compositionPath: `${projectId}.html`
|
|
2835
|
+
};
|
|
3417
2836
|
}
|
|
3418
2837
|
return null;
|
|
3419
2838
|
}
|
|
@@ -3422,6 +2841,7 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3422
2841
|
"Cache-Control": "private, no-cache",
|
|
3423
2842
|
ETag: etag
|
|
3424
2843
|
});
|
|
2844
|
+
const mediaCodecProbeCache = resolvePreviewMediaCodecProbeCache(adapter);
|
|
3425
2845
|
api.get("/projects/:id/preview", async (c) => {
|
|
3426
2846
|
const resolved = await resolveProjectAndSignature(adapter, c.req.param("id"));
|
|
3427
2847
|
if (!resolved) return c.json({ error: "not found" }, 404);
|
|
@@ -3432,7 +2852,10 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3432
2852
|
const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}"`;
|
|
3433
2853
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3434
2854
|
if (ifNoneMatch === etag) {
|
|
3435
|
-
return new Response(null, {
|
|
2855
|
+
return new Response(null, {
|
|
2856
|
+
status: 304,
|
|
2857
|
+
headers: previewCacheHeaders(etag)
|
|
2858
|
+
});
|
|
3436
2859
|
}
|
|
3437
2860
|
const diskMain = resolveProjectMainHtml(project.dir, project.id);
|
|
3438
2861
|
const normalizedDisk = diskMain ? persistHfIdsIfNeeded(join8(project.dir, diskMain.compositionPath), diskMain.html) : null;
|
|
@@ -3455,12 +2878,19 @@ ${runtimeTag}`;
|
|
|
3455
2878
|
bundled = bundled.replace(/<head>/i, `<head><base href="${baseHref}">`);
|
|
3456
2879
|
}
|
|
3457
2880
|
bundled = injectStudioPreviewAugmentations(
|
|
3458
|
-
|
|
2881
|
+
ensureHfIds2(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
|
|
3459
2882
|
adapter,
|
|
3460
2883
|
project.dir,
|
|
3461
2884
|
mainCompositionPath
|
|
3462
2885
|
);
|
|
3463
2886
|
if (previewVariables) bundled = injectPreviewVariables(bundled, previewVariables);
|
|
2887
|
+
bundled = await injectMediaCodecMap(
|
|
2888
|
+
bundled,
|
|
2889
|
+
adapter,
|
|
2890
|
+
project.dir,
|
|
2891
|
+
mainCompositionPath,
|
|
2892
|
+
mediaCodecProbeCache
|
|
2893
|
+
);
|
|
3464
2894
|
return c.html(bundled, 200, previewCacheHeaders(etag));
|
|
3465
2895
|
} catch {
|
|
3466
2896
|
const fallback = resolveProjectMainHtml(project.dir, project.id);
|
|
@@ -3478,6 +2908,13 @@ ${runtimeTag}`;
|
|
|
3478
2908
|
if (previewVariables) {
|
|
3479
2909
|
fallbackAugmented = injectPreviewVariables(fallbackAugmented, previewVariables);
|
|
3480
2910
|
}
|
|
2911
|
+
fallbackAugmented = await injectMediaCodecMap(
|
|
2912
|
+
fallbackAugmented,
|
|
2913
|
+
adapter,
|
|
2914
|
+
project.dir,
|
|
2915
|
+
fallback.compositionPath,
|
|
2916
|
+
mediaCodecProbeCache
|
|
2917
|
+
);
|
|
3481
2918
|
return c.html(fallbackAugmented, 200, previewCacheHeaders(etag));
|
|
3482
2919
|
}
|
|
3483
2920
|
return c.text("not found", 404);
|
|
@@ -3504,7 +2941,10 @@ ${runtimeTag}`;
|
|
|
3504
2941
|
const etag = `"comp:v2:${compPath}:${signature}${variablesEtagSalt(vars.raw)}"`;
|
|
3505
2942
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3506
2943
|
if (ifNoneMatch === etag) {
|
|
3507
|
-
return new Response(null, {
|
|
2944
|
+
return new Response(null, {
|
|
2945
|
+
status: 304,
|
|
2946
|
+
headers: previewCacheHeaders(etag)
|
|
2947
|
+
});
|
|
3508
2948
|
}
|
|
3509
2949
|
const stamped = pinSubCompHfIds(compFile, compPath);
|
|
3510
2950
|
if (stamped === null) return c.text("not found", 404);
|
|
@@ -3517,9 +2957,10 @@ ${runtimeTag}`;
|
|
|
3517
2957
|
stamped
|
|
3518
2958
|
);
|
|
3519
2959
|
if (!html) return c.text("not found", 404);
|
|
3520
|
-
html =
|
|
2960
|
+
html = ensureHfIds2(await transformPreviewHtml(html, adapter, project, compPath));
|
|
3521
2961
|
html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath);
|
|
3522
2962
|
if (previewVariables) html = injectPreviewVariables(html, previewVariables);
|
|
2963
|
+
html = await injectMediaCodecMap(html, adapter, project.dir, compPath, mediaCodecProbeCache);
|
|
3523
2964
|
return c.html(html, 200, previewCacheHeaders(etag));
|
|
3524
2965
|
});
|
|
3525
2966
|
api.get("/projects/:id/preview/*", async (c) => {
|
|
@@ -3538,15 +2979,49 @@ ${runtimeTag}`;
|
|
|
3538
2979
|
}
|
|
3539
2980
|
const contentType = getMimeType(subPath);
|
|
3540
2981
|
const isText = /\.(html|css|js|json|svg|txt|md|cube)$/i.test(subPath);
|
|
3541
|
-
const
|
|
3542
|
-
|
|
2982
|
+
const proxyParam = c.req.query("hf-proxy");
|
|
2983
|
+
let proxyVariant;
|
|
2984
|
+
if (proxyParam !== void 0) {
|
|
2985
|
+
if (!isProxyVariantRequest(proxyParam) || !contentType.startsWith("video/") || !isAutoProxyEnabled(adapter)) {
|
|
2986
|
+
return c.text("not found", 404);
|
|
2987
|
+
}
|
|
2988
|
+
const facts = await probeAssetCodec(file);
|
|
2989
|
+
const eligibility = decideMediaProxyEligibility(facts);
|
|
2990
|
+
if (!eligibility.eligible) {
|
|
2991
|
+
return c.text(`media proxy unavailable: ${eligibility.reason}`, 422);
|
|
2992
|
+
}
|
|
2993
|
+
if (!facts) return c.text("media proxy unavailable: unknown_codec", 422);
|
|
2994
|
+
proxyVariant = resolveProxyVariantRequest(proxyParam, facts) ?? void 0;
|
|
2995
|
+
if (!proxyVariant) {
|
|
2996
|
+
return c.text("media proxy variant does not match asset", 422);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
const etag = `"${stat.mtimeMs.toString(36)}-${stat.size.toString(36)}${proxyEtagSalt(proxyVariant)}"`;
|
|
3000
|
+
const cacheHeaders = isText ? { "Cache-Control": "no-store" } : {
|
|
3001
|
+
"Cache-Control": "private, max-age=3600, must-revalidate",
|
|
3002
|
+
ETag: etag
|
|
3003
|
+
};
|
|
3543
3004
|
if (!isText) {
|
|
3544
3005
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3545
3006
|
if (ifNoneMatch === etag) {
|
|
3546
3007
|
return new Response(null, { status: 304, headers: cacheHeaders });
|
|
3547
3008
|
}
|
|
3548
3009
|
}
|
|
3549
|
-
|
|
3010
|
+
let servedPath = file;
|
|
3011
|
+
let servedContentType = contentType;
|
|
3012
|
+
if (proxyVariant !== void 0) {
|
|
3013
|
+
try {
|
|
3014
|
+
servedPath = await resolveProxy(project.dir, file, proxyVariant);
|
|
3015
|
+
} catch (err) {
|
|
3016
|
+
if (err instanceof ProxyCapacityError) {
|
|
3017
|
+
return c.text(err.message, 503, { "Retry-After": "5" });
|
|
3018
|
+
}
|
|
3019
|
+
const message = err instanceof ProxyTranscodeError ? err.message : "proxy transcode failed";
|
|
3020
|
+
return c.text(message, 502);
|
|
3021
|
+
}
|
|
3022
|
+
servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
|
|
3023
|
+
}
|
|
3024
|
+
const buffer = isText ? Buffer.from(readFileSync7(file, "utf-8"), "utf-8") : readFileSync7(servedPath);
|
|
3550
3025
|
const totalSize = buffer.length;
|
|
3551
3026
|
const rangeHeader = c.req.header("Range");
|
|
3552
3027
|
if (rangeHeader) {
|
|
@@ -3560,7 +3035,7 @@ ${runtimeTag}`;
|
|
|
3560
3035
|
status: 206,
|
|
3561
3036
|
headers: {
|
|
3562
3037
|
...cacheHeaders,
|
|
3563
|
-
"Content-Type":
|
|
3038
|
+
"Content-Type": servedContentType,
|
|
3564
3039
|
"Content-Range": `bytes ${start}-${safeEnd}/${totalSize}`,
|
|
3565
3040
|
"Accept-Ranges": "bytes",
|
|
3566
3041
|
"Content-Length": String(chunkSize)
|
|
@@ -3571,7 +3046,7 @@ ${runtimeTag}`;
|
|
|
3571
3046
|
return new Response(new Uint8Array(buffer), {
|
|
3572
3047
|
headers: {
|
|
3573
3048
|
...cacheHeaders,
|
|
3574
|
-
"Content-Type":
|
|
3049
|
+
"Content-Type": servedContentType,
|
|
3575
3050
|
"Accept-Ranges": "bytes",
|
|
3576
3051
|
"Content-Length": String(totalSize)
|
|
3577
3052
|
}
|
|
@@ -3848,586 +3323,6 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3848
3323
|
import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
|
|
3849
3324
|
import { join as join11 } from "path";
|
|
3850
3325
|
import { createHash as createHash4 } from "crypto";
|
|
3851
|
-
|
|
3852
|
-
// src/helpers/manualEditsRenderScript.ts
|
|
3853
|
-
var STUDIO_MANUAL_EDITS_PATH = ".hyperframes/studio-manual-edits.json";
|
|
3854
|
-
function createStudioManualEditsRenderBodyScript(manifestContent, options = {}) {
|
|
3855
|
-
if (!manifestContent.trim()) return null;
|
|
3856
|
-
return `(${studioManualEditsRenderRuntime.toString()})(${JSON.stringify(manifestContent)}, ${JSON.stringify(options.activeCompositionPath ?? null)});`;
|
|
3857
|
-
}
|
|
3858
|
-
function createStudioPositionSeekReapplyScript() {
|
|
3859
|
-
return `(${studioPositionSeekReapplyRuntime.toString()})();`;
|
|
3860
|
-
}
|
|
3861
|
-
function studioPositionSeekReapplyRuntime() {
|
|
3862
|
-
const OFFSET_X_PROP = "--hf-studio-offset-x";
|
|
3863
|
-
const OFFSET_Y_PROP = "--hf-studio-offset-y";
|
|
3864
|
-
const WIDTH_PROP = "--hf-studio-width";
|
|
3865
|
-
const HEIGHT_PROP = "--hf-studio-height";
|
|
3866
|
-
const ROTATION_PROP = "--hf-studio-rotation";
|
|
3867
|
-
const PATH_OFFSET_ATTR = "data-hf-studio-path-offset";
|
|
3868
|
-
const BOX_SIZE_ATTR = "data-hf-studio-box-size";
|
|
3869
|
-
const ROTATION_ATTR = "data-hf-studio-rotation";
|
|
3870
|
-
const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate";
|
|
3871
|
-
const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate";
|
|
3872
|
-
const MOTION_ATTR = "data-hf-studio-motion";
|
|
3873
|
-
const MOTION_TL_KEY = "studio-motion";
|
|
3874
|
-
const WRAPPED_PROP = "__hfStudioPositionSeekReapplyWrapped";
|
|
3875
|
-
if (!document.querySelector("[" + PATH_OFFSET_ATTR + '="true"]') && !document.querySelector("[" + BOX_SIZE_ATTR + '="true"]') && !document.querySelector("[" + ROTATION_ATTR + '="true"]') && !document.querySelector("[" + MOTION_ATTR + "]"))
|
|
3876
|
-
return;
|
|
3877
|
-
const splitTopLevelWhitespace = (value) => {
|
|
3878
|
-
const parts = [];
|
|
3879
|
-
let depth = 0;
|
|
3880
|
-
let current = "";
|
|
3881
|
-
for (const char of value.trim()) {
|
|
3882
|
-
if (char === "(") depth += 1;
|
|
3883
|
-
if (char === ")") depth = Math.max(0, depth - 1);
|
|
3884
|
-
if (/\s/.test(char) && depth === 0) {
|
|
3885
|
-
if (current) parts.push(current);
|
|
3886
|
-
current = "";
|
|
3887
|
-
} else {
|
|
3888
|
-
current += char;
|
|
3889
|
-
}
|
|
3890
|
-
}
|
|
3891
|
-
if (current) parts.push(current);
|
|
3892
|
-
return parts;
|
|
3893
|
-
};
|
|
3894
|
-
const composeTranslate = (element, x, y) => {
|
|
3895
|
-
const original = element.getAttribute(ORIGINAL_TRANSLATE_ATTR)?.trim();
|
|
3896
|
-
if (!original || original === "none") return x + " " + y;
|
|
3897
|
-
const parts = splitTopLevelWhitespace(original);
|
|
3898
|
-
if (parts.length === 1) return "calc(" + parts[0] + " + " + x + ") " + y;
|
|
3899
|
-
if (parts.length >= 2) {
|
|
3900
|
-
const z = parts.length >= 3 ? " " + parts[2] : "";
|
|
3901
|
-
return "calc(" + parts[0] + " + " + x + ") calc(" + parts[1] + " + " + y + ")" + z;
|
|
3902
|
-
}
|
|
3903
|
-
return x + " " + y;
|
|
3904
|
-
};
|
|
3905
|
-
const isSimpleRotateAngle = (value) => /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
|
|
3906
|
-
const composeRotation = (element, rotationValue) => {
|
|
3907
|
-
const original = element.getAttribute(ORIGINAL_ROTATE_ATTR)?.trim();
|
|
3908
|
-
if (!original || original === "none" || !isSimpleRotateAngle(original)) return rotationValue;
|
|
3909
|
-
return "calc(" + original + " + " + rotationValue + ")";
|
|
3910
|
-
};
|
|
3911
|
-
let lastSeekTime = 0;
|
|
3912
|
-
let cachedMotionKey = "";
|
|
3913
|
-
const finiteNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
3914
|
-
const computeMotionKey = (motionEls) => {
|
|
3915
|
-
let key = "";
|
|
3916
|
-
for (let i = 0; i < motionEls.length; i++) {
|
|
3917
|
-
const json = motionEls[i].getAttribute?.(MOTION_ATTR);
|
|
3918
|
-
if (json) key += (key ? "\n" : "") + json;
|
|
3919
|
-
}
|
|
3920
|
-
return key;
|
|
3921
|
-
};
|
|
3922
|
-
const reapplyMotionTimeline = () => {
|
|
3923
|
-
const motionEls = document.querySelectorAll("[" + MOTION_ATTR + "]");
|
|
3924
|
-
if (motionEls.length === 0) {
|
|
3925
|
-
cachedMotionKey = "";
|
|
3926
|
-
return;
|
|
3927
|
-
}
|
|
3928
|
-
const win = window;
|
|
3929
|
-
const gsap = win.gsap;
|
|
3930
|
-
if (!gsap || typeof gsap.timeline !== "function") return;
|
|
3931
|
-
win.__timelines = win.__timelines || {};
|
|
3932
|
-
const motionKey = computeMotionKey(motionEls);
|
|
3933
|
-
const existing = win.__timelines[MOTION_TL_KEY];
|
|
3934
|
-
if (motionKey && motionKey === cachedMotionKey && existing && typeof existing.totalTime === "function") {
|
|
3935
|
-
existing.totalTime(lastSeekTime, false);
|
|
3936
|
-
return;
|
|
3937
|
-
}
|
|
3938
|
-
if (existing && typeof existing.kill === "function") existing.kill();
|
|
3939
|
-
const tl = gsap.timeline({ paused: true, defaults: { overwrite: "auto" } });
|
|
3940
|
-
const fromTo = tl.fromTo;
|
|
3941
|
-
if (typeof fromTo !== "function") return;
|
|
3942
|
-
let applied = 0;
|
|
3943
|
-
for (let i = 0; i < motionEls.length; i++) {
|
|
3944
|
-
const el = motionEls[i];
|
|
3945
|
-
if (!(el instanceof HTMLElement)) continue;
|
|
3946
|
-
const json = el.getAttribute(MOTION_ATTR);
|
|
3947
|
-
if (!json) continue;
|
|
3948
|
-
try {
|
|
3949
|
-
const m = JSON.parse(json);
|
|
3950
|
-
const start = finiteNum(m.start);
|
|
3951
|
-
const duration = finiteNum(m.duration);
|
|
3952
|
-
if (start == null || duration == null || duration <= 0) continue;
|
|
3953
|
-
const ease = typeof m.ease === "string" ? m.ease : "none";
|
|
3954
|
-
const from = m.from && typeof m.from === "object" ? m.from : {};
|
|
3955
|
-
const to = m.to && typeof m.to === "object" ? m.to : {};
|
|
3956
|
-
const customEase = m.customEase;
|
|
3957
|
-
let resolvedEase = ease;
|
|
3958
|
-
if (customEase?.id && customEase?.data && win.CustomEase?.create) {
|
|
3959
|
-
try {
|
|
3960
|
-
gsap.registerPlugin?.(win.CustomEase);
|
|
3961
|
-
win.CustomEase.create(customEase.id, customEase.data);
|
|
3962
|
-
resolvedEase = customEase.id;
|
|
3963
|
-
} catch {
|
|
3964
|
-
}
|
|
3965
|
-
}
|
|
3966
|
-
fromTo.call(
|
|
3967
|
-
tl,
|
|
3968
|
-
el,
|
|
3969
|
-
{ ...from },
|
|
3970
|
-
{ ...to, duration, ease: resolvedEase, overwrite: "auto", immediateRender: false },
|
|
3971
|
-
start
|
|
3972
|
-
);
|
|
3973
|
-
applied += 1;
|
|
3974
|
-
} catch {
|
|
3975
|
-
}
|
|
3976
|
-
}
|
|
3977
|
-
if (applied === 0) {
|
|
3978
|
-
cachedMotionKey = "";
|
|
3979
|
-
if (typeof tl.kill === "function")
|
|
3980
|
-
tl.kill();
|
|
3981
|
-
return;
|
|
3982
|
-
}
|
|
3983
|
-
cachedMotionKey = motionKey;
|
|
3984
|
-
win.__timelines[MOTION_TL_KEY] = tl;
|
|
3985
|
-
if (typeof tl.pause === "function") tl.pause();
|
|
3986
|
-
if (typeof tl.totalTime === "function")
|
|
3987
|
-
tl.totalTime(lastSeekTime, false);
|
|
3988
|
-
};
|
|
3989
|
-
const stripGsapTranslateFromTransform = (el) => {
|
|
3990
|
-
const transform = el.style.getPropertyValue("transform");
|
|
3991
|
-
if (!transform || transform === "none") return;
|
|
3992
|
-
const win = el.ownerDocument.defaultView;
|
|
3993
|
-
const MatrixCtor = win?.DOMMatrix;
|
|
3994
|
-
if (!MatrixCtor) return;
|
|
3995
|
-
try {
|
|
3996
|
-
const m = new MatrixCtor(transform);
|
|
3997
|
-
if (m.m41 === 0 && m.m42 === 0) return;
|
|
3998
|
-
m.m41 = 0;
|
|
3999
|
-
m.m42 = 0;
|
|
4000
|
-
if (m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1) {
|
|
4001
|
-
el.style.removeProperty("transform");
|
|
4002
|
-
} else {
|
|
4003
|
-
el.style.setProperty("transform", m.toString());
|
|
4004
|
-
}
|
|
4005
|
-
} catch {
|
|
4006
|
-
}
|
|
4007
|
-
};
|
|
4008
|
-
const reapplyAll = () => {
|
|
4009
|
-
const offsetEls = document.querySelectorAll("[" + PATH_OFFSET_ATTR + '="true"]');
|
|
4010
|
-
for (let i = 0; i < offsetEls.length; i++) {
|
|
4011
|
-
const el = offsetEls[i];
|
|
4012
|
-
if (!(el instanceof HTMLElement)) continue;
|
|
4013
|
-
const x = el.style.getPropertyValue(OFFSET_X_PROP);
|
|
4014
|
-
const y = el.style.getPropertyValue(OFFSET_Y_PROP);
|
|
4015
|
-
if (x || y) {
|
|
4016
|
-
el.style.setProperty(
|
|
4017
|
-
"translate",
|
|
4018
|
-
composeTranslate(
|
|
4019
|
-
el,
|
|
4020
|
-
"var(" + OFFSET_X_PROP + ", 0px)",
|
|
4021
|
-
"var(" + OFFSET_Y_PROP + ", 0px)"
|
|
4022
|
-
)
|
|
4023
|
-
);
|
|
4024
|
-
stripGsapTranslateFromTransform(el);
|
|
4025
|
-
}
|
|
4026
|
-
}
|
|
4027
|
-
const boxSizeEls = document.querySelectorAll("[" + BOX_SIZE_ATTR + '="true"]');
|
|
4028
|
-
for (let i = 0; i < boxSizeEls.length; i++) {
|
|
4029
|
-
const el = boxSizeEls[i];
|
|
4030
|
-
if (!(el instanceof HTMLElement)) continue;
|
|
4031
|
-
const w = el.style.getPropertyValue(WIDTH_PROP);
|
|
4032
|
-
const h = el.style.getPropertyValue(HEIGHT_PROP);
|
|
4033
|
-
if (w) el.style.setProperty("width", w);
|
|
4034
|
-
if (h) el.style.setProperty("height", h);
|
|
4035
|
-
}
|
|
4036
|
-
const rotEls = document.querySelectorAll("[" + ROTATION_ATTR + '="true"]');
|
|
4037
|
-
for (let i = 0; i < rotEls.length; i++) {
|
|
4038
|
-
const el = rotEls[i];
|
|
4039
|
-
if (!(el instanceof HTMLElement)) continue;
|
|
4040
|
-
const rot = el.style.getPropertyValue(ROTATION_PROP);
|
|
4041
|
-
if (rot) {
|
|
4042
|
-
el.style.setProperty("rotate", composeRotation(el, "var(" + ROTATION_PROP + ", 0deg)"));
|
|
4043
|
-
stripGsapTranslateFromTransform(el);
|
|
4044
|
-
}
|
|
4045
|
-
}
|
|
4046
|
-
reapplyMotionTimeline();
|
|
4047
|
-
};
|
|
4048
|
-
const runtimeWindow = window;
|
|
4049
|
-
const isWrapped = (fn) => Boolean(fn[WRAPPED_PROP]);
|
|
4050
|
-
const markWrapped = (fn) => {
|
|
4051
|
-
try {
|
|
4052
|
-
Object.defineProperty(fn, WRAPPED_PROP, {
|
|
4053
|
-
configurable: false,
|
|
4054
|
-
enumerable: false,
|
|
4055
|
-
value: true
|
|
4056
|
-
});
|
|
4057
|
-
} catch {
|
|
4058
|
-
try {
|
|
4059
|
-
fn[WRAPPED_PROP] = true;
|
|
4060
|
-
} catch {
|
|
4061
|
-
}
|
|
4062
|
-
}
|
|
4063
|
-
};
|
|
4064
|
-
const wrapFn = (get, set) => {
|
|
4065
|
-
const fn = get();
|
|
4066
|
-
if (typeof fn !== "function") return false;
|
|
4067
|
-
const seek = fn;
|
|
4068
|
-
if (isWrapped(seek)) {
|
|
4069
|
-
reapplyAll();
|
|
4070
|
-
return true;
|
|
4071
|
-
}
|
|
4072
|
-
const wrapped = function(time) {
|
|
4073
|
-
lastSeekTime = typeof time === "number" && Number.isFinite(time) ? Math.max(0, time) : 0;
|
|
4074
|
-
const result = seek.call(this, time);
|
|
4075
|
-
reapplyAll();
|
|
4076
|
-
return result;
|
|
4077
|
-
};
|
|
4078
|
-
markWrapped(wrapped);
|
|
4079
|
-
set(wrapped);
|
|
4080
|
-
reapplyAll();
|
|
4081
|
-
return true;
|
|
4082
|
-
};
|
|
4083
|
-
const wrapSeekFunctions = () => {
|
|
4084
|
-
const a = wrapFn(
|
|
4085
|
-
() => runtimeWindow.__hf?.["seek"],
|
|
4086
|
-
(fn) => {
|
|
4087
|
-
if (runtimeWindow.__hf) runtimeWindow.__hf["seek"] = fn;
|
|
4088
|
-
}
|
|
4089
|
-
);
|
|
4090
|
-
const b = wrapFn(
|
|
4091
|
-
() => runtimeWindow.__player?.["renderSeek"],
|
|
4092
|
-
(fn) => {
|
|
4093
|
-
if (runtimeWindow.__player) runtimeWindow.__player["renderSeek"] = fn;
|
|
4094
|
-
}
|
|
4095
|
-
);
|
|
4096
|
-
return a || b;
|
|
4097
|
-
};
|
|
4098
|
-
const installSeekTrap = (obj, key, getter, setter) => {
|
|
4099
|
-
if (!obj) return;
|
|
4100
|
-
try {
|
|
4101
|
-
let current = obj[key];
|
|
4102
|
-
Object.defineProperty(obj, key, {
|
|
4103
|
-
configurable: true,
|
|
4104
|
-
enumerable: true,
|
|
4105
|
-
get() {
|
|
4106
|
-
return current;
|
|
4107
|
-
},
|
|
4108
|
-
set(value) {
|
|
4109
|
-
current = value;
|
|
4110
|
-
if (typeof value === "function" && !isWrapped(value)) {
|
|
4111
|
-
wrapFn(getter, setter);
|
|
4112
|
-
}
|
|
4113
|
-
}
|
|
4114
|
-
});
|
|
4115
|
-
} catch {
|
|
4116
|
-
}
|
|
4117
|
-
};
|
|
4118
|
-
if (document.readyState === "loading") {
|
|
4119
|
-
document.addEventListener("DOMContentLoaded", () => reapplyAll(), { once: true });
|
|
4120
|
-
} else {
|
|
4121
|
-
reapplyAll();
|
|
4122
|
-
}
|
|
4123
|
-
wrapSeekFunctions();
|
|
4124
|
-
installSeekTrap(
|
|
4125
|
-
runtimeWindow.__hf,
|
|
4126
|
-
"seek",
|
|
4127
|
-
() => runtimeWindow.__hf?.["seek"],
|
|
4128
|
-
(fn) => {
|
|
4129
|
-
if (runtimeWindow.__hf) runtimeWindow.__hf["seek"] = fn;
|
|
4130
|
-
}
|
|
4131
|
-
);
|
|
4132
|
-
installSeekTrap(
|
|
4133
|
-
runtimeWindow.__player,
|
|
4134
|
-
"renderSeek",
|
|
4135
|
-
() => runtimeWindow.__player?.["renderSeek"],
|
|
4136
|
-
(fn) => {
|
|
4137
|
-
if (runtimeWindow.__player) runtimeWindow.__player["renderSeek"] = fn;
|
|
4138
|
-
}
|
|
4139
|
-
);
|
|
4140
|
-
let remaining = 120;
|
|
4141
|
-
const interval = setInterval(() => {
|
|
4142
|
-
wrapSeekFunctions();
|
|
4143
|
-
remaining -= 1;
|
|
4144
|
-
if (remaining <= 0) clearInterval(interval);
|
|
4145
|
-
}, 50);
|
|
4146
|
-
}
|
|
4147
|
-
function studioManualEditsRenderRuntime(manifestContent, activeCompositionPath) {
|
|
4148
|
-
const OFFSET_X_PROP = "--hf-studio-offset-x";
|
|
4149
|
-
const OFFSET_Y_PROP = "--hf-studio-offset-y";
|
|
4150
|
-
const WIDTH_PROP = "--hf-studio-width";
|
|
4151
|
-
const HEIGHT_PROP = "--hf-studio-height";
|
|
4152
|
-
const ROTATION_PROP = "--hf-studio-rotation";
|
|
4153
|
-
const PATH_OFFSET_ATTR = "data-hf-studio-path-offset";
|
|
4154
|
-
const BOX_SIZE_ATTR = "data-hf-studio-box-size";
|
|
4155
|
-
const ROTATION_ATTR = "data-hf-studio-rotation";
|
|
4156
|
-
const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate";
|
|
4157
|
-
const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate";
|
|
4158
|
-
const WRAPPED_SEEK_PROP = "__hfStudioManualEditsWrapped";
|
|
4159
|
-
const ROTATION_TRANSFORM_ORIGIN = "center center";
|
|
4160
|
-
const finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
4161
|
-
const objectRecord = (value) => value && typeof value === "object" ? value : null;
|
|
4162
|
-
const runtimeWindow = window;
|
|
4163
|
-
const parsedManifest = (() => {
|
|
4164
|
-
try {
|
|
4165
|
-
return objectRecord(JSON.parse(manifestContent));
|
|
4166
|
-
} catch {
|
|
4167
|
-
return null;
|
|
4168
|
-
}
|
|
4169
|
-
})();
|
|
4170
|
-
const manifestEdits = Array.isArray(parsedManifest?.edits) ? parsedManifest.edits : [];
|
|
4171
|
-
if (manifestEdits.length === 0) return;
|
|
4172
|
-
const sourceFileForElement = (element) => {
|
|
4173
|
-
let current = element;
|
|
4174
|
-
while (current) {
|
|
4175
|
-
const sourceFile = current.getAttribute("data-composition-file") ?? current.getAttribute("data-composition-src");
|
|
4176
|
-
if (sourceFile) return sourceFile;
|
|
4177
|
-
current = current.parentElement;
|
|
4178
|
-
}
|
|
4179
|
-
return activeCompositionPath ?? "index.html";
|
|
4180
|
-
};
|
|
4181
|
-
const elementMatchesSourceFile = (element, sourceFile) => sourceFileForElement(element) === sourceFile;
|
|
4182
|
-
const styleUsesStudioOffset = (value) => value.includes(OFFSET_X_PROP) || value.includes(OFFSET_Y_PROP);
|
|
4183
|
-
const styleUsesStudioRotation = (value) => value.includes(ROTATION_PROP);
|
|
4184
|
-
const splitTopLevelWhitespace = (value) => {
|
|
4185
|
-
const parts = [];
|
|
4186
|
-
let depth = 0;
|
|
4187
|
-
let current = "";
|
|
4188
|
-
for (const char of value.trim()) {
|
|
4189
|
-
if (char === "(") depth += 1;
|
|
4190
|
-
if (char === ")") depth = Math.max(0, depth - 1);
|
|
4191
|
-
if (/\s/.test(char) && depth === 0) {
|
|
4192
|
-
if (current) parts.push(current);
|
|
4193
|
-
current = "";
|
|
4194
|
-
} else {
|
|
4195
|
-
current += char;
|
|
4196
|
-
}
|
|
4197
|
-
}
|
|
4198
|
-
if (current) parts.push(current);
|
|
4199
|
-
return parts;
|
|
4200
|
-
};
|
|
4201
|
-
const composeTranslate = (element, x, y) => {
|
|
4202
|
-
const original = element.getAttribute(ORIGINAL_TRANSLATE_ATTR)?.trim();
|
|
4203
|
-
if (!original || original === "none") return `${x} ${y}`;
|
|
4204
|
-
const parts = splitTopLevelWhitespace(original);
|
|
4205
|
-
if (parts.length === 1) return `calc(${parts[0]} + ${x}) ${y}`;
|
|
4206
|
-
if (parts.length === 2) return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y})`;
|
|
4207
|
-
if (parts.length === 3) {
|
|
4208
|
-
return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y}) ${parts[2]}`;
|
|
4209
|
-
}
|
|
4210
|
-
return `${x} ${y}`;
|
|
4211
|
-
};
|
|
4212
|
-
const readStyleOrComputed = (element, property) => {
|
|
4213
|
-
try {
|
|
4214
|
-
return element.style.getPropertyValue(property) || getComputedStyle(element).getPropertyValue(property);
|
|
4215
|
-
} catch {
|
|
4216
|
-
return element.style.getPropertyValue(property);
|
|
4217
|
-
}
|
|
4218
|
-
};
|
|
4219
|
-
const readTransformLonghandBase = (element, property) => {
|
|
4220
|
-
const value = readStyleOrComputed(element, property).trim();
|
|
4221
|
-
return value === "none" ? "" : value;
|
|
4222
|
-
};
|
|
4223
|
-
const preparePathOffsetBase = (element) => {
|
|
4224
|
-
const currentTranslate = readTransformLonghandBase(element, "translate");
|
|
4225
|
-
const hasMarker = element.hasAttribute(PATH_OFFSET_ATTR);
|
|
4226
|
-
const wasResetByAnimation = !styleUsesStudioOffset(currentTranslate);
|
|
4227
|
-
if (!hasMarker) {
|
|
4228
|
-
element.setAttribute(ORIGINAL_TRANSLATE_ATTR, wasResetByAnimation ? currentTranslate : "");
|
|
4229
|
-
} else if (wasResetByAnimation) {
|
|
4230
|
-
element.setAttribute(ORIGINAL_TRANSLATE_ATTR, currentTranslate);
|
|
4231
|
-
}
|
|
4232
|
-
};
|
|
4233
|
-
const prepareRotationBase = (element) => {
|
|
4234
|
-
const currentRotate = readTransformLonghandBase(element, "rotate");
|
|
4235
|
-
const hasMarker = element.hasAttribute(ROTATION_ATTR);
|
|
4236
|
-
const wasResetByAnimation = !styleUsesStudioRotation(currentRotate);
|
|
4237
|
-
if (!hasMarker) {
|
|
4238
|
-
element.setAttribute(ORIGINAL_ROTATE_ATTR, wasResetByAnimation ? currentRotate : "");
|
|
4239
|
-
} else if (wasResetByAnimation) {
|
|
4240
|
-
element.setAttribute(ORIGINAL_ROTATE_ATTR, currentRotate);
|
|
4241
|
-
}
|
|
4242
|
-
};
|
|
4243
|
-
const querySelectorCandidates = (selector) => {
|
|
4244
|
-
const isCandidate = (element) => element instanceof HTMLElement;
|
|
4245
|
-
const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1];
|
|
4246
|
-
if (className) {
|
|
4247
|
-
return Array.from(document.getElementsByTagName("*")).filter(
|
|
4248
|
-
(element) => isCandidate(element) && element.classList.contains(className)
|
|
4249
|
-
);
|
|
4250
|
-
}
|
|
4251
|
-
if (/^[A-Za-z][A-Za-z0-9-]*$/.test(selector)) {
|
|
4252
|
-
return Array.from(document.getElementsByTagName(selector)).filter(isCandidate);
|
|
4253
|
-
}
|
|
4254
|
-
return Array.from(document.querySelectorAll(selector)).filter(isCandidate);
|
|
4255
|
-
};
|
|
4256
|
-
const resolveTarget = (edit) => {
|
|
4257
|
-
const targetRecord = objectRecord(edit.target);
|
|
4258
|
-
if (!targetRecord) return null;
|
|
4259
|
-
const sourceFile = typeof targetRecord.sourceFile === "string" ? targetRecord.sourceFile : "";
|
|
4260
|
-
if (!sourceFile) return null;
|
|
4261
|
-
const id = typeof targetRecord.id === "string" ? targetRecord.id : "";
|
|
4262
|
-
if (id) {
|
|
4263
|
-
const byId = document.getElementById(id);
|
|
4264
|
-
if (byId instanceof HTMLElement && elementMatchesSourceFile(byId, sourceFile)) return byId;
|
|
4265
|
-
const matchesById = [
|
|
4266
|
-
document.documentElement,
|
|
4267
|
-
...Array.from(document.getElementsByTagName("*"))
|
|
4268
|
-
].filter(
|
|
4269
|
-
(element) => element instanceof HTMLElement && element.id === id && elementMatchesSourceFile(element, sourceFile)
|
|
4270
|
-
);
|
|
4271
|
-
if (matchesById[0]) return matchesById[0];
|
|
4272
|
-
}
|
|
4273
|
-
const selector = typeof targetRecord.selector === "string" ? targetRecord.selector : "";
|
|
4274
|
-
if (!selector) return null;
|
|
4275
|
-
try {
|
|
4276
|
-
const matches = querySelectorCandidates(selector).filter(
|
|
4277
|
-
(element) => elementMatchesSourceFile(element, sourceFile)
|
|
4278
|
-
);
|
|
4279
|
-
const selectorIndex = finiteNumber(targetRecord.selectorIndex) ?? 0;
|
|
4280
|
-
return matches[Math.max(0, Math.floor(selectorIndex))] ?? null;
|
|
4281
|
-
} catch {
|
|
4282
|
-
return null;
|
|
4283
|
-
}
|
|
4284
|
-
};
|
|
4285
|
-
const roundRotationAngle = (angle) => Math.round(angle * 10) / 10;
|
|
4286
|
-
const isSimpleRotateAngle = (value) => /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
|
|
4287
|
-
const composeRotation = (element, rotationValue) => {
|
|
4288
|
-
const original = element.getAttribute(ORIGINAL_ROTATE_ATTR)?.trim();
|
|
4289
|
-
if (!original || original === "none" || !isSimpleRotateAngle(original)) {
|
|
4290
|
-
return rotationValue;
|
|
4291
|
-
}
|
|
4292
|
-
return `calc(${original} + ${rotationValue})`;
|
|
4293
|
-
};
|
|
4294
|
-
const applyPathOffset = (element, edit) => {
|
|
4295
|
-
const x = finiteNumber(edit.x);
|
|
4296
|
-
const y = finiteNumber(edit.y);
|
|
4297
|
-
if (x == null || y == null) return;
|
|
4298
|
-
preparePathOffsetBase(element);
|
|
4299
|
-
element.setAttribute(PATH_OFFSET_ATTR, "true");
|
|
4300
|
-
element.style.setProperty(OFFSET_X_PROP, `${Math.round(x)}px`);
|
|
4301
|
-
element.style.setProperty(OFFSET_Y_PROP, `${Math.round(y)}px`);
|
|
4302
|
-
element.style.setProperty(
|
|
4303
|
-
"translate",
|
|
4304
|
-
composeTranslate(element, `var(${OFFSET_X_PROP}, 0px)`, `var(${OFFSET_Y_PROP}, 0px)`)
|
|
4305
|
-
);
|
|
4306
|
-
};
|
|
4307
|
-
const readParentFlexBasisPixels = (element, size) => {
|
|
4308
|
-
const parent = element.parentElement;
|
|
4309
|
-
if (!parent) return null;
|
|
4310
|
-
const styles = getComputedStyle(parent);
|
|
4311
|
-
if (styles.display !== "flex" && styles.display !== "inline-flex") return null;
|
|
4312
|
-
return Math.round(
|
|
4313
|
-
Math.max(1, styles.flexDirection.startsWith("column") ? size.height : size.width)
|
|
4314
|
-
);
|
|
4315
|
-
};
|
|
4316
|
-
const applyBoxSize = (element, edit) => {
|
|
4317
|
-
const width = finiteNumber(edit.width);
|
|
4318
|
-
const height = finiteNumber(edit.height);
|
|
4319
|
-
if (width == null || height == null || width <= 0 || height <= 0) return;
|
|
4320
|
-
const rounded = {
|
|
4321
|
-
width: Math.round(Math.max(1, width)),
|
|
4322
|
-
height: Math.round(Math.max(1, height))
|
|
4323
|
-
};
|
|
4324
|
-
element.setAttribute(BOX_SIZE_ATTR, "true");
|
|
4325
|
-
element.style.setProperty(WIDTH_PROP, `${rounded.width}px`);
|
|
4326
|
-
element.style.setProperty(HEIGHT_PROP, `${rounded.height}px`);
|
|
4327
|
-
element.style.setProperty("box-sizing", "border-box");
|
|
4328
|
-
element.style.setProperty("width", `${rounded.width}px`);
|
|
4329
|
-
element.style.setProperty("height", `${rounded.height}px`);
|
|
4330
|
-
element.style.setProperty("min-width", "0px");
|
|
4331
|
-
element.style.setProperty("min-height", "0px");
|
|
4332
|
-
element.style.setProperty("max-width", "none");
|
|
4333
|
-
element.style.setProperty("max-height", "none");
|
|
4334
|
-
const flexBasis = readParentFlexBasisPixels(element, rounded);
|
|
4335
|
-
if (flexBasis != null) {
|
|
4336
|
-
element.style.setProperty("flex-basis", `${flexBasis}px`);
|
|
4337
|
-
element.style.setProperty("flex-grow", "0");
|
|
4338
|
-
element.style.setProperty("flex-shrink", "0");
|
|
4339
|
-
}
|
|
4340
|
-
if (getComputedStyle(element).display === "inline") {
|
|
4341
|
-
element.style.setProperty("display", "inline-block");
|
|
4342
|
-
}
|
|
4343
|
-
};
|
|
4344
|
-
const applyRotation = (element, edit) => {
|
|
4345
|
-
const angle = finiteNumber(edit.angle);
|
|
4346
|
-
if (angle == null) return;
|
|
4347
|
-
prepareRotationBase(element);
|
|
4348
|
-
element.setAttribute(ROTATION_ATTR, "true");
|
|
4349
|
-
element.style.setProperty(ROTATION_PROP, `${roundRotationAngle(angle)}deg`);
|
|
4350
|
-
element.style.setProperty("transform-origin", ROTATION_TRANSFORM_ORIGIN);
|
|
4351
|
-
element.style.setProperty("rotate", composeRotation(element, `var(${ROTATION_PROP}, 0deg)`));
|
|
4352
|
-
};
|
|
4353
|
-
const applyManifest = () => {
|
|
4354
|
-
let applied = 0;
|
|
4355
|
-
for (const edit of manifestEdits) {
|
|
4356
|
-
const editRecord = objectRecord(edit);
|
|
4357
|
-
if (!editRecord) continue;
|
|
4358
|
-
const element = resolveTarget(editRecord);
|
|
4359
|
-
if (!element) continue;
|
|
4360
|
-
if (editRecord.kind === "path-offset") applyPathOffset(element, editRecord);
|
|
4361
|
-
if (editRecord.kind === "box-size") applyBoxSize(element, editRecord);
|
|
4362
|
-
if (editRecord.kind === "rotation") applyRotation(element, editRecord);
|
|
4363
|
-
applied += 1;
|
|
4364
|
-
}
|
|
4365
|
-
return applied;
|
|
4366
|
-
};
|
|
4367
|
-
runtimeWindow.__hfStudioManualEditsApply = applyManifest;
|
|
4368
|
-
const markWrapped = (fn) => {
|
|
4369
|
-
try {
|
|
4370
|
-
Object.defineProperty(fn, WRAPPED_SEEK_PROP, {
|
|
4371
|
-
configurable: false,
|
|
4372
|
-
enumerable: false,
|
|
4373
|
-
value: true
|
|
4374
|
-
});
|
|
4375
|
-
} catch {
|
|
4376
|
-
try {
|
|
4377
|
-
fn[WRAPPED_SEEK_PROP] = true;
|
|
4378
|
-
} catch {
|
|
4379
|
-
}
|
|
4380
|
-
}
|
|
4381
|
-
};
|
|
4382
|
-
const isWrapped = (fn) => Boolean(fn[WRAPPED_SEEK_PROP]);
|
|
4383
|
-
const wrapFunction = (get, set) => {
|
|
4384
|
-
const fn = get();
|
|
4385
|
-
if (!fn) return false;
|
|
4386
|
-
const seek = fn;
|
|
4387
|
-
if (isWrapped(seek)) {
|
|
4388
|
-
applyManifest();
|
|
4389
|
-
return true;
|
|
4390
|
-
}
|
|
4391
|
-
const wrappedSeek = function(time) {
|
|
4392
|
-
const result = seek.call(this, time);
|
|
4393
|
-
applyManifest();
|
|
4394
|
-
return result;
|
|
4395
|
-
};
|
|
4396
|
-
markWrapped(wrappedSeek);
|
|
4397
|
-
set(wrappedSeek);
|
|
4398
|
-
applyManifest();
|
|
4399
|
-
return true;
|
|
4400
|
-
};
|
|
4401
|
-
const wrapSeekFunctions = () => {
|
|
4402
|
-
const wrappedHfSeek = wrapFunction(
|
|
4403
|
-
() => runtimeWindow.__hf?.seek,
|
|
4404
|
-
(fn) => {
|
|
4405
|
-
if (runtimeWindow.__hf) runtimeWindow.__hf.seek = fn;
|
|
4406
|
-
}
|
|
4407
|
-
);
|
|
4408
|
-
const wrappedPlayerRenderSeek = wrapFunction(
|
|
4409
|
-
() => runtimeWindow.__player?.renderSeek,
|
|
4410
|
-
(fn) => {
|
|
4411
|
-
if (runtimeWindow.__player) runtimeWindow.__player.renderSeek = fn;
|
|
4412
|
-
}
|
|
4413
|
-
);
|
|
4414
|
-
return wrappedHfSeek || wrappedPlayerRenderSeek;
|
|
4415
|
-
};
|
|
4416
|
-
if (document.readyState === "loading") {
|
|
4417
|
-
document.addEventListener("DOMContentLoaded", () => applyManifest(), { once: true });
|
|
4418
|
-
} else {
|
|
4419
|
-
applyManifest();
|
|
4420
|
-
}
|
|
4421
|
-
wrapSeekFunctions();
|
|
4422
|
-
let remainingSeekWrapAttempts = 120;
|
|
4423
|
-
const seekWrapInterval = setInterval(() => {
|
|
4424
|
-
wrapSeekFunctions();
|
|
4425
|
-
remainingSeekWrapAttempts -= 1;
|
|
4426
|
-
if (remainingSeekWrapAttempts <= 0) clearInterval(seekWrapInterval);
|
|
4427
|
-
}, 50);
|
|
4428
|
-
}
|
|
4429
|
-
|
|
4430
|
-
// src/routes/thumbnail.ts
|
|
4431
3326
|
var THUMBNAIL_CACHE_VERSION = "v4";
|
|
4432
3327
|
function registerThumbnailRoutes(api, adapter) {
|
|
4433
3328
|
api.get("/projects/:id/thumbnail/*", async (c) => {
|
|
@@ -4824,101 +3719,28 @@ function registerSelectionRoutes(api, adapter) {
|
|
|
4824
3719
|
// src/routes/media.ts
|
|
4825
3720
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
4826
3721
|
import { existsSync as existsSync9, mkdirSync as mkdirSync7 } from "fs";
|
|
4827
|
-
import { basename as basename2, dirname as dirname2, extname as
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
if (VIDEO_EXT2.has(ext)) return "video";
|
|
4841
|
-
if (IMAGE_EXT.has(ext)) return "image";
|
|
4842
|
-
if (AUDIO_EXT2.has(ext)) return "audio";
|
|
4843
|
-
return "unknown";
|
|
4844
|
-
}
|
|
4845
|
-
function colorLabel(input) {
|
|
4846
|
-
if (input.isHdr) {
|
|
4847
|
-
if (input.hdrTransfer === "pq") return "HDR PQ";
|
|
4848
|
-
if (input.hdrTransfer === "hlg") return "HDR HLG";
|
|
4849
|
-
return "HDR";
|
|
4850
|
-
}
|
|
4851
|
-
if (input.colorPrimaries.includes("bt709") || input.colorSpace.includes("bt709") || input.colorTransfer.includes("bt709")) {
|
|
4852
|
-
return "SDR Rec.709";
|
|
4853
|
-
}
|
|
4854
|
-
return "SDR/unknown";
|
|
4855
|
-
}
|
|
4856
|
-
function classifyMediaColor(stream) {
|
|
4857
|
-
const colorPrimaries = lower(stream?.color_primaries);
|
|
4858
|
-
const colorSpace = lower(stream?.color_space);
|
|
4859
|
-
const colorTransfer = lower(stream?.color_transfer);
|
|
4860
|
-
const isHdr = colorPrimaries.includes("bt2020") || colorSpace.includes("bt2020") || colorTransfer === "smpte2084" || colorTransfer === "arib-std-b67";
|
|
4861
|
-
const hdrTransfer = isHdr ? colorTransfer === "smpte2084" ? "pq" : colorTransfer === "arib-std-b67" ? "hlg" : "unknown" : null;
|
|
4862
|
-
return {
|
|
4863
|
-
dynamicRange: stream ? isHdr ? "hdr" : "sdr" : "unknown",
|
|
4864
|
-
hdrTransfer,
|
|
4865
|
-
label: stream ? colorLabel({ isHdr, hdrTransfer, colorPrimaries, colorSpace, colorTransfer }) : "Unknown",
|
|
4866
|
-
isHdr,
|
|
4867
|
-
codecName: stream?.codec_name,
|
|
4868
|
-
profile: stream?.profile,
|
|
4869
|
-
pixelFormat: stream?.pix_fmt,
|
|
4870
|
-
colorSpace: stream?.color_space,
|
|
4871
|
-
colorTransfer: stream?.color_transfer,
|
|
4872
|
-
colorPrimaries: stream?.color_primaries,
|
|
4873
|
-
bitsPerRawSample: stream?.bits_per_raw_sample
|
|
4874
|
-
};
|
|
4875
|
-
}
|
|
4876
|
-
function probeMediaMetadata(filePath, runner = spawnSync2) {
|
|
4877
|
-
const kind = inferKindFromPath(filePath);
|
|
4878
|
-
if (kind === "audio" || kind === "unknown") {
|
|
4879
|
-
return { kind, color: classifyMediaColor(null) };
|
|
4880
|
-
}
|
|
4881
|
-
const result = runner(
|
|
4882
|
-
"ffprobe",
|
|
4883
|
-
[
|
|
4884
|
-
"-v",
|
|
4885
|
-
"error",
|
|
4886
|
-
"-show_entries",
|
|
4887
|
-
"stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample",
|
|
4888
|
-
"-of",
|
|
4889
|
-
"json",
|
|
4890
|
-
filePath
|
|
4891
|
-
],
|
|
4892
|
-
{ timeout: 15e3, maxBuffer: 1024 * 1024 }
|
|
4893
|
-
);
|
|
4894
|
-
if (result.error?.code === "ENOENT") {
|
|
4895
|
-
return { kind, color: classifyMediaColor(null), probeError: "ffprobe unavailable" };
|
|
4896
|
-
}
|
|
4897
|
-
if (result.status !== 0) {
|
|
4898
|
-
return { kind, color: classifyMediaColor(null), probeError: "ffprobe failed" };
|
|
4899
|
-
}
|
|
4900
|
-
try {
|
|
4901
|
-
const parsed = JSON.parse(String(result.stdout || "{}"));
|
|
4902
|
-
const stream = parsed.streams?.find(
|
|
4903
|
-
(item) => kind === "image" ? item.codec_type === "video" : item.codec_type === kind
|
|
4904
|
-
);
|
|
4905
|
-
return { kind, color: classifyMediaColor(stream) };
|
|
4906
|
-
} catch {
|
|
4907
|
-
return { kind, color: classifyMediaColor(null), probeError: "ffprobe returned invalid json" };
|
|
4908
|
-
}
|
|
4909
|
-
}
|
|
4910
|
-
|
|
4911
|
-
// src/routes/media.ts
|
|
4912
|
-
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi"]);
|
|
3722
|
+
import { basename as basename2, dirname as dirname2, extname as extname2, join as join13 } from "path";
|
|
3723
|
+
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
3724
|
+
".mp4",
|
|
3725
|
+
".mov",
|
|
3726
|
+
".webm",
|
|
3727
|
+
".mkv",
|
|
3728
|
+
".avi",
|
|
3729
|
+
".m4v",
|
|
3730
|
+
".mxf",
|
|
3731
|
+
".mts",
|
|
3732
|
+
".m2ts",
|
|
3733
|
+
".ts"
|
|
3734
|
+
]);
|
|
4913
3735
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp"]);
|
|
4914
3736
|
var VIDEO_OUTPUT_EXTENSIONS = /* @__PURE__ */ new Set([".webm", ".mov"]);
|
|
4915
3737
|
var QUALITIES = /* @__PURE__ */ new Set(["fast", "balanced", "best"]);
|
|
4916
3738
|
var DEVICES = /* @__PURE__ */ new Set(["auto", "cpu", "coreml", "cuda"]);
|
|
4917
3739
|
function isVideoPath(path) {
|
|
4918
|
-
return VIDEO_EXTENSIONS.has(
|
|
3740
|
+
return VIDEO_EXTENSIONS.has(extname2(path).toLowerCase());
|
|
4919
3741
|
}
|
|
4920
3742
|
function isImagePath(path) {
|
|
4921
|
-
return IMAGE_EXTENSIONS.has(
|
|
3743
|
+
return IMAGE_EXTENSIONS.has(extname2(path).toLowerCase());
|
|
4922
3744
|
}
|
|
4923
3745
|
function normalizeProjectAssetPath(path) {
|
|
4924
3746
|
return path.trim().replace(/^[.]\//, "").replace(/[?#].*$/, "");
|
|
@@ -4927,11 +3749,11 @@ function containsNullByte(path) {
|
|
|
4927
3749
|
return path.includes("\0");
|
|
4928
3750
|
}
|
|
4929
3751
|
function slugFileBase(path) {
|
|
4930
|
-
const name = basename2(path,
|
|
3752
|
+
const name = basename2(path, extname2(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4931
3753
|
return name || "media";
|
|
4932
3754
|
}
|
|
4933
3755
|
function uniqueAssetPath(projectDir, assetPath) {
|
|
4934
|
-
const ext =
|
|
3756
|
+
const ext = extname2(assetPath);
|
|
4935
3757
|
const withoutExt = assetPath.slice(0, -ext.length);
|
|
4936
3758
|
let candidate = assetPath;
|
|
4937
3759
|
for (let index = 2; existsSync9(join13(projectDir, candidate)); index++) {
|
|
@@ -4986,7 +3808,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
4986
3808
|
const filePath = resolveWithinProject(project.dir, assetPath);
|
|
4987
3809
|
if (!filePath) return c.json({ error: "forbidden" }, 403);
|
|
4988
3810
|
if (!existsSync9(filePath)) return c.json({ error: "media not found" }, 404);
|
|
4989
|
-
return c.json({ path: assetPath, metadata: readMediaMetadata(filePath) });
|
|
3811
|
+
return c.json({ path: assetPath, metadata: await readMediaMetadata(filePath) });
|
|
4990
3812
|
});
|
|
4991
3813
|
api.post(
|
|
4992
3814
|
"/projects/:id/media/remove-background",
|
|
@@ -5023,10 +3845,10 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5023
3845
|
const outputAssetPath = requestedOutput ? uniqueAssetPath(project.dir, requestedOutput) : defaultOutputPath(project.dir, inputAssetPath);
|
|
5024
3846
|
const outputPath = resolveWithinProject(project.dir, outputAssetPath);
|
|
5025
3847
|
if (!outputPath) return c.json({ error: "forbidden" }, 403);
|
|
5026
|
-
if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(
|
|
3848
|
+
if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname2(outputAssetPath).toLowerCase())) {
|
|
5027
3849
|
return c.json({ error: "video background removal output must be .webm or .mov" }, 400);
|
|
5028
3850
|
}
|
|
5029
|
-
if (inputIsImage &&
|
|
3851
|
+
if (inputIsImage && extname2(outputAssetPath).toLowerCase() !== ".png") {
|
|
5030
3852
|
return c.json({ error: "image background removal output must be .png" }, 400);
|
|
5031
3853
|
}
|
|
5032
3854
|
let backgroundOutputAssetPath;
|
|
@@ -5142,29 +3964,6 @@ function createStudioApi(adapter) {
|
|
|
5142
3964
|
return api;
|
|
5143
3965
|
}
|
|
5144
3966
|
|
|
5145
|
-
// src/helpers/screenshotClip.ts
|
|
5146
|
-
function getElementScreenshotClip(selector, selectorIndex) {
|
|
5147
|
-
const matches = Array.from(document.querySelectorAll(selector)).filter(
|
|
5148
|
-
(el2) => el2 instanceof HTMLElement
|
|
5149
|
-
);
|
|
5150
|
-
const safeIndex = Math.max(0, Math.min(matches.length - 1, Math.floor(selectorIndex ?? 0)));
|
|
5151
|
-
const el = matches[safeIndex] ?? null;
|
|
5152
|
-
if (!(el instanceof HTMLElement)) return void 0;
|
|
5153
|
-
const rect = el.getBoundingClientRect();
|
|
5154
|
-
if (rect.width < 4 || rect.height < 4) return void 0;
|
|
5155
|
-
const pad = 8;
|
|
5156
|
-
const x = Math.max(0, rect.left - pad);
|
|
5157
|
-
const y = Math.max(0, rect.top - pad);
|
|
5158
|
-
const maxWidth = window.innerWidth - x;
|
|
5159
|
-
const maxHeight = window.innerHeight - y;
|
|
5160
|
-
return {
|
|
5161
|
-
x,
|
|
5162
|
-
y,
|
|
5163
|
-
width: Math.max(1, Math.min(rect.width + pad * 2, maxWidth)),
|
|
5164
|
-
height: Math.max(1, Math.min(rect.height + pad * 2, maxHeight))
|
|
5165
|
-
};
|
|
5166
|
-
}
|
|
5167
|
-
|
|
5168
3967
|
// src/helpers/backgroundRemovalJob.ts
|
|
5169
3968
|
function createBackgroundRemovalJob(opts, render) {
|
|
5170
3969
|
const state = {
|