mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
304 lines
12 KiB
TypeScript
304 lines
12 KiB
TypeScript
import postcss, { type AtRule, type Node, type Rule } from "postcss";
|
|
|
|
function escapeRegExp(value: string): string {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
function escapeCssAttributeValue(value: string): string {
|
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
}
|
|
|
|
function scopeSelector(selector: string, scope: string, compositionId: string): string {
|
|
const selectorWithoutRootTiming = normalizeCompositionRootSelector(
|
|
selector,
|
|
scope,
|
|
compositionId,
|
|
);
|
|
const trimmed = selectorWithoutRootTiming.trim();
|
|
if (!trimmed) return selector;
|
|
if (/^(html|body|:root|\*)$/i.test(trimmed)) return selector;
|
|
const compositionIdPattern = new RegExp(
|
|
`\\[\\s*data-composition-id\\s*=\\s*(["'])${escapeRegExp(compositionId)}\\1\\s*\\]`,
|
|
"g",
|
|
);
|
|
if (compositionIdPattern.test(trimmed)) {
|
|
return selectorWithoutRootTiming.replace(compositionIdPattern, scope);
|
|
}
|
|
const leading = selectorWithoutRootTiming.match(/^\s*/)?.[0] ?? "";
|
|
const trailing = selectorWithoutRootTiming.match(/\s*$/)?.[0] ?? "";
|
|
return `${leading}${scope} ${trimmed}${trailing}`;
|
|
}
|
|
|
|
function normalizeCompositionRootSelector(
|
|
selector: string,
|
|
scope: string,
|
|
compositionId: string,
|
|
): string {
|
|
const quotedCompId = escapeRegExp(compositionId);
|
|
const compAttr = String.raw`\[\s*data-composition-id\s*=\s*(?:"${quotedCompId}"|'${quotedCompId}')\s*\]`;
|
|
const timingAttr = String.raw`\s*\[\s*data-(?:start|duration)\s*=\s*(?:"[^"]*"|'[^']*')\s*\]`;
|
|
return selector
|
|
.replace(new RegExp(`${compAttr}(?:${timingAttr})+`, "g"), scope)
|
|
.replace(new RegExp(`(?:${timingAttr})+${compAttr}`, "g"), scope);
|
|
}
|
|
|
|
const GLOBAL_AT_RULES = new Set(["keyframes", "-webkit-keyframes", "font-face"]);
|
|
|
|
function isAtRuleNode(node: Node["parent"]): node is AtRule {
|
|
return node?.type === "atrule";
|
|
}
|
|
|
|
function isInsideGlobalAtRule(rule: Rule): boolean {
|
|
let current: Node["parent"] = rule.parent;
|
|
while (current) {
|
|
if (isAtRuleNode(current) && GLOBAL_AT_RULES.has(current.name.toLowerCase())) {
|
|
return true;
|
|
}
|
|
current = current.parent;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function scopeCssToComposition(
|
|
css: string,
|
|
compositionId: string,
|
|
scopeSelectorOverride?: string,
|
|
): string {
|
|
const trimmedCompositionId = compositionId.trim();
|
|
if (!css || !trimmedCompositionId) return css;
|
|
const scope =
|
|
scopeSelectorOverride ||
|
|
`[data-composition-id="${escapeCssAttributeValue(trimmedCompositionId)}"]`;
|
|
const root = postcss.parse(css);
|
|
|
|
root.walkRules((rule) => {
|
|
if (isInsideGlobalAtRule(rule)) return;
|
|
rule.selectors = rule.selectors.map((selector) =>
|
|
scopeSelector(selector, scope, trimmedCompositionId),
|
|
);
|
|
});
|
|
|
|
return root.toResult({ map: false }).css;
|
|
}
|
|
|
|
export function wrapScopedCompositionScript(
|
|
source: string,
|
|
compositionId: string,
|
|
errorLabel = "[HyperFrames] composition script error:",
|
|
scopeSelectorOverride?: string,
|
|
timelineCompositionId = compositionId,
|
|
): string {
|
|
const compositionIdLiteral = JSON.stringify(compositionId);
|
|
const timelineCompositionIdLiteral = JSON.stringify(timelineCompositionId);
|
|
const errorLabelLiteral = JSON.stringify(errorLabel);
|
|
const escapedCompositionId = escapeRegExp(compositionId);
|
|
const scopeSelectorLiteral = JSON.stringify(scopeSelectorOverride ?? null);
|
|
const rootSelectorPatternLiteral = JSON.stringify(
|
|
String.raw`\[\s*data-composition-id\s*=\s*(?:"${escapedCompositionId}"|'${escapedCompositionId}')\s*\]`,
|
|
);
|
|
const timingSelectorPatternLiteral = JSON.stringify(
|
|
String.raw`\s*\[\s*data-(?:start|duration)\s*=\s*(?:"[^"]*"|'[^']*')\s*\]`,
|
|
);
|
|
return `(function(){
|
|
var __hfCompId = ${compositionIdLiteral};
|
|
var __hfTimelineCompId = ${timelineCompositionIdLiteral};
|
|
var __hfErrorLabel = ${errorLabelLiteral};
|
|
var __hfEscapeAttr = function(value) {
|
|
return (value + "").replace(/\\\\/g, "\\\\\\\\").replace(/"/g, "\\\\\\"");
|
|
};
|
|
var __hfRootSelector = ${scopeSelectorLiteral} || (__hfCompId
|
|
? '[data-composition-id="' + __hfEscapeAttr(__hfCompId) + '"]'
|
|
: "");
|
|
var __hfRoot = null;
|
|
var __hfRootSelectorPattern = ${rootSelectorPatternLiteral};
|
|
var __hfTimingSelectorPattern = ${timingSelectorPatternLiteral};
|
|
var __hfNormalizeSelector = function(selector) {
|
|
if (!__hfCompId || typeof selector !== "string") return selector;
|
|
return selector
|
|
.replace(new RegExp(__hfRootSelectorPattern + '(?:' + __hfTimingSelectorPattern + ')+', 'g'), __hfRootSelector)
|
|
.replace(new RegExp('(?:' + __hfTimingSelectorPattern + ')+' + __hfRootSelectorPattern, 'g'), __hfRootSelector);
|
|
};
|
|
var __hfFindRoot = function() {
|
|
if (!__hfRoot && __hfRootSelector) {
|
|
__hfRoot = window.document.querySelector(__hfRootSelector);
|
|
}
|
|
return __hfRoot;
|
|
};
|
|
var __hfContains = function(node) {
|
|
var root = __hfFindRoot();
|
|
return !root || node === root || root.contains(node);
|
|
};
|
|
var __hfQueryAll = function(selector) {
|
|
var root = __hfFindRoot();
|
|
if (!root || typeof selector !== "string") {
|
|
return window.document.querySelectorAll(selector);
|
|
}
|
|
return Array.prototype.filter.call(window.document.querySelectorAll(__hfNormalizeSelector(selector)), function(node) {
|
|
return __hfContains(node);
|
|
});
|
|
};
|
|
var __hfQueryOne = function(selector) {
|
|
var matches = __hfQueryAll(selector);
|
|
return matches[0] || null;
|
|
};
|
|
var __hfGetElementById = function(id) {
|
|
var found = window.document.getElementById(id);
|
|
if (found && __hfContains(found)) return found;
|
|
var root = __hfFindRoot();
|
|
if (!root) return found || null;
|
|
var idValue = id + "";
|
|
if (root.id === idValue) return root;
|
|
if (typeof root.querySelector !== "function") return null;
|
|
if (typeof CSS !== "undefined" && CSS && typeof CSS.escape === "function") {
|
|
try {
|
|
return root.querySelector("#" + CSS.escape(idValue)) || null;
|
|
} catch {}
|
|
}
|
|
try {
|
|
return root.querySelector('[id="' + __hfEscapeAttr(idValue) + '"]') || null;
|
|
} catch {}
|
|
return null;
|
|
};
|
|
var __hfScopedDocument = typeof Proxy === "function"
|
|
? new Proxy(window.document, {
|
|
get: function(target, prop, receiver) {
|
|
if (prop === "querySelector") return __hfQueryOne;
|
|
if (prop === "querySelectorAll") return __hfQueryAll;
|
|
if (prop === "getElementById") return __hfGetElementById;
|
|
var value = Reflect.get(target, prop, target);
|
|
return typeof value === "function" ? value.bind(target) : value;
|
|
},
|
|
})
|
|
: window.document;
|
|
var __hfTimelineRegistryProxy = null;
|
|
var __hfGetTimelineRegistry = function() {
|
|
window.__timelines = window.__timelines || {};
|
|
if (!__hfCompId || __hfCompId === __hfTimelineCompId || typeof Proxy !== "function") {
|
|
return window.__timelines;
|
|
}
|
|
if (!__hfTimelineRegistryProxy) {
|
|
__hfTimelineRegistryProxy = new Proxy(window.__timelines, {
|
|
get: function(target, prop, receiver) {
|
|
return Reflect.get(target, prop === __hfCompId ? __hfTimelineCompId : prop, target);
|
|
},
|
|
set: function(target, prop, value, receiver) {
|
|
return Reflect.set(target, prop === __hfCompId ? __hfTimelineCompId : prop, value, target);
|
|
},
|
|
});
|
|
}
|
|
return __hfTimelineRegistryProxy;
|
|
};
|
|
var __hfScopedWindow = typeof Proxy === "function"
|
|
? new Proxy(window, {
|
|
get: function(target, prop, receiver) {
|
|
if (prop === "__timelines") return __hfGetTimelineRegistry();
|
|
var value = Reflect.get(target, prop, target);
|
|
return typeof value === "function" ? value.bind(target) : value;
|
|
},
|
|
set: function(target, prop, value, receiver) {
|
|
if (prop === "__timelines") {
|
|
target.__timelines = value || {};
|
|
__hfTimelineRegistryProxy = null;
|
|
return true;
|
|
}
|
|
return Reflect.set(target, prop, value, target);
|
|
},
|
|
})
|
|
: window;
|
|
var __hfResolveGsapTarget = function(target) {
|
|
if (typeof target !== "string") return target;
|
|
return __hfQueryAll(target);
|
|
};
|
|
var __hfScopeTimeline = function(timeline) {
|
|
if (!timeline || timeline.__hfScopedCompositionRoot === __hfFindRoot()) return timeline;
|
|
["to", "from", "fromTo", "set"].forEach(function(method) {
|
|
var original = timeline[method];
|
|
if (typeof original !== "function") return;
|
|
timeline[method] = function(target) {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
args[0] = __hfResolveGsapTarget(target);
|
|
return original.apply(timeline, args);
|
|
};
|
|
});
|
|
try {
|
|
Object.defineProperty(timeline, "__hfScopedCompositionRoot", {
|
|
value: __hfFindRoot(),
|
|
configurable: true,
|
|
});
|
|
} catch {
|
|
// Best-effort: timelines coming from user code may have a frozen target
|
|
// or a non-extensible defineProperty path. Swallow — the scoped root
|
|
// is an enrichment, not a correctness invariant for playback.
|
|
}
|
|
return timeline;
|
|
};
|
|
var __hfBaseGsap = typeof gsap === "undefined" ? window.gsap : gsap;
|
|
var __hfScopedGsap = !__hfBaseGsap || typeof Proxy !== "function"
|
|
? __hfBaseGsap
|
|
: new Proxy(__hfBaseGsap, {
|
|
get: function(target, prop, receiver) {
|
|
if (prop === "timeline") {
|
|
return function() {
|
|
return __hfScopeTimeline(target.timeline.apply(target, arguments));
|
|
};
|
|
}
|
|
if (prop === "to" || prop === "from" || prop === "fromTo" || prop === "set") {
|
|
return function(firstArg) {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
args[0] = __hfResolveGsapTarget(firstArg);
|
|
return target[prop].apply(target, args);
|
|
};
|
|
}
|
|
if (prop === "utils" && target.utils && typeof Proxy === "function") {
|
|
return new Proxy(target.utils, {
|
|
get: function(utilsTarget, utilsProp, utilsReceiver) {
|
|
if (utilsProp === "toArray") {
|
|
return function(firstArg) {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
args[0] = __hfResolveGsapTarget(firstArg);
|
|
return utilsTarget.toArray.apply(utilsTarget, args);
|
|
};
|
|
}
|
|
if (utilsProp === "selector") {
|
|
return function(base) {
|
|
var baseEl = typeof base === "string" ? __hfQueryOne(base) : base;
|
|
var root = baseEl || __hfFindRoot();
|
|
return function(selector) {
|
|
if (!root || typeof selector !== "string") return [];
|
|
return Array.prototype.slice.call(root.querySelectorAll(selector));
|
|
};
|
|
};
|
|
}
|
|
var value = Reflect.get(utilsTarget, utilsProp, utilsTarget);
|
|
return typeof value === "function" ? value.bind(utilsTarget) : value;
|
|
},
|
|
});
|
|
}
|
|
var value = Reflect.get(target, prop, target);
|
|
return typeof value === "function" ? value.bind(target) : value;
|
|
},
|
|
});
|
|
var __hfBaseHyperframes = window.__hyperframes;
|
|
var __hfScopedHyperframes = !__hfBaseHyperframes
|
|
? __hfBaseHyperframes
|
|
: Object.assign({}, __hfBaseHyperframes, {
|
|
getVariables: function() {
|
|
var byComp = window.__hfVariablesByComp;
|
|
var scoped = byComp && __hfCompId ? byComp[__hfCompId] : null;
|
|
return scoped ? Object.assign({}, scoped) : {};
|
|
},
|
|
});
|
|
var __hfRun = function() {
|
|
try {
|
|
(function(document, gsap, window, __hyperframes) {
|
|
${source}
|
|
}).call(window, __hfScopedDocument, __hfScopedGsap, __hfScopedWindow, __hfScopedHyperframes);
|
|
} catch (_err) {
|
|
console.error(__hfErrorLabel, __hfCompId, _err);
|
|
}
|
|
};
|
|
__hfFindRoot();
|
|
__hfRun();
|
|
})();`;
|
|
}
|