mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +00:00
Run `fallow fix --auto-fixable` to remove `export` keywords from symbols fallow's reachability analysis identifies as unused. Keeps only the cases where the symbol is still referenced internally in its own file (so removing `export` doesn't surface a new oxlint `no-unused-vars` error). Result: fallow dead-code findings drop from 276 → 208 (68 fewer unused exports), with no behavior change — each symbol is still defined and used exactly the same way within its file. Reverted ~20 files where fallow's auto-fix would have created cascading "declared but never used" lint errors — those are cases where the symbol isn't used at all, and properly cleaning them up means deleting the declaration, not just dropping `export`. Better to land that as a separate, narrower PR rather than mixing it into a mechanical de-export. Also reverted four false positives where fallow missed real consumers: - `captureCost.ts` (renderOrchestrator has two separate import blocks from the same module; fallow only saw the first) - `propertyPanelHelpers.ts`, `domEditingLayers.ts` (real internal uses fallow's reachability missed) - `render.ts` (functions imported via `await import()` dynamic import, which fallow's static analysis doesn't follow) Test plan: bun run --filter '*' typecheck (clean), oxlint + oxfmt clean, cli/core/studio/engine vitest suites pass (335 + 917 + 576 + 605 tests).
304 lines
11 KiB
TypeScript
304 lines
11 KiB
TypeScript
/**
|
|
* Low-level helpers for building and identifying TimelineElement objects.
|
|
*
|
|
* Covers: duration reading, media-element metadata extraction, selector/key/
|
|
* identity builders, DOM node lookup, and implicit layer detection. These are
|
|
* intentionally dependency-free (no store, no hooks) so they can be used in
|
|
* both the React hook and test environments.
|
|
*/
|
|
|
|
import type { TimelineElement } from "../store/playerStore";
|
|
import type { ClipManifestClip } from "./playbackTypes";
|
|
import { isFinitePositive } from "./playbackAdapter";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Duration attribute helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function readDurationAttribute(el: Element | null | undefined): number {
|
|
if (!el) return 0;
|
|
const duration =
|
|
Number.parseFloat(el.getAttribute("data-duration") ?? "") ||
|
|
Number.parseFloat(el.getAttribute("data-hf-authored-duration") ?? "");
|
|
return isFinitePositive(duration) ? duration : 0;
|
|
}
|
|
|
|
export function readTimelineDurationFromDocument(doc: Document | null | undefined): number {
|
|
if (!doc) return 0;
|
|
const rootDuration = readDurationAttribute(doc.querySelector("[data-composition-id]"));
|
|
if (rootDuration > 0) return rootDuration;
|
|
|
|
let maxEnd = 0;
|
|
for (const node of Array.from(doc.querySelectorAll("[data-start]"))) {
|
|
const start = Number.parseFloat(node.getAttribute("data-start") ?? "");
|
|
const duration = readDurationAttribute(node);
|
|
if (!Number.isFinite(start) || start < 0 || duration <= 0) continue;
|
|
maxEnd = Math.max(maxEnd, start + duration);
|
|
}
|
|
return maxEnd;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DOM element type guards
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function isHtmlElement(el: Element): el is HTMLElement {
|
|
const HtmlElementCtor = el.ownerDocument.defaultView?.HTMLElement ?? globalThis.HTMLElement;
|
|
return typeof HtmlElementCtor !== "undefined" && el instanceof HtmlElementCtor;
|
|
}
|
|
|
|
export function resolveMediaElement(el: Element): HTMLMediaElement | HTMLImageElement | null {
|
|
const win = el.ownerDocument.defaultView ?? window;
|
|
const MediaElementCtor = win.HTMLMediaElement ?? globalThis.HTMLMediaElement;
|
|
const ImageElementCtor = win.HTMLImageElement ?? globalThis.HTMLImageElement;
|
|
if (el instanceof MediaElementCtor || el instanceof ImageElementCtor) return el;
|
|
const candidate = el.querySelector("video, audio, img");
|
|
return candidate instanceof MediaElementCtor || candidate instanceof ImageElementCtor
|
|
? candidate
|
|
: null;
|
|
}
|
|
|
|
export function applyMediaMetadataFromElement(entry: TimelineElement, el: Element): void {
|
|
const mediaStartAttr = el.getAttribute("data-playback-start")
|
|
? "playback-start"
|
|
: el.getAttribute("data-media-start")
|
|
? "media-start"
|
|
: undefined;
|
|
const mediaStartValue =
|
|
el.getAttribute("data-playback-start") ?? el.getAttribute("data-media-start");
|
|
if (mediaStartValue != null) {
|
|
const playbackStart = parseFloat(mediaStartValue);
|
|
if (Number.isFinite(playbackStart)) entry.playbackStart = playbackStart;
|
|
}
|
|
if (mediaStartAttr) entry.playbackStartAttr = mediaStartAttr;
|
|
|
|
const mediaEl = resolveMediaElement(el);
|
|
if (!mediaEl) return;
|
|
|
|
entry.tag = mediaEl.tagName.toLowerCase();
|
|
const src = mediaEl.getAttribute("src");
|
|
if (src) entry.src = src;
|
|
|
|
const win = mediaEl.ownerDocument.defaultView ?? window;
|
|
const MediaElementCtor = win.HTMLMediaElement ?? globalThis.HTMLMediaElement;
|
|
if (typeof MediaElementCtor === "undefined" || !(mediaEl instanceof MediaElementCtor)) return;
|
|
|
|
const sourceDurationAttr =
|
|
el.getAttribute("data-source-duration") ?? mediaEl.getAttribute("data-source-duration");
|
|
const sourceDuration = sourceDurationAttr ? parseFloat(sourceDurationAttr) : mediaEl.duration;
|
|
if (Number.isFinite(sourceDuration) && sourceDuration > 0) {
|
|
entry.sourceDuration = sourceDuration;
|
|
}
|
|
|
|
const playbackRate = mediaEl.defaultPlaybackRate;
|
|
if (Number.isFinite(playbackRate) && playbackRate > 0) {
|
|
entry.playbackRate = playbackRate;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Label helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function getTimelineElementDisplayLabel(input: {
|
|
id?: string | null;
|
|
label?: string | null;
|
|
tag?: string | null;
|
|
}): string {
|
|
const label = input.label?.trim();
|
|
if (label) return label;
|
|
const id = input.id?.trim();
|
|
if (id) return id;
|
|
const tag = input.tag?.trim().toLowerCase();
|
|
return tag ? `${tag} clip` : "Timeline clip";
|
|
}
|
|
|
|
const IMPLICIT_TIMELINE_LAYER_SKIP_TAGS = new Set([
|
|
"base",
|
|
"link",
|
|
"meta",
|
|
"noscript",
|
|
"script",
|
|
"style",
|
|
"template",
|
|
]);
|
|
|
|
function humanizeTimelineIdentifier(value: string): string {
|
|
return value
|
|
.trim()
|
|
.replace(/[_-]+/g, " ")
|
|
.replace(/\s+/g, " ")
|
|
.replace(/\b\w/g, (match) => match.toUpperCase());
|
|
}
|
|
|
|
export function getImplicitTimelineLayerLabel(el: HTMLElement): string {
|
|
const explicitLabel =
|
|
el.getAttribute("data-timeline-label") ??
|
|
el.getAttribute("data-label") ??
|
|
el.getAttribute("aria-label");
|
|
if (explicitLabel?.trim()) return explicitLabel.trim();
|
|
if (el.id.trim()) return humanizeTimelineIdentifier(el.id);
|
|
const classes = el.className.split(/\s+/).filter(Boolean);
|
|
const className = classes.find((value) => value !== "clip") ?? classes[0];
|
|
if (className) return humanizeTimelineIdentifier(className);
|
|
return getTimelineElementDisplayLabel({ tag: el.tagName });
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Selector / identity / key builders
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function getTimelineElementSelector(el: Element): string | undefined {
|
|
if (isHtmlElement(el) && el.id) return `#${el.id}`;
|
|
const compId = el.getAttribute("data-composition-id");
|
|
if (compId) return `[data-composition-id="${compId}"]`;
|
|
if (isHtmlElement(el)) {
|
|
const classes = el.className.split(/\s+/).filter(Boolean);
|
|
const firstClass = classes.find((className) => className !== "clip") ?? classes[0];
|
|
if (firstClass) return `.${firstClass}`;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function getTimelineElementSourceFile(el: Element): string | undefined {
|
|
const ownerRoot = el.parentElement?.closest("[data-composition-id]");
|
|
return (
|
|
ownerRoot?.getAttribute("data-composition-file") ??
|
|
ownerRoot?.getAttribute("data-composition-src") ??
|
|
undefined
|
|
);
|
|
}
|
|
|
|
export function getTimelineElementSelectorIndex(
|
|
doc: Document,
|
|
el: Element,
|
|
selector: string | undefined,
|
|
): number | undefined {
|
|
if (!selector || selector.startsWith("#") || selector.startsWith("[data-composition-id=")) {
|
|
return undefined;
|
|
}
|
|
|
|
try {
|
|
const matches = Array.from(doc.querySelectorAll(selector));
|
|
const matchIndex = matches.indexOf(el);
|
|
return matchIndex >= 0 ? matchIndex : undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export function buildTimelineElementKey(params: {
|
|
id: string;
|
|
fallbackIndex: number;
|
|
domId?: string;
|
|
selector?: string;
|
|
selectorIndex?: number;
|
|
sourceFile?: string;
|
|
}): string {
|
|
const scope = params.sourceFile ?? "index.html";
|
|
if (params.domId) return `${scope}#${params.domId}`;
|
|
if (params.selector) return `${scope}:${params.selector}:${params.selectorIndex ?? 0}`;
|
|
return `${scope}:${params.id}:${params.fallbackIndex}`;
|
|
}
|
|
|
|
export function buildTimelineElementIdentity(params: {
|
|
preferredId?: string | null;
|
|
label: string;
|
|
fallbackIndex: number;
|
|
domId?: string;
|
|
selector?: string;
|
|
selectorIndex?: number;
|
|
sourceFile?: string;
|
|
}): { id: string; key: string } {
|
|
const id =
|
|
params.preferredId?.trim() ||
|
|
buildTimelineElementKey({
|
|
id: params.label,
|
|
fallbackIndex: params.fallbackIndex,
|
|
domId: params.domId,
|
|
selector: params.selector,
|
|
selectorIndex: params.selectorIndex,
|
|
sourceFile: params.sourceFile,
|
|
});
|
|
const key = buildTimelineElementKey({
|
|
id,
|
|
fallbackIndex: params.fallbackIndex,
|
|
domId: params.domId,
|
|
selector: params.selector,
|
|
selectorIndex: params.selectorIndex,
|
|
sourceFile: params.sourceFile,
|
|
});
|
|
return { id, key };
|
|
}
|
|
|
|
export function getTimelineElementIdentity(element: TimelineElement): string {
|
|
return element.key ?? element.id;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DOM node querying
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function getTimelineDomNodes(doc: Document): Element[] {
|
|
const rootComp = doc.querySelector("[data-composition-id]");
|
|
return Array.from(doc.querySelectorAll("[data-start]")).filter((node) => node !== rootComp);
|
|
}
|
|
|
|
function numbersNearlyEqual(a: number, b: number): boolean {
|
|
return Math.abs(a - b) < 0.001;
|
|
}
|
|
|
|
function nodeMatchesManifestClip(node: Element, clip: ClipManifestClip): boolean {
|
|
const tagName = clip.tagName?.toLowerCase();
|
|
if (tagName && node.tagName.toLowerCase() !== tagName) return false;
|
|
|
|
const start = Number.parseFloat(node.getAttribute("data-start") ?? "");
|
|
if (Number.isFinite(start) && !numbersNearlyEqual(start, clip.start)) return false;
|
|
|
|
const duration = Number.parseFloat(node.getAttribute("data-duration") ?? "");
|
|
if (Number.isFinite(duration) && !numbersNearlyEqual(duration, clip.duration)) return false;
|
|
|
|
const track = Number.parseInt(node.getAttribute("data-track-index") ?? "", 10);
|
|
if (Number.isFinite(track) && track !== clip.track) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
function findTimelineDomNode(doc: Document, id: string): Element | null {
|
|
return (
|
|
doc.getElementById(id) ??
|
|
doc.querySelector(`[data-composition-id="${id}"]`) ??
|
|
doc.querySelector(`.${id}`) ??
|
|
null
|
|
);
|
|
}
|
|
|
|
export function findTimelineDomNodeForClip(
|
|
doc: Document,
|
|
clip: ClipManifestClip,
|
|
fallbackIndex: number,
|
|
usedNodes = new Set<Element>(),
|
|
): Element | null {
|
|
const byIdentity = clip.id ? findTimelineDomNode(doc, clip.id) : null;
|
|
if (byIdentity && !usedNodes.has(byIdentity)) return byIdentity;
|
|
|
|
const candidates = getTimelineDomNodes(doc).filter((node) => !usedNodes.has(node));
|
|
const exact = candidates.find((node) => nodeMatchesManifestClip(node, clip));
|
|
if (exact) return exact;
|
|
|
|
return candidates[fallbackIndex] ?? null;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Implicit layer detection
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function isImplicitTimelineLayerCandidate(root: Element, el: Element): el is HTMLElement {
|
|
if (!isHtmlElement(el)) return false;
|
|
if (el.parentElement !== root) return false;
|
|
const tagName = el.tagName.toLowerCase();
|
|
if (IMPLICIT_TIMELINE_LAYER_SKIP_TAGS.has(tagName)) return false;
|
|
if (el.hasAttribute("data-start") || el.hasAttribute("data-track-index")) return false;
|
|
return Boolean(getTimelineElementSelector(el));
|
|
}
|