@hyperframes/studio-server 0.7.15 → 0.7.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +303 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -540,7 +540,7 @@ function isHTMLElement(el) {
|
|
|
540
540
|
const HTMLEl = el.ownerDocument.defaultView?.HTMLElement;
|
|
541
541
|
return HTMLEl ? el instanceof HTMLEl : "style" in el;
|
|
542
542
|
}
|
|
543
|
-
function
|
|
543
|
+
function parseStyleDecls(style) {
|
|
544
544
|
const props = /* @__PURE__ */ new Map();
|
|
545
545
|
const order = [];
|
|
546
546
|
let i = 0;
|
|
@@ -571,6 +571,13 @@ function patchStyleAttrString(style, property, value) {
|
|
|
571
571
|
if (!props.has(key)) order.push(key);
|
|
572
572
|
props.set(key, val);
|
|
573
573
|
}
|
|
574
|
+
return { props, order };
|
|
575
|
+
}
|
|
576
|
+
function serializeStyleDecls(props, order) {
|
|
577
|
+
return order.map((k) => `${k}: ${props.get(k) ?? ""}`).filter((d) => d.trim()).join("; ");
|
|
578
|
+
}
|
|
579
|
+
function patchStyleAttrString(style, property, value) {
|
|
580
|
+
const { props, order } = parseStyleDecls(style);
|
|
574
581
|
if (value === null) {
|
|
575
582
|
props.delete(property);
|
|
576
583
|
const idx = order.indexOf(property);
|
|
@@ -579,7 +586,7 @@ function patchStyleAttrString(style, property, value) {
|
|
|
579
586
|
if (!props.has(property)) order.push(property);
|
|
580
587
|
props.set(property, value);
|
|
581
588
|
}
|
|
582
|
-
return
|
|
589
|
+
return serializeStyleDecls(props, order);
|
|
583
590
|
}
|
|
584
591
|
function patchElementInHtml(source, target, operations) {
|
|
585
592
|
const { document: document2, wrappedFragment } = parseSourceDocument(source);
|
|
@@ -706,6 +713,127 @@ function splitElementInHtml(source, target, splitTime, newId, fallbackTiming) {
|
|
|
706
713
|
newId
|
|
707
714
|
};
|
|
708
715
|
}
|
|
716
|
+
function getInlineStylePx(el, property) {
|
|
717
|
+
const style = (isHTMLElement(el) ? el.getAttribute("style") : null) ?? "";
|
|
718
|
+
const { props } = parseStyleDecls(style);
|
|
719
|
+
const raw = props.get(property);
|
|
720
|
+
if (!raw) return 0;
|
|
721
|
+
const n = parseFloat(raw);
|
|
722
|
+
return Number.isFinite(n) ? n : 0;
|
|
723
|
+
}
|
|
724
|
+
function setInlineLeftTop(el, left, top) {
|
|
725
|
+
let style = el.getAttribute("style") ?? "";
|
|
726
|
+
style = patchStyleAttrString(style, "left", `${left}px`);
|
|
727
|
+
style = patchStyleAttrString(style, "top", `${top}px`);
|
|
728
|
+
el.setAttribute("style", style);
|
|
729
|
+
}
|
|
730
|
+
function uniqueGroupDomId(document2, groupId) {
|
|
731
|
+
const base = groupId.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "group";
|
|
732
|
+
let id = base;
|
|
733
|
+
let n = 2;
|
|
734
|
+
while (document2.getElementById(id)) {
|
|
735
|
+
id = `${base}-${n}`;
|
|
736
|
+
n += 1;
|
|
737
|
+
}
|
|
738
|
+
return id;
|
|
739
|
+
}
|
|
740
|
+
function wrapElementsInHtml(source, targets, groupId, bbox, rebases) {
|
|
741
|
+
const { document: document2, wrappedFragment } = parseSourceDocument(source);
|
|
742
|
+
if (targets.length === 0) {
|
|
743
|
+
return { html: source, matched: false, groupId: null, error: "no targets" };
|
|
744
|
+
}
|
|
745
|
+
const els = [];
|
|
746
|
+
const seen = /* @__PURE__ */ new Set();
|
|
747
|
+
for (const target of targets) {
|
|
748
|
+
const el = findTargetElement(document2, target);
|
|
749
|
+
if (!el || !isHTMLElement(el) || seen.has(el)) continue;
|
|
750
|
+
seen.add(el);
|
|
751
|
+
els.push(el);
|
|
752
|
+
}
|
|
753
|
+
if (els.length === 0) {
|
|
754
|
+
return { html: source, matched: false, groupId: null, error: "no targets matched" };
|
|
755
|
+
}
|
|
756
|
+
const parent = els[0]?.parentElement;
|
|
757
|
+
if (!parent || els.some((el) => el.parentElement !== parent)) {
|
|
758
|
+
return {
|
|
759
|
+
html: source,
|
|
760
|
+
matched: false,
|
|
761
|
+
groupId: null,
|
|
762
|
+
error: "grouped elements must share a single parent"
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const memberSet = new Set(els);
|
|
766
|
+
const ordered = Array.from(parent.children).filter((c) => memberSet.has(c));
|
|
767
|
+
const rebaseByEl = /* @__PURE__ */ new Map();
|
|
768
|
+
for (const rebase of rebases) {
|
|
769
|
+
const el = findTargetElement(document2, rebase.target);
|
|
770
|
+
if (el) rebaseByEl.set(el, { left: rebase.left, top: rebase.top });
|
|
771
|
+
}
|
|
772
|
+
const wrapper = document2.createElement("div");
|
|
773
|
+
wrapper.setAttribute("data-hf-group", groupId);
|
|
774
|
+
wrapper.setAttribute("id", uniqueGroupDomId(document2, groupId));
|
|
775
|
+
const memberZIndexes = ordered.map(
|
|
776
|
+
(el) => Number.parseInt(
|
|
777
|
+
parseStyleDecls(el.getAttribute("style") ?? "").props.get("z-index") ?? "",
|
|
778
|
+
10
|
|
779
|
+
)
|
|
780
|
+
).filter((z) => Number.isFinite(z));
|
|
781
|
+
const maxZ = memberZIndexes.length > 0 ? Math.max(...memberZIndexes) : null;
|
|
782
|
+
wrapper.setAttribute(
|
|
783
|
+
"style",
|
|
784
|
+
`position: absolute; left: ${bbox.left}px; top: ${bbox.top}px; width: ${bbox.width}px; height: ${bbox.height}px` + (maxZ !== null ? `; z-index: ${maxZ}` : "")
|
|
785
|
+
);
|
|
786
|
+
parent.insertBefore(wrapper, ordered[ordered.length - 1] ?? null);
|
|
787
|
+
for (const el of ordered) {
|
|
788
|
+
const rebase = rebaseByEl.get(el);
|
|
789
|
+
if (rebase) setInlineLeftTop(el, rebase.left, rebase.top);
|
|
790
|
+
wrapper.appendChild(el);
|
|
791
|
+
}
|
|
792
|
+
return {
|
|
793
|
+
html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
|
|
794
|
+
matched: true,
|
|
795
|
+
groupId
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
function unwrapElementsFromHtml(source, groupTarget) {
|
|
799
|
+
const { document: document2, wrappedFragment } = parseSourceDocument(source);
|
|
800
|
+
const group = findTargetElement(document2, groupTarget);
|
|
801
|
+
if (!group || !isHTMLElement(group)) return { html: source, unwrapped: false };
|
|
802
|
+
if (!group.hasAttribute("data-hf-group")) return { html: source, unwrapped: false };
|
|
803
|
+
const parent = group.parentElement;
|
|
804
|
+
if (!parent) return { html: source, unwrapped: false };
|
|
805
|
+
const wLeft = getInlineStylePx(group, "left");
|
|
806
|
+
const wTop = getInlineStylePx(group, "top");
|
|
807
|
+
const groupCenter = {
|
|
808
|
+
cx: wLeft + getInlineStylePx(group, "width") / 2,
|
|
809
|
+
cy: wTop + getInlineStylePx(group, "height") / 2
|
|
810
|
+
};
|
|
811
|
+
const members = [];
|
|
812
|
+
for (const child of Array.from(group.children)) {
|
|
813
|
+
if (isHTMLElement(child)) {
|
|
814
|
+
const newLeft = getInlineStylePx(child, "left") + wLeft;
|
|
815
|
+
const newTop = getInlineStylePx(child, "top") + wTop;
|
|
816
|
+
setInlineLeftTop(child, newLeft, newTop);
|
|
817
|
+
if (child.id) {
|
|
818
|
+
members.push({
|
|
819
|
+
id: child.id,
|
|
820
|
+
cx: newLeft + getInlineStylePx(child, "width") / 2,
|
|
821
|
+
cy: newTop + getInlineStylePx(child, "height") / 2
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
parent.insertBefore(child, group);
|
|
826
|
+
}
|
|
827
|
+
const groupId = group.id || void 0;
|
|
828
|
+
group.remove();
|
|
829
|
+
return {
|
|
830
|
+
html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
|
|
831
|
+
unwrapped: true,
|
|
832
|
+
unwrappedGroupId: groupId,
|
|
833
|
+
members,
|
|
834
|
+
groupCenter
|
|
835
|
+
};
|
|
836
|
+
}
|
|
709
837
|
|
|
710
838
|
// src/routes/files.ts
|
|
711
839
|
import { parseHTML as parseHTML2 } from "linkedom";
|
|
@@ -844,6 +972,97 @@ function extractGsapScriptBlock(html) {
|
|
|
844
972
|
}
|
|
845
973
|
return null;
|
|
846
974
|
}
|
|
975
|
+
function stripGsapAnimationsForSelector(html, selector) {
|
|
976
|
+
const block = extractGsapScriptBlock(html);
|
|
977
|
+
if (!block) return html;
|
|
978
|
+
const parsed = parseGsapScriptAcorn(block.scriptText);
|
|
979
|
+
const matching = parsed.animations.filter((a) => a.targetSelector === selector);
|
|
980
|
+
if (matching.length === 0) return html;
|
|
981
|
+
let script = block.scriptText;
|
|
982
|
+
for (const anim of [...matching].reverse()) {
|
|
983
|
+
script = removeAnimationFromScript(script, anim.id);
|
|
984
|
+
}
|
|
985
|
+
return block.replaceScript(script);
|
|
986
|
+
}
|
|
987
|
+
function bakeGroupTransformIntoMembers(html, groupId, members, groupCenter) {
|
|
988
|
+
const block = extractGsapScriptBlock(html);
|
|
989
|
+
if (!block) return html;
|
|
990
|
+
const parsed = parseGsapScriptAcorn(block.scriptText);
|
|
991
|
+
const groupSel = `#${groupId}`;
|
|
992
|
+
const groupSets = parsed.animations.filter(
|
|
993
|
+
(a) => a.targetSelector === groupSel && a.method === "set"
|
|
994
|
+
);
|
|
995
|
+
if (groupSets.length === 0) return html;
|
|
996
|
+
const gt = {};
|
|
997
|
+
for (const s of groupSets) {
|
|
998
|
+
for (const [k, v] of Object.entries(s.properties)) if (typeof v === "number") gt[k] = v;
|
|
999
|
+
}
|
|
1000
|
+
const gx = gt.x ?? 0;
|
|
1001
|
+
const gy = gt.y ?? 0;
|
|
1002
|
+
const gz = gt.z ?? 0;
|
|
1003
|
+
const grot = gt.rotation ?? 0;
|
|
1004
|
+
const gscale = gt.scale ?? 1;
|
|
1005
|
+
const isScaleAxis = (k) => k === "scale" || k === "scaleX" || k === "scaleY";
|
|
1006
|
+
const groupIsIdentity = Object.entries(gt).every(
|
|
1007
|
+
([k, v]) => isScaleAxis(k) ? v === 1 : v === 0
|
|
1008
|
+
);
|
|
1009
|
+
if (groupIsIdentity) return html;
|
|
1010
|
+
const rad = grot * Math.PI / 180;
|
|
1011
|
+
const cos = Math.cos(rad);
|
|
1012
|
+
const sin = Math.sin(rad);
|
|
1013
|
+
const round3 = (n) => Math.round(n * 1e3) / 1e3;
|
|
1014
|
+
let script = block.scriptText;
|
|
1015
|
+
for (const m of members) {
|
|
1016
|
+
const memberSel = `#${m.id}`;
|
|
1017
|
+
const sets = parsed.animations.filter(
|
|
1018
|
+
(a) => a.targetSelector === memberSel && a.method === "set"
|
|
1019
|
+
);
|
|
1020
|
+
const mProps = {};
|
|
1021
|
+
for (const s of sets) Object.assign(mProps, s.properties);
|
|
1022
|
+
const mx = typeof mProps.x === "number" ? mProps.x : 0;
|
|
1023
|
+
const my = typeof mProps.y === "number" ? mProps.y : 0;
|
|
1024
|
+
const dx = m.cx + mx - groupCenter.cx;
|
|
1025
|
+
const dy = m.cy + my - groupCenter.cy;
|
|
1026
|
+
const visX = groupCenter.cx + gscale * (cos * dx - sin * dy) + gx;
|
|
1027
|
+
const visY = groupCenter.cy + gscale * (sin * dx + cos * dy) + gy;
|
|
1028
|
+
const newProps = {
|
|
1029
|
+
...mProps,
|
|
1030
|
+
x: round3(visX - m.cx),
|
|
1031
|
+
y: round3(visY - m.cy)
|
|
1032
|
+
};
|
|
1033
|
+
if (gz !== 0) newProps.z = (typeof mProps.z === "number" ? mProps.z : 0) + gz;
|
|
1034
|
+
if (grot !== 0) {
|
|
1035
|
+
newProps.rotation = round3(
|
|
1036
|
+
(typeof mProps.rotation === "number" ? mProps.rotation : 0) + grot
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
if (gscale !== 1) {
|
|
1040
|
+
newProps.scale = round3((typeof mProps.scale === "number" ? mProps.scale : 1) * gscale);
|
|
1041
|
+
}
|
|
1042
|
+
const pivoted = /* @__PURE__ */ new Set(["x", "y", "z", "rotation", "scale"]);
|
|
1043
|
+
for (const [k, v] of Object.entries(gt)) {
|
|
1044
|
+
if (pivoted.has(k) || typeof v !== "number") continue;
|
|
1045
|
+
if (k === "scaleX" || k === "scaleY") {
|
|
1046
|
+
if (v !== 1) newProps[k] = round3((typeof mProps[k] === "number" ? mProps[k] : 1) * v);
|
|
1047
|
+
} else if (k === "transformPerspective") {
|
|
1048
|
+
if (typeof mProps[k] !== "number") newProps[k] = v;
|
|
1049
|
+
} else if (v !== 0) {
|
|
1050
|
+
newProps[k] = round3((typeof mProps[k] === "number" ? mProps[k] : 0) + v);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
for (const s of [...sets].reverse()) {
|
|
1054
|
+
script = removeAnimationFromScript(script, s.id);
|
|
1055
|
+
}
|
|
1056
|
+
script = addAnimationToScript(script, {
|
|
1057
|
+
targetSelector: memberSel,
|
|
1058
|
+
method: "set",
|
|
1059
|
+
position: 0,
|
|
1060
|
+
properties: newProps,
|
|
1061
|
+
global: true
|
|
1062
|
+
}).script;
|
|
1063
|
+
}
|
|
1064
|
+
return block.replaceScript(script);
|
|
1065
|
+
}
|
|
847
1066
|
function stripStudioEditsFromTarget(document2, selector) {
|
|
848
1067
|
if (!selector) return 0;
|
|
849
1068
|
let stripped = 0;
|
|
@@ -1692,6 +1911,88 @@ function registerFileRoutes(api, adapter) {
|
|
|
1692
1911
|
backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
|
|
1693
1912
|
});
|
|
1694
1913
|
});
|
|
1914
|
+
api.post("/projects/:id/file-mutations/wrap-elements/*", async (c) => {
|
|
1915
|
+
const ctx = await resolveFileMutationContext(c, adapter, "wrap-elements");
|
|
1916
|
+
if ("error" in ctx) return ctx.error;
|
|
1917
|
+
const body = await c.req.json().catch(() => null);
|
|
1918
|
+
if (!Array.isArray(body?.targets) || body.targets.length === 0 || !body.groupId) {
|
|
1919
|
+
return c.json({ error: "targets and groupId required" }, 400);
|
|
1920
|
+
}
|
|
1921
|
+
const bbox = body.bbox ?? {};
|
|
1922
|
+
const bboxNums = [bbox.left, bbox.top, bbox.width, bbox.height];
|
|
1923
|
+
const rebases = body.rebases ?? [];
|
|
1924
|
+
const allNumeric = bboxNums.every((n) => typeof n === "number" && Number.isFinite(n)) && rebases.every(
|
|
1925
|
+
(r) => typeof r?.left === "number" && Number.isFinite(r.left) && typeof r?.top === "number" && Number.isFinite(r.top)
|
|
1926
|
+
);
|
|
1927
|
+
if (!allNumeric) {
|
|
1928
|
+
return c.json({ error: "bbox and rebase coordinates must be finite numbers" }, 400);
|
|
1929
|
+
}
|
|
1930
|
+
let originalContent;
|
|
1931
|
+
try {
|
|
1932
|
+
originalContent = readFileSync3(ctx.absPath, "utf-8");
|
|
1933
|
+
} catch {
|
|
1934
|
+
return c.json({ error: "not found" }, 404);
|
|
1935
|
+
}
|
|
1936
|
+
const result = wrapElementsInHtml(
|
|
1937
|
+
originalContent,
|
|
1938
|
+
body.targets,
|
|
1939
|
+
body.groupId,
|
|
1940
|
+
{ left: bbox.left, top: bbox.top, width: bbox.width, height: bbox.height },
|
|
1941
|
+
rebases
|
|
1942
|
+
);
|
|
1943
|
+
if (!result.matched) {
|
|
1944
|
+
return c.json(
|
|
1945
|
+
{
|
|
1946
|
+
ok: false,
|
|
1947
|
+
changed: false,
|
|
1948
|
+
content: originalContent,
|
|
1949
|
+
path: ctx.filePath,
|
|
1950
|
+
error: result.error
|
|
1951
|
+
},
|
|
1952
|
+
result.error === "grouped elements must share a single parent" ? 422 : 400
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
|
|
1956
|
+
if (backup.error) console.warn(`Failed to create backup for ${ctx.filePath}: ${backup.error}`);
|
|
1957
|
+
writeFileSync4(ctx.absPath, result.html, "utf-8");
|
|
1958
|
+
return c.json({
|
|
1959
|
+
ok: true,
|
|
1960
|
+
changed: true,
|
|
1961
|
+
groupId: result.groupId,
|
|
1962
|
+
content: result.html,
|
|
1963
|
+
path: ctx.filePath,
|
|
1964
|
+
backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
|
|
1965
|
+
});
|
|
1966
|
+
});
|
|
1967
|
+
api.post("/projects/:id/file-mutations/unwrap-elements/*", async (c) => {
|
|
1968
|
+
const ctx = await resolveFileMutationContext(c, adapter, "unwrap-elements");
|
|
1969
|
+
if ("error" in ctx) return ctx.error;
|
|
1970
|
+
const parsed = await parseMutationBody(c);
|
|
1971
|
+
if ("error" in parsed) return parsed.error;
|
|
1972
|
+
let originalContent;
|
|
1973
|
+
try {
|
|
1974
|
+
originalContent = readFileSync3(ctx.absPath, "utf-8");
|
|
1975
|
+
} catch {
|
|
1976
|
+
return c.json({ error: "not found" }, 404);
|
|
1977
|
+
}
|
|
1978
|
+
const result = unwrapElementsFromHtml(originalContent, parsed.target);
|
|
1979
|
+
if (!result.unwrapped) {
|
|
1980
|
+
return c.json({ ok: false, changed: false, content: originalContent, path: ctx.filePath });
|
|
1981
|
+
}
|
|
1982
|
+
let cleaned = result.html;
|
|
1983
|
+
if (result.unwrappedGroupId && result.members && result.groupCenter) {
|
|
1984
|
+
cleaned = bakeGroupTransformIntoMembers(
|
|
1985
|
+
cleaned,
|
|
1986
|
+
result.unwrappedGroupId,
|
|
1987
|
+
result.members,
|
|
1988
|
+
result.groupCenter
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
if (result.unwrappedGroupId) {
|
|
1992
|
+
cleaned = stripGsapAnimationsForSelector(cleaned, `#${result.unwrappedGroupId}`);
|
|
1993
|
+
}
|
|
1994
|
+
return writeIfChanged(c, ctx.project.dir, ctx.filePath, ctx.absPath, originalContent, cleaned);
|
|
1995
|
+
});
|
|
1695
1996
|
api.post("/projects/:id/file-mutations/probe-element/*", async (c) => {
|
|
1696
1997
|
const ctx = await resolveFileMutationContext(c, adapter, "probe-element");
|
|
1697
1998
|
if ("error" in ctx) return ctx.error;
|