Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/core/src/compiler/htmlBundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { validateHyperframeHtmlContract } from "./staticGuard";
import { getHyperframeRuntimeScript } from "../generated/runtime-inline";
import { readDeclaredDefaults } from "../runtime/getVariables";
import { inlineSubCompositions } from "./inlineSubCompositions";
import { queryByAttr } from "../utils/cssSelector";
import { isSafePath, resolveWithinProject } from "../safePath.js";
import { HF_COLOR_GRADING_ATTR } from "../colorGrading";

Expand Down Expand Up @@ -277,7 +278,7 @@ function rewriteCssUrlsWithInlinedAssets(cssText: string, projectDir: string): s
}

function cssAttributeSelector(attr: string, value: string): string {
return `[${attr}="${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"]`;
return `[${attr}="${value}"]`;
}

function uniqueCompositionId(baseId: string, index: number): string {
Expand Down Expand Up @@ -624,7 +625,7 @@ export interface BundleOptions {
*/

function ensureExternalScriptTag(doc: Document, src: string): void {
if (doc.querySelector(`script[src="${src}"]`)) return;
if (queryByAttr(doc, "src", src, "script")) return;
const el = doc.createElement("script");
el.setAttribute("src", src);
doc.body.appendChild(el);
Expand Down Expand Up @@ -825,7 +826,7 @@ export async function bundleToSingleHtml(
continue;
}
}
if (!document.querySelector(`script[src="${extSrc}"]`)) {
if (!queryByAttr(document, "src", extSrc, "script")) {
const extScript = document.createElement("script");
extScript.setAttribute("src", extSrc);
document.body.appendChild(extScript);
Expand Down Expand Up @@ -857,7 +858,7 @@ export async function bundleToSingleHtml(
const hostIdentity = hostIdentityByElement.get(host);
const runtimeCompId = hostIdentity?.runtimeCompositionId || compId;
const innerDoc = parseHTMLContent(templateHtml);
const innerRoot = innerDoc.querySelector(`[data-composition-id="${compId}"]`);
const innerRoot = queryByAttr(innerDoc, "data-composition-id", compId);
const authoredRootId = innerRoot?.getAttribute("id")?.trim() || null;
const runtimeScope = runtimeCompId
? cssAttributeSelector("data-composition-id", runtimeCompId)
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/compiler/inlineSubCompositions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
rewriteCssAssetUrls,
rewriteInlineStyleAssetUrls,
} from "./rewriteSubCompPaths";
import { queryByAttr } from "../utils/cssSelector";
import {
scopeCssToComposition,
wrapInlineScriptWithErrorBoundary,
Expand Down Expand Up @@ -225,7 +226,7 @@ export function inlineSubCompositions(

// Find the inner composition root
const innerRoot = compId
? contentDoc.querySelector(`[data-composition-id="${compId}"]`)
? queryByAttr(contentDoc, "data-composition-id", compId)
: contentDoc.querySelector("[data-composition-id]");
const inferredCompId = innerRoot?.getAttribute("data-composition-id")?.trim() || "";
const authoredRootId = innerRoot?.getAttribute("id")?.trim() || null;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export {
rewriteCssAssetUrls,
} from "./compiler/rewriteSubCompPaths";
export { CSS_URL_RE, isNonRelativeUrl, isPathInside } from "./compiler/assetPaths";
export { queryByAttr } from "./utils/cssSelector";
export { decodeUrlPathVariants } from "./utils/urlPath";
export { parseAnimatedGifMetadata, type AnimatedGifMetadata } from "./media/gif";
export {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/parsers/htmlParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
import { validateCompositionGsap } from "./gsapSerialize";
import { ensureHfIds } from "./hfIds.js";
import { parseGsapScriptAcornForWrite } from "./gsapParserAcorn.js";
import { queryByAttr } from "../utils/cssSelector";
import { removeAnimationFromScript } from "./gsapWriterAcorn.js";
import type { ValidationResult } from "../core.types";

Expand Down Expand Up @@ -519,7 +520,7 @@ export function updateElementInHtml(
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");

const el = doc.getElementById(elementId) || doc.querySelector(`[data-name="${elementId}"]`);
const el = doc.getElementById(elementId) || queryByAttr(doc, "data-name", elementId);
if (!el) return html;

if (updates.startTime !== undefined) {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/runtime/picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ export function createPickerModule(deps: PickerModuleDeps): PickerModule {
const htmlEl = el as HTMLElement;
if (htmlEl.id) return `#${htmlEl.id}`;
const compositionId = el.getAttribute("data-composition-id");
if (compositionId) return `[data-composition-id="${compositionId}"]`;
if (compositionId) return `[data-composition-id="${CSS.escape(compositionId)}"]`;
const compositionSrc = el.getAttribute("data-composition-src");
if (compositionSrc) return `[data-composition-src="${compositionSrc}"]`;
if (compositionSrc) return `[data-composition-src="${CSS.escape(compositionSrc)}"]`;
const track = el.getAttribute("data-track-index");
if (track) return `[data-track-index="${track}"]`;
if (track) return `[data-track-index="${CSS.escape(track)}"]`;
const tag = el.tagName.toLowerCase();
const parent = el.parentElement;
if (!parent) return tag;
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/utils/cssSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// ponytail: queries DOM by exact attribute match without interpolating
// the value into a selector string — zero injection surface.
export function queryByAttr(
root: ParentNode,
attr: string,
value: string,
tag?: string,
): Element | null {
const selector = tag ? `${tag}[${attr}]` : `[${attr}]`;
for (const el of root.querySelectorAll(selector)) {
if (el.getAttribute(attr) === value) return el;
}
return null;
}
2 changes: 1 addition & 1 deletion packages/studio/src/components/editor/LayersPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const LayersPanel = memo(function LayersPanel() {
if (doc) {
const found =
(layer.id ? doc.getElementById(layer.id) : null) ??
(layer.hfId ? doc.querySelector(`[data-hf-id="${layer.hfId}"]`) : null) ??
(layer.hfId ? doc.querySelector(`[data-hf-id="${CSS.escape(layer.hfId)}"]`) : null) ??
doc.getElementById(layer.key);
if (found instanceof HTMLElement) el = found;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/studio/src/components/editor/domEditingElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export function findElementForSelection(
activeCompositionPath: string | null = null,
): HTMLElement | null {
if (selection.hfId) {
const byHfId = doc.querySelector(`[data-hf-id="${selection.hfId}"]`);
const byHfId = doc.querySelector(`[data-hf-id="${CSS.escape(selection.hfId)}"]`);
if (isHtmlElement(byHfId)) return byHfId;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/studio/src/player/lib/timelineDOM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ export function createTimelineElementFromManifestClip(params: {
if (clip.kind === "composition" && clip.compositionId) {
let resolvedSrc = clip.compositionSrc;
if (!resolvedSrc) {
hostEl = doc?.querySelector(`[data-composition-id="${clip.compositionId}"]`) ?? hostEl;
hostEl =
doc?.querySelector(`[data-composition-id="${CSS.escape(clip.compositionId)}"]`) ?? hostEl;
resolvedSrc =
hostEl?.getAttribute("data-composition-src") ??
hostEl?.getAttribute("data-composition-file") ??
Expand Down
4 changes: 2 additions & 2 deletions packages/studio/src/player/lib/timelineElementHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,8 @@ function nodeMatchesManifestClip(node: Element, clip: ClipManifestClip): boolean
function findTimelineDomNode(doc: Document, id: string): Element | null {
return (
doc.getElementById(id) ??
doc.querySelector(`[data-composition-id="${id}"]`) ??
doc.querySelector(`.${id}`) ??
doc.querySelector(`[data-composition-id="${CSS.escape(id)}"]`) ??
doc.querySelector(`.${CSS.escape(id)}`) ??
null
);
}
Expand Down
7 changes: 4 additions & 3 deletions packages/studio/src/player/lib/timelineIframeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,15 +295,16 @@ export function buildMissingCompositionElements(
let start = parseFloat(startAttr);
if (isNaN(start)) {
const ref =
doc.getElementById(startAttr) || doc.querySelector(`[data-composition-id="${startAttr}"]`);
doc.getElementById(startAttr) ||
doc.querySelector(`[data-composition-id="${CSS.escape(startAttr)}"]`);
if (ref) {
const refStartAttr = ref.getAttribute("data-start") ?? "0";
let refStart = parseFloat(refStartAttr);
// Recursively resolve one level of reference for the ref's own start
if (isNaN(refStart)) {
const refRef =
doc.getElementById(refStartAttr) ||
doc.querySelector(`[data-composition-id="${refStartAttr}"]`);
doc.querySelector(`[data-composition-id="${CSS.escape(refStartAttr)}"]`);
const rrStart = parseFloat(refRef?.getAttribute("data-start") ?? "0") || 0;
const rrCompId = refRef?.getAttribute("data-composition-id");
const rrDur =
Expand Down Expand Up @@ -400,7 +401,7 @@ export function buildMissingCompositionElements(
// Find the matching DOM host by element id or composition id
const host =
doc.getElementById(existing.id) ??
doc.querySelector(`[data-composition-id="${existing.id}"]`);
doc.querySelector(`[data-composition-id="${CSS.escape(existing.id)}"]`);
if (!host) return existing;
const compSrc =
host.getAttribute("data-composition-src") || host.getAttribute("data-composition-file");
Expand Down
Loading