@hyperframes/sdk 0.8.190 → 0.8.192
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/engine/apply-patches.d.ts +6 -1
- package/dist/engine/apply-patches.js +17 -4
- package/dist/engine/model.d.ts +8 -0
- package/dist/engine/scriptLookups.d.ts +1 -0
- package/dist/engine/scriptLookups.js +48 -0
- package/dist/engine/serialize.d.ts +1 -1
- package/dist/engine/serialize.js +53 -5
- package/dist/session.js +8 -1
- package/package.json +3 -3
|
@@ -16,4 +16,9 @@ import type { ParsedDocument } from "./model.js";
|
|
|
16
16
|
* property is a no-op in applyOne, so this is safe against fresh-base misses.)
|
|
17
17
|
*/
|
|
18
18
|
export declare function applyOverrideSet(parsed: ParsedDocument, overrides: OverrideSet): void;
|
|
19
|
-
|
|
19
|
+
interface ApplyOptions {
|
|
20
|
+
/** Keep a copy of each removed element an inline script looks up (stored deletes replayed onto a base). */
|
|
21
|
+
keepScriptTargets?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function applyPatchesToDocument(parsed: ParsedDocument, patches: readonly JsonPatchOp[], options?: ApplyOptions): void;
|
|
24
|
+
export {};
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { findById, findRoot, declarationElement, setElementStyles, setOwnText, setGsapScript, setStyleSheet, } from "./model.js";
|
|
11
11
|
import { keyToPath, stylePath } from "./patches.js";
|
|
12
|
+
import { scriptLooksUp } from "./scriptLookups.js";
|
|
12
13
|
import { writeVariableDefault, clearVariableDefault, writeVariableDeclaration, removeVariableDeclarationEntry, } from "./variableModel.js";
|
|
13
14
|
function isRawDeclarationEntry(value) {
|
|
14
15
|
return (typeof value === "object" &&
|
|
@@ -120,9 +121,19 @@ export function applyOverrideSet(parsed, overrides) {
|
|
|
120
121
|
patches.push({ op: "remove", path: stylePath(rootId, `--${key.slice("var.".length)}`) });
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
|
-
applyPatchesToDocument(parsed, patches);
|
|
124
|
+
applyPatchesToDocument(parsed, patches, { keepScriptTargets: true });
|
|
124
125
|
}
|
|
125
|
-
|
|
126
|
+
/** Keep a deleted node a script looks up: serialize writes it back hidden, so the script finds it. */
|
|
127
|
+
function keepDelete(parsed, el) {
|
|
128
|
+
const parent = el.parentElement;
|
|
129
|
+
(parsed.keptDeletes ??= []).push({
|
|
130
|
+
hfId: el.getAttribute("data-hf-id") ?? "",
|
|
131
|
+
html: el.outerHTML,
|
|
132
|
+
parentId: parent?.getAttribute("data-hf-id") ?? null,
|
|
133
|
+
siblingIndex: parent ? Array.from(parent.children).indexOf(el) : 0,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
export function applyPatchesToDocument(parsed, patches, options = {}) {
|
|
126
137
|
for (const patch of patches) {
|
|
127
138
|
const p = parsePath(patch.path);
|
|
128
139
|
if (!p)
|
|
@@ -130,11 +141,11 @@ export function applyPatchesToDocument(parsed, patches) {
|
|
|
130
141
|
if (p.id && patch.path.startsWith("/elements/")) {
|
|
131
142
|
p.id = p.id.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
132
143
|
}
|
|
133
|
-
applyOne(parsed, patch, p);
|
|
144
|
+
applyOne(parsed, patch, p, options);
|
|
134
145
|
}
|
|
135
146
|
}
|
|
136
147
|
// fallow-ignore-next-line complexity
|
|
137
|
-
function applyOne(parsed, patch, p) {
|
|
148
|
+
function applyOne(parsed, patch, p, options) {
|
|
138
149
|
switch (p.type) {
|
|
139
150
|
case "style": {
|
|
140
151
|
const el = p.id ? findById(parsed.document, p.id) : null;
|
|
@@ -220,6 +231,8 @@ function applyOne(parsed, patch, p) {
|
|
|
220
231
|
return;
|
|
221
232
|
if (patch.op === "remove") {
|
|
222
233
|
const el = findById(parsed.document, p.id);
|
|
234
|
+
if (el && options.keepScriptTargets && scriptLooksUp(parsed.document, el))
|
|
235
|
+
keepDelete(parsed, el);
|
|
223
236
|
el?.remove();
|
|
224
237
|
}
|
|
225
238
|
else if (patch.op === "add" && patch.value) {
|
package/dist/engine/model.d.ts
CHANGED
|
@@ -10,6 +10,14 @@ export interface ParsedDocument {
|
|
|
10
10
|
wrapped: boolean;
|
|
11
11
|
/** ensureHfIds-stamped original HTML — used as fallback / diff base. */
|
|
12
12
|
stamped: string;
|
|
13
|
+
/** Replayed deletes a script still looks up: left out of the live document, written back hidden by serialize. */
|
|
14
|
+
keptDeletes?: KeptDelete[];
|
|
15
|
+
}
|
|
16
|
+
export interface KeptDelete {
|
|
17
|
+
hfId: string;
|
|
18
|
+
html: string;
|
|
19
|
+
parentId: string | null;
|
|
20
|
+
siblingIndex: number;
|
|
13
21
|
}
|
|
14
22
|
export declare function parseMutable(html: string): ParsedDocument;
|
|
15
23
|
export declare function findById(document: Document, id: string): Element | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function scriptLooksUp(document: Document, el: Element): boolean;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Whether an inline script could look the element up. A replayed delete of such a node is hidden,
|
|
2
|
+
// not removed: a missing node makes the script throw before the timeline registers.
|
|
3
|
+
const LITERAL_LOOKUP = /(getElementById|getElementsByClassName|getElementsByTagName|querySelector(?:All)?)\s*\(\s*(["'`])([\s\S]*?)\2\s*(\S?)/g;
|
|
4
|
+
// A lookup whose argument is a variable or an expression: it can name any node.
|
|
5
|
+
const DYNAMIC_LOOKUP = /(?:getElementById|getElementsByClassName|getElementsByTagName|querySelector(?:All)?)\s*\(\s*(?!["'`)\s])/;
|
|
6
|
+
const PLACEHOLDER = /\$\{[^}]*\}/;
|
|
7
|
+
function idPattern(text) {
|
|
8
|
+
const parts = text.split(new RegExp(PLACEHOLDER.source, "g"));
|
|
9
|
+
const escaped = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
10
|
+
return new RegExp(`^${escaped.join("[\\w-]+")}$`);
|
|
11
|
+
}
|
|
12
|
+
function matchesSelector(el, selector) {
|
|
13
|
+
if (PLACEHOLDER.test(selector))
|
|
14
|
+
return true;
|
|
15
|
+
try {
|
|
16
|
+
return el.matches(selector) || el.querySelector(selector) !== null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function lookupReaches(el, method, argument) {
|
|
23
|
+
if (PLACEHOLDER.test(argument))
|
|
24
|
+
return true;
|
|
25
|
+
const nodes = [el, ...Array.from(el.querySelectorAll("*"))];
|
|
26
|
+
if (method === "getElementById") {
|
|
27
|
+
const pattern = idPattern(argument);
|
|
28
|
+
return nodes.some((node) => pattern.test(node.getAttribute("id") ?? "\0"));
|
|
29
|
+
}
|
|
30
|
+
if (method === "getElementsByClassName") {
|
|
31
|
+
const wanted = argument.split(/\s+/).filter(Boolean);
|
|
32
|
+
return nodes.some((node) => wanted.every((name) => node.classList.contains(name)));
|
|
33
|
+
}
|
|
34
|
+
if (method === "getElementsByTagName") {
|
|
35
|
+
return argument === "*" || nodes.some((node) => node.localName === argument.toLowerCase());
|
|
36
|
+
}
|
|
37
|
+
return matchesSelector(el, argument);
|
|
38
|
+
}
|
|
39
|
+
export function scriptLooksUp(document, el) {
|
|
40
|
+
const scripts = Array.from(document.querySelectorAll("script:not([src])"), (s) => s.textContent ?? "").join("\n");
|
|
41
|
+
if (DYNAMIC_LOOKUP.test(scripts))
|
|
42
|
+
return true;
|
|
43
|
+
for (const [, method, , argument, next] of scripts.matchAll(LITERAL_LOOKUP)) {
|
|
44
|
+
if (next !== ")" || lookupReaches(el, method ?? "", argument ?? ""))
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Phase 3a: generates from the live DOM. The DOM IS the mutable state.
|
|
5
5
|
* Phase 3b: GSAP script section will use the meriyah/offset-splice path once available.
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import { type ParsedDocument } from "./model.js";
|
|
8
8
|
/**
|
|
9
9
|
* Serialize the live document back to HTML.
|
|
10
10
|
*
|
package/dist/engine/serialize.js
CHANGED
|
@@ -4,6 +4,52 @@
|
|
|
4
4
|
* Phase 3a: generates from the live DOM. The DOM IS the mutable state.
|
|
5
5
|
* Phase 3b: GSAP script section will use the meriyah/offset-splice path once available.
|
|
6
6
|
*/
|
|
7
|
+
import { findById, getElementStyles, setElementStyles, } from "./model.js";
|
|
8
|
+
// A kept delete is not a clip: without these the renderer would still schedule its media and stretch the film.
|
|
9
|
+
const CLIP_ATTRIBUTES = ["data-start", "data-duration", "data-end", "data-track-index"];
|
|
10
|
+
// A hidden copy still gets its media fetched, and extracted when it is a video, so it keeps no source.
|
|
11
|
+
const MEDIA_SOURCE_ATTRIBUTES = ["src", "srcset", "poster"];
|
|
12
|
+
/** The deleted node, hidden and no longer a clip, so a script that looks it up still finds it. */
|
|
13
|
+
function hiddenCopy(doc, kept) {
|
|
14
|
+
const holder = doc.createElement("div");
|
|
15
|
+
holder.innerHTML = kept.html;
|
|
16
|
+
const node = holder.firstElementChild;
|
|
17
|
+
if (!node)
|
|
18
|
+
return null;
|
|
19
|
+
for (const el of [node, ...Array.from(node.querySelectorAll("*"))]) {
|
|
20
|
+
for (const name of CLIP_ATTRIBUTES)
|
|
21
|
+
el.removeAttribute(name);
|
|
22
|
+
for (const name of MEDIA_SOURCE_ATTRIBUTES)
|
|
23
|
+
el.removeAttribute(name);
|
|
24
|
+
const remote = Object.entries(getElementStyles(el)).filter(([, v]) => v.includes("url("));
|
|
25
|
+
if (remote.length > 0)
|
|
26
|
+
setElementStyles(el, Object.fromEntries(remote.map(([k]) => [k, null])));
|
|
27
|
+
}
|
|
28
|
+
setElementStyles(node, { display: "none" });
|
|
29
|
+
node.setAttribute("data-hidden", "");
|
|
30
|
+
return node;
|
|
31
|
+
}
|
|
32
|
+
/** Put the hidden copy back where the node was; null when an undo already made it live or its parent is gone. */
|
|
33
|
+
function placeKeptDelete(doc, kept) {
|
|
34
|
+
if (kept.hfId && findById(doc, kept.hfId))
|
|
35
|
+
return null;
|
|
36
|
+
const node = hiddenCopy(doc, kept);
|
|
37
|
+
const parent = kept.parentId ? findById(doc, kept.parentId) : doc.body;
|
|
38
|
+
if (!node || !parent)
|
|
39
|
+
return null;
|
|
40
|
+
parent.insertBefore(node, parent.children[kept.siblingIndex] ?? null);
|
|
41
|
+
return node;
|
|
42
|
+
}
|
|
43
|
+
function withKeptDeletes(parsed, serialize) {
|
|
44
|
+
const placed = (parsed.keptDeletes ?? []).flatMap((kept) => placeKeptDelete(parsed.document, kept) ?? []);
|
|
45
|
+
try {
|
|
46
|
+
return serialize();
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
for (const node of placed)
|
|
50
|
+
node.remove();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
7
53
|
/**
|
|
8
54
|
* Serialize the live document back to HTML.
|
|
9
55
|
*
|
|
@@ -11,9 +57,11 @@
|
|
|
11
57
|
* If the original input had a full HTML shell (wrapped=false), returns the full document.
|
|
12
58
|
*/
|
|
13
59
|
export function serializeDocument(parsed) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
60
|
+
return withKeptDeletes(parsed, () => {
|
|
61
|
+
const doc = parsed.document;
|
|
62
|
+
if (parsed.wrapped) {
|
|
63
|
+
return doc.body.innerHTML ?? "";
|
|
64
|
+
}
|
|
65
|
+
return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
|
|
66
|
+
});
|
|
19
67
|
}
|
package/dist/session.js
CHANGED
|
@@ -49,6 +49,7 @@ class CompositionImpl {
|
|
|
49
49
|
selectionHandlers = [];
|
|
50
50
|
patchHandlers = [];
|
|
51
51
|
errorHandlers = [];
|
|
52
|
+
persistErrorUnsubscribes = new Set();
|
|
52
53
|
previewSelectionUnsubscribe = null;
|
|
53
54
|
/** Attached by openComposition() for standalone mode. */
|
|
54
55
|
historyModule = null;
|
|
@@ -626,9 +627,12 @@ class CompositionImpl {
|
|
|
626
627
|
const typedH = h;
|
|
627
628
|
this.errorHandlers.push(typedH);
|
|
628
629
|
const offPersist = this.persist?.on("persist:error", typedH);
|
|
630
|
+
if (offPersist)
|
|
631
|
+
this.persistErrorUnsubscribes.add(offPersist);
|
|
629
632
|
return () => {
|
|
630
633
|
this.errorHandlers = this.errorHandlers.filter((x) => x !== typedH);
|
|
631
|
-
offPersist
|
|
634
|
+
if (offPersist && this.persistErrorUnsubscribes.delete(offPersist))
|
|
635
|
+
offPersist();
|
|
632
636
|
};
|
|
633
637
|
}
|
|
634
638
|
return () => { };
|
|
@@ -679,6 +683,9 @@ class CompositionImpl {
|
|
|
679
683
|
dispose() {
|
|
680
684
|
this.previewSelectionUnsubscribe?.();
|
|
681
685
|
this.previewSelectionUnsubscribe = null;
|
|
686
|
+
for (const unsubscribe of this.persistErrorUnsubscribes)
|
|
687
|
+
unsubscribe();
|
|
688
|
+
this.persistErrorUnsubscribes.clear();
|
|
682
689
|
this.persistQueueModule?.dispose();
|
|
683
690
|
this.historyModule?.dispose();
|
|
684
691
|
this.changeHandlers = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/sdk",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.192",
|
|
4
4
|
"description": "Headless, framework-neutral HyperFrames composition editing engine",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"linkedom": "^0.18.12",
|
|
47
|
-
"@hyperframes/
|
|
48
|
-
"@hyperframes/
|
|
47
|
+
"@hyperframes/core": "0.8.192",
|
|
48
|
+
"@hyperframes/parsers": "0.8.192"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^25.0.10",
|