@hyperframes/studio 0.8.29 → 0.8.31
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/assets/{hyperframes-player-DMIVO02i.js → hyperframes-player-0QzsrRGz.js} +1 -1
- package/dist/assets/{index-CAyl2GL2.js → index-CfQexXc6.js} +5 -5
- package/dist/assets/{index-ok1knGut.js → index-DdTv4TqQ.js} +1 -1
- package/dist/assets/{index-Cerhq2OW.js → index-Dyhj0geW.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +9 -9
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/editor/colorGradingScopePatch.test.ts +20 -0
- package/src/components/editor/colorGradingScopePatch.ts +1 -1
- package/src/utils/htmlEditor.test.ts +42 -0
- package/src/utils/htmlEditor.ts +4 -4
- package/src/utils/sourcePatcher.test.ts +33 -0
- package/src/utils/sourcePatcher.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.31",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"gsap": "^3.13.0",
|
|
49
49
|
"marked": "^14.1.4",
|
|
50
50
|
"mediabunny": "^1.45.3",
|
|
51
|
-
"@hyperframes/
|
|
52
|
-
"@hyperframes/
|
|
53
|
-
"@hyperframes/player": "0.8.
|
|
54
|
-
"@hyperframes/sdk": "0.8.
|
|
55
|
-
"@hyperframes/studio-server": "0.8.
|
|
51
|
+
"@hyperframes/parsers": "0.8.31",
|
|
52
|
+
"@hyperframes/core": "0.8.31",
|
|
53
|
+
"@hyperframes/player": "0.8.31",
|
|
54
|
+
"@hyperframes/sdk": "0.8.31",
|
|
55
|
+
"@hyperframes/studio-server": "0.8.31"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/react": "19",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"vite": "^6.4.2",
|
|
70
70
|
"vitest": "^3.2.4",
|
|
71
71
|
"zustand": "^5.0.0",
|
|
72
|
-
"@hyperframes/producer": "0.8.
|
|
72
|
+
"@hyperframes/producer": "0.8.31"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"react": "19",
|
|
@@ -2,6 +2,26 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { patchMediaColorGradingInHtml } from "./colorGradingScopePatch";
|
|
3
3
|
|
|
4
4
|
describe("patchMediaColorGradingInHtml", () => {
|
|
5
|
+
it.each(['"', "'"])("replaces and removes long grading values quoted with %s", (quote) => {
|
|
6
|
+
const otherQuote = quote === '"' ? "'" : '"';
|
|
7
|
+
for (const value of [
|
|
8
|
+
"",
|
|
9
|
+
"line1\nline2",
|
|
10
|
+
"a".repeat(100_000),
|
|
11
|
+
`data-color-grading=${otherQuote}a`.repeat(10_000),
|
|
12
|
+
]) {
|
|
13
|
+
const html = `<img alt="kept"\tDATA-COLOR-GRADING=${quote}${value}${quote} />`;
|
|
14
|
+
expect(patchMediaColorGradingInHtml(html, "new")).toEqual({
|
|
15
|
+
html: '<img alt="kept" data-color-grading="new" />',
|
|
16
|
+
count: 1,
|
|
17
|
+
});
|
|
18
|
+
expect(patchMediaColorGradingInHtml(html, null)).toEqual({
|
|
19
|
+
html: '<img alt="kept" />',
|
|
20
|
+
count: 1,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
5
25
|
it("adds color grading to video and image tags only", () => {
|
|
6
26
|
const { html, count } = patchMediaColorGradingInHtml(
|
|
7
27
|
`<div><video id="v"></video><img id="i" /><audio id="a"></audio></div>`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const MEDIA_TAG_RE = /<\s*(video|img)\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi;
|
|
2
|
-
const COLOR_GRADING_ATTR_RE = /\sdata-color-grading=(["'
|
|
2
|
+
const COLOR_GRADING_ATTR_RE = /\sdata-color-grading=(?:"[^"]*"|'[^']*')/i;
|
|
3
3
|
const IGNORED_HTML_RANGE_RE = /<!--[\s\S]*?-->|<(script|style)\b[\s\S]*?<\/\1\s*>/gi;
|
|
4
4
|
|
|
5
5
|
interface TextRange {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { mergeStyleIntoTag } from "./htmlEditor";
|
|
3
|
+
|
|
4
|
+
describe("mergeStyleIntoTag", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
['<div style="color: red; opacity: 1">', '<div style="color: red; opacity: 0.5">'],
|
|
7
|
+
["<div style='color: red; opacity: 1'>", "<div style='color: red; opacity: 0.5'>"],
|
|
8
|
+
['<div style="">', '<div style="opacity: 0.5">'],
|
|
9
|
+
["<div style=''>", "<div style='opacity: 0.5'>"],
|
|
10
|
+
['<div style="color:\nred">', '<div style="color: red; opacity: 0.5">'],
|
|
11
|
+
["<div style='font-family: \"a\"'>", "<div style='font-family: \"a\"; opacity: 0.5'>"],
|
|
12
|
+
["<div style=\"font-family: 'a'\">", "<div style=\"font-family: 'a'; opacity: 0.5\">"],
|
|
13
|
+
[
|
|
14
|
+
"<div style=\"color:red\" style='color:blue'>",
|
|
15
|
+
"<div style=\"color: red; opacity: 0.5\" style='color:blue'>",
|
|
16
|
+
],
|
|
17
|
+
['<div STYLE="color:red">', '<div STYLE="color:red" style="opacity: 0.5">'],
|
|
18
|
+
['<div style = "color:red">', '<div style = "color:red" style="opacity: 0.5">'],
|
|
19
|
+
['<div data-style="color:red">', '<div data-style="color: red; opacity: 0.5">'],
|
|
20
|
+
["<img/>", '<img style="opacity: 0.5"/>'],
|
|
21
|
+
['<div style="color:red>', '<div style="color:red style="opacity: 0.5">'],
|
|
22
|
+
])("preserves quote and source behavior (case %#)", (tag, expected) => {
|
|
23
|
+
expect(mergeStyleIntoTag(tag, "opacity: 0.5")).toBe(expected);
|
|
24
|
+
expect(mergeStyleIntoTag(tag, " \n")).toBe(tag);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it.each(['"', "'"])("handles long values delimited by %s", (quote) => {
|
|
28
|
+
const value = "a".repeat(100_000);
|
|
29
|
+
const tag = `<div style=${quote}--label:${value}${quote}>`;
|
|
30
|
+
expect(mergeStyleIntoTag(tag, "opacity: 0.5")).toBe(
|
|
31
|
+
`<div style=${quote}--label: ${value}; opacity: 0.5${quote}>`,
|
|
32
|
+
);
|
|
33
|
+
const opener = `style=${quote}a`;
|
|
34
|
+
expect(mergeStyleIntoTag(`<div style=${quote}${opener.repeat(10_000)}>`, "opacity: 0.5")).toBe(
|
|
35
|
+
`<div style=${quote}opacity: 0.5${quote}a${opener.repeat(9_999)}>`,
|
|
36
|
+
);
|
|
37
|
+
const unterminated = `<div style=${quote}${value}>`;
|
|
38
|
+
expect(mergeStyleIntoTag(unterminated, "opacity: 0.5")).toBe(
|
|
39
|
+
`<div style=${quote}${value} style="opacity: 0.5">`,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/src/utils/htmlEditor.ts
CHANGED
|
@@ -29,13 +29,13 @@ export function mergeStyleIntoTag(tag: string, newStyles: string): string {
|
|
|
29
29
|
|
|
30
30
|
const incoming = parseStyleString(newStyles);
|
|
31
31
|
|
|
32
|
-
// Match
|
|
33
|
-
const styleAttrRe = /style=(["
|
|
32
|
+
// Match each quote-delimited form explicitly, including multi-line values.
|
|
33
|
+
const styleAttrRe = /style=(")([^"]*)"|style=(')([^']*)'/;
|
|
34
34
|
const match = tag.match(styleAttrRe);
|
|
35
35
|
|
|
36
36
|
if (match) {
|
|
37
|
-
const quote = match[1];
|
|
38
|
-
const existing = parseStyleString(match[2]);
|
|
37
|
+
const quote = match[1] ?? match[3];
|
|
38
|
+
const existing = parseStyleString(match[2] ?? match[4]);
|
|
39
39
|
const merged = { ...existing, ...incoming };
|
|
40
40
|
const serialized = Object.entries(merged)
|
|
41
41
|
.map(([k, v]) => `${k}: ${v}`)
|
|
@@ -8,6 +8,22 @@ import {
|
|
|
8
8
|
} from "./sourcePatcher";
|
|
9
9
|
|
|
10
10
|
describe("applyPatchByTarget", () => {
|
|
11
|
+
it.each(['"', "'"])("patches long and multiline styles quoted with %s", (quote) => {
|
|
12
|
+
const otherQuote = quote === '"' ? "'" : '"';
|
|
13
|
+
const op: PatchOperation = { type: "inline-style", property: "opacity", value: "0.5" };
|
|
14
|
+
for (const value of [
|
|
15
|
+
"",
|
|
16
|
+
"line1\nline2",
|
|
17
|
+
"a".repeat(100_000),
|
|
18
|
+
`style=${otherQuote}a`.repeat(10_000),
|
|
19
|
+
]) {
|
|
20
|
+
const html = `<img id="hero" class="hero" style=${quote}--label:${value}${quote} />`;
|
|
21
|
+
const expected = `<img id="hero" class="hero" style=${quote}--label: ${value}; opacity: 0.5${quote} />`;
|
|
22
|
+
expect(applyPatch(html, "hero", op)).toBe(expected);
|
|
23
|
+
expect(applyPatchByTarget(html, { selector: ".hero" }, op)).toBe(expected);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
11
27
|
it("updates a composition host by data-composition-id selector", () => {
|
|
12
28
|
const html = `<div data-composition-id="intro" data-start="0" data-track-index="1"></div>`;
|
|
13
29
|
const op: PatchOperation = { type: "attribute", property: "start", value: "2.5" };
|
|
@@ -66,6 +82,23 @@ describe("applyPatchByTarget", () => {
|
|
|
66
82
|
expect(result).not.toContain("/ style");
|
|
67
83
|
});
|
|
68
84
|
|
|
85
|
+
it.each(["", " ", "\t\r\n", "\u00a0\u2028", " ".repeat(100_000)])(
|
|
86
|
+
"preserves self-closing whitespace handling (case %#)",
|
|
87
|
+
(whitespace) => {
|
|
88
|
+
const prefix = '<img id="hero" class="hero"';
|
|
89
|
+
const op: PatchOperation = { type: "inline-style", property: "opacity", value: "0.5" };
|
|
90
|
+
for (const suffix of ["/", "/\n", "/\r", "/\r\n", "/\u2028", "", "x", "/ "]) {
|
|
91
|
+
const tag = prefix + whitespace + suffix;
|
|
92
|
+
const selfClosing = suffix === "/";
|
|
93
|
+
// The original operation removes whitespace before the slash only.
|
|
94
|
+
const expectedBase = selfClosing ? prefix + suffix.slice(1) : tag;
|
|
95
|
+
const expected = `${expectedBase} style="opacity: 0.5"${selfClosing ? " /" : ""}>`;
|
|
96
|
+
expect(applyPatch(tag + ">", "hero", op)).toBe(expected);
|
|
97
|
+
expect(applyPatchByTarget(tag + ">", { selector: ".hero" }, op)).toBe(expected);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
69
102
|
it("patches inline move styles by target", () => {
|
|
70
103
|
const html = `<div id="card" style="position: absolute; left: 108px; top: 112px"></div>`;
|
|
71
104
|
|
|
@@ -184,10 +184,10 @@ function patchInlineStyleInTag(
|
|
|
184
184
|
if (!tag) return html;
|
|
185
185
|
|
|
186
186
|
// Check if there's an existing style attribute
|
|
187
|
-
const styleMatch = /\bstyle=(["
|
|
187
|
+
const styleMatch = /\bstyle=(")([^"]*)"|\bstyle=(')([^']*)'/.exec(tag);
|
|
188
188
|
if (styleMatch) {
|
|
189
|
-
const existingStyle = styleMatch[2];
|
|
190
|
-
const quote = styleMatch[1];
|
|
189
|
+
const existingStyle = styleMatch[2] ?? styleMatch[4];
|
|
190
|
+
const quote = styleMatch[1] ?? styleMatch[3];
|
|
191
191
|
// Parse existing properties
|
|
192
192
|
const props = new Map<string, string>();
|
|
193
193
|
for (const part of splitInlineStyleDeclarations(existingStyle)) {
|
|
@@ -212,8 +212,8 @@ function patchInlineStyleInTag(
|
|
|
212
212
|
} else {
|
|
213
213
|
// No existing style attribute
|
|
214
214
|
if (value === null) return html; // nothing to remove
|
|
215
|
-
const selfClosing =
|
|
216
|
-
const base = selfClosing ? tag.
|
|
215
|
+
const selfClosing = tag.endsWith("/");
|
|
216
|
+
const base = selfClosing ? tag.slice(0, -1).trimEnd() : tag;
|
|
217
217
|
const newTag = `${base} style="${prop}: ${escapeStyleAttributeValue(value, '"')}"${selfClosing ? " /" : ""}`;
|
|
218
218
|
return html.replace(tag, newTag);
|
|
219
219
|
}
|