@hyperframes/studio-server 0.7.59 → 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 +16 -202
- package/dist/index.js +334 -1392
- 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
|
|
|
@@ -288,9 +333,13 @@ function registerStoryboardRoutes(api, adapter) {
|
|
|
288
333
|
// src/routes/files.ts
|
|
289
334
|
import { bodyLimit } from "hono/body-limit";
|
|
290
335
|
import {
|
|
336
|
+
closeSync,
|
|
291
337
|
existsSync as existsSync3,
|
|
338
|
+
ftruncateSync,
|
|
339
|
+
openSync,
|
|
292
340
|
readFileSync as readFileSync4,
|
|
293
341
|
writeFileSync as writeFileSync4,
|
|
342
|
+
writeSync,
|
|
294
343
|
mkdirSync as mkdirSync3,
|
|
295
344
|
unlinkSync as unlinkSync2,
|
|
296
345
|
rmSync as rmSync2,
|
|
@@ -298,7 +347,7 @@ import {
|
|
|
298
347
|
renameSync,
|
|
299
348
|
readdirSync as readdirSync4
|
|
300
349
|
} from "fs";
|
|
301
|
-
import { resolve as
|
|
350
|
+
import { resolve as resolve2, dirname, join as join6 } from "path";
|
|
302
351
|
|
|
303
352
|
// src/helpers/mime.ts
|
|
304
353
|
var MIME_TYPES = {
|
|
@@ -315,7 +364,13 @@ var MIME_TYPES = {
|
|
|
315
364
|
".webp": "image/webp",
|
|
316
365
|
".ico": "image/x-icon",
|
|
317
366
|
".mp4": "video/mp4",
|
|
367
|
+
".m4v": "video/mp4",
|
|
318
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",
|
|
319
374
|
".webm": "video/webm",
|
|
320
375
|
".mp3": "audio/mpeg",
|
|
321
376
|
".wav": "audio/wav",
|
|
@@ -343,7 +398,8 @@ function isAudioFile(name) {
|
|
|
343
398
|
// src/helpers/waveform.ts
|
|
344
399
|
import { spawn } from "child_process";
|
|
345
400
|
import { existsSync as existsSync2, writeFileSync, mkdirSync } from "fs";
|
|
346
|
-
import { join as join3
|
|
401
|
+
import { join as join3 } from "path";
|
|
402
|
+
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
347
403
|
var SAMPLE_RATE = 4e3;
|
|
348
404
|
var PEAK_COUNT = 4e3;
|
|
349
405
|
var WAVEFORM_CACHE_VERSION = "v2";
|
|
@@ -366,15 +422,10 @@ function computePeaks(floats, count) {
|
|
|
366
422
|
const maxPeak = Math.max(...peaks, 1e-3);
|
|
367
423
|
return peaks.map((p) => p / maxPeak);
|
|
368
424
|
}
|
|
369
|
-
function ffmpegBinary() {
|
|
370
|
-
const configured = process.env.HYPERFRAMES_FFMPEG_PATH?.trim();
|
|
371
|
-
if (configured) return resolve2(configured);
|
|
372
|
-
return "ffmpeg";
|
|
373
|
-
}
|
|
374
425
|
function decodeAudioPeaks(audioPath) {
|
|
375
426
|
return new Promise((resolvePromise, reject) => {
|
|
376
427
|
const proc = spawn(
|
|
377
|
-
|
|
428
|
+
findFfBinary("ffmpeg") ?? "ffmpeg",
|
|
378
429
|
[
|
|
379
430
|
"-i",
|
|
380
431
|
audioPath,
|
|
@@ -426,7 +477,7 @@ import { spawnSync } from "child_process";
|
|
|
426
477
|
import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
427
478
|
import { tmpdir } from "os";
|
|
428
479
|
import { basename, join as join4 } from "path";
|
|
429
|
-
var VIDEO_EXT = /\.(mp4|webm|mov)$/i;
|
|
480
|
+
var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
|
|
430
481
|
var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
|
|
431
482
|
function validateUploadedMedia(filePath, runner = spawnSync) {
|
|
432
483
|
const isVideo = VIDEO_EXT.test(filePath);
|
|
@@ -544,29 +595,36 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
|
|
|
544
595
|
}
|
|
545
596
|
}
|
|
546
597
|
|
|
547
|
-
// src/helpers/
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
([key, item]) => findUnsafeMutationValues(item, `${path}.${key}`, options)
|
|
598
|
+
// src/helpers/fileVersion.ts
|
|
599
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
600
|
+
var RECEIPT_TTL_MS = 1e4;
|
|
601
|
+
var receipts = /* @__PURE__ */ new Map();
|
|
602
|
+
function fileContentVersion(content) {
|
|
603
|
+
return `"sha256:${createHash2("sha256").update(content, "utf8").digest("hex")}"`;
|
|
604
|
+
}
|
|
605
|
+
function createWriteToken(requestToken) {
|
|
606
|
+
const token = requestToken?.trim();
|
|
607
|
+
return token && token.length <= 200 ? token : randomUUID();
|
|
608
|
+
}
|
|
609
|
+
function recordFileWriteReceipt(absPath, receipt) {
|
|
610
|
+
const now = Date.now();
|
|
611
|
+
const current = (receipts.get(absPath) ?? []).filter(
|
|
612
|
+
(entry) => now - entry.recordedAt < RECEIPT_TTL_MS
|
|
563
613
|
);
|
|
614
|
+
current.push({ ...receipt, recordedAt: now });
|
|
615
|
+
receipts.set(absPath, current);
|
|
564
616
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
617
|
+
function consumeFileWriteReceipt(absPath) {
|
|
618
|
+
const now = Date.now();
|
|
619
|
+
const current = (receipts.get(absPath) ?? []).filter(
|
|
620
|
+
(entry) => now - entry.recordedAt < RECEIPT_TTL_MS
|
|
621
|
+
);
|
|
622
|
+
const receipt = current.shift() ?? null;
|
|
623
|
+
if (current.length > 0) receipts.set(absPath, current);
|
|
624
|
+
else receipts.delete(absPath);
|
|
625
|
+
if (!receipt) return null;
|
|
626
|
+
const { path, version, writeToken } = receipt;
|
|
627
|
+
return { path, version, writeToken };
|
|
570
628
|
}
|
|
571
629
|
|
|
572
630
|
// src/routes/files.ts
|
|
@@ -596,429 +654,7 @@ import {
|
|
|
596
654
|
scalePositionsInScript,
|
|
597
655
|
dedupePositionWritesInScript
|
|
598
656
|
} from "@hyperframes/parsers/gsap-writer-acorn";
|
|
599
|
-
|
|
600
|
-
// src/helpers/sourceMutation.ts
|
|
601
657
|
import { parseHTML } from "linkedom";
|
|
602
|
-
import postcss from "postcss";
|
|
603
|
-
import selectorParser from "postcss-selector-parser";
|
|
604
|
-
import { isAllowedHtmlAttribute, isSafeAttributeValue } from "@hyperframes/core/html-attr-safety";
|
|
605
|
-
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
|
606
|
-
|
|
607
|
-
// src/helpers/sourceStyleMutation.ts
|
|
608
|
-
function parseStyleDecls(style) {
|
|
609
|
-
const props = /* @__PURE__ */ new Map();
|
|
610
|
-
const order = [];
|
|
611
|
-
let i = 0;
|
|
612
|
-
while (i < style.length) {
|
|
613
|
-
let depth = 0;
|
|
614
|
-
let inSingle = false;
|
|
615
|
-
let inDouble = false;
|
|
616
|
-
const start = i;
|
|
617
|
-
while (i < style.length) {
|
|
618
|
-
const ch = style[i];
|
|
619
|
-
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
620
|
-
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
621
|
-
else if (!inSingle && !inDouble) {
|
|
622
|
-
if (ch === "(") depth++;
|
|
623
|
-
else if (ch === ")") depth = Math.max(0, depth - 1);
|
|
624
|
-
else if (ch === ";" && depth === 0) break;
|
|
625
|
-
}
|
|
626
|
-
i++;
|
|
627
|
-
}
|
|
628
|
-
const decl = style.slice(start, i).trim();
|
|
629
|
-
i++;
|
|
630
|
-
if (!decl) continue;
|
|
631
|
-
const colon = decl.indexOf(":");
|
|
632
|
-
if (colon < 0) continue;
|
|
633
|
-
const key = decl.slice(0, colon).trim();
|
|
634
|
-
const val = decl.slice(colon + 1).trim();
|
|
635
|
-
if (!key) continue;
|
|
636
|
-
if (!props.has(key)) order.push(key);
|
|
637
|
-
props.set(key, val);
|
|
638
|
-
}
|
|
639
|
-
return { props, order };
|
|
640
|
-
}
|
|
641
|
-
function serializeStyleDecls(props, order) {
|
|
642
|
-
return order.map((k) => `${k}: ${props.get(k) ?? ""}`).filter((d) => d.trim()).join("; ");
|
|
643
|
-
}
|
|
644
|
-
function patchStyleAttrString(style, property, value) {
|
|
645
|
-
const { props, order } = parseStyleDecls(style);
|
|
646
|
-
if (value === null) {
|
|
647
|
-
props.delete(property);
|
|
648
|
-
const idx = order.indexOf(property);
|
|
649
|
-
if (idx >= 0) order.splice(idx, 1);
|
|
650
|
-
} else {
|
|
651
|
-
if (!props.has(property)) order.push(property);
|
|
652
|
-
props.set(property, value);
|
|
653
|
-
}
|
|
654
|
-
return serializeStyleDecls(props, order);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// src/helpers/sourceMutation.ts
|
|
658
|
-
function parseSourceDocument(source) {
|
|
659
|
-
const hasDocumentShell = /<!doctype|<html[\s>]/i.test(source);
|
|
660
|
-
if (hasDocumentShell) {
|
|
661
|
-
return { document: parseHTML(source).document, wrappedFragment: false };
|
|
662
|
-
}
|
|
663
|
-
return {
|
|
664
|
-
document: parseHTML(`<!DOCTYPE html><html><head></head><body>${source}</body></html>`).document,
|
|
665
|
-
wrappedFragment: true
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
function duplicateCssRulesForId(document2, originalId, newId) {
|
|
669
|
-
const idToken = `#${originalId}`;
|
|
670
|
-
const transform = selectorParser((selectors) => {
|
|
671
|
-
selectors.walkIds((node) => {
|
|
672
|
-
if (node.value === originalId) node.value = newId;
|
|
673
|
-
});
|
|
674
|
-
});
|
|
675
|
-
for (const styleEl of document2.querySelectorAll("style")) {
|
|
676
|
-
const css = styleEl.textContent ?? "";
|
|
677
|
-
let root;
|
|
678
|
-
try {
|
|
679
|
-
root = postcss.parse(css);
|
|
680
|
-
} catch {
|
|
681
|
-
continue;
|
|
682
|
-
}
|
|
683
|
-
const clones = [];
|
|
684
|
-
root.walkRules((rule) => {
|
|
685
|
-
if (!rule.selector.includes(idToken)) return;
|
|
686
|
-
const newSelector = transform.processSync(rule.selector);
|
|
687
|
-
if (newSelector === rule.selector) return;
|
|
688
|
-
const clone = rule.clone({ selector: newSelector });
|
|
689
|
-
clones.push(clone);
|
|
690
|
-
});
|
|
691
|
-
if (clones.length > 0) {
|
|
692
|
-
for (const c of clones) root.append(c);
|
|
693
|
-
styleEl.textContent = root.toString();
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
function querySelectorAllWithTemplates(root, selector) {
|
|
698
|
-
const matches = Array.from(root.querySelectorAll(selector));
|
|
699
|
-
if (matches.length > 0) return matches;
|
|
700
|
-
const templates = Array.from(root.querySelectorAll("template"));
|
|
701
|
-
for (const tmpl of templates) {
|
|
702
|
-
const inner = querySelectorAllWithTemplates(tmpl, selector);
|
|
703
|
-
if (inner.length > 0) return inner;
|
|
704
|
-
}
|
|
705
|
-
return [];
|
|
706
|
-
}
|
|
707
|
-
function escapeCssAttrValue(value) {
|
|
708
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
709
|
-
}
|
|
710
|
-
function findByHfId(document2, hfId) {
|
|
711
|
-
try {
|
|
712
|
-
const matches = querySelectorAllWithTemplates(
|
|
713
|
-
document2,
|
|
714
|
-
`[data-hf-id="${escapeCssAttrValue(hfId)}"]`
|
|
715
|
-
);
|
|
716
|
-
if (matches.length > 1) {
|
|
717
|
-
console.warn(
|
|
718
|
-
`sourceMutation: data-hf-id "${hfId}" matched ${matches.length} elements; using the first. ids must be unique per document.`
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
return matches[0] ?? null;
|
|
722
|
-
} catch {
|
|
723
|
-
return null;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
function findTargetElement(document2, target) {
|
|
727
|
-
if (target.hfId) {
|
|
728
|
-
const el = findByHfId(document2, target.hfId);
|
|
729
|
-
if (el) return el;
|
|
730
|
-
}
|
|
731
|
-
if (target.id) {
|
|
732
|
-
const byId = document2.getElementById(target.id);
|
|
733
|
-
if (byId) return byId;
|
|
734
|
-
}
|
|
735
|
-
if (!target.selector) return null;
|
|
736
|
-
try {
|
|
737
|
-
const matches = querySelectorAllWithTemplates(document2, target.selector);
|
|
738
|
-
return matches[target.selectorIndex ?? 0] ?? null;
|
|
739
|
-
} catch {
|
|
740
|
-
return null;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
function removeElementFromHtml(source, target) {
|
|
744
|
-
const { document: document2, wrappedFragment } = parseSourceDocument(source);
|
|
745
|
-
const element = findTargetElement(document2, target);
|
|
746
|
-
if (!element) return source;
|
|
747
|
-
element.remove();
|
|
748
|
-
return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
|
|
749
|
-
}
|
|
750
|
-
function isHTMLElement(el) {
|
|
751
|
-
const HTMLEl = el.ownerDocument?.defaultView?.HTMLElement;
|
|
752
|
-
return HTMLEl ? el instanceof HTMLEl : el.nodeType === 1 && "style" in el;
|
|
753
|
-
}
|
|
754
|
-
function resolveOperationTarget(parent, op) {
|
|
755
|
-
if (op.childSelector === void 0) return parent;
|
|
756
|
-
try {
|
|
757
|
-
const child = parent.querySelectorAll(op.childSelector)[op.childIndex ?? 0] ?? null;
|
|
758
|
-
return child && isHTMLElement(child) ? child : null;
|
|
759
|
-
} catch {
|
|
760
|
-
return null;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
function patchElementInHtml(source, target, operations) {
|
|
764
|
-
const { document: document2, wrappedFragment } = parseSourceDocument(source);
|
|
765
|
-
const el = findTargetElement(document2, target);
|
|
766
|
-
if (!el || !isHTMLElement(el)) return { html: source, matched: false };
|
|
767
|
-
const htmlEl = el;
|
|
768
|
-
const resolved = [];
|
|
769
|
-
for (const op of operations) {
|
|
770
|
-
const opTarget = resolveOperationTarget(htmlEl, op);
|
|
771
|
-
if (!opTarget) return { html: source, matched: false };
|
|
772
|
-
resolved.push({ op, target: opTarget });
|
|
773
|
-
}
|
|
774
|
-
for (const { op, target: opTarget } of resolved) {
|
|
775
|
-
switch (op.type) {
|
|
776
|
-
case "inline-style":
|
|
777
|
-
{
|
|
778
|
-
const raw = opTarget.getAttribute("style") ?? "";
|
|
779
|
-
const patched = patchStyleAttrString(raw, op.property, op.value);
|
|
780
|
-
opTarget.setAttribute("style", patched);
|
|
781
|
-
}
|
|
782
|
-
break;
|
|
783
|
-
case "attribute":
|
|
784
|
-
{
|
|
785
|
-
const fullAttr = op.property.startsWith("data-") ? op.property : `data-${op.property}`;
|
|
786
|
-
if (op.value != null) {
|
|
787
|
-
opTarget.setAttribute(fullAttr, op.value);
|
|
788
|
-
} else {
|
|
789
|
-
opTarget.removeAttribute(fullAttr);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
break;
|
|
793
|
-
case "html-attribute":
|
|
794
|
-
if (!isAllowedHtmlAttribute(op.property)) break;
|
|
795
|
-
if (op.value != null) {
|
|
796
|
-
if (!isSafeAttributeValue(op.property, op.value)) break;
|
|
797
|
-
opTarget.setAttribute(op.property, op.value);
|
|
798
|
-
} else {
|
|
799
|
-
opTarget.removeAttribute(op.property);
|
|
800
|
-
}
|
|
801
|
-
break;
|
|
802
|
-
case "text-content":
|
|
803
|
-
if (op.value != null) {
|
|
804
|
-
const inner = opTarget.children.length === 1 ? opTarget.firstElementChild : null;
|
|
805
|
-
const textTarget = inner && isHTMLElement(inner) ? inner : opTarget;
|
|
806
|
-
textTarget.textContent = op.value;
|
|
807
|
-
}
|
|
808
|
-
break;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
return {
|
|
812
|
-
html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
|
|
813
|
-
matched: true
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
function probeElementInSource(source, target) {
|
|
817
|
-
if (!target.id && !target.hfId && !target.selector) return false;
|
|
818
|
-
const { document: document2 } = parseSourceDocument(source);
|
|
819
|
-
const el = findTargetElement(document2, target);
|
|
820
|
-
return el != null && isHTMLElement(el);
|
|
821
|
-
}
|
|
822
|
-
function resolveElementTiming(el) {
|
|
823
|
-
const start = parseFloat(el.getAttribute("data-start") ?? "0") || 0;
|
|
824
|
-
const usesDataEnd = el.hasAttribute("data-end");
|
|
825
|
-
const duration = usesDataEnd ? parseFloat(el.getAttribute("data-end") ?? "") - start || 0 : parseFloat(el.getAttribute("data-duration") ?? "0") || 0;
|
|
826
|
-
return { start, duration, usesDataEnd };
|
|
827
|
-
}
|
|
828
|
-
function setElementDuration(el, start, duration, usesDataEnd) {
|
|
829
|
-
if (usesDataEnd) {
|
|
830
|
-
const endTime = String(Math.round((start + duration) * 1e3) / 1e3);
|
|
831
|
-
el.setAttribute("data-end", endTime);
|
|
832
|
-
el.removeAttribute("data-duration");
|
|
833
|
-
} else {
|
|
834
|
-
el.setAttribute("data-duration", String(Math.round(duration * 1e3) / 1e3));
|
|
835
|
-
el.removeAttribute("data-end");
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
function splitElementInHtml(source, target, splitTime, newId, fallbackTiming) {
|
|
839
|
-
const { document: document2, wrappedFragment } = parseSourceDocument(source);
|
|
840
|
-
const el = findTargetElement(document2, target);
|
|
841
|
-
if (!el || !isHTMLElement(el)) return { html: source, matched: false, newId: null };
|
|
842
|
-
const timing = resolveElementTiming(el);
|
|
843
|
-
const { usesDataEnd } = timing;
|
|
844
|
-
let { start, duration } = timing;
|
|
845
|
-
if (duration <= 0 && fallbackTiming && fallbackTiming.duration > 0) {
|
|
846
|
-
start = fallbackTiming.start;
|
|
847
|
-
duration = fallbackTiming.duration;
|
|
848
|
-
}
|
|
849
|
-
if (duration <= 0 || splitTime <= start || splitTime >= start + duration) {
|
|
850
|
-
return { html: source, matched: false, newId: null };
|
|
851
|
-
}
|
|
852
|
-
if (document2.getElementById(newId)) {
|
|
853
|
-
let suffix = 2;
|
|
854
|
-
const base = newId;
|
|
855
|
-
while (document2.getElementById(newId)) {
|
|
856
|
-
newId = `${base}-${suffix++}`;
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
const firstDuration = splitTime - start;
|
|
860
|
-
const secondDuration = duration - firstDuration;
|
|
861
|
-
const clone = el.cloneNode(true);
|
|
862
|
-
if (!isHTMLElement(clone)) return { html: source, matched: false, newId: null };
|
|
863
|
-
clone.setAttribute("id", newId);
|
|
864
|
-
clone.removeAttribute("data-hf-id");
|
|
865
|
-
for (const node of clone.querySelectorAll("[data-hf-id]")) node.removeAttribute("data-hf-id");
|
|
866
|
-
clone.setAttribute("data-start", String(Math.round(splitTime * 1e3) / 1e3));
|
|
867
|
-
setElementDuration(clone, splitTime, secondDuration, usesDataEnd);
|
|
868
|
-
const playbackStartAttr = el.hasAttribute("data-playback-start") ? "data-playback-start" : el.hasAttribute("data-media-start") ? "data-media-start" : null;
|
|
869
|
-
if (playbackStartAttr) {
|
|
870
|
-
const currentTrim = parseFloat(el.getAttribute(playbackStartAttr) ?? "0") || 0;
|
|
871
|
-
const rateRaw = parseFloat(el.getAttribute("data-playback-rate") ?? "");
|
|
872
|
-
const rate = Number.isFinite(rateRaw) ? rateRaw : 1;
|
|
873
|
-
clone.setAttribute(
|
|
874
|
-
playbackStartAttr,
|
|
875
|
-
String(Math.round((currentTrim + firstDuration * rate) * 1e3) / 1e3)
|
|
876
|
-
);
|
|
877
|
-
}
|
|
878
|
-
const originalId = el.getAttribute("id");
|
|
879
|
-
if (originalId) {
|
|
880
|
-
duplicateCssRulesForId(document2, originalId, newId);
|
|
881
|
-
}
|
|
882
|
-
el.setAttribute("data-start", String(Math.round(start * 1e3) / 1e3));
|
|
883
|
-
setElementDuration(el, start, firstDuration, usesDataEnd);
|
|
884
|
-
if (el.nextSibling) {
|
|
885
|
-
el.parentElement.insertBefore(clone, el.nextSibling);
|
|
886
|
-
} else {
|
|
887
|
-
el.parentElement.appendChild(clone);
|
|
888
|
-
}
|
|
889
|
-
const html = wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
|
|
890
|
-
return {
|
|
891
|
-
// The split owns its new nodes' stable ids. Leaving the clone unstamped makes
|
|
892
|
-
// the next preview request persist different bytes after history is recorded.
|
|
893
|
-
html: ensureHfIds(html),
|
|
894
|
-
matched: true,
|
|
895
|
-
newId
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
function getInlineStylePx(el, property) {
|
|
899
|
-
const style = (isHTMLElement(el) ? el.getAttribute("style") : null) ?? "";
|
|
900
|
-
const { props } = parseStyleDecls(style);
|
|
901
|
-
const raw = props.get(property);
|
|
902
|
-
if (!raw) return 0;
|
|
903
|
-
const n = parseFloat(raw);
|
|
904
|
-
return Number.isFinite(n) ? n : 0;
|
|
905
|
-
}
|
|
906
|
-
function setInlineLeftTop(el, left, top) {
|
|
907
|
-
let style = el.getAttribute("style") ?? "";
|
|
908
|
-
style = patchStyleAttrString(style, "left", `${left}px`);
|
|
909
|
-
style = patchStyleAttrString(style, "top", `${top}px`);
|
|
910
|
-
el.setAttribute("style", style);
|
|
911
|
-
}
|
|
912
|
-
function uniqueGroupDomId(document2, groupId) {
|
|
913
|
-
const base = groupId.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "group";
|
|
914
|
-
let id = base;
|
|
915
|
-
let n = 2;
|
|
916
|
-
while (document2.getElementById(id)) {
|
|
917
|
-
id = `${base}-${n}`;
|
|
918
|
-
n += 1;
|
|
919
|
-
}
|
|
920
|
-
return id;
|
|
921
|
-
}
|
|
922
|
-
function wrapElementsInHtml(source, targets, groupId, bbox, rebases) {
|
|
923
|
-
const { document: document2, wrappedFragment } = parseSourceDocument(source);
|
|
924
|
-
if (targets.length === 0) {
|
|
925
|
-
return { html: source, matched: false, groupId: null, error: "no targets" };
|
|
926
|
-
}
|
|
927
|
-
const els = [];
|
|
928
|
-
const seen = /* @__PURE__ */ new Set();
|
|
929
|
-
for (const target of targets) {
|
|
930
|
-
const el = findTargetElement(document2, target);
|
|
931
|
-
if (!el || !isHTMLElement(el) || seen.has(el)) continue;
|
|
932
|
-
seen.add(el);
|
|
933
|
-
els.push(el);
|
|
934
|
-
}
|
|
935
|
-
if (els.length === 0) {
|
|
936
|
-
return { html: source, matched: false, groupId: null, error: "no targets matched" };
|
|
937
|
-
}
|
|
938
|
-
const parent = els[0]?.parentElement;
|
|
939
|
-
if (!parent || els.some((el) => el.parentElement !== parent)) {
|
|
940
|
-
return {
|
|
941
|
-
html: source,
|
|
942
|
-
matched: false,
|
|
943
|
-
groupId: null,
|
|
944
|
-
error: "grouped elements must share a single parent"
|
|
945
|
-
};
|
|
946
|
-
}
|
|
947
|
-
const memberSet = new Set(els);
|
|
948
|
-
const ordered = Array.from(parent.children).filter((c) => memberSet.has(c));
|
|
949
|
-
const rebaseByEl = /* @__PURE__ */ new Map();
|
|
950
|
-
for (const rebase of rebases) {
|
|
951
|
-
const el = findTargetElement(document2, rebase.target);
|
|
952
|
-
if (el) rebaseByEl.set(el, { left: rebase.left, top: rebase.top });
|
|
953
|
-
}
|
|
954
|
-
const wrapper = document2.createElement("div");
|
|
955
|
-
wrapper.setAttribute("data-hf-group", groupId);
|
|
956
|
-
wrapper.setAttribute("id", uniqueGroupDomId(document2, groupId));
|
|
957
|
-
const memberZIndexes = ordered.map(
|
|
958
|
-
(el) => Number.parseInt(
|
|
959
|
-
parseStyleDecls(el.getAttribute("style") ?? "").props.get("z-index") ?? "",
|
|
960
|
-
10
|
|
961
|
-
)
|
|
962
|
-
).filter((z) => Number.isFinite(z));
|
|
963
|
-
const maxZ = memberZIndexes.length > 0 ? Math.max(...memberZIndexes) : null;
|
|
964
|
-
wrapper.setAttribute(
|
|
965
|
-
"style",
|
|
966
|
-
`position: absolute; left: ${bbox.left}px; top: ${bbox.top}px; width: ${bbox.width}px; height: ${bbox.height}px` + (maxZ !== null ? `; z-index: ${maxZ}` : "")
|
|
967
|
-
);
|
|
968
|
-
parent.insertBefore(wrapper, ordered[ordered.length - 1] ?? null);
|
|
969
|
-
for (const el of ordered) {
|
|
970
|
-
const rebase = rebaseByEl.get(el);
|
|
971
|
-
if (rebase) setInlineLeftTop(el, rebase.left, rebase.top);
|
|
972
|
-
wrapper.appendChild(el);
|
|
973
|
-
}
|
|
974
|
-
return {
|
|
975
|
-
html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
|
|
976
|
-
matched: true,
|
|
977
|
-
groupId
|
|
978
|
-
};
|
|
979
|
-
}
|
|
980
|
-
function unwrapElementsFromHtml(source, groupTarget) {
|
|
981
|
-
const { document: document2, wrappedFragment } = parseSourceDocument(source);
|
|
982
|
-
const group = findTargetElement(document2, groupTarget);
|
|
983
|
-
if (!group || !isHTMLElement(group)) return { html: source, unwrapped: false };
|
|
984
|
-
if (!group.hasAttribute("data-hf-group")) return { html: source, unwrapped: false };
|
|
985
|
-
const parent = group.parentElement;
|
|
986
|
-
if (!parent) return { html: source, unwrapped: false };
|
|
987
|
-
const wLeft = getInlineStylePx(group, "left");
|
|
988
|
-
const wTop = getInlineStylePx(group, "top");
|
|
989
|
-
const groupCenter = {
|
|
990
|
-
cx: wLeft + getInlineStylePx(group, "width") / 2,
|
|
991
|
-
cy: wTop + getInlineStylePx(group, "height") / 2
|
|
992
|
-
};
|
|
993
|
-
const members = [];
|
|
994
|
-
for (const child of Array.from(group.children)) {
|
|
995
|
-
if (isHTMLElement(child)) {
|
|
996
|
-
const newLeft = getInlineStylePx(child, "left") + wLeft;
|
|
997
|
-
const newTop = getInlineStylePx(child, "top") + wTop;
|
|
998
|
-
setInlineLeftTop(child, newLeft, newTop);
|
|
999
|
-
if (child.id) {
|
|
1000
|
-
members.push({
|
|
1001
|
-
id: child.id,
|
|
1002
|
-
cx: newLeft + getInlineStylePx(child, "width") / 2,
|
|
1003
|
-
cy: newTop + getInlineStylePx(child, "height") / 2
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
parent.insertBefore(child, group);
|
|
1008
|
-
}
|
|
1009
|
-
const groupId = group.id || void 0;
|
|
1010
|
-
group.remove();
|
|
1011
|
-
return {
|
|
1012
|
-
html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
|
|
1013
|
-
unwrapped: true,
|
|
1014
|
-
unwrappedGroupId: groupId,
|
|
1015
|
-
members,
|
|
1016
|
-
groupCenter
|
|
1017
|
-
};
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// src/routes/files.ts
|
|
1021
|
-
import { parseHTML as parseHTML2 } from "linkedom";
|
|
1022
658
|
function isAcornGsapWriterEnabled() {
|
|
1023
659
|
const val = process.env["STUDIO_SDK_CUTOVER_ENABLED"];
|
|
1024
660
|
return val === "true" || val === "1";
|
|
@@ -1208,7 +844,7 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
1208
844
|
const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
|
|
1209
845
|
let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
|
|
1210
846
|
let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
|
|
1211
|
-
while (existsSync3(
|
|
847
|
+
while (existsSync3(resolve2(projectDir, candidate))) {
|
|
1212
848
|
num++;
|
|
1213
849
|
candidate = `${cleanBase} (copy ${num})${ext}`;
|
|
1214
850
|
}
|
|
@@ -1219,7 +855,7 @@ function walkFiles(dir, filter) {
|
|
|
1219
855
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
1220
856
|
const full = join6(dir, entry.name);
|
|
1221
857
|
if (entry.isDirectory()) {
|
|
1222
|
-
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")
|
|
1223
859
|
continue;
|
|
1224
860
|
results.push(...walkFiles(full, filter));
|
|
1225
861
|
} else if (filter(entry.name)) {
|
|
@@ -1246,10 +882,10 @@ function updateReferences(projectDir, oldPath, newPath) {
|
|
|
1246
882
|
return updatedCount;
|
|
1247
883
|
}
|
|
1248
884
|
function extractGsapScriptBlock(html) {
|
|
1249
|
-
const { document
|
|
885
|
+
const { document } = parseHTML(html);
|
|
1250
886
|
const scripts = [
|
|
1251
|
-
...
|
|
1252
|
-
...Array.from(
|
|
887
|
+
...document.querySelectorAll("script:not([src])"),
|
|
888
|
+
...Array.from(document.querySelectorAll("template")).flatMap(
|
|
1253
889
|
(tmpl) => Array.from(tmpl.querySelectorAll("script:not([src])"))
|
|
1254
890
|
)
|
|
1255
891
|
];
|
|
@@ -1258,10 +894,10 @@ function extractGsapScriptBlock(html) {
|
|
|
1258
894
|
if (content.includes("gsap.timeline") || content.includes(".set(") || content.includes(".to(")) {
|
|
1259
895
|
return {
|
|
1260
896
|
scriptText: content,
|
|
1261
|
-
document
|
|
897
|
+
document,
|
|
1262
898
|
replaceScript(newText) {
|
|
1263
899
|
script.textContent = newText;
|
|
1264
|
-
return
|
|
900
|
+
return document.toString();
|
|
1265
901
|
}
|
|
1266
902
|
};
|
|
1267
903
|
}
|
|
@@ -1359,11 +995,11 @@ function bakeGroupTransformIntoMembers(html, groupId, members, groupCenter) {
|
|
|
1359
995
|
}
|
|
1360
996
|
return block.replaceScript(script);
|
|
1361
997
|
}
|
|
1362
|
-
function stripStudioEditsFromTarget(
|
|
998
|
+
function stripStudioEditsFromTarget(document, selector) {
|
|
1363
999
|
if (!selector) return 0;
|
|
1364
1000
|
let stripped = 0;
|
|
1365
1001
|
try {
|
|
1366
|
-
for (const el of
|
|
1002
|
+
for (const el of document.querySelectorAll(selector)) {
|
|
1367
1003
|
if (!isHTMLElement(el)) continue;
|
|
1368
1004
|
const htmlEl = el;
|
|
1369
1005
|
let touched = false;
|
|
@@ -1433,11 +1069,11 @@ function resolveFinalOpacity(anim) {
|
|
|
1433
1069
|
const num = Number(raw);
|
|
1434
1070
|
return Number.isFinite(num) && num !== 0 ? num : null;
|
|
1435
1071
|
}
|
|
1436
|
-
function bakeVisibilityOnDelete(
|
|
1072
|
+
function bakeVisibilityOnDelete(document, anim) {
|
|
1437
1073
|
const opacity = resolveFinalOpacity(anim);
|
|
1438
1074
|
if (opacity === null) return;
|
|
1439
1075
|
try {
|
|
1440
|
-
for (const el of
|
|
1076
|
+
for (const el of document.querySelectorAll(anim.targetSelector)) {
|
|
1441
1077
|
if (isHTMLElement(el)) el.style.setProperty("opacity", String(opacity));
|
|
1442
1078
|
}
|
|
1443
1079
|
} catch {
|
|
@@ -1570,9 +1206,11 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1570
1206
|
after: newHtml,
|
|
1571
1207
|
scriptText: block.scriptText,
|
|
1572
1208
|
path: res.filePath,
|
|
1209
|
+
version: fileContentVersion(newHtml),
|
|
1573
1210
|
backupPath
|
|
1574
1211
|
};
|
|
1575
1212
|
if (skippedSelectors.size > 0) responsePayload.skippedSelectors = [...skippedSelectors];
|
|
1213
|
+
c.header("ETag", responsePayload.version);
|
|
1576
1214
|
return c.json(responsePayload);
|
|
1577
1215
|
}
|
|
1578
1216
|
function executeGsapMutationAcorn(body, block, respond) {
|
|
@@ -2188,7 +1826,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2188
1826
|
skipped.push(name);
|
|
2189
1827
|
continue;
|
|
2190
1828
|
}
|
|
2191
|
-
const destPath =
|
|
1829
|
+
const destPath = resolve2(targetDir, name);
|
|
2192
1830
|
if (!isSafePath(projectDir, destPath)) continue;
|
|
2193
1831
|
let finalPath = destPath;
|
|
2194
1832
|
let finalName = name;
|
|
@@ -2198,13 +1836,13 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2198
1836
|
const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
|
|
2199
1837
|
let n = 2;
|
|
2200
1838
|
const MAX_COPY_INDEX = 1e4;
|
|
2201
|
-
while (n < MAX_COPY_INDEX && existsSync3(
|
|
1839
|
+
while (n < MAX_COPY_INDEX && existsSync3(resolve2(targetDir, `${base} (${n})${ext}`))) n++;
|
|
2202
1840
|
if (n >= MAX_COPY_INDEX) {
|
|
2203
1841
|
skipped.push(name);
|
|
2204
1842
|
continue;
|
|
2205
1843
|
}
|
|
2206
1844
|
finalName = `${base} (${n})${ext}`;
|
|
2207
|
-
finalPath =
|
|
1845
|
+
finalPath = resolve2(targetDir, finalName);
|
|
2208
1846
|
}
|
|
2209
1847
|
const buffer = Buffer.from(await value.arrayBuffer());
|
|
2210
1848
|
const validation = validateUploadedMediaBuffer(finalName, buffer);
|
|
@@ -2233,19 +1871,111 @@ function registerFileRoutes(api, adapter) {
|
|
|
2233
1871
|
return c.json({ error: "not found" }, 404);
|
|
2234
1872
|
}
|
|
2235
1873
|
const content = readFileSync4(res.absPath, "utf-8");
|
|
2236
|
-
|
|
1874
|
+
const version = fileContentVersion(content);
|
|
1875
|
+
c.header("ETag", version);
|
|
1876
|
+
return c.json({ filename: res.filePath, content, version });
|
|
2237
1877
|
});
|
|
2238
1878
|
api.put("/projects/:id/files/*", async (c) => {
|
|
2239
1879
|
const res = await resolveProjectFile(c, adapter);
|
|
2240
1880
|
if ("error" in res) return res.error;
|
|
2241
|
-
ensureDir(res.absPath);
|
|
2242
1881
|
const body = await c.req.text();
|
|
2243
|
-
const
|
|
2244
|
-
|
|
2245
|
-
|
|
1882
|
+
const expectedVersion = c.req.header("If-Match")?.trim() ?? null;
|
|
1883
|
+
const createOnly = c.req.header("If-None-Match")?.trim() === "*";
|
|
1884
|
+
if (expectedVersion === null && !createOnly) {
|
|
1885
|
+
let currentContent = null;
|
|
1886
|
+
try {
|
|
1887
|
+
currentContent = readFileSync4(res.absPath, "utf-8");
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
1890
|
+
throw error;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
return c.json(
|
|
1894
|
+
{
|
|
1895
|
+
error: "precondition required",
|
|
1896
|
+
path: res.filePath,
|
|
1897
|
+
currentVersion: currentContent === null ? null : fileContentVersion(currentContent),
|
|
1898
|
+
currentContent
|
|
1899
|
+
},
|
|
1900
|
+
428
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
let backup = { backupPath: null };
|
|
1904
|
+
if (createOnly) {
|
|
1905
|
+
ensureDir(res.absPath);
|
|
1906
|
+
let fd;
|
|
1907
|
+
try {
|
|
1908
|
+
fd = openSync(res.absPath, "wx");
|
|
1909
|
+
} catch (error) {
|
|
1910
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
1911
|
+
throw error;
|
|
1912
|
+
}
|
|
1913
|
+
const currentContent = readFileSync4(res.absPath, "utf-8");
|
|
1914
|
+
return c.json(
|
|
1915
|
+
{
|
|
1916
|
+
error: "file conflict",
|
|
1917
|
+
path: res.filePath,
|
|
1918
|
+
currentVersion: fileContentVersion(currentContent),
|
|
1919
|
+
currentContent
|
|
1920
|
+
},
|
|
1921
|
+
409
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
try {
|
|
1925
|
+
writeSync(fd, body, 0, "utf-8");
|
|
1926
|
+
} finally {
|
|
1927
|
+
closeSync(fd);
|
|
1928
|
+
}
|
|
1929
|
+
} else {
|
|
1930
|
+
let fd;
|
|
1931
|
+
try {
|
|
1932
|
+
fd = openSync(res.absPath, "r+");
|
|
1933
|
+
} catch (error) {
|
|
1934
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
1935
|
+
throw error;
|
|
1936
|
+
}
|
|
1937
|
+
return c.json(
|
|
1938
|
+
{
|
|
1939
|
+
error: "file conflict",
|
|
1940
|
+
path: res.filePath,
|
|
1941
|
+
currentVersion: null,
|
|
1942
|
+
currentContent: null
|
|
1943
|
+
},
|
|
1944
|
+
409
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
try {
|
|
1948
|
+
const currentContent = readFileSync4(fd, "utf-8");
|
|
1949
|
+
const currentVersion = fileContentVersion(currentContent);
|
|
1950
|
+
if (expectedVersion !== currentVersion) {
|
|
1951
|
+
return c.json(
|
|
1952
|
+
{
|
|
1953
|
+
error: "file conflict",
|
|
1954
|
+
path: res.filePath,
|
|
1955
|
+
currentVersion,
|
|
1956
|
+
currentContent
|
|
1957
|
+
},
|
|
1958
|
+
409
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
backup = snapshotBeforeWrite(res.project.dir, res.absPath);
|
|
1962
|
+
if (backup.error)
|
|
1963
|
+
console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
|
|
1964
|
+
ftruncateSync(fd, 0);
|
|
1965
|
+
writeSync(fd, body, 0, "utf-8");
|
|
1966
|
+
} finally {
|
|
1967
|
+
closeSync(fd);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
const version = fileContentVersion(body);
|
|
1971
|
+
const writeToken = createWriteToken(c.req.header("X-Hyperframes-Write-Token"));
|
|
1972
|
+
recordFileWriteReceipt(res.absPath, { path: res.filePath, version, writeToken });
|
|
1973
|
+
c.header("ETag", version);
|
|
2246
1974
|
return c.json({
|
|
2247
1975
|
ok: true,
|
|
2248
1976
|
path: res.filePath,
|
|
1977
|
+
version,
|
|
1978
|
+
writeToken,
|
|
2249
1979
|
backupPath: backupPathForResponse(res.project.dir, backup.backupPath)
|
|
2250
1980
|
});
|
|
2251
1981
|
});
|
|
@@ -2317,17 +2047,28 @@ function registerFileRoutes(api, adapter) {
|
|
|
2317
2047
|
fallbackTiming
|
|
2318
2048
|
);
|
|
2319
2049
|
if (!result.matched) {
|
|
2320
|
-
|
|
2050
|
+
const version2 = fileContentVersion(originalContent);
|
|
2051
|
+
c.header("ETag", version2);
|
|
2052
|
+
return c.json({
|
|
2053
|
+
ok: false,
|
|
2054
|
+
changed: false,
|
|
2055
|
+
content: originalContent,
|
|
2056
|
+
path: ctx.filePath,
|
|
2057
|
+
version: version2
|
|
2058
|
+
});
|
|
2321
2059
|
}
|
|
2322
2060
|
const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
|
|
2323
2061
|
if (backup.error) console.warn(`Failed to create backup for ${ctx.filePath}: ${backup.error}`);
|
|
2324
2062
|
writeFileSync4(ctx.absPath, result.html, "utf-8");
|
|
2063
|
+
const version = fileContentVersion(result.html);
|
|
2064
|
+
c.header("ETag", version);
|
|
2325
2065
|
return c.json({
|
|
2326
2066
|
ok: true,
|
|
2327
2067
|
changed: true,
|
|
2328
2068
|
content: result.html,
|
|
2329
2069
|
newId: result.newId,
|
|
2330
2070
|
path: ctx.filePath,
|
|
2071
|
+
version,
|
|
2331
2072
|
backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
|
|
2332
2073
|
});
|
|
2333
2074
|
});
|
|
@@ -2650,13 +2391,13 @@ function registerFileRoutes(api, adapter) {
|
|
|
2650
2391
|
// src/routes/preview.ts
|
|
2651
2392
|
import { existsSync as existsSync5, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
2652
2393
|
import { join as join8 } from "path";
|
|
2653
|
-
import { createHash as
|
|
2394
|
+
import { createHash as createHash3 } from "crypto";
|
|
2654
2395
|
import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
|
|
2655
2396
|
|
|
2656
2397
|
// src/helpers/subComposition.ts
|
|
2657
2398
|
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
2658
2399
|
import { join as join7 } from "path";
|
|
2659
|
-
import { parseHTML as
|
|
2400
|
+
import { parseHTML as parseHTML2 } from "linkedom";
|
|
2660
2401
|
import {
|
|
2661
2402
|
rewriteAssetPaths,
|
|
2662
2403
|
rewriteCssAssetUrls,
|
|
@@ -2704,7 +2445,7 @@ function fixDigitLeadingIdSelectors(root) {
|
|
|
2704
2445
|
}
|
|
2705
2446
|
}
|
|
2706
2447
|
function extractFullDocumentParts(rawHtml, compPath) {
|
|
2707
|
-
const { document: doc } =
|
|
2448
|
+
const { document: doc } = parseHTML2(rawHtml);
|
|
2708
2449
|
const rewriteTargets = [doc.head, doc.body].filter(Boolean);
|
|
2709
2450
|
for (const target of rewriteTargets) {
|
|
2710
2451
|
rewriteRelativePaths(target, compPath);
|
|
@@ -2718,7 +2459,7 @@ function extractFullDocumentParts(rawHtml, compPath) {
|
|
|
2718
2459
|
return { headContent, bodyContent, htmlAttrs, bodyAttrs };
|
|
2719
2460
|
}
|
|
2720
2461
|
function extractTemplateInnerHtml(rawComp) {
|
|
2721
|
-
const { document: doc } =
|
|
2462
|
+
const { document: doc } = parseHTML2(rawComp);
|
|
2722
2463
|
const template = doc.querySelector("template");
|
|
2723
2464
|
return template ? template.innerHTML : null;
|
|
2724
2465
|
}
|
|
@@ -2770,7 +2511,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
|
|
|
2770
2511
|
let bodyAttrs = "";
|
|
2771
2512
|
const templateInner = extractTemplateInnerHtml(rawComp);
|
|
2772
2513
|
if (templateInner != null) {
|
|
2773
|
-
const { document: contentDoc } =
|
|
2514
|
+
const { document: contentDoc } = parseHTML2(
|
|
2774
2515
|
`<!DOCTYPE html><html><head></head><body>${templateInner}</body></html>`
|
|
2775
2516
|
);
|
|
2776
2517
|
rewriteRelativePaths(contentDoc, compPath);
|
|
@@ -2784,7 +2525,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
|
|
|
2784
2525
|
htmlAttrs = parts.htmlAttrs;
|
|
2785
2526
|
bodyAttrs = parts.bodyAttrs;
|
|
2786
2527
|
} else {
|
|
2787
|
-
const { document: contentDoc } =
|
|
2528
|
+
const { document: contentDoc } = parseHTML2(
|
|
2788
2529
|
`<!DOCTYPE html><html><head></head><body>${rawComp}</body></html>`
|
|
2789
2530
|
);
|
|
2790
2531
|
rewriteRelativePaths(contentDoc, compPath);
|
|
@@ -2829,210 +2570,23 @@ ${rewrittenContent}
|
|
|
2829
2570
|
</html>`;
|
|
2830
2571
|
}
|
|
2831
2572
|
|
|
2832
|
-
// src/helpers/studioMotionRenderScript.ts
|
|
2833
|
-
var STUDIO_MOTION_PATH = ".hyperframes/studio-motion.json";
|
|
2834
|
-
function hasStudioMotionEntries(manifestContent) {
|
|
2835
|
-
try {
|
|
2836
|
-
const parsed = JSON.parse(manifestContent);
|
|
2837
|
-
return Array.isArray(parsed.motions) && parsed.motions.length > 0;
|
|
2838
|
-
} catch {
|
|
2839
|
-
return false;
|
|
2840
|
-
}
|
|
2841
|
-
}
|
|
2842
|
-
function createStudioMotionRenderBodyScript(manifestContent, options = {}) {
|
|
2843
|
-
if (!manifestContent.trim() || !hasStudioMotionEntries(manifestContent)) return null;
|
|
2844
|
-
return `(${studioMotionRenderRuntime.toString()})(${JSON.stringify(manifestContent)}, ${JSON.stringify(options.activeCompositionPath ?? null)});`;
|
|
2845
|
-
}
|
|
2846
|
-
function studioMotionRenderRuntime(manifestContent, activeCompositionPath) {
|
|
2847
|
-
const STUDIO_MOTION_TIMELINE_ID = "studio-motion";
|
|
2848
|
-
const STUDIO_MOTION_ATTR = "data-hf-studio-motion";
|
|
2849
|
-
const ORIGINAL_TRANSFORM_ATTR = "data-hf-studio-motion-original-transform";
|
|
2850
|
-
const ORIGINAL_OPACITY_ATTR = "data-hf-studio-motion-original-opacity";
|
|
2851
|
-
const ORIGINAL_VISIBILITY_ATTR = "data-hf-studio-motion-original-visibility";
|
|
2852
|
-
const objectRecord = (value) => value && typeof value === "object" ? value : null;
|
|
2853
|
-
const finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
2854
|
-
const runtimeWindow = window;
|
|
2855
|
-
const parseMotionValues = (value) => {
|
|
2856
|
-
const record = objectRecord(value);
|
|
2857
|
-
if (!record) return null;
|
|
2858
|
-
const parsed = {};
|
|
2859
|
-
for (const key of ["x", "y", "scale", "rotation", "opacity", "autoAlpha"]) {
|
|
2860
|
-
const next = finiteNumber(record[key]);
|
|
2861
|
-
if (next != null) parsed[key] = next;
|
|
2862
|
-
}
|
|
2863
|
-
return Object.keys(parsed).length > 0 ? parsed : null;
|
|
2864
|
-
};
|
|
2865
|
-
const parseCustomEase = (value) => {
|
|
2866
|
-
const record = objectRecord(value);
|
|
2867
|
-
if (!record) return null;
|
|
2868
|
-
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
2869
|
-
const data = typeof record.data === "string" ? record.data.trim() : "";
|
|
2870
|
-
if (!id || !data) return null;
|
|
2871
|
-
return { id, data };
|
|
2872
|
-
};
|
|
2873
|
-
const parsedManifest = (() => {
|
|
2874
|
-
try {
|
|
2875
|
-
return objectRecord(JSON.parse(manifestContent));
|
|
2876
|
-
} catch {
|
|
2877
|
-
return null;
|
|
2878
|
-
}
|
|
2879
|
-
})();
|
|
2880
|
-
const manifestMotions = Array.isArray(parsedManifest?.motions) ? parsedManifest.motions : [];
|
|
2881
|
-
const sourceFileForElement = (element) => {
|
|
2882
|
-
let current = element;
|
|
2883
|
-
while (current) {
|
|
2884
|
-
const sourceFile = current.getAttribute("data-composition-file") ?? current.getAttribute("data-composition-src");
|
|
2885
|
-
if (sourceFile) return sourceFile;
|
|
2886
|
-
current = current.parentElement;
|
|
2887
|
-
}
|
|
2888
|
-
return activeCompositionPath ?? "index.html";
|
|
2889
|
-
};
|
|
2890
|
-
const elementMatchesSourceFile = (element, sourceFile) => sourceFileForElement(element) === sourceFile;
|
|
2891
|
-
const isHTMLElement2 = (element) => element instanceof HTMLElement;
|
|
2892
|
-
const querySelectorCandidates = (selector) => {
|
|
2893
|
-
const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1];
|
|
2894
|
-
if (className) {
|
|
2895
|
-
return Array.from(document.getElementsByTagName("*")).filter(
|
|
2896
|
-
(element) => isHTMLElement2(element) && element.classList.contains(className)
|
|
2897
|
-
);
|
|
2898
|
-
}
|
|
2899
|
-
if (/^[A-Za-z][A-Za-z0-9-]*$/.test(selector)) {
|
|
2900
|
-
return Array.from(document.getElementsByTagName(selector)).filter(isHTMLElement2);
|
|
2901
|
-
}
|
|
2902
|
-
return Array.from(document.querySelectorAll(selector)).filter(isHTMLElement2);
|
|
2903
|
-
};
|
|
2904
|
-
const resolveTarget = (targetRecord) => {
|
|
2905
|
-
const sourceFile = typeof targetRecord.sourceFile === "string" ? targetRecord.sourceFile : "";
|
|
2906
|
-
if (!sourceFile) return null;
|
|
2907
|
-
const id = typeof targetRecord.id === "string" ? targetRecord.id : "";
|
|
2908
|
-
if (id) {
|
|
2909
|
-
const byId = document.getElementById(id);
|
|
2910
|
-
if (isHTMLElement2(byId) && elementMatchesSourceFile(byId, sourceFile)) return byId;
|
|
2911
|
-
}
|
|
2912
|
-
const selector = typeof targetRecord.selector === "string" ? targetRecord.selector : "";
|
|
2913
|
-
if (!selector) return null;
|
|
2914
|
-
try {
|
|
2915
|
-
const selectorIndex = Math.max(0, Math.floor(finiteNumber(targetRecord.selectorIndex) ?? 0));
|
|
2916
|
-
return querySelectorCandidates(selector).filter(
|
|
2917
|
-
(element) => elementMatchesSourceFile(element, sourceFile)
|
|
2918
|
-
)[selectorIndex] ?? null;
|
|
2919
|
-
} catch {
|
|
2920
|
-
return null;
|
|
2921
|
-
}
|
|
2922
|
-
};
|
|
2923
|
-
const restoreElement = (element) => {
|
|
2924
|
-
runtimeWindow.gsap?.set?.(element, { clearProps: "transform,opacity,visibility" });
|
|
2925
|
-
element.style.transform = element.getAttribute(ORIGINAL_TRANSFORM_ATTR) ?? "";
|
|
2926
|
-
element.style.opacity = element.getAttribute(ORIGINAL_OPACITY_ATTR) ?? "";
|
|
2927
|
-
element.style.visibility = element.getAttribute(ORIGINAL_VISIBILITY_ATTR) ?? "";
|
|
2928
|
-
element.removeAttribute(STUDIO_MOTION_ATTR);
|
|
2929
|
-
element.removeAttribute(ORIGINAL_TRANSFORM_ATTR);
|
|
2930
|
-
element.removeAttribute(ORIGINAL_OPACITY_ATTR);
|
|
2931
|
-
element.removeAttribute(ORIGINAL_VISIBILITY_ATTR);
|
|
2932
|
-
};
|
|
2933
|
-
const restoreStudioMotionElements = () => {
|
|
2934
|
-
for (const element of Array.from(document.querySelectorAll(`[${STUDIO_MOTION_ATTR}]`))) {
|
|
2935
|
-
if (isHTMLElement2(element)) restoreElement(element);
|
|
2936
|
-
}
|
|
2937
|
-
};
|
|
2938
|
-
const readCurrentTime = () => {
|
|
2939
|
-
try {
|
|
2940
|
-
const playerTime = runtimeWindow.__player?.getTime?.();
|
|
2941
|
-
if (typeof playerTime === "number" && Number.isFinite(playerTime)) {
|
|
2942
|
-
return Math.max(0, playerTime);
|
|
2943
|
-
}
|
|
2944
|
-
} catch {
|
|
2945
|
-
}
|
|
2946
|
-
try {
|
|
2947
|
-
const timelineTime = runtimeWindow.__timeline?.time?.();
|
|
2948
|
-
if (typeof timelineTime === "number" && Number.isFinite(timelineTime)) {
|
|
2949
|
-
return Math.max(0, timelineTime);
|
|
2950
|
-
}
|
|
2951
|
-
} catch {
|
|
2952
|
-
}
|
|
2953
|
-
return 0;
|
|
2954
|
-
};
|
|
2955
|
-
const resolveEase = (motion) => {
|
|
2956
|
-
const fallback = typeof motion.ease === "string" && motion.ease.trim() ? motion.ease.trim() : "none";
|
|
2957
|
-
const customEase = parseCustomEase(motion.customEase);
|
|
2958
|
-
const customEasePlugin = runtimeWindow.CustomEase;
|
|
2959
|
-
if (!customEase || typeof customEasePlugin?.create !== "function") return fallback;
|
|
2960
|
-
try {
|
|
2961
|
-
runtimeWindow.gsap?.registerPlugin?.(customEasePlugin);
|
|
2962
|
-
customEasePlugin.create(customEase.id, customEase.data);
|
|
2963
|
-
return customEase.id;
|
|
2964
|
-
} catch {
|
|
2965
|
-
return fallback;
|
|
2966
|
-
}
|
|
2967
|
-
};
|
|
2968
|
-
const applyManifest = () => {
|
|
2969
|
-
runtimeWindow.__timelines = runtimeWindow.__timelines ?? {};
|
|
2970
|
-
runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID]?.kill?.();
|
|
2971
|
-
delete runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID];
|
|
2972
|
-
restoreStudioMotionElements();
|
|
2973
|
-
const gsap = runtimeWindow.gsap;
|
|
2974
|
-
if (!gsap?.timeline || manifestMotions.length === 0) return 0;
|
|
2975
|
-
const timeline = gsap.timeline({ paused: true, defaults: { overwrite: "auto" } });
|
|
2976
|
-
let applied = 0;
|
|
2977
|
-
for (const motionValue of manifestMotions) {
|
|
2978
|
-
const motion = objectRecord(motionValue);
|
|
2979
|
-
if (!motion || motion.kind !== "gsap-motion") continue;
|
|
2980
|
-
const targetRecord = objectRecord(motion.target);
|
|
2981
|
-
if (!targetRecord) continue;
|
|
2982
|
-
const target = resolveTarget(targetRecord);
|
|
2983
|
-
if (!target || typeof timeline.fromTo !== "function") continue;
|
|
2984
|
-
const start = finiteNumber(motion.start);
|
|
2985
|
-
const duration = finiteNumber(motion.duration);
|
|
2986
|
-
if (start == null || duration == null || start < 0 || duration <= 0) continue;
|
|
2987
|
-
const from = parseMotionValues(motion.from);
|
|
2988
|
-
const to = parseMotionValues(motion.to);
|
|
2989
|
-
if (!from || !to) continue;
|
|
2990
|
-
if (!target.hasAttribute(STUDIO_MOTION_ATTR)) {
|
|
2991
|
-
target.setAttribute(ORIGINAL_TRANSFORM_ATTR, target.style.transform);
|
|
2992
|
-
target.setAttribute(ORIGINAL_OPACITY_ATTR, target.style.opacity);
|
|
2993
|
-
target.setAttribute(ORIGINAL_VISIBILITY_ATTR, target.style.visibility);
|
|
2994
|
-
}
|
|
2995
|
-
target.setAttribute(STUDIO_MOTION_ATTR, "true");
|
|
2996
|
-
timeline.fromTo(
|
|
2997
|
-
target,
|
|
2998
|
-
from,
|
|
2999
|
-
{ ...to, duration, ease: resolveEase(motion), overwrite: "auto", immediateRender: false },
|
|
3000
|
-
start
|
|
3001
|
-
);
|
|
3002
|
-
applied += 1;
|
|
3003
|
-
}
|
|
3004
|
-
if (applied === 0) {
|
|
3005
|
-
timeline.kill?.();
|
|
3006
|
-
return 0;
|
|
3007
|
-
}
|
|
3008
|
-
runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID] = timeline;
|
|
3009
|
-
timeline.pause?.();
|
|
3010
|
-
const currentTime = readCurrentTime();
|
|
3011
|
-
if (typeof timeline.totalTime === "function") timeline.totalTime(currentTime, false);
|
|
3012
|
-
else timeline.time?.(currentTime);
|
|
3013
|
-
return applied;
|
|
3014
|
-
};
|
|
3015
|
-
runtimeWindow.__hfStudioMotionApply = applyManifest;
|
|
3016
|
-
applyManifest();
|
|
3017
|
-
}
|
|
3018
|
-
|
|
3019
2573
|
// src/routes/preview.ts
|
|
3020
|
-
import { ensureHfIds as
|
|
2574
|
+
import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
|
|
3021
2575
|
|
|
3022
2576
|
// src/helpers/hfIdPersist.ts
|
|
3023
|
-
import { ensureHfIds
|
|
2577
|
+
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
|
3024
2578
|
import {
|
|
3025
|
-
closeSync,
|
|
2579
|
+
closeSync as closeSync2,
|
|
3026
2580
|
constants,
|
|
3027
2581
|
fstatSync,
|
|
3028
|
-
ftruncateSync,
|
|
3029
|
-
openSync,
|
|
2582
|
+
ftruncateSync as ftruncateSync2,
|
|
2583
|
+
openSync as openSync2,
|
|
3030
2584
|
readFileSync as readFileSync6,
|
|
3031
2585
|
writeFileSync as writeFileSync5,
|
|
3032
|
-
writeSync
|
|
2586
|
+
writeSync as writeSync2
|
|
3033
2587
|
} from "fs";
|
|
3034
2588
|
function persistHfIdsIfNeeded(filePath, html) {
|
|
3035
|
-
const normalized =
|
|
2589
|
+
const normalized = ensureHfIds(html);
|
|
3036
2590
|
const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3037
2591
|
const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3038
2592
|
if (idsAfter > idsBefore) {
|
|
@@ -3050,7 +2604,7 @@ function persistHfIdsIfNeeded(filePath, html) {
|
|
|
3050
2604
|
function openNoFollow(filePath, flags) {
|
|
3051
2605
|
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
3052
2606
|
try {
|
|
3053
|
-
return
|
|
2607
|
+
return openSync2(filePath, flags | noFollow);
|
|
3054
2608
|
} catch {
|
|
3055
2609
|
return null;
|
|
3056
2610
|
}
|
|
@@ -3066,19 +2620,19 @@ function stampFileHfIds(filePath) {
|
|
|
3066
2620
|
try {
|
|
3067
2621
|
if (!fstatSync(fd).isFile()) return null;
|
|
3068
2622
|
const html = readFileSync6(fd, "utf-8");
|
|
3069
|
-
const normalized =
|
|
2623
|
+
const normalized = ensureHfIds(html);
|
|
3070
2624
|
const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3071
2625
|
const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3072
2626
|
if (writable && idsAfter > idsBefore) {
|
|
3073
|
-
|
|
3074
|
-
|
|
2627
|
+
ftruncateSync2(fd, 0);
|
|
2628
|
+
writeSync2(fd, normalized, 0, "utf-8");
|
|
3075
2629
|
}
|
|
3076
2630
|
return normalized;
|
|
3077
2631
|
} catch (err) {
|
|
3078
2632
|
console.warn("[hyperframes] stampFileHfIds: failed to stamp ids:", err);
|
|
3079
2633
|
return null;
|
|
3080
2634
|
} finally {
|
|
3081
|
-
|
|
2635
|
+
closeSync2(fd);
|
|
3082
2636
|
}
|
|
3083
2637
|
}
|
|
3084
2638
|
|
|
@@ -3234,7 +2788,7 @@ function parsePreviewVariablesParam(raw) {
|
|
|
3234
2788
|
}
|
|
3235
2789
|
function variablesEtagSalt(raw) {
|
|
3236
2790
|
if (!raw) return "";
|
|
3237
|
-
return `:vars:${
|
|
2791
|
+
return `:vars:${createHash3("sha1").update(raw).digest("hex").slice(0, 12)}`;
|
|
3238
2792
|
}
|
|
3239
2793
|
function previewVariablesFromRequest(rawVariables) {
|
|
3240
2794
|
const parse = parsePreviewVariablesParam(rawVariables);
|
|
@@ -3268,11 +2822,17 @@ async function transformPreviewHtml(html, adapter, project, activeCompositionPat
|
|
|
3268
2822
|
function resolveProjectMainHtml(projectDir, projectId) {
|
|
3269
2823
|
const indexPath = join8(projectDir, "index.html");
|
|
3270
2824
|
if (existsSync5(indexPath)) {
|
|
3271
|
-
return {
|
|
2825
|
+
return {
|
|
2826
|
+
html: readFileSync7(indexPath, "utf-8"),
|
|
2827
|
+
compositionPath: "index.html"
|
|
2828
|
+
};
|
|
3272
2829
|
}
|
|
3273
2830
|
const blockHtmlPath = join8(projectDir, `${projectId}.html`);
|
|
3274
2831
|
if (existsSync5(blockHtmlPath)) {
|
|
3275
|
-
return {
|
|
2832
|
+
return {
|
|
2833
|
+
html: readFileSync7(blockHtmlPath, "utf-8"),
|
|
2834
|
+
compositionPath: `${projectId}.html`
|
|
2835
|
+
};
|
|
3276
2836
|
}
|
|
3277
2837
|
return null;
|
|
3278
2838
|
}
|
|
@@ -3281,6 +2841,7 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3281
2841
|
"Cache-Control": "private, no-cache",
|
|
3282
2842
|
ETag: etag
|
|
3283
2843
|
});
|
|
2844
|
+
const mediaCodecProbeCache = resolvePreviewMediaCodecProbeCache(adapter);
|
|
3284
2845
|
api.get("/projects/:id/preview", async (c) => {
|
|
3285
2846
|
const resolved = await resolveProjectAndSignature(adapter, c.req.param("id"));
|
|
3286
2847
|
if (!resolved) return c.json({ error: "not found" }, 404);
|
|
@@ -3291,7 +2852,10 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3291
2852
|
const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}"`;
|
|
3292
2853
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3293
2854
|
if (ifNoneMatch === etag) {
|
|
3294
|
-
return new Response(null, {
|
|
2855
|
+
return new Response(null, {
|
|
2856
|
+
status: 304,
|
|
2857
|
+
headers: previewCacheHeaders(etag)
|
|
2858
|
+
});
|
|
3295
2859
|
}
|
|
3296
2860
|
const diskMain = resolveProjectMainHtml(project.dir, project.id);
|
|
3297
2861
|
const normalizedDisk = diskMain ? persistHfIdsIfNeeded(join8(project.dir, diskMain.compositionPath), diskMain.html) : null;
|
|
@@ -3314,12 +2878,19 @@ ${runtimeTag}`;
|
|
|
3314
2878
|
bundled = bundled.replace(/<head>/i, `<head><base href="${baseHref}">`);
|
|
3315
2879
|
}
|
|
3316
2880
|
bundled = injectStudioPreviewAugmentations(
|
|
3317
|
-
|
|
2881
|
+
ensureHfIds2(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
|
|
3318
2882
|
adapter,
|
|
3319
2883
|
project.dir,
|
|
3320
2884
|
mainCompositionPath
|
|
3321
2885
|
);
|
|
3322
2886
|
if (previewVariables) bundled = injectPreviewVariables(bundled, previewVariables);
|
|
2887
|
+
bundled = await injectMediaCodecMap(
|
|
2888
|
+
bundled,
|
|
2889
|
+
adapter,
|
|
2890
|
+
project.dir,
|
|
2891
|
+
mainCompositionPath,
|
|
2892
|
+
mediaCodecProbeCache
|
|
2893
|
+
);
|
|
3323
2894
|
return c.html(bundled, 200, previewCacheHeaders(etag));
|
|
3324
2895
|
} catch {
|
|
3325
2896
|
const fallback = resolveProjectMainHtml(project.dir, project.id);
|
|
@@ -3337,6 +2908,13 @@ ${runtimeTag}`;
|
|
|
3337
2908
|
if (previewVariables) {
|
|
3338
2909
|
fallbackAugmented = injectPreviewVariables(fallbackAugmented, previewVariables);
|
|
3339
2910
|
}
|
|
2911
|
+
fallbackAugmented = await injectMediaCodecMap(
|
|
2912
|
+
fallbackAugmented,
|
|
2913
|
+
adapter,
|
|
2914
|
+
project.dir,
|
|
2915
|
+
fallback.compositionPath,
|
|
2916
|
+
mediaCodecProbeCache
|
|
2917
|
+
);
|
|
3340
2918
|
return c.html(fallbackAugmented, 200, previewCacheHeaders(etag));
|
|
3341
2919
|
}
|
|
3342
2920
|
return c.text("not found", 404);
|
|
@@ -3363,7 +2941,10 @@ ${runtimeTag}`;
|
|
|
3363
2941
|
const etag = `"comp:v2:${compPath}:${signature}${variablesEtagSalt(vars.raw)}"`;
|
|
3364
2942
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3365
2943
|
if (ifNoneMatch === etag) {
|
|
3366
|
-
return new Response(null, {
|
|
2944
|
+
return new Response(null, {
|
|
2945
|
+
status: 304,
|
|
2946
|
+
headers: previewCacheHeaders(etag)
|
|
2947
|
+
});
|
|
3367
2948
|
}
|
|
3368
2949
|
const stamped = pinSubCompHfIds(compFile, compPath);
|
|
3369
2950
|
if (stamped === null) return c.text("not found", 404);
|
|
@@ -3376,9 +2957,10 @@ ${runtimeTag}`;
|
|
|
3376
2957
|
stamped
|
|
3377
2958
|
);
|
|
3378
2959
|
if (!html) return c.text("not found", 404);
|
|
3379
|
-
html =
|
|
2960
|
+
html = ensureHfIds2(await transformPreviewHtml(html, adapter, project, compPath));
|
|
3380
2961
|
html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath);
|
|
3381
2962
|
if (previewVariables) html = injectPreviewVariables(html, previewVariables);
|
|
2963
|
+
html = await injectMediaCodecMap(html, adapter, project.dir, compPath, mediaCodecProbeCache);
|
|
3382
2964
|
return c.html(html, 200, previewCacheHeaders(etag));
|
|
3383
2965
|
});
|
|
3384
2966
|
api.get("/projects/:id/preview/*", async (c) => {
|
|
@@ -3397,15 +2979,49 @@ ${runtimeTag}`;
|
|
|
3397
2979
|
}
|
|
3398
2980
|
const contentType = getMimeType(subPath);
|
|
3399
2981
|
const isText = /\.(html|css|js|json|svg|txt|md|cube)$/i.test(subPath);
|
|
3400
|
-
const
|
|
3401
|
-
|
|
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
|
+
};
|
|
3402
3004
|
if (!isText) {
|
|
3403
3005
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3404
3006
|
if (ifNoneMatch === etag) {
|
|
3405
3007
|
return new Response(null, { status: 304, headers: cacheHeaders });
|
|
3406
3008
|
}
|
|
3407
3009
|
}
|
|
3408
|
-
|
|
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);
|
|
3409
3025
|
const totalSize = buffer.length;
|
|
3410
3026
|
const rangeHeader = c.req.header("Range");
|
|
3411
3027
|
if (rangeHeader) {
|
|
@@ -3419,7 +3035,7 @@ ${runtimeTag}`;
|
|
|
3419
3035
|
status: 206,
|
|
3420
3036
|
headers: {
|
|
3421
3037
|
...cacheHeaders,
|
|
3422
|
-
"Content-Type":
|
|
3038
|
+
"Content-Type": servedContentType,
|
|
3423
3039
|
"Content-Range": `bytes ${start}-${safeEnd}/${totalSize}`,
|
|
3424
3040
|
"Accept-Ranges": "bytes",
|
|
3425
3041
|
"Content-Length": String(chunkSize)
|
|
@@ -3430,7 +3046,7 @@ ${runtimeTag}`;
|
|
|
3430
3046
|
return new Response(new Uint8Array(buffer), {
|
|
3431
3047
|
headers: {
|
|
3432
3048
|
...cacheHeaders,
|
|
3433
|
-
"Content-Type":
|
|
3049
|
+
"Content-Type": servedContentType,
|
|
3434
3050
|
"Accept-Ranges": "bytes",
|
|
3435
3051
|
"Content-Length": String(totalSize)
|
|
3436
3052
|
}
|
|
@@ -3706,587 +3322,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3706
3322
|
// src/routes/thumbnail.ts
|
|
3707
3323
|
import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
|
|
3708
3324
|
import { join as join11 } from "path";
|
|
3709
|
-
import { createHash as
|
|
3710
|
-
|
|
3711
|
-
// src/helpers/manualEditsRenderScript.ts
|
|
3712
|
-
var STUDIO_MANUAL_EDITS_PATH = ".hyperframes/studio-manual-edits.json";
|
|
3713
|
-
function createStudioManualEditsRenderBodyScript(manifestContent, options = {}) {
|
|
3714
|
-
if (!manifestContent.trim()) return null;
|
|
3715
|
-
return `(${studioManualEditsRenderRuntime.toString()})(${JSON.stringify(manifestContent)}, ${JSON.stringify(options.activeCompositionPath ?? null)});`;
|
|
3716
|
-
}
|
|
3717
|
-
function createStudioPositionSeekReapplyScript() {
|
|
3718
|
-
return `(${studioPositionSeekReapplyRuntime.toString()})();`;
|
|
3719
|
-
}
|
|
3720
|
-
function studioPositionSeekReapplyRuntime() {
|
|
3721
|
-
const OFFSET_X_PROP = "--hf-studio-offset-x";
|
|
3722
|
-
const OFFSET_Y_PROP = "--hf-studio-offset-y";
|
|
3723
|
-
const WIDTH_PROP = "--hf-studio-width";
|
|
3724
|
-
const HEIGHT_PROP = "--hf-studio-height";
|
|
3725
|
-
const ROTATION_PROP = "--hf-studio-rotation";
|
|
3726
|
-
const PATH_OFFSET_ATTR = "data-hf-studio-path-offset";
|
|
3727
|
-
const BOX_SIZE_ATTR = "data-hf-studio-box-size";
|
|
3728
|
-
const ROTATION_ATTR = "data-hf-studio-rotation";
|
|
3729
|
-
const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate";
|
|
3730
|
-
const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate";
|
|
3731
|
-
const MOTION_ATTR = "data-hf-studio-motion";
|
|
3732
|
-
const MOTION_TL_KEY = "studio-motion";
|
|
3733
|
-
const WRAPPED_PROP = "__hfStudioPositionSeekReapplyWrapped";
|
|
3734
|
-
if (!document.querySelector("[" + PATH_OFFSET_ATTR + '="true"]') && !document.querySelector("[" + BOX_SIZE_ATTR + '="true"]') && !document.querySelector("[" + ROTATION_ATTR + '="true"]') && !document.querySelector("[" + MOTION_ATTR + "]"))
|
|
3735
|
-
return;
|
|
3736
|
-
const splitTopLevelWhitespace = (value) => {
|
|
3737
|
-
const parts = [];
|
|
3738
|
-
let depth = 0;
|
|
3739
|
-
let current = "";
|
|
3740
|
-
for (const char of value.trim()) {
|
|
3741
|
-
if (char === "(") depth += 1;
|
|
3742
|
-
if (char === ")") depth = Math.max(0, depth - 1);
|
|
3743
|
-
if (/\s/.test(char) && depth === 0) {
|
|
3744
|
-
if (current) parts.push(current);
|
|
3745
|
-
current = "";
|
|
3746
|
-
} else {
|
|
3747
|
-
current += char;
|
|
3748
|
-
}
|
|
3749
|
-
}
|
|
3750
|
-
if (current) parts.push(current);
|
|
3751
|
-
return parts;
|
|
3752
|
-
};
|
|
3753
|
-
const composeTranslate = (element, x, y) => {
|
|
3754
|
-
const original = element.getAttribute(ORIGINAL_TRANSLATE_ATTR)?.trim();
|
|
3755
|
-
if (!original || original === "none") return x + " " + y;
|
|
3756
|
-
const parts = splitTopLevelWhitespace(original);
|
|
3757
|
-
if (parts.length === 1) return "calc(" + parts[0] + " + " + x + ") " + y;
|
|
3758
|
-
if (parts.length >= 2) {
|
|
3759
|
-
const z = parts.length >= 3 ? " " + parts[2] : "";
|
|
3760
|
-
return "calc(" + parts[0] + " + " + x + ") calc(" + parts[1] + " + " + y + ")" + z;
|
|
3761
|
-
}
|
|
3762
|
-
return x + " " + y;
|
|
3763
|
-
};
|
|
3764
|
-
const isSimpleRotateAngle = (value) => /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
|
|
3765
|
-
const composeRotation = (element, rotationValue) => {
|
|
3766
|
-
const original = element.getAttribute(ORIGINAL_ROTATE_ATTR)?.trim();
|
|
3767
|
-
if (!original || original === "none" || !isSimpleRotateAngle(original)) return rotationValue;
|
|
3768
|
-
return "calc(" + original + " + " + rotationValue + ")";
|
|
3769
|
-
};
|
|
3770
|
-
let lastSeekTime = 0;
|
|
3771
|
-
let cachedMotionKey = "";
|
|
3772
|
-
const finiteNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
3773
|
-
const computeMotionKey = (motionEls) => {
|
|
3774
|
-
let key = "";
|
|
3775
|
-
for (let i = 0; i < motionEls.length; i++) {
|
|
3776
|
-
const json = motionEls[i].getAttribute?.(MOTION_ATTR);
|
|
3777
|
-
if (json) key += (key ? "\n" : "") + json;
|
|
3778
|
-
}
|
|
3779
|
-
return key;
|
|
3780
|
-
};
|
|
3781
|
-
const reapplyMotionTimeline = () => {
|
|
3782
|
-
const motionEls = document.querySelectorAll("[" + MOTION_ATTR + "]");
|
|
3783
|
-
if (motionEls.length === 0) {
|
|
3784
|
-
cachedMotionKey = "";
|
|
3785
|
-
return;
|
|
3786
|
-
}
|
|
3787
|
-
const win = window;
|
|
3788
|
-
const gsap = win.gsap;
|
|
3789
|
-
if (!gsap || typeof gsap.timeline !== "function") return;
|
|
3790
|
-
win.__timelines = win.__timelines || {};
|
|
3791
|
-
const motionKey = computeMotionKey(motionEls);
|
|
3792
|
-
const existing = win.__timelines[MOTION_TL_KEY];
|
|
3793
|
-
if (motionKey && motionKey === cachedMotionKey && existing && typeof existing.totalTime === "function") {
|
|
3794
|
-
existing.totalTime(lastSeekTime, false);
|
|
3795
|
-
return;
|
|
3796
|
-
}
|
|
3797
|
-
if (existing && typeof existing.kill === "function") existing.kill();
|
|
3798
|
-
const tl = gsap.timeline({ paused: true, defaults: { overwrite: "auto" } });
|
|
3799
|
-
const fromTo = tl.fromTo;
|
|
3800
|
-
if (typeof fromTo !== "function") return;
|
|
3801
|
-
let applied = 0;
|
|
3802
|
-
for (let i = 0; i < motionEls.length; i++) {
|
|
3803
|
-
const el = motionEls[i];
|
|
3804
|
-
if (!(el instanceof HTMLElement)) continue;
|
|
3805
|
-
const json = el.getAttribute(MOTION_ATTR);
|
|
3806
|
-
if (!json) continue;
|
|
3807
|
-
try {
|
|
3808
|
-
const m = JSON.parse(json);
|
|
3809
|
-
const start = finiteNum(m.start);
|
|
3810
|
-
const duration = finiteNum(m.duration);
|
|
3811
|
-
if (start == null || duration == null || duration <= 0) continue;
|
|
3812
|
-
const ease = typeof m.ease === "string" ? m.ease : "none";
|
|
3813
|
-
const from = m.from && typeof m.from === "object" ? m.from : {};
|
|
3814
|
-
const to = m.to && typeof m.to === "object" ? m.to : {};
|
|
3815
|
-
const customEase = m.customEase;
|
|
3816
|
-
let resolvedEase = ease;
|
|
3817
|
-
if (customEase?.id && customEase?.data && win.CustomEase?.create) {
|
|
3818
|
-
try {
|
|
3819
|
-
gsap.registerPlugin?.(win.CustomEase);
|
|
3820
|
-
win.CustomEase.create(customEase.id, customEase.data);
|
|
3821
|
-
resolvedEase = customEase.id;
|
|
3822
|
-
} catch {
|
|
3823
|
-
}
|
|
3824
|
-
}
|
|
3825
|
-
fromTo.call(
|
|
3826
|
-
tl,
|
|
3827
|
-
el,
|
|
3828
|
-
{ ...from },
|
|
3829
|
-
{ ...to, duration, ease: resolvedEase, overwrite: "auto", immediateRender: false },
|
|
3830
|
-
start
|
|
3831
|
-
);
|
|
3832
|
-
applied += 1;
|
|
3833
|
-
} catch {
|
|
3834
|
-
}
|
|
3835
|
-
}
|
|
3836
|
-
if (applied === 0) {
|
|
3837
|
-
cachedMotionKey = "";
|
|
3838
|
-
if (typeof tl.kill === "function")
|
|
3839
|
-
tl.kill();
|
|
3840
|
-
return;
|
|
3841
|
-
}
|
|
3842
|
-
cachedMotionKey = motionKey;
|
|
3843
|
-
win.__timelines[MOTION_TL_KEY] = tl;
|
|
3844
|
-
if (typeof tl.pause === "function") tl.pause();
|
|
3845
|
-
if (typeof tl.totalTime === "function")
|
|
3846
|
-
tl.totalTime(lastSeekTime, false);
|
|
3847
|
-
};
|
|
3848
|
-
const stripGsapTranslateFromTransform = (el) => {
|
|
3849
|
-
const transform = el.style.getPropertyValue("transform");
|
|
3850
|
-
if (!transform || transform === "none") return;
|
|
3851
|
-
const win = el.ownerDocument.defaultView;
|
|
3852
|
-
const MatrixCtor = win?.DOMMatrix;
|
|
3853
|
-
if (!MatrixCtor) return;
|
|
3854
|
-
try {
|
|
3855
|
-
const m = new MatrixCtor(transform);
|
|
3856
|
-
if (m.m41 === 0 && m.m42 === 0) return;
|
|
3857
|
-
m.m41 = 0;
|
|
3858
|
-
m.m42 = 0;
|
|
3859
|
-
if (m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1) {
|
|
3860
|
-
el.style.removeProperty("transform");
|
|
3861
|
-
} else {
|
|
3862
|
-
el.style.setProperty("transform", m.toString());
|
|
3863
|
-
}
|
|
3864
|
-
} catch {
|
|
3865
|
-
}
|
|
3866
|
-
};
|
|
3867
|
-
const reapplyAll = () => {
|
|
3868
|
-
const offsetEls = document.querySelectorAll("[" + PATH_OFFSET_ATTR + '="true"]');
|
|
3869
|
-
for (let i = 0; i < offsetEls.length; i++) {
|
|
3870
|
-
const el = offsetEls[i];
|
|
3871
|
-
if (!(el instanceof HTMLElement)) continue;
|
|
3872
|
-
const x = el.style.getPropertyValue(OFFSET_X_PROP);
|
|
3873
|
-
const y = el.style.getPropertyValue(OFFSET_Y_PROP);
|
|
3874
|
-
if (x || y) {
|
|
3875
|
-
el.style.setProperty(
|
|
3876
|
-
"translate",
|
|
3877
|
-
composeTranslate(
|
|
3878
|
-
el,
|
|
3879
|
-
"var(" + OFFSET_X_PROP + ", 0px)",
|
|
3880
|
-
"var(" + OFFSET_Y_PROP + ", 0px)"
|
|
3881
|
-
)
|
|
3882
|
-
);
|
|
3883
|
-
stripGsapTranslateFromTransform(el);
|
|
3884
|
-
}
|
|
3885
|
-
}
|
|
3886
|
-
const boxSizeEls = document.querySelectorAll("[" + BOX_SIZE_ATTR + '="true"]');
|
|
3887
|
-
for (let i = 0; i < boxSizeEls.length; i++) {
|
|
3888
|
-
const el = boxSizeEls[i];
|
|
3889
|
-
if (!(el instanceof HTMLElement)) continue;
|
|
3890
|
-
const w = el.style.getPropertyValue(WIDTH_PROP);
|
|
3891
|
-
const h = el.style.getPropertyValue(HEIGHT_PROP);
|
|
3892
|
-
if (w) el.style.setProperty("width", w);
|
|
3893
|
-
if (h) el.style.setProperty("height", h);
|
|
3894
|
-
}
|
|
3895
|
-
const rotEls = document.querySelectorAll("[" + ROTATION_ATTR + '="true"]');
|
|
3896
|
-
for (let i = 0; i < rotEls.length; i++) {
|
|
3897
|
-
const el = rotEls[i];
|
|
3898
|
-
if (!(el instanceof HTMLElement)) continue;
|
|
3899
|
-
const rot = el.style.getPropertyValue(ROTATION_PROP);
|
|
3900
|
-
if (rot) {
|
|
3901
|
-
el.style.setProperty("rotate", composeRotation(el, "var(" + ROTATION_PROP + ", 0deg)"));
|
|
3902
|
-
stripGsapTranslateFromTransform(el);
|
|
3903
|
-
}
|
|
3904
|
-
}
|
|
3905
|
-
reapplyMotionTimeline();
|
|
3906
|
-
};
|
|
3907
|
-
const runtimeWindow = window;
|
|
3908
|
-
const isWrapped = (fn) => Boolean(fn[WRAPPED_PROP]);
|
|
3909
|
-
const markWrapped = (fn) => {
|
|
3910
|
-
try {
|
|
3911
|
-
Object.defineProperty(fn, WRAPPED_PROP, {
|
|
3912
|
-
configurable: false,
|
|
3913
|
-
enumerable: false,
|
|
3914
|
-
value: true
|
|
3915
|
-
});
|
|
3916
|
-
} catch {
|
|
3917
|
-
try {
|
|
3918
|
-
fn[WRAPPED_PROP] = true;
|
|
3919
|
-
} catch {
|
|
3920
|
-
}
|
|
3921
|
-
}
|
|
3922
|
-
};
|
|
3923
|
-
const wrapFn = (get, set) => {
|
|
3924
|
-
const fn = get();
|
|
3925
|
-
if (typeof fn !== "function") return false;
|
|
3926
|
-
const seek = fn;
|
|
3927
|
-
if (isWrapped(seek)) {
|
|
3928
|
-
reapplyAll();
|
|
3929
|
-
return true;
|
|
3930
|
-
}
|
|
3931
|
-
const wrapped = function(time) {
|
|
3932
|
-
lastSeekTime = typeof time === "number" && Number.isFinite(time) ? Math.max(0, time) : 0;
|
|
3933
|
-
const result = seek.call(this, time);
|
|
3934
|
-
reapplyAll();
|
|
3935
|
-
return result;
|
|
3936
|
-
};
|
|
3937
|
-
markWrapped(wrapped);
|
|
3938
|
-
set(wrapped);
|
|
3939
|
-
reapplyAll();
|
|
3940
|
-
return true;
|
|
3941
|
-
};
|
|
3942
|
-
const wrapSeekFunctions = () => {
|
|
3943
|
-
const a = wrapFn(
|
|
3944
|
-
() => runtimeWindow.__hf?.["seek"],
|
|
3945
|
-
(fn) => {
|
|
3946
|
-
if (runtimeWindow.__hf) runtimeWindow.__hf["seek"] = fn;
|
|
3947
|
-
}
|
|
3948
|
-
);
|
|
3949
|
-
const b = wrapFn(
|
|
3950
|
-
() => runtimeWindow.__player?.["renderSeek"],
|
|
3951
|
-
(fn) => {
|
|
3952
|
-
if (runtimeWindow.__player) runtimeWindow.__player["renderSeek"] = fn;
|
|
3953
|
-
}
|
|
3954
|
-
);
|
|
3955
|
-
return a || b;
|
|
3956
|
-
};
|
|
3957
|
-
const installSeekTrap = (obj, key, getter, setter) => {
|
|
3958
|
-
if (!obj) return;
|
|
3959
|
-
try {
|
|
3960
|
-
let current = obj[key];
|
|
3961
|
-
Object.defineProperty(obj, key, {
|
|
3962
|
-
configurable: true,
|
|
3963
|
-
enumerable: true,
|
|
3964
|
-
get() {
|
|
3965
|
-
return current;
|
|
3966
|
-
},
|
|
3967
|
-
set(value) {
|
|
3968
|
-
current = value;
|
|
3969
|
-
if (typeof value === "function" && !isWrapped(value)) {
|
|
3970
|
-
wrapFn(getter, setter);
|
|
3971
|
-
}
|
|
3972
|
-
}
|
|
3973
|
-
});
|
|
3974
|
-
} catch {
|
|
3975
|
-
}
|
|
3976
|
-
};
|
|
3977
|
-
if (document.readyState === "loading") {
|
|
3978
|
-
document.addEventListener("DOMContentLoaded", () => reapplyAll(), { once: true });
|
|
3979
|
-
} else {
|
|
3980
|
-
reapplyAll();
|
|
3981
|
-
}
|
|
3982
|
-
wrapSeekFunctions();
|
|
3983
|
-
installSeekTrap(
|
|
3984
|
-
runtimeWindow.__hf,
|
|
3985
|
-
"seek",
|
|
3986
|
-
() => runtimeWindow.__hf?.["seek"],
|
|
3987
|
-
(fn) => {
|
|
3988
|
-
if (runtimeWindow.__hf) runtimeWindow.__hf["seek"] = fn;
|
|
3989
|
-
}
|
|
3990
|
-
);
|
|
3991
|
-
installSeekTrap(
|
|
3992
|
-
runtimeWindow.__player,
|
|
3993
|
-
"renderSeek",
|
|
3994
|
-
() => runtimeWindow.__player?.["renderSeek"],
|
|
3995
|
-
(fn) => {
|
|
3996
|
-
if (runtimeWindow.__player) runtimeWindow.__player["renderSeek"] = fn;
|
|
3997
|
-
}
|
|
3998
|
-
);
|
|
3999
|
-
let remaining = 120;
|
|
4000
|
-
const interval = setInterval(() => {
|
|
4001
|
-
wrapSeekFunctions();
|
|
4002
|
-
remaining -= 1;
|
|
4003
|
-
if (remaining <= 0) clearInterval(interval);
|
|
4004
|
-
}, 50);
|
|
4005
|
-
}
|
|
4006
|
-
function studioManualEditsRenderRuntime(manifestContent, activeCompositionPath) {
|
|
4007
|
-
const OFFSET_X_PROP = "--hf-studio-offset-x";
|
|
4008
|
-
const OFFSET_Y_PROP = "--hf-studio-offset-y";
|
|
4009
|
-
const WIDTH_PROP = "--hf-studio-width";
|
|
4010
|
-
const HEIGHT_PROP = "--hf-studio-height";
|
|
4011
|
-
const ROTATION_PROP = "--hf-studio-rotation";
|
|
4012
|
-
const PATH_OFFSET_ATTR = "data-hf-studio-path-offset";
|
|
4013
|
-
const BOX_SIZE_ATTR = "data-hf-studio-box-size";
|
|
4014
|
-
const ROTATION_ATTR = "data-hf-studio-rotation";
|
|
4015
|
-
const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate";
|
|
4016
|
-
const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate";
|
|
4017
|
-
const WRAPPED_SEEK_PROP = "__hfStudioManualEditsWrapped";
|
|
4018
|
-
const ROTATION_TRANSFORM_ORIGIN = "center center";
|
|
4019
|
-
const finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
4020
|
-
const objectRecord = (value) => value && typeof value === "object" ? value : null;
|
|
4021
|
-
const runtimeWindow = window;
|
|
4022
|
-
const parsedManifest = (() => {
|
|
4023
|
-
try {
|
|
4024
|
-
return objectRecord(JSON.parse(manifestContent));
|
|
4025
|
-
} catch {
|
|
4026
|
-
return null;
|
|
4027
|
-
}
|
|
4028
|
-
})();
|
|
4029
|
-
const manifestEdits = Array.isArray(parsedManifest?.edits) ? parsedManifest.edits : [];
|
|
4030
|
-
if (manifestEdits.length === 0) return;
|
|
4031
|
-
const sourceFileForElement = (element) => {
|
|
4032
|
-
let current = element;
|
|
4033
|
-
while (current) {
|
|
4034
|
-
const sourceFile = current.getAttribute("data-composition-file") ?? current.getAttribute("data-composition-src");
|
|
4035
|
-
if (sourceFile) return sourceFile;
|
|
4036
|
-
current = current.parentElement;
|
|
4037
|
-
}
|
|
4038
|
-
return activeCompositionPath ?? "index.html";
|
|
4039
|
-
};
|
|
4040
|
-
const elementMatchesSourceFile = (element, sourceFile) => sourceFileForElement(element) === sourceFile;
|
|
4041
|
-
const styleUsesStudioOffset = (value) => value.includes(OFFSET_X_PROP) || value.includes(OFFSET_Y_PROP);
|
|
4042
|
-
const styleUsesStudioRotation = (value) => value.includes(ROTATION_PROP);
|
|
4043
|
-
const splitTopLevelWhitespace = (value) => {
|
|
4044
|
-
const parts = [];
|
|
4045
|
-
let depth = 0;
|
|
4046
|
-
let current = "";
|
|
4047
|
-
for (const char of value.trim()) {
|
|
4048
|
-
if (char === "(") depth += 1;
|
|
4049
|
-
if (char === ")") depth = Math.max(0, depth - 1);
|
|
4050
|
-
if (/\s/.test(char) && depth === 0) {
|
|
4051
|
-
if (current) parts.push(current);
|
|
4052
|
-
current = "";
|
|
4053
|
-
} else {
|
|
4054
|
-
current += char;
|
|
4055
|
-
}
|
|
4056
|
-
}
|
|
4057
|
-
if (current) parts.push(current);
|
|
4058
|
-
return parts;
|
|
4059
|
-
};
|
|
4060
|
-
const composeTranslate = (element, x, y) => {
|
|
4061
|
-
const original = element.getAttribute(ORIGINAL_TRANSLATE_ATTR)?.trim();
|
|
4062
|
-
if (!original || original === "none") return `${x} ${y}`;
|
|
4063
|
-
const parts = splitTopLevelWhitespace(original);
|
|
4064
|
-
if (parts.length === 1) return `calc(${parts[0]} + ${x}) ${y}`;
|
|
4065
|
-
if (parts.length === 2) return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y})`;
|
|
4066
|
-
if (parts.length === 3) {
|
|
4067
|
-
return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y}) ${parts[2]}`;
|
|
4068
|
-
}
|
|
4069
|
-
return `${x} ${y}`;
|
|
4070
|
-
};
|
|
4071
|
-
const readStyleOrComputed = (element, property) => {
|
|
4072
|
-
try {
|
|
4073
|
-
return element.style.getPropertyValue(property) || getComputedStyle(element).getPropertyValue(property);
|
|
4074
|
-
} catch {
|
|
4075
|
-
return element.style.getPropertyValue(property);
|
|
4076
|
-
}
|
|
4077
|
-
};
|
|
4078
|
-
const readTransformLonghandBase = (element, property) => {
|
|
4079
|
-
const value = readStyleOrComputed(element, property).trim();
|
|
4080
|
-
return value === "none" ? "" : value;
|
|
4081
|
-
};
|
|
4082
|
-
const preparePathOffsetBase = (element) => {
|
|
4083
|
-
const currentTranslate = readTransformLonghandBase(element, "translate");
|
|
4084
|
-
const hasMarker = element.hasAttribute(PATH_OFFSET_ATTR);
|
|
4085
|
-
const wasResetByAnimation = !styleUsesStudioOffset(currentTranslate);
|
|
4086
|
-
if (!hasMarker) {
|
|
4087
|
-
element.setAttribute(ORIGINAL_TRANSLATE_ATTR, wasResetByAnimation ? currentTranslate : "");
|
|
4088
|
-
} else if (wasResetByAnimation) {
|
|
4089
|
-
element.setAttribute(ORIGINAL_TRANSLATE_ATTR, currentTranslate);
|
|
4090
|
-
}
|
|
4091
|
-
};
|
|
4092
|
-
const prepareRotationBase = (element) => {
|
|
4093
|
-
const currentRotate = readTransformLonghandBase(element, "rotate");
|
|
4094
|
-
const hasMarker = element.hasAttribute(ROTATION_ATTR);
|
|
4095
|
-
const wasResetByAnimation = !styleUsesStudioRotation(currentRotate);
|
|
4096
|
-
if (!hasMarker) {
|
|
4097
|
-
element.setAttribute(ORIGINAL_ROTATE_ATTR, wasResetByAnimation ? currentRotate : "");
|
|
4098
|
-
} else if (wasResetByAnimation) {
|
|
4099
|
-
element.setAttribute(ORIGINAL_ROTATE_ATTR, currentRotate);
|
|
4100
|
-
}
|
|
4101
|
-
};
|
|
4102
|
-
const querySelectorCandidates = (selector) => {
|
|
4103
|
-
const isCandidate = (element) => element instanceof HTMLElement;
|
|
4104
|
-
const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1];
|
|
4105
|
-
if (className) {
|
|
4106
|
-
return Array.from(document.getElementsByTagName("*")).filter(
|
|
4107
|
-
(element) => isCandidate(element) && element.classList.contains(className)
|
|
4108
|
-
);
|
|
4109
|
-
}
|
|
4110
|
-
if (/^[A-Za-z][A-Za-z0-9-]*$/.test(selector)) {
|
|
4111
|
-
return Array.from(document.getElementsByTagName(selector)).filter(isCandidate);
|
|
4112
|
-
}
|
|
4113
|
-
return Array.from(document.querySelectorAll(selector)).filter(isCandidate);
|
|
4114
|
-
};
|
|
4115
|
-
const resolveTarget = (edit) => {
|
|
4116
|
-
const targetRecord = objectRecord(edit.target);
|
|
4117
|
-
if (!targetRecord) return null;
|
|
4118
|
-
const sourceFile = typeof targetRecord.sourceFile === "string" ? targetRecord.sourceFile : "";
|
|
4119
|
-
if (!sourceFile) return null;
|
|
4120
|
-
const id = typeof targetRecord.id === "string" ? targetRecord.id : "";
|
|
4121
|
-
if (id) {
|
|
4122
|
-
const byId = document.getElementById(id);
|
|
4123
|
-
if (byId instanceof HTMLElement && elementMatchesSourceFile(byId, sourceFile)) return byId;
|
|
4124
|
-
const matchesById = [
|
|
4125
|
-
document.documentElement,
|
|
4126
|
-
...Array.from(document.getElementsByTagName("*"))
|
|
4127
|
-
].filter(
|
|
4128
|
-
(element) => element instanceof HTMLElement && element.id === id && elementMatchesSourceFile(element, sourceFile)
|
|
4129
|
-
);
|
|
4130
|
-
if (matchesById[0]) return matchesById[0];
|
|
4131
|
-
}
|
|
4132
|
-
const selector = typeof targetRecord.selector === "string" ? targetRecord.selector : "";
|
|
4133
|
-
if (!selector) return null;
|
|
4134
|
-
try {
|
|
4135
|
-
const matches = querySelectorCandidates(selector).filter(
|
|
4136
|
-
(element) => elementMatchesSourceFile(element, sourceFile)
|
|
4137
|
-
);
|
|
4138
|
-
const selectorIndex = finiteNumber(targetRecord.selectorIndex) ?? 0;
|
|
4139
|
-
return matches[Math.max(0, Math.floor(selectorIndex))] ?? null;
|
|
4140
|
-
} catch {
|
|
4141
|
-
return null;
|
|
4142
|
-
}
|
|
4143
|
-
};
|
|
4144
|
-
const roundRotationAngle = (angle) => Math.round(angle * 10) / 10;
|
|
4145
|
-
const isSimpleRotateAngle = (value) => /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
|
|
4146
|
-
const composeRotation = (element, rotationValue) => {
|
|
4147
|
-
const original = element.getAttribute(ORIGINAL_ROTATE_ATTR)?.trim();
|
|
4148
|
-
if (!original || original === "none" || !isSimpleRotateAngle(original)) {
|
|
4149
|
-
return rotationValue;
|
|
4150
|
-
}
|
|
4151
|
-
return `calc(${original} + ${rotationValue})`;
|
|
4152
|
-
};
|
|
4153
|
-
const applyPathOffset = (element, edit) => {
|
|
4154
|
-
const x = finiteNumber(edit.x);
|
|
4155
|
-
const y = finiteNumber(edit.y);
|
|
4156
|
-
if (x == null || y == null) return;
|
|
4157
|
-
preparePathOffsetBase(element);
|
|
4158
|
-
element.setAttribute(PATH_OFFSET_ATTR, "true");
|
|
4159
|
-
element.style.setProperty(OFFSET_X_PROP, `${Math.round(x)}px`);
|
|
4160
|
-
element.style.setProperty(OFFSET_Y_PROP, `${Math.round(y)}px`);
|
|
4161
|
-
element.style.setProperty(
|
|
4162
|
-
"translate",
|
|
4163
|
-
composeTranslate(element, `var(${OFFSET_X_PROP}, 0px)`, `var(${OFFSET_Y_PROP}, 0px)`)
|
|
4164
|
-
);
|
|
4165
|
-
};
|
|
4166
|
-
const readParentFlexBasisPixels = (element, size) => {
|
|
4167
|
-
const parent = element.parentElement;
|
|
4168
|
-
if (!parent) return null;
|
|
4169
|
-
const styles = getComputedStyle(parent);
|
|
4170
|
-
if (styles.display !== "flex" && styles.display !== "inline-flex") return null;
|
|
4171
|
-
return Math.round(
|
|
4172
|
-
Math.max(1, styles.flexDirection.startsWith("column") ? size.height : size.width)
|
|
4173
|
-
);
|
|
4174
|
-
};
|
|
4175
|
-
const applyBoxSize = (element, edit) => {
|
|
4176
|
-
const width = finiteNumber(edit.width);
|
|
4177
|
-
const height = finiteNumber(edit.height);
|
|
4178
|
-
if (width == null || height == null || width <= 0 || height <= 0) return;
|
|
4179
|
-
const rounded = {
|
|
4180
|
-
width: Math.round(Math.max(1, width)),
|
|
4181
|
-
height: Math.round(Math.max(1, height))
|
|
4182
|
-
};
|
|
4183
|
-
element.setAttribute(BOX_SIZE_ATTR, "true");
|
|
4184
|
-
element.style.setProperty(WIDTH_PROP, `${rounded.width}px`);
|
|
4185
|
-
element.style.setProperty(HEIGHT_PROP, `${rounded.height}px`);
|
|
4186
|
-
element.style.setProperty("box-sizing", "border-box");
|
|
4187
|
-
element.style.setProperty("width", `${rounded.width}px`);
|
|
4188
|
-
element.style.setProperty("height", `${rounded.height}px`);
|
|
4189
|
-
element.style.setProperty("min-width", "0px");
|
|
4190
|
-
element.style.setProperty("min-height", "0px");
|
|
4191
|
-
element.style.setProperty("max-width", "none");
|
|
4192
|
-
element.style.setProperty("max-height", "none");
|
|
4193
|
-
const flexBasis = readParentFlexBasisPixels(element, rounded);
|
|
4194
|
-
if (flexBasis != null) {
|
|
4195
|
-
element.style.setProperty("flex-basis", `${flexBasis}px`);
|
|
4196
|
-
element.style.setProperty("flex-grow", "0");
|
|
4197
|
-
element.style.setProperty("flex-shrink", "0");
|
|
4198
|
-
}
|
|
4199
|
-
if (getComputedStyle(element).display === "inline") {
|
|
4200
|
-
element.style.setProperty("display", "inline-block");
|
|
4201
|
-
}
|
|
4202
|
-
};
|
|
4203
|
-
const applyRotation = (element, edit) => {
|
|
4204
|
-
const angle = finiteNumber(edit.angle);
|
|
4205
|
-
if (angle == null) return;
|
|
4206
|
-
prepareRotationBase(element);
|
|
4207
|
-
element.setAttribute(ROTATION_ATTR, "true");
|
|
4208
|
-
element.style.setProperty(ROTATION_PROP, `${roundRotationAngle(angle)}deg`);
|
|
4209
|
-
element.style.setProperty("transform-origin", ROTATION_TRANSFORM_ORIGIN);
|
|
4210
|
-
element.style.setProperty("rotate", composeRotation(element, `var(${ROTATION_PROP}, 0deg)`));
|
|
4211
|
-
};
|
|
4212
|
-
const applyManifest = () => {
|
|
4213
|
-
let applied = 0;
|
|
4214
|
-
for (const edit of manifestEdits) {
|
|
4215
|
-
const editRecord = objectRecord(edit);
|
|
4216
|
-
if (!editRecord) continue;
|
|
4217
|
-
const element = resolveTarget(editRecord);
|
|
4218
|
-
if (!element) continue;
|
|
4219
|
-
if (editRecord.kind === "path-offset") applyPathOffset(element, editRecord);
|
|
4220
|
-
if (editRecord.kind === "box-size") applyBoxSize(element, editRecord);
|
|
4221
|
-
if (editRecord.kind === "rotation") applyRotation(element, editRecord);
|
|
4222
|
-
applied += 1;
|
|
4223
|
-
}
|
|
4224
|
-
return applied;
|
|
4225
|
-
};
|
|
4226
|
-
runtimeWindow.__hfStudioManualEditsApply = applyManifest;
|
|
4227
|
-
const markWrapped = (fn) => {
|
|
4228
|
-
try {
|
|
4229
|
-
Object.defineProperty(fn, WRAPPED_SEEK_PROP, {
|
|
4230
|
-
configurable: false,
|
|
4231
|
-
enumerable: false,
|
|
4232
|
-
value: true
|
|
4233
|
-
});
|
|
4234
|
-
} catch {
|
|
4235
|
-
try {
|
|
4236
|
-
fn[WRAPPED_SEEK_PROP] = true;
|
|
4237
|
-
} catch {
|
|
4238
|
-
}
|
|
4239
|
-
}
|
|
4240
|
-
};
|
|
4241
|
-
const isWrapped = (fn) => Boolean(fn[WRAPPED_SEEK_PROP]);
|
|
4242
|
-
const wrapFunction = (get, set) => {
|
|
4243
|
-
const fn = get();
|
|
4244
|
-
if (!fn) return false;
|
|
4245
|
-
const seek = fn;
|
|
4246
|
-
if (isWrapped(seek)) {
|
|
4247
|
-
applyManifest();
|
|
4248
|
-
return true;
|
|
4249
|
-
}
|
|
4250
|
-
const wrappedSeek = function(time) {
|
|
4251
|
-
const result = seek.call(this, time);
|
|
4252
|
-
applyManifest();
|
|
4253
|
-
return result;
|
|
4254
|
-
};
|
|
4255
|
-
markWrapped(wrappedSeek);
|
|
4256
|
-
set(wrappedSeek);
|
|
4257
|
-
applyManifest();
|
|
4258
|
-
return true;
|
|
4259
|
-
};
|
|
4260
|
-
const wrapSeekFunctions = () => {
|
|
4261
|
-
const wrappedHfSeek = wrapFunction(
|
|
4262
|
-
() => runtimeWindow.__hf?.seek,
|
|
4263
|
-
(fn) => {
|
|
4264
|
-
if (runtimeWindow.__hf) runtimeWindow.__hf.seek = fn;
|
|
4265
|
-
}
|
|
4266
|
-
);
|
|
4267
|
-
const wrappedPlayerRenderSeek = wrapFunction(
|
|
4268
|
-
() => runtimeWindow.__player?.renderSeek,
|
|
4269
|
-
(fn) => {
|
|
4270
|
-
if (runtimeWindow.__player) runtimeWindow.__player.renderSeek = fn;
|
|
4271
|
-
}
|
|
4272
|
-
);
|
|
4273
|
-
return wrappedHfSeek || wrappedPlayerRenderSeek;
|
|
4274
|
-
};
|
|
4275
|
-
if (document.readyState === "loading") {
|
|
4276
|
-
document.addEventListener("DOMContentLoaded", () => applyManifest(), { once: true });
|
|
4277
|
-
} else {
|
|
4278
|
-
applyManifest();
|
|
4279
|
-
}
|
|
4280
|
-
wrapSeekFunctions();
|
|
4281
|
-
let remainingSeekWrapAttempts = 120;
|
|
4282
|
-
const seekWrapInterval = setInterval(() => {
|
|
4283
|
-
wrapSeekFunctions();
|
|
4284
|
-
remainingSeekWrapAttempts -= 1;
|
|
4285
|
-
if (remainingSeekWrapAttempts <= 0) clearInterval(seekWrapInterval);
|
|
4286
|
-
}, 50);
|
|
4287
|
-
}
|
|
4288
|
-
|
|
4289
|
-
// src/routes/thumbnail.ts
|
|
3325
|
+
import { createHash as createHash4 } from "crypto";
|
|
4290
3326
|
var THUMBNAIL_CACHE_VERSION = "v4";
|
|
4291
3327
|
function registerThumbnailRoutes(api, adapter) {
|
|
4292
3328
|
api.get("/projects/:id/thumbnail/*", async (c) => {
|
|
@@ -4318,7 +3354,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4318
3354
|
const htmlFile = join11(project.dir, compPath);
|
|
4319
3355
|
if (existsSync7(htmlFile)) {
|
|
4320
3356
|
const html = readFileSync10(htmlFile, "utf-8");
|
|
4321
|
-
sourceKey = `_${
|
|
3357
|
+
sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
|
|
4322
3358
|
sourceMtime = Math.round(statSync4(htmlFile).mtimeMs);
|
|
4323
3359
|
if (!vpWidth) {
|
|
4324
3360
|
const wMatch = html.match(/data-width=["'](\d+)["']/);
|
|
@@ -4331,14 +3367,14 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4331
3367
|
let manualEditsKey = "";
|
|
4332
3368
|
if (existsSync7(manualEditsFile)) {
|
|
4333
3369
|
const manualEditsContent = readFileSync10(manualEditsFile, "utf-8");
|
|
4334
|
-
manualEditsKey = `_${
|
|
3370
|
+
manualEditsKey = `_${createHash4("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
|
|
4335
3371
|
sourceMtime = Math.max(sourceMtime, Math.round(statSync4(manualEditsFile).mtimeMs));
|
|
4336
3372
|
}
|
|
4337
3373
|
const motionFile = join11(project.dir, STUDIO_MOTION_PATH);
|
|
4338
3374
|
let motionKey = "";
|
|
4339
3375
|
if (existsSync7(motionFile)) {
|
|
4340
3376
|
const motionContent = readFileSync10(motionFile, "utf-8");
|
|
4341
|
-
motionKey = `_${
|
|
3377
|
+
motionKey = `_${createHash4("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
|
|
4342
3378
|
sourceMtime = Math.max(sourceMtime, Math.round(statSync4(motionFile).mtimeMs));
|
|
4343
3379
|
}
|
|
4344
3380
|
const previewUrl = compPath === "index.html" ? `http://${c.req.header("host")}/api/projects/${project.id}/preview` : `http://${c.req.header("host")}/api/projects/${project.id}/preview/comp/${compPath}`;
|
|
@@ -4419,7 +3455,7 @@ function registerWaveformRoutes(api, adapter) {
|
|
|
4419
3455
|
}
|
|
4420
3456
|
|
|
4421
3457
|
// src/routes/fonts.ts
|
|
4422
|
-
import { closeSync as
|
|
3458
|
+
import { closeSync as closeSync3, constants as constants2, fstatSync as fstatSync2, openSync as openSync3, readSync } from "fs";
|
|
4423
3459
|
import {
|
|
4424
3460
|
collectFontFileEntries,
|
|
4425
3461
|
fontDirectories,
|
|
@@ -4526,7 +3562,7 @@ function registerFontRoutes(api) {
|
|
|
4526
3562
|
if (!located) return c.json({ error: "font not found" }, 404);
|
|
4527
3563
|
let fd;
|
|
4528
3564
|
try {
|
|
4529
|
-
fd =
|
|
3565
|
+
fd = openSync3(located.path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
4530
3566
|
} catch {
|
|
4531
3567
|
return c.json({ error: "font file not accessible" }, 404);
|
|
4532
3568
|
}
|
|
@@ -4548,7 +3584,7 @@ function registerFontRoutes(api) {
|
|
|
4548
3584
|
} catch {
|
|
4549
3585
|
return c.json({ error: "failed to read font file" }, 500);
|
|
4550
3586
|
} finally {
|
|
4551
|
-
|
|
3587
|
+
closeSync3(fd);
|
|
4552
3588
|
}
|
|
4553
3589
|
});
|
|
4554
3590
|
}
|
|
@@ -4683,101 +3719,28 @@ function registerSelectionRoutes(api, adapter) {
|
|
|
4683
3719
|
// src/routes/media.ts
|
|
4684
3720
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
4685
3721
|
import { existsSync as existsSync9, mkdirSync as mkdirSync7 } from "fs";
|
|
4686
|
-
import { basename as basename2, dirname as dirname2, extname as
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
if (VIDEO_EXT2.has(ext)) return "video";
|
|
4700
|
-
if (IMAGE_EXT.has(ext)) return "image";
|
|
4701
|
-
if (AUDIO_EXT2.has(ext)) return "audio";
|
|
4702
|
-
return "unknown";
|
|
4703
|
-
}
|
|
4704
|
-
function colorLabel(input) {
|
|
4705
|
-
if (input.isHdr) {
|
|
4706
|
-
if (input.hdrTransfer === "pq") return "HDR PQ";
|
|
4707
|
-
if (input.hdrTransfer === "hlg") return "HDR HLG";
|
|
4708
|
-
return "HDR";
|
|
4709
|
-
}
|
|
4710
|
-
if (input.colorPrimaries.includes("bt709") || input.colorSpace.includes("bt709") || input.colorTransfer.includes("bt709")) {
|
|
4711
|
-
return "SDR Rec.709";
|
|
4712
|
-
}
|
|
4713
|
-
return "SDR/unknown";
|
|
4714
|
-
}
|
|
4715
|
-
function classifyMediaColor(stream) {
|
|
4716
|
-
const colorPrimaries = lower(stream?.color_primaries);
|
|
4717
|
-
const colorSpace = lower(stream?.color_space);
|
|
4718
|
-
const colorTransfer = lower(stream?.color_transfer);
|
|
4719
|
-
const isHdr = colorPrimaries.includes("bt2020") || colorSpace.includes("bt2020") || colorTransfer === "smpte2084" || colorTransfer === "arib-std-b67";
|
|
4720
|
-
const hdrTransfer = isHdr ? colorTransfer === "smpte2084" ? "pq" : colorTransfer === "arib-std-b67" ? "hlg" : "unknown" : null;
|
|
4721
|
-
return {
|
|
4722
|
-
dynamicRange: stream ? isHdr ? "hdr" : "sdr" : "unknown",
|
|
4723
|
-
hdrTransfer,
|
|
4724
|
-
label: stream ? colorLabel({ isHdr, hdrTransfer, colorPrimaries, colorSpace, colorTransfer }) : "Unknown",
|
|
4725
|
-
isHdr,
|
|
4726
|
-
codecName: stream?.codec_name,
|
|
4727
|
-
profile: stream?.profile,
|
|
4728
|
-
pixelFormat: stream?.pix_fmt,
|
|
4729
|
-
colorSpace: stream?.color_space,
|
|
4730
|
-
colorTransfer: stream?.color_transfer,
|
|
4731
|
-
colorPrimaries: stream?.color_primaries,
|
|
4732
|
-
bitsPerRawSample: stream?.bits_per_raw_sample
|
|
4733
|
-
};
|
|
4734
|
-
}
|
|
4735
|
-
function probeMediaMetadata(filePath, runner = spawnSync2) {
|
|
4736
|
-
const kind = inferKindFromPath(filePath);
|
|
4737
|
-
if (kind === "audio" || kind === "unknown") {
|
|
4738
|
-
return { kind, color: classifyMediaColor(null) };
|
|
4739
|
-
}
|
|
4740
|
-
const result = runner(
|
|
4741
|
-
"ffprobe",
|
|
4742
|
-
[
|
|
4743
|
-
"-v",
|
|
4744
|
-
"error",
|
|
4745
|
-
"-show_entries",
|
|
4746
|
-
"stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample",
|
|
4747
|
-
"-of",
|
|
4748
|
-
"json",
|
|
4749
|
-
filePath
|
|
4750
|
-
],
|
|
4751
|
-
{ timeout: 15e3, maxBuffer: 1024 * 1024 }
|
|
4752
|
-
);
|
|
4753
|
-
if (result.error?.code === "ENOENT") {
|
|
4754
|
-
return { kind, color: classifyMediaColor(null), probeError: "ffprobe unavailable" };
|
|
4755
|
-
}
|
|
4756
|
-
if (result.status !== 0) {
|
|
4757
|
-
return { kind, color: classifyMediaColor(null), probeError: "ffprobe failed" };
|
|
4758
|
-
}
|
|
4759
|
-
try {
|
|
4760
|
-
const parsed = JSON.parse(String(result.stdout || "{}"));
|
|
4761
|
-
const stream = parsed.streams?.find(
|
|
4762
|
-
(item) => kind === "image" ? item.codec_type === "video" : item.codec_type === kind
|
|
4763
|
-
);
|
|
4764
|
-
return { kind, color: classifyMediaColor(stream) };
|
|
4765
|
-
} catch {
|
|
4766
|
-
return { kind, color: classifyMediaColor(null), probeError: "ffprobe returned invalid json" };
|
|
4767
|
-
}
|
|
4768
|
-
}
|
|
4769
|
-
|
|
4770
|
-
// src/routes/media.ts
|
|
4771
|
-
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
|
+
]);
|
|
4772
3735
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp"]);
|
|
4773
3736
|
var VIDEO_OUTPUT_EXTENSIONS = /* @__PURE__ */ new Set([".webm", ".mov"]);
|
|
4774
3737
|
var QUALITIES = /* @__PURE__ */ new Set(["fast", "balanced", "best"]);
|
|
4775
3738
|
var DEVICES = /* @__PURE__ */ new Set(["auto", "cpu", "coreml", "cuda"]);
|
|
4776
3739
|
function isVideoPath(path) {
|
|
4777
|
-
return VIDEO_EXTENSIONS.has(
|
|
3740
|
+
return VIDEO_EXTENSIONS.has(extname2(path).toLowerCase());
|
|
4778
3741
|
}
|
|
4779
3742
|
function isImagePath(path) {
|
|
4780
|
-
return IMAGE_EXTENSIONS.has(
|
|
3743
|
+
return IMAGE_EXTENSIONS.has(extname2(path).toLowerCase());
|
|
4781
3744
|
}
|
|
4782
3745
|
function normalizeProjectAssetPath(path) {
|
|
4783
3746
|
return path.trim().replace(/^[.]\//, "").replace(/[?#].*$/, "");
|
|
@@ -4786,11 +3749,11 @@ function containsNullByte(path) {
|
|
|
4786
3749
|
return path.includes("\0");
|
|
4787
3750
|
}
|
|
4788
3751
|
function slugFileBase(path) {
|
|
4789
|
-
const name = basename2(path,
|
|
3752
|
+
const name = basename2(path, extname2(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4790
3753
|
return name || "media";
|
|
4791
3754
|
}
|
|
4792
3755
|
function uniqueAssetPath(projectDir, assetPath) {
|
|
4793
|
-
const ext =
|
|
3756
|
+
const ext = extname2(assetPath);
|
|
4794
3757
|
const withoutExt = assetPath.slice(0, -ext.length);
|
|
4795
3758
|
let candidate = assetPath;
|
|
4796
3759
|
for (let index = 2; existsSync9(join13(projectDir, candidate)); index++) {
|
|
@@ -4845,7 +3808,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
4845
3808
|
const filePath = resolveWithinProject(project.dir, assetPath);
|
|
4846
3809
|
if (!filePath) return c.json({ error: "forbidden" }, 403);
|
|
4847
3810
|
if (!existsSync9(filePath)) return c.json({ error: "media not found" }, 404);
|
|
4848
|
-
return c.json({ path: assetPath, metadata: readMediaMetadata(filePath) });
|
|
3811
|
+
return c.json({ path: assetPath, metadata: await readMediaMetadata(filePath) });
|
|
4849
3812
|
});
|
|
4850
3813
|
api.post(
|
|
4851
3814
|
"/projects/:id/media/remove-background",
|
|
@@ -4882,10 +3845,10 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
4882
3845
|
const outputAssetPath = requestedOutput ? uniqueAssetPath(project.dir, requestedOutput) : defaultOutputPath(project.dir, inputAssetPath);
|
|
4883
3846
|
const outputPath = resolveWithinProject(project.dir, outputAssetPath);
|
|
4884
3847
|
if (!outputPath) return c.json({ error: "forbidden" }, 403);
|
|
4885
|
-
if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(
|
|
3848
|
+
if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname2(outputAssetPath).toLowerCase())) {
|
|
4886
3849
|
return c.json({ error: "video background removal output must be .webm or .mov" }, 400);
|
|
4887
3850
|
}
|
|
4888
|
-
if (inputIsImage &&
|
|
3851
|
+
if (inputIsImage && extname2(outputAssetPath).toLowerCase() !== ".png") {
|
|
4889
3852
|
return c.json({ error: "image background removal output must be .png" }, 400);
|
|
4890
3853
|
}
|
|
4891
3854
|
let backgroundOutputAssetPath;
|
|
@@ -5001,29 +3964,6 @@ function createStudioApi(adapter) {
|
|
|
5001
3964
|
return api;
|
|
5002
3965
|
}
|
|
5003
3966
|
|
|
5004
|
-
// src/helpers/screenshotClip.ts
|
|
5005
|
-
function getElementScreenshotClip(selector, selectorIndex) {
|
|
5006
|
-
const matches = Array.from(document.querySelectorAll(selector)).filter(
|
|
5007
|
-
(el2) => el2 instanceof HTMLElement
|
|
5008
|
-
);
|
|
5009
|
-
const safeIndex = Math.max(0, Math.min(matches.length - 1, Math.floor(selectorIndex ?? 0)));
|
|
5010
|
-
const el = matches[safeIndex] ?? null;
|
|
5011
|
-
if (!(el instanceof HTMLElement)) return void 0;
|
|
5012
|
-
const rect = el.getBoundingClientRect();
|
|
5013
|
-
if (rect.width < 4 || rect.height < 4) return void 0;
|
|
5014
|
-
const pad = 8;
|
|
5015
|
-
const x = Math.max(0, rect.left - pad);
|
|
5016
|
-
const y = Math.max(0, rect.top - pad);
|
|
5017
|
-
const maxWidth = window.innerWidth - x;
|
|
5018
|
-
const maxHeight = window.innerHeight - y;
|
|
5019
|
-
return {
|
|
5020
|
-
x,
|
|
5021
|
-
y,
|
|
5022
|
-
width: Math.max(1, Math.min(rect.width + pad * 2, maxWidth)),
|
|
5023
|
-
height: Math.max(1, Math.min(rect.height + pad * 2, maxHeight))
|
|
5024
|
-
};
|
|
5025
|
-
}
|
|
5026
|
-
|
|
5027
3967
|
// src/helpers/backgroundRemovalJob.ts
|
|
5028
3968
|
function createBackgroundRemovalJob(opts, render) {
|
|
5029
3969
|
const state = {
|
|
@@ -5082,12 +4022,14 @@ export {
|
|
|
5082
4022
|
STUDIO_MANUAL_EDITS_PATH,
|
|
5083
4023
|
STUDIO_MOTION_PATH,
|
|
5084
4024
|
buildSubCompositionHtml,
|
|
4025
|
+
consumeFileWriteReceipt,
|
|
5085
4026
|
createBackgroundRemovalJob,
|
|
5086
4027
|
createProjectSignature,
|
|
5087
4028
|
createStudioApi,
|
|
5088
4029
|
createStudioManualEditsRenderBodyScript,
|
|
5089
4030
|
createStudioMotionRenderBodyScript,
|
|
5090
4031
|
createStudioPositionSeekReapplyScript,
|
|
4032
|
+
fileContentVersion,
|
|
5091
4033
|
getElementScreenshotClip,
|
|
5092
4034
|
getMimeType,
|
|
5093
4035
|
isSafePath,
|