@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.8.29",
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/core": "0.8.29",
52
- "@hyperframes/parsers": "0.8.29",
53
- "@hyperframes/player": "0.8.29",
54
- "@hyperframes/sdk": "0.8.29",
55
- "@hyperframes/studio-server": "0.8.29"
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.29"
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=(["'])([\s\S]*?)\1/i;
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
+ });
@@ -29,13 +29,13 @@ export function mergeStyleIntoTag(tag: string, newStyles: string): string {
29
29
 
30
30
  const incoming = parseStyleString(newStyles);
31
31
 
32
- // Match style="..." or style='...' handle multi-line attrs via dotall-like trick
33
- const styleAttrRe = /style=(["'])([\s\S]*?)\1/;
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=(["'])([\s\S]*?)\1/.exec(tag);
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 = /\s*\/$/.test(tag);
216
- const base = selfClosing ? tag.replace(/\s*\/$/, "") : 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
  }