@hyperframes/studio 0.8.142 → 0.8.143

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.
Files changed (32) hide show
  1. package/dist/assets/{hyperframes-player-sAsRuV73.js → hyperframes-player-BXSuksFh.js} +3 -3
  2. package/dist/assets/{index-D3Vij9t1.js → index-BgOVCqjo.js} +1 -1
  3. package/dist/assets/{index-D5q2_7Gw.js → index-FgyO_5Kv.js} +1 -1
  4. package/dist/assets/{index-B-wffzRH.js → index-J7jpL4pj.js} +150 -150
  5. package/dist/index.html +1 -1
  6. package/dist/index.js +122 -160
  7. package/dist/index.js.map +1 -1
  8. package/package.json +8 -8
  9. package/src/components/MediaPreview.tsx +2 -1
  10. package/src/components/editor/mediaAudioEdits.test.ts +12 -0
  11. package/src/components/editor/mediaAudioEdits.ts +5 -5
  12. package/src/components/editor/propertyPanelFill.tsx +2 -1
  13. package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -4
  14. package/src/components/editor/propertyPanelMediaSection.tsx +3 -4
  15. package/src/components/editor/useColorGradingController.ts +2 -1
  16. package/src/hooks/useDomEditCommits.ts +2 -1
  17. package/src/hooks/useFileManager.ts +2 -1
  18. package/src/hooks/useFileTree.ts +2 -1
  19. package/src/hooks/useRenderClipContent.ts +15 -3
  20. package/src/player/components/clipMenuNormalize.ts +2 -2
  21. package/src/player/components/clipPeakMap.ts +2 -2
  22. package/src/player/components/thumbnailUtils.test.ts +33 -0
  23. package/src/player/components/thumbnailUtils.ts +5 -0
  24. package/src/player/lib/mediaProbe.test.ts +12 -0
  25. package/src/player/lib/mediaProbe.ts +4 -2
  26. package/src/utils/sourcePatcher.test.ts +135 -0
  27. package/src/utils/sourcePatcher.ts +102 -186
  28. package/src/utils/studioHelpers.test.ts +23 -0
  29. package/src/utils/studioHelpers.ts +3 -2
  30. package/src/utils/studioSelectionSnapshot.test.ts +31 -0
  31. package/src/utils/timelineAssetDrop.test.ts +14 -0
  32. package/src/utils/timelineAssetDrop.ts +2 -1
package/dist/index.html CHANGED
@@ -14,7 +14,7 @@
14
14
  document.documentElement.dataset.theme = "paper";
15
15
  }
16
16
  </script>
17
- <script type="module" crossorigin src="/assets/index-B-wffzRH.js"></script>
17
+ <script type="module" crossorigin src="/assets/index-J7jpL4pj.js"></script>
18
18
  <link rel="stylesheet" crossorigin href="/assets/index-CVd4O6eH.css">
19
19
  </head>
20
20
  <body>
package/dist/index.js CHANGED
@@ -3597,6 +3597,7 @@ function useTimelineZoom() {
3597
3597
  import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef6, useState as useState4 } from "react";
3598
3598
 
3599
3599
  // src/utils/timelineAssetDrop.ts
3600
+ import { encodeUrlPath } from "@hyperframes/parsers";
3600
3601
  import { buildTimelineAssetId } from "@hyperframes/core/timeline-asset-id";
3601
3602
 
3602
3603
  // src/utils/mediaTypes.ts
@@ -3701,7 +3702,7 @@ function fitTimelineAssetGeometry(natural, comp) {
3701
3702
  };
3702
3703
  }
3703
3704
  function buildTimelineAssetInsertHtml(input) {
3704
- const sharedAttrs = `id="${input.id}" data-hf-id="${input.hfId}" class="clip" src="${input.assetPath}" data-start="${input.start}" data-duration="${input.duration}" data-track-index="${input.track}"`;
3705
+ const sharedAttrs = `id="${input.id}" data-hf-id="${input.hfId}" class="clip" src="${encodeUrlPath(input.assetPath)}" data-start="${input.start}" data-duration="${input.duration}" data-track-index="${input.track}"`;
3705
3706
  const geometry = input.geometry ?? { left: 0, top: 0, width: 640, height: 360 };
3706
3707
  const visualStyles = `position: absolute; left: ${geometry.left}px; top: ${geometry.top}px; width: ${geometry.width}px; height: ${geometry.height}px; object-fit: contain; z-index: ${input.zIndex}`;
3707
3708
  if (input.kind === "image") {
@@ -17143,17 +17144,41 @@ function useOwnPreviewIframe() {
17143
17144
  import { isHtmlElement as isHtmlElement4 } from "@hyperframes/core/runtime/dom-realm";
17144
17145
 
17145
17146
  // src/utils/sourcePatcher.ts
17147
+ import {
17148
+ scanHtmlOpeningTags,
17149
+ decodeAuthoredAttribute,
17150
+ HTML_ATTRIBUTE_ENTITIES
17151
+ } from "@hyperframes/parsers";
17146
17152
  function escapeRegex(s) {
17147
17153
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17148
17154
  }
17155
+ function tagAttribute(tag, name) {
17156
+ return scanHtmlOpeningTags(tag)[0]?.attributes.find((attr) => attr.name === name.toLowerCase());
17157
+ }
17158
+ function patchTagAttribute(tag, name, edit) {
17159
+ if (edit.kind === "remove") {
17160
+ const attributes = scanHtmlOpeningTags(tag)[0]?.attributes.filter((attr2) => attr2.name === name.toLowerCase()) ?? [];
17161
+ const parts = [];
17162
+ let cursor = 0;
17163
+ for (const attribute of attributes) {
17164
+ let start = attribute.start;
17165
+ while (start > cursor && /[\t\n\f\r ]/.test(tag[start - 1])) start--;
17166
+ parts.push(tag.slice(cursor, start));
17167
+ cursor = attribute.end;
17168
+ }
17169
+ parts.push(tag.slice(cursor));
17170
+ return parts.join("");
17171
+ }
17172
+ const attr = tagAttribute(tag, name);
17173
+ const replacement = edit.kind === "boolean" ? name : `${name}="${escapeHtmlAttribute(edit.value)}"`;
17174
+ if (attr) return tag.slice(0, attr.start) + replacement + tag.slice(attr.end);
17175
+ return tag.endsWith("/") ? `${tag.slice(0, -1).trimEnd()} ${replacement} /` : `${tag} ${replacement}`;
17176
+ }
17149
17177
  function escapeStyleAttributeValue(value, quote) {
17150
17178
  return quote === '"' ? value.replace(/"/g, "&quot;") : value.replace(/'/g, "&#39;");
17151
17179
  }
17152
17180
  function escapeHtmlAttribute(value) {
17153
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
17154
- }
17155
- function unescapeHtmlAttribute(value) {
17156
- return value.replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
17181
+ return value.replace(/[&"<>]/g, (char) => HTML_ATTRIBUTE_ENTITIES[char][0]);
17157
17182
  }
17158
17183
  function splitInlineStyleDeclarations(style) {
17159
17184
  const declarations = [];
@@ -17206,7 +17231,7 @@ function resolveSourceFile(elementId2, selector, files) {
17206
17231
  if (!elementId2 && !selector) return null;
17207
17232
  if (elementId2) {
17208
17233
  for (const [path, content] of Object.entries(files)) {
17209
- if (content.includes(`id="${elementId2}"`) || content.includes(`id='${elementId2}'`)) {
17234
+ if (findTagByAttribute(content, "id", elementId2)) {
17210
17235
  return path;
17211
17236
  }
17212
17237
  }
@@ -17215,7 +17240,7 @@ function resolveSourceFile(elementId2, selector, files) {
17215
17240
  if (compIdMatch) {
17216
17241
  const compId = compIdMatch[1];
17217
17242
  for (const [path, content] of Object.entries(files)) {
17218
- if (content.includes(`data-composition-id="${compId}"`)) {
17243
+ if (findTagByAttribute(content, "data-composition-id", compId)) {
17219
17244
  return path;
17220
17245
  }
17221
17246
  }
@@ -17224,7 +17249,7 @@ function resolveSourceFile(elementId2, selector, files) {
17224
17249
  if (classMatch) {
17225
17250
  const cls = classMatch[1];
17226
17251
  for (const [path, content] of Object.entries(files)) {
17227
- if (content.includes(`class="${cls}"`) || content.includes(`class="${cls} `) || content.includes(` ${cls}"`)) {
17252
+ if (findTagByClass(content, { selector: `.${cls}` })) {
17228
17253
  return path;
17229
17254
  }
17230
17255
  }
@@ -17233,18 +17258,14 @@ function resolveSourceFile(elementId2, selector, files) {
17233
17258
  return null;
17234
17259
  }
17235
17260
  function patchInlineStyle(html2, elementId2, prop, value) {
17236
- const idPattern = new RegExp(`(<[^>]*\\bid=(["'])${escapeRegex(elementId2)}\\2[^>]*)>`, "i");
17237
- const match = idPattern.exec(html2);
17238
- if (!match) return html2;
17239
- const tag = match[1];
17240
- return patchInlineStyleInTag(html2, tag, prop, value);
17241
- }
17242
- function patchInlineStyleInTag(html2, tag, prop, value) {
17243
- if (!tag) return html2;
17244
- const styleMatch = /\bstyle=(")([^"]*)"|\bstyle=(')([^']*)'/.exec(tag);
17245
- if (styleMatch) {
17246
- const existingStyle = styleMatch[2] ?? styleMatch[4];
17247
- const quote = styleMatch[1] ?? styleMatch[3];
17261
+ return patchInlineStyleByTarget(html2, { id: elementId2 }, prop, value);
17262
+ }
17263
+ function patchInlineStyleInTag(tag, prop, value) {
17264
+ if (!tag) return tag;
17265
+ const styleMatch = tagAttribute(tag, "style");
17266
+ if (styleMatch?.kind === "value") {
17267
+ const existingStyle = styleMatch.value;
17268
+ const quote = styleMatch.quote || '"';
17248
17269
  const props = /* @__PURE__ */ new Map();
17249
17270
  for (const part of splitInlineStyleDeclarations(existingStyle)) {
17250
17271
  const colon = part.indexOf(":");
@@ -17259,67 +17280,54 @@ function patchInlineStyleInTag(html2, tag, prop, value) {
17259
17280
  props.set(prop, value);
17260
17281
  }
17261
17282
  const newStyle = Array.from(props.entries()).map(([k, v]) => `${k}: ${escapeStyleAttributeValue(v, quote)}`).join("; ");
17262
- const newTag = tag.replace(styleMatch[0], `style=${quote}${newStyle}${quote}`);
17263
- return html2.replace(tag, newTag);
17283
+ const newTag = tag.slice(0, styleMatch.start) + `style=${quote}${newStyle}${quote}` + tag.slice(styleMatch.end);
17284
+ return newTag;
17264
17285
  } else {
17265
- if (value === null) return html2;
17286
+ if (value === null) return tag;
17266
17287
  const selfClosing = tag.endsWith("/");
17267
17288
  const base2 = selfClosing ? tag.slice(0, -1).trimEnd() : tag;
17268
17289
  const newTag = `${base2} style="${prop}: ${escapeStyleAttributeValue(value, '"')}"${selfClosing ? " /" : ""}`;
17269
- return html2.replace(tag, newTag);
17290
+ return newTag;
17270
17291
  }
17271
17292
  }
17272
17293
  function patchInlineStyleByTarget(html2, target, prop, value) {
17273
17294
  const match = findTagByTarget(html2, target);
17274
17295
  if (!match) return html2;
17275
- const newTag = patchInlineStyleInTag(match.tag, match.tag, prop, value);
17296
+ const newTag = patchInlineStyleInTag(match.tag, prop, value);
17276
17297
  return replaceTagAtMatch(html2, match, newTag);
17277
17298
  }
17278
17299
  function replaceTagAtMatch(html2, match, newTag) {
17279
17300
  return `${html2.slice(0, match.start)}${newTag}${html2.slice(match.end)}`;
17280
17301
  }
17281
- function execDataAttrPattern(html2, attr, value) {
17282
- const pattern = new RegExp(`(<[^>]*\\b${attr}=(["'])${escapeRegex(value)}\\2[^>]*)>`, "i");
17283
- const match = pattern.exec(html2);
17284
- if (match?.index == null) return null;
17285
- return { tag: match[1], start: match.index, end: match.index + match[1].length };
17302
+ function findTagByAttribute(html2, name, value) {
17303
+ const found = scanHtmlOpeningTags(html2).find((tag) => {
17304
+ const attr = tag.attributes.find((attr2) => attr2.name === name);
17305
+ return tag.closed && attr?.kind === "value" && decodeAuthoredAttribute(attr.value) === value;
17306
+ });
17307
+ return found ? { tag: html2.slice(found.start, found.bodyEnd), start: found.start, end: found.bodyEnd } : null;
17286
17308
  }
17287
17309
  function findTagByClass(html2, target) {
17288
17310
  const classMatch = target.selector?.match(/^\.([a-zA-Z0-9_-]+)$/);
17289
17311
  if (!classMatch) return null;
17290
- const cls = classMatch[1];
17291
- const pattern = new RegExp(
17292
- `(<[^>]*\\bclass=(["'])[^"']*\\b${escapeRegex(cls)}\\b[^"']*\\2[^>]*)>`,
17293
- "gi"
17294
- );
17295
- const selectorIndex = target.selectorIndex ?? 0;
17296
- let match;
17297
- let currentIndex = 0;
17298
- while ((match = pattern.exec(html2)) !== null) {
17299
- if (currentIndex === selectorIndex && match.index != null) {
17300
- return {
17301
- tag: match[1],
17302
- start: match.index,
17303
- end: match.index + match[1].length
17304
- };
17305
- }
17306
- currentIndex += 1;
17307
- }
17308
- return null;
17312
+ const found = scanHtmlOpeningTags(html2).filter((tag) => {
17313
+ const attr = tag.attributes.find((attr2) => attr2.name === "class");
17314
+ return tag.closed && attr?.kind === "value" && decodeAuthoredAttribute(attr.value).split(/\s+/).includes(classMatch[1]);
17315
+ })[target.selectorIndex ?? 0];
17316
+ return found ? { tag: html2.slice(found.start, found.bodyEnd), start: found.start, end: found.bodyEnd } : null;
17309
17317
  }
17310
17318
  function findTagByTarget(html2, target) {
17311
17319
  if (target.hfId) {
17312
- const result = execDataAttrPattern(html2, "data-hf-id", target.hfId);
17320
+ const result = findTagByAttribute(html2, "data-hf-id", target.hfId);
17313
17321
  if (result) return result;
17314
17322
  }
17315
17323
  if (target.id) {
17316
- const result = execDataAttrPattern(html2, "id", target.id);
17324
+ const result = findTagByAttribute(html2, "id", target.id);
17317
17325
  if (result) return result;
17318
17326
  }
17319
17327
  if (!target.selector) return null;
17320
17328
  const compositionIdMatch = target.selector.match(/^\[data-composition-id="([^"]+)"\]$/);
17321
17329
  if (compositionIdMatch) {
17322
- const result = execDataAttrPattern(html2, "data-composition-id", compositionIdMatch[1]);
17330
+ const result = findTagByAttribute(html2, "data-composition-id", compositionIdMatch[1]);
17323
17331
  if (result) return result;
17324
17332
  }
17325
17333
  return findTagByClass(html2, target);
@@ -17327,9 +17335,11 @@ function findTagByTarget(html2, target) {
17327
17335
  function readAttributeByTarget(html2, target, attr) {
17328
17336
  const match = findTagByTarget(html2, target);
17329
17337
  if (!match) return void 0;
17330
- const fullAttr = attr.startsWith("data-") ? attr : `data-${attr}`;
17331
- const valueMatch = new RegExp(`\\b${fullAttr}=(["'])([^"']*)\\1`).exec(match.tag);
17332
- return valueMatch?.[2] != null ? unescapeHtmlAttribute(valueMatch[2]) : void 0;
17338
+ return readTagAttribute(match.tag, attr.startsWith("data-") ? attr : `data-${attr}`);
17339
+ }
17340
+ function readTagAttribute(tag, attr) {
17341
+ const value = tagAttribute(tag, attr);
17342
+ return value?.kind === "value" ? decodeAuthoredAttribute(value.value) : void 0;
17333
17343
  }
17334
17344
  function readTagSnippetByTarget(html2, target) {
17335
17345
  const match = findTagByTarget(html2, target);
@@ -17338,60 +17348,15 @@ function readTagSnippetByTarget(html2, target) {
17338
17348
  function patchAttributeByTarget(html2, target, attr, value) {
17339
17349
  const match = findTagByTarget(html2, target);
17340
17350
  if (!match) return html2;
17341
- const fullAttr = attr.startsWith("data-") ? attr : `data-${attr}`;
17342
- const attrPattern = new RegExp(`\\b${escapeRegex(fullAttr)}=(["'])([^"']*)\\1`);
17343
- const tag = match.tag;
17344
- if (value === null) {
17345
- const boolAttrPattern = new RegExp(`\\b${escapeRegex(fullAttr)}(?:=(["'])[^"']*\\1)?`);
17346
- if (!boolAttrPattern.test(tag)) return html2;
17347
- const removePattern = new RegExp(`\\s+${escapeRegex(fullAttr)}(?:=(["'])[^"']*\\1)?`);
17348
- const newTag2 = tag.replace(removePattern, "");
17349
- return replaceTagAtMatch(html2, match, newTag2);
17350
- }
17351
- const escaped = escapeHtmlAttribute(value);
17352
- if (attrPattern.test(tag)) {
17353
- const newTag2 = tag.replace(attrPattern, `${fullAttr}="${escaped}"`);
17354
- return replaceTagAtMatch(html2, match, newTag2);
17355
- }
17356
- const newTag = tag + ` ${fullAttr}="${escaped}"`;
17357
- return replaceTagAtMatch(html2, match, newTag);
17351
+ const name = attr.startsWith("data-") ? attr : `data-${attr}`;
17352
+ const edit = value === null ? { kind: "remove" } : { kind: "value", value };
17353
+ return replaceTagAtMatch(html2, match, patchTagAttribute(match.tag, name, edit));
17358
17354
  }
17359
17355
  function patchAttribute(html2, elementId2, attr, value) {
17360
- const idPattern = new RegExp(`(<[^>]*\\bid=(["'])${escapeRegex(elementId2)}\\2[^>]*)>`, "i");
17361
- const match = idPattern.exec(html2);
17362
- if (!match) return html2;
17363
- const tag = match[1];
17364
- const fullAttr = attr.startsWith("data-") ? attr : `data-${attr}`;
17365
- const attrPattern = new RegExp(`\\b${escapeRegex(fullAttr)}=(["'])([^"']*)\\1`);
17366
- if (value === null) {
17367
- const boolAttrPattern = new RegExp(`\\b${escapeRegex(fullAttr)}(?:=(["'])[^"']*\\1)?`);
17368
- if (!boolAttrPattern.test(tag)) return html2;
17369
- const removePattern = new RegExp(`\\s+${escapeRegex(fullAttr)}(?:=(["'])[^"']*\\1)?`);
17370
- const newTag = tag.replace(removePattern, "");
17371
- return html2.replace(tag, newTag);
17372
- }
17373
- const escaped = escapeHtmlAttribute(value);
17374
- if (attrPattern.test(tag)) {
17375
- const newTag = tag.replace(attrPattern, `${fullAttr}="${escaped}"`);
17376
- return html2.replace(tag, newTag);
17377
- } else {
17378
- const newTag = tag + ` ${fullAttr}="${escaped}"`;
17379
- return html2.replace(tag, newTag);
17380
- }
17356
+ return patchAttributeByTarget(html2, { id: elementId2 }, attr, value);
17381
17357
  }
17382
17358
  function patchTextContent(html2, elementId2, value) {
17383
- const openTagPattern = new RegExp(
17384
- `(<([a-z0-9-]+)[^>]*\\bid=(["'])${escapeRegex(elementId2)}\\3[^>]*>)`,
17385
- "i"
17386
- );
17387
- const match = openTagPattern.exec(html2);
17388
- if (!match || match.index == null) return html2;
17389
- const openingTag = match[1];
17390
- const tagName = match[2];
17391
- const contentStart = match.index + openingTag.length;
17392
- const closingIndex = findMatchingClosingTagIndex(html2, tagName, contentStart);
17393
- if (closingIndex < 0) return html2;
17394
- return `${html2.slice(0, contentStart)}${value}${html2.slice(closingIndex)}`;
17359
+ return patchTextContentByTarget(html2, { id: elementId2 }, value);
17395
17360
  }
17396
17361
  function findMatchingClosingTagIndex(html2, tagName, contentStart) {
17397
17362
  const tagPattern = new RegExp(`</?${escapeRegex(tagName)}\\b[^>]*>`, "gi");
@@ -17426,47 +17391,26 @@ var HTML_BOOLEAN_ATTRIBUTES = /* @__PURE__ */ new Set([
17426
17391
  "reversed",
17427
17392
  "selected"
17428
17393
  ]);
17429
- function patchHtmlAttributeInTag(html2, tag, attr, value) {
17430
- if (!tag) return html2;
17431
- const isBoolean = HTML_BOOLEAN_ATTRIBUTES.has(attr);
17432
- if (isBoolean) {
17433
- const escapedAttr = escapeRegex(attr);
17434
- const hasBoolAttr = new RegExp(`(?:^|\\s)${escapedAttr}(?:\\s|=|$)`).test(tag);
17435
- if (value === null || value === "" || value === "false") {
17436
- if (!hasBoolAttr) return html2;
17437
- const removePattern = new RegExp(`\\s+${escapedAttr}(?:=(["'])[^"']*\\1)?`);
17438
- const newTag3 = tag.replace(removePattern, "");
17439
- return html2.replace(tag, newTag3);
17440
- }
17441
- if (hasBoolAttr) return html2;
17442
- const newTag2 = tag + ` ${attr}`;
17443
- return html2.replace(tag, newTag2);
17444
- }
17445
- const attrPattern = new RegExp(`\\b${escapeRegex(attr)}=(["'])([^"']*)\\1`);
17446
- if (value === null) {
17447
- if (!attrPattern.test(tag)) return html2;
17448
- const removePattern = new RegExp(`\\s+${escapeRegex(attr)}=(["'])[^"']*\\1`);
17449
- const newTag2 = tag.replace(removePattern, "");
17450
- return html2.replace(tag, newTag2);
17451
- }
17452
- const escaped = escapeHtmlAttribute(value);
17453
- if (attrPattern.test(tag)) {
17454
- const newTag2 = tag.replace(attrPattern, `${attr}="${escaped}"`);
17455
- return html2.replace(tag, newTag2);
17456
- }
17457
- const newTag = tag + ` ${attr}="${escaped}"`;
17458
- return html2.replace(tag, newTag);
17394
+ function patchHtmlAttributeInTag(tag, attr, value) {
17395
+ if (!tag) return tag;
17396
+ let edit;
17397
+ if (HTML_BOOLEAN_ATTRIBUTES.has(attr)) {
17398
+ if (value === null || value === "" || value === "false") edit = { kind: "remove" };
17399
+ else {
17400
+ if (tagAttribute(tag, attr)) return tag;
17401
+ edit = { kind: "boolean" };
17402
+ }
17403
+ } else edit = value === null ? { kind: "remove" } : { kind: "value", value };
17404
+ const newTag = patchTagAttribute(tag, attr, edit);
17405
+ return newTag;
17459
17406
  }
17460
17407
  function patchHtmlAttribute(html2, elementId2, attr, value) {
17461
- const idPattern = new RegExp(`(<[^>]*\\bid=(["'])${escapeRegex(elementId2)}\\2[^>]*)>`, "i");
17462
- const match = idPattern.exec(html2);
17463
- if (!match) return html2;
17464
- return patchHtmlAttributeInTag(html2, match[1], attr, value);
17408
+ return patchHtmlAttributeByTarget(html2, { id: elementId2 }, attr, value);
17465
17409
  }
17466
17410
  function patchHtmlAttributeByTarget(html2, target, attr, value) {
17467
17411
  const match = findTagByTarget(html2, target);
17468
17412
  if (!match) return html2;
17469
- const newTag = patchHtmlAttributeInTag(match.tag, match.tag, attr, value);
17413
+ const newTag = patchHtmlAttributeInTag(match.tag, attr, value);
17470
17414
  return replaceTagAtMatch(html2, match, newTag);
17471
17415
  }
17472
17416
  function patchTextContentByTarget(html2, target, value) {
@@ -21837,6 +21781,7 @@ import {
21837
21781
  } from "@hyperframes/core/audio-automation";
21838
21782
 
21839
21783
  // src/utils/studioHelpers.ts
21784
+ import { encodeUrlPath as encodeUrlPath2 } from "@hyperframes/parsers";
21840
21785
  function getTimelineElementLabel(element) {
21841
21786
  return element.label || element.id || element.tag;
21842
21787
  }
@@ -21991,7 +21936,7 @@ async function resolveDroppedAssetDuration(projectId, assetPath, kind) {
21991
21936
  if (kind === "image") return DEFAULT_TIMELINE_ASSET_DURATION.image;
21992
21937
  const media = document.createElement(kind === "video" ? "video" : "audio");
21993
21938
  media.preload = "metadata";
21994
- media.src = buildProjectApiPath(projectId, `/preview/${assetPath}`);
21939
+ media.src = buildProjectApiPath(projectId, `/preview/${encodeUrlPath2(assetPath)}`);
21995
21940
  const duration = await new Promise((resolve) => {
21996
21941
  const timeout = window.setTimeout(() => resolve(DEFAULT_TIMELINE_ASSET_DURATION[kind]), 3e3);
21997
21942
  const finalize = (value) => {
@@ -25307,6 +25252,7 @@ function isPeakMap(value) {
25307
25252
  }
25308
25253
 
25309
25254
  // src/player/components/thumbnailUtils.ts
25255
+ import { decodedUrlPath } from "@hyperframes/parsers";
25310
25256
  function quantizeThumbnailFrameCount(frameCount) {
25311
25257
  const safeCount = Math.max(1, Number.isFinite(frameCount) ? Math.ceil(frameCount) : 1);
25312
25258
  const cap = 2 ** Math.floor(Math.log2(MAX_VISIBLE_THUMBNAIL_FRAMES));
@@ -25357,6 +25303,9 @@ function computeThumbnailStrip(containerWidth, aspect, clipHeight, minFrameWidth
25357
25303
  const frameCount = measured ? Math.max(1, Math.ceil(containerWidth / frameW)) : 1;
25358
25304
  return { frameW, frameCount };
25359
25305
  }
25306
+ function authoredSrcPath(src) {
25307
+ return /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(src) ? src : decodedUrlPath(src);
25308
+ }
25360
25309
  function encodePreviewPath(relativePath) {
25361
25310
  return relativePath.split("/").map(encodeURIComponent).join("/");
25362
25311
  }
@@ -25401,7 +25350,7 @@ function loadPeakMap(url) {
25401
25350
  function clipPeaksUrl(src, projectId) {
25402
25351
  const origin2 = window.location.origin;
25403
25352
  const relative = resolvePreviewRelative(
25404
- resolveMediaPreviewUrl(src ?? "", projectId, origin2),
25353
+ resolveMediaPreviewUrl(authoredSrcPath(src ?? ""), projectId, origin2),
25405
25354
  projectId,
25406
25355
  origin2
25407
25356
  );
@@ -25472,7 +25421,7 @@ function isNormalizePlan(value) {
25472
25421
  async function requestNormalizePlan(projectId, el, fetchImpl = studioApiFetch) {
25473
25422
  const origin2 = window.location.origin;
25474
25423
  const relative = resolvePreviewRelative(
25475
- resolveMediaPreviewUrl(el.src ?? "", projectId, origin2),
25424
+ resolveMediaPreviewUrl(authoredSrcPath(el.src ?? ""), projectId, origin2),
25476
25425
  projectId,
25477
25426
  origin2
25478
25427
  );
@@ -26986,10 +26935,10 @@ function formatAttrs(attrs) {
26986
26935
  function readAuthoredSrc(source, target) {
26987
26936
  const tag = findTagByTarget(source, target);
26988
26937
  if (!tag) return "";
26989
- const own = /\bsrc=(["'])([^"']*)\1/.exec(tag.tag);
26990
- if (own?.[2]) return unescapeHtmlAttribute(own[2]);
26991
- const firstSource = /^\s*<source\b[^>]*?\bsrc=(["'])([^"']*)\1/i.exec(source.slice(tag.end + 1));
26992
- return firstSource?.[2] ? unescapeHtmlAttribute(firstSource[2]) : "";
26938
+ const own = readTagAttribute(tag.tag, "src");
26939
+ if (own) return own;
26940
+ const firstSource = /^\s*<source\b[^>]*>/i.exec(source.slice(tag.end + 1));
26941
+ return firstSource && readTagAttribute(firstSource[0], "src") || "";
26993
26942
  }
26994
26943
  function insertBeforeTarget(source, target, markup) {
26995
26944
  const match = findTagByTarget(source, target);
@@ -32184,7 +32133,7 @@ function getCachedProbe(url) {
32184
32133
  return cached5?.result;
32185
32134
  }
32186
32135
  function resolveProbeSource(src, projectId) {
32187
- return projectId ? resolveMediaPreviewUrl(src, projectId, window.location.origin) : src;
32136
+ return projectId ? resolveMediaPreviewUrl(authoredSrcPath(src), projectId, window.location.origin) : src;
32188
32137
  }
32189
32138
  function evictMetadataOverflow() {
32190
32139
  const overflow = cache.size + failed.size - TIMELINE_VIEWPORT_BUDGETS.metadataRegistryEntries;
@@ -52882,7 +52831,11 @@ function trimFractions(el) {
52882
52831
  return { start, end };
52883
52832
  }
52884
52833
  function renderAudioClip(el, pid, sessionEpoch, labelColor, context, elements, labelInset) {
52885
- const audioUrl = resolveMediaPreviewUrl(el.src ?? "", pid, window.location.origin);
52834
+ const audioUrl = resolveMediaPreviewUrl(
52835
+ authoredSrcPath(el.src ?? ""),
52836
+ pid,
52837
+ window.location.origin
52838
+ );
52886
52839
  const srcRelative = resolvePreviewRelative(audioUrl, pid, window.location.origin);
52887
52840
  const encodedRelative = srcRelative ? encodePreviewPath(srcRelative) : null;
52888
52841
  const waveformUrl = encodedRelative ? buildProjectApiPath(pid, `/waveform/${encodedRelative}`) : void 0;
@@ -52982,7 +52935,11 @@ function useRenderClipContent({
52982
52935
  }
52983
52936
  const htmlPreviewEligible = el.duration > 0 && effectiveTimelineDuration > 0;
52984
52937
  if ((el.tag === "video" || el.tag === "img") && el.src) {
52985
- const mediaSrc = resolveMediaPreviewUrl(el.src, pid, window.location.origin);
52938
+ const mediaSrc = resolveMediaPreviewUrl(
52939
+ authoredSrcPath(el.src),
52940
+ pid,
52941
+ window.location.origin
52942
+ );
52986
52943
  if (el.tag === "img") {
52987
52944
  return createElement(ImageThumbnail, {
52988
52945
  imageSrc: mediaSrc,
@@ -54700,8 +54657,8 @@ function MediaSection({
54700
54657
  const [removeProgress, setRemoveProgress] = useState65(null);
54701
54658
  const [createPlate, setCreatePlate] = useState65(false);
54702
54659
  const [quality, setQuality] = useState65("balanced");
54703
- const absoluteSrc = projectDir && srcAttr && !srcAttr.startsWith("http") ? `${projectDir}/${srcAttr}` : srcAttr;
54704
- const projectSrc = srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr) ? stripQueryAndHash(srcAttr.startsWith("./") ? srcAttr.slice(2) : srcAttr) : "";
54660
+ const projectSrc = srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr) ? authoredSrcPath(srcAttr.startsWith("./") ? srcAttr.slice(2) : srcAttr) : "";
54661
+ const absoluteSrc = projectDir && projectSrc ? `${projectDir}/${projectSrc}` : srcAttr;
54705
54662
  const canRemoveBackground = Boolean(onRemoveBackground && isVisualMedia && projectSrc);
54706
54663
  const panelTitle = isImage ? "Image" : isVideo ? "Video" : "Audio";
54707
54664
  useEffect72(() => {
@@ -56024,7 +55981,7 @@ function resolveProjectAssetPath(sourceFile, src, projectId) {
56024
55981
  return stripPreviewAssetPath(trimmed, projectId);
56025
55982
  }
56026
55983
  const sourceDir = sourceFile.includes("/") ? sourceFile.slice(0, sourceFile.lastIndexOf("/")) : "";
56027
- const parts = `${sourceDir}/${trimmed}`.split("/");
55984
+ const parts = `${sourceDir}/${authoredSrcPath(trimmed)}`.split("/");
56028
55985
  const normalized = [];
56029
55986
  for (const part of parts) {
56030
55987
  if (!part || part === ".") continue;
@@ -58498,6 +58455,7 @@ function ReverseGradientIcon({ size = 16 }) {
58498
58455
  }
58499
58456
 
58500
58457
  // src/components/editor/propertyPanelFill.tsx
58458
+ import { encodeUrlPath as encodeUrlPath3 } from "@hyperframes/parsers";
58501
58459
  import { jsx as jsx137, jsxs as jsxs99 } from "react/jsx-runtime";
58502
58460
  function normalizeProjectPath(value) {
58503
58461
  const trimmed = value.trim();
@@ -58624,7 +58582,7 @@ function ImageFillField({
58624
58582
  selectedAsset && /* @__PURE__ */ jsx137("div", { className: "overflow-hidden rounded-xl border border-neutral-800 bg-neutral-900/80", children: /* @__PURE__ */ jsx137(
58625
58583
  "img",
58626
58584
  {
58627
- src: buildProjectApiPath(projectId, `/preview/${selectedAsset}`),
58585
+ src: buildProjectApiPath(projectId, `/preview/${encodeUrlPath3(selectedAsset)}`),
58628
58586
  alt: selectedAsset.split("/").pop() ?? selectedAsset,
58629
58587
  className: "h-28 w-full object-contain bg-neutral-950/80"
58630
58588
  }
@@ -67090,8 +67048,8 @@ function FlatMediaSection({
67090
67048
  const [removeProgress, setRemoveProgress] = useState94(null);
67091
67049
  const [createPlate, setCreatePlate] = useState94(false);
67092
67050
  const [quality, setQuality] = useState94("balanced");
67093
- const absoluteSrc = projectDir && srcAttr && !srcAttr.startsWith("http") ? `${projectDir}/${srcAttr}` : srcAttr;
67094
- const projectSrc = srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr) ? stripQueryAndHash(srcAttr.startsWith("./") ? srcAttr.slice(2) : srcAttr) : "";
67051
+ const projectSrc = srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr) ? authoredSrcPath(srcAttr.startsWith("./") ? srcAttr.slice(2) : srcAttr) : "";
67052
+ const absoluteSrc = projectDir && projectSrc ? `${projectDir}/${projectSrc}` : srcAttr;
67095
67053
  const canRemoveBackground = Boolean(onRemoveBackground && isVisualMedia && projectSrc);
67096
67054
  useEffect94(() => {
67097
67055
  setRemoveProgress(null);
@@ -75118,6 +75076,7 @@ import { useState as useState117, useCallback as useCallback104, useRef as useRe
75118
75076
 
75119
75077
  // src/hooks/useFileTree.ts
75120
75078
  import { useState as useState116, useCallback as useCallback101, useEffect as useEffect113, useMemo as useMemo71, useRef as useRef128 } from "react";
75079
+ import { encodeUrlPath as encodeUrlPath4 } from "@hyperframes/parsers";
75121
75080
  var EMPTY_FILE_LIST = [];
75122
75081
  function useFileTree({ projectId, projectIdRef }) {
75123
75082
  const [fetched, setFetched] = useState116(null);
@@ -75189,7 +75148,7 @@ function useFileTree({ projectId, projectIdRef }) {
75189
75148
  () => (projectId ? assets : []).filter((asset) => FONT_EXT.test(asset)).map((asset) => ({
75190
75149
  family: fontFamilyFromAssetPath(asset),
75191
75150
  path: asset,
75192
- url: projectId ? buildProjectApiPath(projectId, `/preview/${asset}`) : ""
75151
+ url: projectId ? buildProjectApiPath(projectId, `/preview/${encodeUrlPath4(asset)}`) : ""
75193
75152
  })),
75194
75153
  [assets, projectId]
75195
75154
  );
@@ -75454,6 +75413,7 @@ function useProjectFileWriter({ projectId }) {
75454
75413
  }
75455
75414
 
75456
75415
  // src/hooks/useFileManager.ts
75416
+ import { encodeUrlPath as encodeUrlPath5 } from "@hyperframes/parsers";
75457
75417
  function useFileManager({
75458
75418
  projectId,
75459
75419
  showToast,
@@ -75763,7 +75723,7 @@ function useFileManager({
75763
75723
  const imported = uploaded.filter((asset) => FONT_EXT.test(asset)).map((asset) => ({
75764
75724
  family: fontFamilyFromAssetPath(asset),
75765
75725
  path: asset,
75766
- url: `/api/projects/${encodeURIComponent(pid)}/preview/${asset}`
75726
+ url: `/api/projects/${encodeURIComponent(pid)}/preview/${encodeUrlPath5(asset)}`
75767
75727
  }));
75768
75728
  importedFontAssetsRef.current = [
75769
75729
  ...imported,
@@ -81799,6 +81759,7 @@ function useDomGeometryCommits({
81799
81759
  }
81800
81760
 
81801
81761
  // src/hooks/useDomEditCommits.ts
81762
+ import { encodeUrlPath as encodeUrlPath6 } from "@hyperframes/parsers";
81802
81763
  function useDomEditCommits({
81803
81764
  activeCompPath,
81804
81765
  previewIframeRef,
@@ -81837,7 +81798,7 @@ function useDomEditCommits({
81837
81798
  return {
81838
81799
  family: fontFamilyFromAssetPath(asset),
81839
81800
  path: asset,
81840
- url: buildProjectApiPath(projectId, `/preview/${asset}`)
81801
+ url: buildProjectApiPath(projectId, `/preview/${encodeUrlPath6(asset)}`)
81841
81802
  };
81842
81803
  },
81843
81804
  [fileTree, projectId, importedFontAssetsRef]
@@ -91352,6 +91313,7 @@ import { memo as memo50, useCallback as useCallback171 } from "react";
91352
91313
 
91353
91314
  // src/components/MediaPreview.tsx
91354
91315
  import { useState as useState136 } from "react";
91316
+ import { encodeUrlPath as encodeUrlPath7 } from "@hyperframes/parsers";
91355
91317
  import { jsx as jsx212, jsxs as jsxs166 } from "react/jsx-runtime";
91356
91318
  function MediaErrorPanel({ name, filePath }) {
91357
91319
  return /* @__PURE__ */ jsxs166("div", { className: "flex flex-col items-center justify-center h-full p-4 bg-neutral-950 gap-2", children: [
@@ -91379,7 +91341,7 @@ function MediaErrorPanel({ name, filePath }) {
91379
91341
  ] });
91380
91342
  }
91381
91343
  function MediaPreview({ projectId, filePath }) {
91382
- const serveUrl = buildProjectApiPath(projectId, `/preview/${filePath}`);
91344
+ const serveUrl = buildProjectApiPath(projectId, `/preview/${encodeUrlPath7(filePath)}`);
91383
91345
  const name = filePath.split("/").pop() ?? filePath;
91384
91346
  const [failedPath, setFailedPath] = useState136(null);
91385
91347
  const failed2 = failedPath === filePath;