mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
fix(sdk,studio): restore DOM edit cutover parity (#1565)
- Add splitStyleDeclarations with quote/paren-aware CSS parsing - Fix backslash escape handling inside quoted CSS string values - Close html-attribute safety gap in SDK cutover (event handlers, dangerous URIs) - Consolidate HTML attribute safety constants to core/utils/htmlAttrSafety.ts - Extract NON_HTML_CHILD_TAGS set for foreign-content decline gate - Add sdkCutoverParity test corpus (shorthand/longhand, mixed batches)
This commit is contained in:
@@ -8,6 +8,7 @@ import { trackStudioEvent } from "./studioTelemetry";
|
||||
import { markSelfWrite } from "../hooks/sdkSelfWriteRegistry";
|
||||
import { patchOpsToSdkEditOps } from "./sdkOpMapping";
|
||||
import { recordResolverParity, recordAnimationResolverParity } from "./sdkResolverShadow";
|
||||
import { isAllowedHtmlAttribute, isSafeAttributeValue } from "./htmlAttrSafety";
|
||||
|
||||
const CUTOVER_OP_TYPES = new Set<PatchOperation["type"]>([
|
||||
"inline-style",
|
||||
@@ -52,6 +53,54 @@ function mapsToReservedAttr(op: PatchOperation): boolean {
|
||||
return name !== null && RESERVED_CUTOVER_ATTRS.has(name.toLowerCase());
|
||||
}
|
||||
|
||||
// ─── html-attribute safety ───────────────────────────────────────────────────
|
||||
|
||||
function hasUnsafeHtmlAttributeOp(ops: PatchOperation[]): boolean {
|
||||
return ops.some(
|
||||
(op) =>
|
||||
op.type === "html-attribute" &&
|
||||
(!isAllowedHtmlAttribute(op.property) ||
|
||||
(op.value !== null && !isSafeAttributeValue(op.property, op.value))),
|
||||
);
|
||||
}
|
||||
|
||||
function hasTextContentOp(ops: PatchOperation[]): boolean {
|
||||
return ops.some((op) => op.type === "text-content");
|
||||
}
|
||||
|
||||
function targetChildren(target: unknown): unknown[] | null {
|
||||
if (!target || typeof target !== "object" || !("children" in target)) return null;
|
||||
const children = (target as { children?: unknown }).children;
|
||||
return Array.isArray(children) ? children : null;
|
||||
}
|
||||
|
||||
function elementTag(element: unknown): string | null {
|
||||
if (!element || typeof element !== "object" || !("tag" in element)) return null;
|
||||
const tag = (element as { tag?: unknown }).tag;
|
||||
return typeof tag === "string" ? tag.toLowerCase() : null;
|
||||
}
|
||||
|
||||
// Tags that are non-HTML namespace elements in a linkedom-parsed HTML body.
|
||||
// Mirrors the engine's `isHTMLElementTarget` (model.ts) which uses `instanceof
|
||||
// HTMLElement` — that runtime check catches the same set, but we can't use it
|
||||
// here because `target` is a plain SDK object, not a DOM Element. If linkedom
|
||||
// (or a future parser) surfaces additional foreign-content elements as
|
||||
// non-HTMLElement, add them here.
|
||||
const NON_HTML_CHILD_TAGS = new Set(["svg", "math"]);
|
||||
|
||||
function shouldDeclineTextCutoverForTarget(target: unknown, ops: PatchOperation[]): boolean {
|
||||
if (!hasTextContentOp(ops)) return false;
|
||||
const children = targetChildren(target);
|
||||
if (!children) return false;
|
||||
// Legacy patch-element replaces the whole element for multi-child targets and
|
||||
// for single non-HTML children. The SDK text patch stream stores a scalar
|
||||
// inverse, so those shapes cannot be made both byte-identical and undo-safe
|
||||
// here. Let the server path remain authoritative for them.
|
||||
if (children.length > 1) return true;
|
||||
const tag = elementTag(children[0]);
|
||||
return tag !== null && NON_HTML_CHILD_TAGS.has(tag);
|
||||
}
|
||||
|
||||
export function shouldUseSdkCutover(
|
||||
flagEnabled: boolean,
|
||||
hasSession: boolean,
|
||||
@@ -64,7 +113,8 @@ export function shouldUseSdkCutover(
|
||||
!!hfId &&
|
||||
ops.length > 0 &&
|
||||
ops.every((o) => CUTOVER_OP_TYPES.has(o.type)) &&
|
||||
!ops.some(mapsToReservedAttr)
|
||||
!ops.some(mapsToReservedAttr) &&
|
||||
!hasUnsafeHtmlAttributeOp(ops)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -186,7 +236,9 @@ export async function sdkCutoverPersist(
|
||||
if (!sdkSession) return false;
|
||||
const hfId = selection.hfId;
|
||||
if (!hfId) return false;
|
||||
if (!sdkSession.getElement(hfId)) return false;
|
||||
const target = sdkSession.getElement(hfId);
|
||||
if (!target) return false;
|
||||
if (shouldDeclineTextCutoverForTarget(target, ops)) return false;
|
||||
if (wrongCompositionFile(deps, targetPath)) return false;
|
||||
try {
|
||||
const before = sdkSession.serialize();
|
||||
|
||||
Reference in New Issue
Block a user