@hyperframes/studio-server 0.8.141 → 0.8.142

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.
@@ -1,4 +1,5 @@
1
1
  // src/helpers/sourceMutation.ts
2
+ import { replacementTimelineAssetId } from "@hyperframes/core/timeline-asset-id";
2
3
  import { parseHTML } from "linkedom";
3
4
  import { removeElementWithGsapCascade } from "@hyperframes/parsers";
4
5
  import { readMediaOffsetSeconds, readPlaybackRate } from "@hyperframes/parsers/media-duration";
@@ -242,6 +243,8 @@ function patchElementInHtml(source, target, operations, takenIds) {
242
243
  if (!isAllowedHtmlAttribute(op.property)) break;
243
244
  if (op.value != null) {
244
245
  if (!isSafeAttributeValue(op.property, op.value)) break;
246
+ const replacementId = op.property === "src" ? replacementTimelineAssetId(document, opTarget, op.value) : null;
247
+ if (replacementId !== null) opTarget.setAttribute("id", replacementId);
245
248
  opTarget.setAttribute(op.property, op.value);
246
249
  } else {
247
250
  opTarget.removeAttribute(op.property);
@@ -519,4 +522,4 @@ export {
519
522
  unwrapElementsFromHtml,
520
523
  relinkSplitHalvesInHtml
521
524
  };
522
- //# sourceMappingURL=chunk-CLWFZQUU.js.map
525
+ //# sourceMappingURL=chunk-JJ4N3PXT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/helpers/sourceMutation.ts","../src/helpers/sourceStyleMutation.ts"],"sourcesContent":["import { replacementTimelineAssetId } from \"@hyperframes/core/timeline-asset-id\";\nimport { parseHTML } from \"linkedom\";\nimport { removeElementWithGsapCascade } from \"@hyperframes/parsers\";\nimport { readMediaOffsetSeconds, readPlaybackRate } from \"@hyperframes/parsers/media-duration\";\nimport postcss from \"postcss\";\nimport selectorParser from \"postcss-selector-parser\";\nimport { isAllowedHtmlAttribute, isSafeAttributeValue } from \"@hyperframes/core/html-attr-safety\";\nimport { sanitizeRichTextChildren } from \"@hyperframes/core/rich-text-sanitize\";\nimport {\n EXCLUDED_TAGS,\n ensureHfIds,\n mintHfId,\n walkCompositionDescendants,\n} from \"@hyperframes/parsers/hf-ids\";\nimport { readClipTiming, writeClipTiming } from \"@hyperframes/core/composition-contract\";\nimport { relinkSplitHalves } from \"@hyperframes/core/media-link\";\nimport { parseStyleDecls, patchStyleAttrString } from \"./sourceStyleMutation.js\";\n\nexport interface SourceMutationTarget {\n id?: string | null;\n hfId?: string;\n selector?: string;\n selectorIndex?: number;\n}\n\nexport function parseSourceDocument(raw: string): {\n document: Document;\n wrappedFragment: boolean;\n} {\n const source = ensureHfIds(raw);\n const hasDocumentShell = /<!doctype|<html[\\s>]/i.test(source);\n if (hasDocumentShell) {\n return { document: parseHTML(source).document, wrappedFragment: false };\n }\n return {\n document: parseHTML(`<!DOCTYPE html><html><head></head><body>${source}</body></html>`).document,\n wrappedFragment: true,\n };\n}\n\nfunction duplicateCssRulesForId(document: Document, originalId: string, newId: string): void {\n const idToken = `#${originalId}`;\n const transform = selectorParser((selectors) => {\n selectors.walkIds((node) => {\n if (node.value === originalId) node.value = newId;\n });\n });\n for (const styleEl of document.querySelectorAll(\"style\")) {\n const css = styleEl.textContent ?? \"\";\n let root: postcss.Root;\n try {\n root = postcss.parse(css);\n } catch {\n continue;\n }\n const clones: postcss.Rule[] = [];\n root.walkRules((rule) => {\n if (!rule.selector.includes(idToken)) return;\n const newSelector = transform.processSync(rule.selector);\n if (newSelector === rule.selector) return;\n const clone = rule.clone({ selector: newSelector });\n clones.push(clone);\n });\n if (clones.length > 0) {\n for (const c of clones) root.append(c);\n styleEl.textContent = root.toString();\n }\n }\n}\n\nfunction querySelectorAllWithTemplates(root: Document | Element, selector: string): Element[] {\n const matches = Array.from(root.querySelectorAll(selector));\n if (matches.length > 0) return matches;\n // querySelectorAll doesn't traverse <template> content in linkedom.\n // Search directly on each template element (NOT .content — removing from\n // .content's DocumentFragment doesn't update the serialized output).\n // Recurse so NESTED templates resolve too — ensureHfIds and the SDK's\n // querySelectorAllDeep descend nested composition templates, so ids exist at\n // any template depth; a one-level search here would silently no-op\n // server-side ops on those ids while the SDK resolves them.\n const templates = Array.from(root.querySelectorAll(\"template\"));\n for (const tmpl of templates) {\n const inner = querySelectorAllWithTemplates(tmpl, selector);\n if (inner.length > 0) return inner;\n }\n return [];\n}\n\n// Prevent CSS attribute-selector injection via a crafted hfId: escape\n// backslashes first, then double-quotes. Keeps a malformed/hostile value from\n// breaking out of the `[data-hf-id=\"…\"]` selector once callers beyond the\n// internal mint contract (R2+ user flows) pass values here.\nfunction escapeCssAttrValue(value: string): string {\n return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\nfunction findByHfId(document: Document, hfId: string): Element | null {\n try {\n const matches = querySelectorAllWithTemplates(\n document,\n `[data-hf-id=\"${escapeCssAttrValue(hfId)}\"]`,\n );\n if (matches.length > 1) {\n // The mint contract guarantees uniqueness; a duplicate means upstream\n // id drift. Don't silently patch an arbitrary one — surface it.\n // eslint-disable-next-line no-console\n console.warn(\n `sourceMutation: data-hf-id \"${hfId}\" matched ${matches.length} elements; using the first. ids must be unique per document.`,\n );\n }\n return matches[0] ?? null;\n } catch {\n // Malformed selector despite escaping — let the caller fall back.\n return null;\n }\n}\n\nexport function findTargetElement(\n document: Document,\n target: SourceMutationTarget,\n): Element | null {\n if (target.hfId) {\n const el = findByHfId(document, target.hfId);\n if (el) return el;\n }\n\n if (target.id) {\n const byId = document.getElementById(target.id);\n if (byId) return byId;\n }\n\n if (!target.selector) return null;\n try {\n const matches = querySelectorAllWithTemplates(document, target.selector);\n return matches[target.selectorIndex ?? 0] ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Removes every target in one parse and one serialization. A target nested inside one already\n * removed no longer matches, which is a normal outcome rather than a failure.\n */\nexport function removeElementsFromHtml(\n source: string,\n targets: readonly SourceMutationTarget[],\n): string {\n const { document, wrappedFragment } = parseSourceDocument(source);\n let removed = false;\n for (const target of targets) {\n const element = findTargetElement(document, target);\n if (!element) continue;\n removeElementWithGsapCascade(document, element);\n removed = true;\n }\n if (!removed) return source;\n return wrappedFragment ? document.body.innerHTML || \"\" : document.toString();\n}\n\nexport function removeElementFromHtml(source: string, target: SourceMutationTarget): string {\n return removeElementsFromHtml(source, [target]);\n}\n\nexport function isHTMLElement(el: Node): el is HTMLElement {\n const HTMLEl = el.ownerDocument?.defaultView?.HTMLElement;\n return HTMLEl ? el instanceof HTMLEl : el.nodeType === 1 && \"style\" in el;\n}\n\nexport function dedupeClonedCompositionId(document: Document, clone: Element): void {\n const compositionId = clone.getAttribute(\"data-composition-id\");\n if (!compositionId) return;\n const usedCompositionIds = new Set(\n querySelectorAllWithTemplates(document, \"[data-composition-id]\").map((node) =>\n node.getAttribute(\"data-composition-id\"),\n ),\n );\n const base = `${compositionId}-split`;\n let nextCompositionId = base;\n let suffix = 2;\n while (usedCompositionIds.has(nextCompositionId)) nextCompositionId = `${base}-${suffix++}`;\n clone.setAttribute(\"data-composition-id\", nextCompositionId);\n}\n\nexport interface PatchOperation {\n /** `ensure-id` keeps the element's id, else writes `value` made unique in this file and `takenIds`. */\n type:\n | \"inline-style\"\n | \"attribute\"\n | \"html-attribute\"\n | \"text-content\"\n | \"rich-text\"\n | \"ensure-id\";\n property: string;\n value: string | null;\n childSelector?: string;\n childIndex?: number;\n}\n\ninterface ResolvedPatchOperation {\n op: PatchOperation;\n target: HTMLElement;\n}\n\nfunction resolveOperationTarget(parent: HTMLElement, op: PatchOperation): HTMLElement | null {\n if (op.childSelector === undefined) return parent;\n try {\n const child = parent.querySelectorAll(op.childSelector)[op.childIndex ?? 0] ?? null;\n return child && isHTMLElement(child) ? child : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Give the elements a rich-text patch just introduced their stable ids, here,\n * in the bytes about to be written and handed back.\n *\n * Otherwise the next preview request mints them and writes the file a second\n * time, after Studio has already recorded the edit in its history. The recorded\n * \"after\" stops matching disk, the content check refuses, and undo reports the\n * file as changed outside Studio — for every colour applied to a run of\n * characters and every text layer added. The clip split stamps its own clone\n * for exactly this reason.\n *\n * Minted one element at a time with the same function `ensureHfIds` uses, so\n * these ids are the ones the next pass would have assigned. Not `ensureHfIds`\n * itself: it takes a whole document, and handing it this element's markup would\n * put the markup back as one.\n */\nfunction stampNewChildIds(parent: Element): void {\n const assigned = new Set<string>();\n const root = parent.ownerDocument?.body ?? parent;\n walkCompositionDescendants(root, (el) => {\n const id = el.getAttribute(\"data-hf-id\");\n if (id) assigned.add(id);\n });\n for (const el of parent.querySelectorAll(\"*\")) {\n if (el.getAttribute(\"data-hf-id\")) continue;\n if (EXCLUDED_TAGS.has(el.tagName.toLowerCase())) continue;\n el.setAttribute(\"data-hf-id\", mintHfId(el, assigned));\n }\n}\n\n// fallow-ignore-next-line complexity\nexport function patchElementInHtml(\n source: string,\n target: SourceMutationTarget,\n operations: PatchOperation[],\n takenIds?: ReadonlySet<string>,\n): { html: string; matched: boolean; elementId?: string | null } {\n const { document, wrappedFragment } = parseSourceDocument(source);\n const el = findTargetElement(document, target);\n if (!el || !isHTMLElement(el)) return { html: source, matched: false };\n const htmlEl = el;\n const originalHtml = wrappedFragment ? document.body.innerHTML || \"\" : document.toString();\n\n const resolved: ResolvedPatchOperation[] = [];\n for (const op of operations) {\n const opTarget = resolveOperationTarget(htmlEl, op);\n if (!opTarget) return { html: source, matched: false };\n resolved.push({ op, target: opTarget });\n }\n\n for (const { op, target: opTarget } of resolved) {\n switch (op.type) {\n case \"inline-style\":\n // linkedom's CSSStyleDeclaration does not support CSS custom properties\n // (--foo) or newer individual transform properties (translate, rotate,\n // scale) via style.setProperty(). Manipulate the style attribute string\n // directly so all property names survive the round-trip.\n {\n const raw = opTarget.getAttribute(\"style\") ?? \"\";\n const patched = patchStyleAttrString(raw, op.property, op.value);\n opTarget.setAttribute(\"style\", patched);\n }\n break;\n case \"attribute\":\n {\n const fullAttr = op.property.startsWith(\"data-\") ? op.property : `data-${op.property}`;\n if (op.value != null) {\n opTarget.setAttribute(fullAttr, op.value);\n } else {\n opTarget.removeAttribute(fullAttr);\n }\n }\n break;\n case \"html-attribute\":\n if (!isAllowedHtmlAttribute(op.property)) break;\n if (op.value != null) {\n if (!isSafeAttributeValue(op.property, op.value)) break;\n const replacementId =\n op.property === \"src\" ? replacementTimelineAssetId(document, opTarget, op.value) : null;\n if (replacementId !== null) opTarget.setAttribute(\"id\", replacementId);\n opTarget.setAttribute(op.property, op.value);\n } else {\n opTarget.removeAttribute(op.property);\n }\n break;\n case \"ensure-id\":\n if (opTarget.getAttribute(\"id\") || !op.value) break;\n opTarget.setAttribute(\"id\", nextUniqueId(document, op.value, takenIds));\n break;\n case \"text-content\":\n if (op.value != null) {\n const inner = opTarget.children.length === 1 ? opTarget.firstElementChild : null;\n const textTarget = inner && isHTMLElement(inner) ? inner : opTarget;\n textTarget.textContent = op.value;\n }\n break;\n // The one operation that can write markup, so the one that has to check\n // it. Assigned first and sanitised after, rather than sanitising a\n // string: parsing is what turns a payload into the tree the allowlist\n // can actually judge, and linkedom never runs anything it parses.\n case \"rich-text\":\n if (op.value != null) {\n opTarget.innerHTML = op.value;\n sanitizeRichTextChildren(opTarget);\n stampNewChildIds(opTarget);\n }\n break;\n }\n }\n\n const html = wrappedFragment ? document.body.innerHTML || \"\" : document.toString();\n const elementId = htmlEl.getAttribute(\"id\");\n if (html === originalHtml) return { html: source, matched: true, elementId };\n return { html: ensureHfIds(html), matched: true, elementId };\n}\n\nexport function nextUniqueId(\n document: Document,\n base: string,\n takenIds?: ReadonlySet<string>,\n): string {\n let id = base;\n let suffix = 2;\n while (document.getElementById(id) || takenIds?.has(id)) id = `${base}-${suffix++}`;\n return id;\n}\n\n/** Whether each target exists in `source`; the document is parsed once however many targets ask. */\nexport function probeElementsInSource(\n source: string,\n targets: readonly SourceMutationTarget[],\n): boolean[] {\n const { document } = parseSourceDocument(source);\n return targets.map((target) => {\n if (!target.id && !target.hfId && !target.selector) return false;\n const el = findTargetElement(document, target);\n return el != null && isHTMLElement(el);\n });\n}\n\nexport function probeElementInSource(source: string, target: SourceMutationTarget): boolean {\n return probeElementsInSource(source, [target])[0] ?? false;\n}\n\nexport interface SplitElementResult {\n html: string;\n matched: boolean;\n newId: string | null;\n}\n\nfunction resolveElementTiming(el: Element): {\n start: number;\n duration: number;\n} {\n const timing = readClipTiming(el);\n return { start: timing.start ?? 0, duration: timing.duration ?? 0 };\n}\n\nfunction setElementDuration(\n el: Element,\n start: number,\n duration: number,\n trackIndex?: number,\n): void {\n writeClipTiming(el, {\n start: Math.round(start * 1000) / 1000,\n duration: Math.round(duration * 1000) / 1000,\n ...(trackIndex != null ? { trackIndex } : {}),\n });\n}\n\n// fallow-ignore-next-line complexity\nexport function splitElementInHtml(\n source: string,\n target: SourceMutationTarget,\n splitTime: number,\n newId: string,\n fallbackTiming?: {\n start: number;\n duration: number;\n playbackStart?: number;\n playbackRate?: number;\n stampPlaybackStart?: boolean;\n // The element's current resolved track (authored, or the runtime's\n // positional-index fallback when unauthored). Stamped onto both halves so\n // inserting the clone can't shift either one to a different row — see\n // parseAuthoredTrack's fallback in core/runtime/timeline.ts.\n track?: number;\n },\n): SplitElementResult {\n const { document, wrappedFragment } = parseSourceDocument(source);\n const el = findTargetElement(document, target);\n if (!el || !isHTMLElement(el)) return { html: source, matched: false, newId: null };\n\n const timing = resolveElementTiming(el);\n let { start, duration } = timing;\n // GSAP-animated elements carry their timing in the script, not in data-* attrs,\n // so the source has no authored duration. Fall back to the store's (GSAP-derived)\n // range — the runtime windows visibility off data-start/data-duration regardless\n // of class, so stamping both halves below makes each half show only in its window.\n if (duration <= 0 && fallbackTiming && fallbackTiming.duration > 0) {\n start = fallbackTiming.start;\n duration = fallbackTiming.duration;\n }\n if (duration <= 0 || splitTime <= start || splitTime >= start + duration) {\n return { html: source, matched: false, newId: null };\n }\n\n newId = nextUniqueId(document, newId);\n\n const firstDuration = splitTime - start;\n const secondDuration = duration - firstDuration;\n\n const clone = el.cloneNode(true);\n if (!isHTMLElement(clone)) return { html: source, matched: false, newId: null };\n clone.setAttribute(\"id\", newId);\n dedupeClonedCompositionId(document, clone);\n clone.removeAttribute(\"data-hf-id\");\n // Descendants carry their own data-hf-id; leaving them duplicates the id of\n // every nested node (e.g. an inner <span>), so strip them on the clone too.\n for (const node of clone.querySelectorAll(\"[data-hf-id]\")) node.removeAttribute(\"data-hf-id\");\n setElementDuration(clone, splitTime, secondDuration, fallbackTiming?.track);\n\n // Keep the \"clip\" class — the runtime uses it to control visibility\n // based on data-start/data-duration timing.\n\n // A split creates two views over the same media source. Even an untrimmed\n // audio/video element needs an explicit zero in-point stamped on the first\n // half so the second half can advance from it instead of restarting at zero.\n const playbackStartAttr = el.hasAttribute(\"data-playback-start\")\n ? \"data-playback-start\"\n : el.hasAttribute(\"data-media-start\")\n ? \"data-media-start\"\n : fallbackTiming?.stampPlaybackStart\n ? \"data-playback-start\"\n : el.matches(\"audio, video\")\n ? \"data-media-start\"\n : null;\n if (playbackStartAttr) {\n const readAttr = (name: string) => el.getAttribute(name);\n const authoredTrim = el.getAttribute(playbackStartAttr);\n const currentTrim =\n authoredTrim !== null\n ? readMediaOffsetSeconds(readAttr)\n : (fallbackTiming?.playbackStart ?? 0);\n const rate = readPlaybackRate(readAttr, fallbackTiming?.playbackRate);\n if (authoredTrim === null || Number(authoredTrim) !== currentTrim)\n el.setAttribute(playbackStartAttr, String(Math.round(currentTrim * 1000) / 1000));\n clone.setAttribute(\n playbackStartAttr,\n String(Math.round((currentTrim + firstDuration * rate) * 1000) / 1000),\n );\n }\n\n // Duplicate CSS rules targeting the original ID so the clone inherits the same styles.\n const originalId = el.getAttribute(\"id\");\n if (originalId) {\n duplicateCssRulesForId(document, originalId, newId);\n }\n\n // Trim the original element's duration. A GSAP element had no data-start; stamp\n // it so the runtime windows the first half (visibility selects on [data-start]).\n setElementDuration(el, start, firstDuration, fallbackTiming?.track);\n\n // Insert clone after original\n if (el.nextSibling) {\n el.parentElement!.insertBefore(clone, el.nextSibling);\n } else {\n el.parentElement!.appendChild(clone);\n }\n\n const html = wrappedFragment ? document.body.innerHTML || \"\" : document.toString();\n return {\n // The split owns its new nodes' stable ids. Leaving the clone unstamped makes\n // the next preview request persist different bytes after history is recorded.\n html: ensureHfIds(html),\n matched: true,\n newId,\n };\n}\n\n// --- Element grouping -------------------------------------------------------\n// A group is a real `<div data-hf-group=\"…\">` wrapping its members in the DOM.\n// Wrapping rebases each member's left/top so its absolute position is unchanged:\n// the wrapper sits at the selection bbox top-left, and each child's new left/top\n// is its old left/top minus the wrapper origin (computed client-side, where live\n// layout is available, and passed in via `rebases`). GSAP x/y, CSS translate and\n// --hf-studio-offset vars are deltas relative to flow position and stay untouched.\n\nexport interface WrapElementsResult {\n html: string;\n matched: boolean;\n groupId: string | null;\n error?: string;\n}\n\nexport interface UnwrapElementsResult {\n html: string;\n unwrapped: boolean;\n /** The unwrapped wrapper's id, so callers can strip GSAP that targeted it\n * (the wrapper is gone; a leftover `gsap.set(\"#id\")` would throw at runtime). */\n unwrappedGroupId?: string;\n /** Members (id'd children) with their absolute layout centres (post un-rebase),\n * so the caller can BAKE the group's GSAP transform into each member before\n * stripping it — otherwise the group's moves are lost on ungroup. */\n members?: Array<{ id: string; cx: number; cy: number }>;\n /** The wrapper's layout centre — the pivot for baking the group's rotation/scale. */\n groupCenter?: { cx: number; cy: number };\n}\n\nexport interface ElementRebase {\n target: SourceMutationTarget;\n left: number;\n top: number;\n /** The member's current resolved track (authored, or the runtime's\n * positional-index fallback). Stamped explicitly so moving it into the\n * wrapper can't shift its computed row — same hazard split closes. */\n track?: number;\n}\n\nfunction getInlineStylePx(el: Element, property: string): number {\n const style = (isHTMLElement(el) ? el.getAttribute(\"style\") : null) ?? \"\";\n const { props } = parseStyleDecls(style);\n const raw = props.get(property);\n if (!raw) return 0;\n const n = parseFloat(raw);\n return Number.isFinite(n) ? n : 0;\n}\n\nfunction setInlineLeftTop(el: HTMLElement, left: number, top: number): void {\n let style = el.getAttribute(\"style\") ?? \"\";\n style = patchStyleAttrString(style, \"left\", `${left}px`);\n style = patchStyleAttrString(style, \"top\", `${top}px`);\n el.setAttribute(\"style\", style);\n}\n\n// Slug the group name (\"Group 1\" → \"group-1\") into a unique, valid element id.\nfunction uniqueGroupDomId(document: Document, groupId: string): string {\n const base =\n groupId\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n // Normalization above leaves at most one hyphen at either edge.\n .replace(/^-|-$/g, \"\") || \"group\";\n return nextUniqueId(document, base);\n}\n\n// fallow-ignore-next-line complexity\nexport function wrapElementsInHtml(\n source: string,\n targets: SourceMutationTarget[],\n groupId: string,\n bbox: { left: number; top: number; width: number; height: number },\n rebases: ElementRebase[],\n): WrapElementsResult {\n const { document, wrappedFragment } = parseSourceDocument(source);\n if (targets.length === 0) {\n return { html: source, matched: false, groupId: null, error: \"no targets\" };\n }\n\n // Resolve + dedupe by element ref (two targets may point at the same node).\n const els: HTMLElement[] = [];\n const seen = new Set<Element>();\n for (const target of targets) {\n const el = findTargetElement(document, target);\n if (!el || !isHTMLElement(el) || seen.has(el)) continue;\n seen.add(el);\n els.push(el);\n }\n if (els.length === 0) {\n return { html: source, matched: false, groupId: null, error: \"no targets matched\" };\n }\n\n // P1: require a single common parent (LCA multi-parent wrapping is P2).\n const parent = els[0]?.parentElement;\n if (!parent || els.some((el) => el.parentElement !== parent)) {\n return {\n html: source,\n matched: false,\n groupId: null,\n error: \"grouped elements must share a single parent\",\n };\n }\n\n // Order members by their position in the parent (= z-order / stacking order).\n const memberSet = new Set<Element>(els);\n const ordered = Array.from(parent.children).filter((c): c is HTMLElement => memberSet.has(c));\n\n // Map each member to its rebased left/top and track (resolved against the same document).\n const rebaseByEl = new Map<Element, { left: number; top: number; track?: number }>();\n for (const rebase of rebases) {\n const el = findTargetElement(document, rebase.target);\n if (el) rebaseByEl.set(el, { left: rebase.left, top: rebase.top, track: rebase.track });\n }\n\n const wrapper = document.createElement(\"div\");\n wrapper.setAttribute(\"data-hf-group\", groupId);\n // A real `id` (slug of the group name) makes the wrapper a first-class node in the\n // clip manifest / timeline parent-map (both keyed by id) and a clean GSAP target —\n // without it the wrapper is invisible to the timeline and breaks child enumeration.\n wrapper.setAttribute(\"id\", uniqueGroupDomId(document, groupId));\n // Adopt the topmost member's stacking level. A group is one stacking unit, so a\n // non-member interleaved between two selected members can't stay \"between\" them\n // once they unify. Matching Figma/Sketch, the group lifts to the topmost selected\n // layer: the wrapper goes at the LAST member's slot and carries the max member\n // z-index — so an interleaved non-member falls below the group instead of hoisting\n // above it, and explicit member z-indexes are honored.\n const memberZIndexes = ordered\n .map((el) =>\n Number.parseInt(\n parseStyleDecls(el.getAttribute(\"style\") ?? \"\").props.get(\"z-index\") ?? \"\",\n 10,\n ),\n )\n .filter((z) => Number.isFinite(z));\n const maxZ = memberZIndexes.length > 0 ? Math.max(...memberZIndexes) : null;\n wrapper.setAttribute(\n \"style\",\n `position: absolute; left: ${bbox.left}px; top: ${bbox.top}px; width: ${bbox.width}px; height: ${bbox.height}px` +\n (maxZ !== null ? `; z-index: ${maxZ}` : \"\"),\n );\n\n // Insert the wrapper at the topmost member's slot, then move members into it.\n parent.insertBefore(wrapper, ordered[ordered.length - 1] ?? null);\n for (const el of ordered) {\n const rebase = rebaseByEl.get(el);\n if (rebase) setInlineLeftTop(el, rebase.left, rebase.top);\n if (rebase?.track != null) writeClipTiming(el, { trackIndex: Math.round(rebase.track) });\n wrapper.appendChild(el); // appendChild moves the node, preserving order\n }\n\n return {\n html: wrappedFragment ? document.body.innerHTML || \"\" : document.toString(),\n matched: true,\n groupId,\n };\n}\n\nexport interface UnwrapChildTrack {\n target: SourceMutationTarget;\n /** The child's current resolved track, same hazard and fix as wrap's members. */\n track?: number;\n}\n\n// Only children actually inside the group and given a resolved track qualify —\n// same hazard and fix as wrap's members, scoped to this group's own children.\nfunction buildChildTrackMap(\n document: Document,\n group: Element,\n childTracks: UnwrapChildTrack[],\n): Map<Element, number> {\n const trackByEl = new Map<Element, number>();\n for (const entry of childTracks) {\n const el = findTargetElement(document, entry.target);\n if (el && group.contains(el) && entry.track != null) trackByEl.set(el, entry.track);\n }\n return trackByEl;\n}\n\n// Undoes the wrap-side rebase (child absolute = child rebased + wrapper\n// origin), stamps each child's resolved track where one was given, and moves\n// every child back into the parent ahead of the wrapper — preserving order.\nfunction relocateGroupChildren(\n group: Element,\n parent: Element,\n wLeft: number,\n wTop: number,\n trackByEl: Map<Element, number>,\n): Array<{ id: string; cx: number; cy: number }> {\n const members: Array<{ id: string; cx: number; cy: number }> = [];\n for (const child of Array.from(group.children)) {\n if (isHTMLElement(child)) {\n const newLeft = getInlineStylePx(child, \"left\") + wLeft;\n const newTop = getInlineStylePx(child, \"top\") + wTop;\n setInlineLeftTop(child, newLeft, newTop);\n const track = trackByEl.get(child);\n if (track != null) writeClipTiming(child, { trackIndex: Math.round(track) });\n if (child.id) {\n members.push({\n id: child.id,\n cx: newLeft + getInlineStylePx(child, \"width\") / 2,\n cy: newTop + getInlineStylePx(child, \"height\") / 2,\n });\n }\n }\n parent.insertBefore(child, group);\n }\n return members;\n}\n\nexport function unwrapElementsFromHtml(\n source: string,\n groupTarget: SourceMutationTarget,\n childTracks: UnwrapChildTrack[] = [],\n): UnwrapElementsResult {\n const { document, wrappedFragment } = parseSourceDocument(source);\n const group = findTargetElement(document, groupTarget);\n if (!group || !isHTMLElement(group)) return { html: source, unwrapped: false };\n // Shape guard mirroring the wrap-side contract: only ever dissolve an actual\n // group wrapper. A stale/desynced selection that resolves to a plain <div>\n // would otherwise be unwrapped — rebasing its children by the parent's origin\n // (silent corruption). Wrap enforces invariants; unwrap must too.\n if (!group.hasAttribute(\"data-hf-group\")) return { html: source, unwrapped: false };\n\n const parent = group.parentElement;\n if (!parent) return { html: source, unwrapped: false };\n\n const trackByEl = buildChildTrackMap(document, group, childTracks);\n\n // Undo the rebase: child absolute position = child (rebased) + wrapper origin.\n const wLeft = getInlineStylePx(group, \"left\");\n const wTop = getInlineStylePx(group, \"top\");\n const groupCenter = {\n cx: wLeft + getInlineStylePx(group, \"width\") / 2,\n cy: wTop + getInlineStylePx(group, \"height\") / 2,\n };\n\n const members = relocateGroupChildren(group, parent, wLeft, wTop, trackByEl);\n const groupId = group.id || undefined;\n group.remove();\n\n return {\n html: wrappedFragment ? document.body.innerHTML || \"\" : document.toString(),\n unwrapped: true,\n unwrappedGroupId: groupId,\n members,\n groupCenter,\n };\n}\n\n/** After a cut, give each linked group's right halves their own `data-link` and `data-sync-origin`. */\nexport function relinkSplitHalvesInHtml(source: string, rightHalfIds: readonly string[]): string {\n const { document, wrappedFragment } = parseSourceDocument(source);\n const carriesPairing = (id: string) => {\n const el = document.getElementById(id);\n return Boolean(el?.hasAttribute(\"data-link\") || el?.hasAttribute(\"data-sync-origin\"));\n };\n if (!rightHalfIds.some(carriesPairing)) {\n return source;\n }\n relinkSplitHalves(document, rightHalfIds);\n return wrappedFragment ? document.body.innerHTML || \"\" : document.toString();\n}\n","// fallow-ignore-next-line complexity\nexport function parseStyleDecls(style: string): { props: Map<string, string>; order: string[] } {\n const props = new Map<string, string>();\n const order: string[] = [];\n let i = 0;\n while (i < style.length) {\n let depth = 0;\n let inSingle = false;\n let inDouble = false;\n const start = i;\n while (i < style.length) {\n const ch = style[i];\n if (ch === \"'\" && !inDouble) inSingle = !inSingle;\n else if (ch === '\"' && !inSingle) inDouble = !inDouble;\n else if (!inSingle && !inDouble) {\n if (ch === \"(\") depth++;\n else if (ch === \")\") depth = Math.max(0, depth - 1);\n else if (ch === \";\" && depth === 0) break;\n }\n i++;\n }\n const decl = style.slice(start, i).trim();\n i++;\n if (!decl) continue;\n const colon = decl.indexOf(\":\");\n if (colon < 0) continue;\n const key = decl.slice(0, colon).trim();\n const val = decl.slice(colon + 1).trim();\n if (!key) continue;\n if (!props.has(key)) order.push(key);\n props.set(key, val);\n }\n return { props, order };\n}\n\nfunction serializeStyleDecls(props: Map<string, string>, order: string[]): string {\n return order\n .map((k) => `${k}: ${props.get(k) ?? \"\"}`)\n .filter((d) => d.trim())\n .join(\"; \");\n}\n\nexport function patchStyleAttrString(\n style: string,\n property: string,\n value: string | null,\n): string {\n const { props, order } = parseStyleDecls(style);\n if (value === null) {\n props.delete(property);\n const idx = order.indexOf(property);\n if (idx >= 0) order.splice(idx, 1);\n } else {\n if (!props.has(property)) order.push(property);\n props.set(property, value);\n }\n return serializeStyleDecls(props, order);\n}\n"],"mappings":"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","names":[]}
@@ -13,7 +13,7 @@ import {
13
13
  splitElementInHtml,
14
14
  unwrapElementsFromHtml,
15
15
  wrapElementsInHtml
16
- } from "../chunk-CLWFZQUU.js";
16
+ } from "../chunk-JJ4N3PXT.js";
17
17
  export {
18
18
  dedupeClonedCompositionId,
19
19
  findTargetElement,
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  splitElementInHtml,
18
18
  unwrapElementsFromHtml,
19
19
  wrapElementsInHtml
20
- } from "./chunk-CLWFZQUU.js";
20
+ } from "./chunk-JJ4N3PXT.js";
21
21
  import {
22
22
  isCompositionSource,
23
23
  stampProjectHfIds
@@ -4339,10 +4339,25 @@ function registerLintRoutes(api, adapter) {
4339
4339
  // src/routes/render.ts
4340
4340
  import { streamSSE } from "hono/streaming";
4341
4341
  import { existsSync as existsSync6, lstatSync as lstatSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
4342
- import { join as join10 } from "path";
4342
+ import { dirname as dirname5, join as join10 } from "path";
4343
4343
  import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
4344
4344
  import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
4345
4345
  var VALID_RESOLUTIONS = new Set(VALID_CANVAS_RESOLUTIONS);
4346
+ var RENDER_MIME = {
4347
+ ".mp4": "video/mp4",
4348
+ ".webm": "video/webm",
4349
+ ".mov": "video/quicktime"
4350
+ };
4351
+ var RENDER_EXTENSIONS = Object.keys(RENDER_MIME);
4352
+ var RENDER_ARTIFACT_EXTENSIONS = [...RENDER_EXTENSIONS, ".meta.json"];
4353
+ function resolveRenderArtifacts(directories, jobId) {
4354
+ const artifacts = [...new Set(directories)].filter(existsSync6).flatMap(
4355
+ (directory) => RENDER_ARTIFACT_EXTENSIONS.map(
4356
+ (extension) => resolveWithinProject(directory, `${jobId}${extension}`)
4357
+ )
4358
+ );
4359
+ return artifacts.every((path) => path !== null) ? artifacts : null;
4360
+ }
4346
4361
  function contentDispositionHeader(disposition, filename) {
4347
4362
  const fallback = filename.replace(/[^\x20-\x7e]|["\\]/g, "_");
4348
4363
  const encoded = encodeURIComponent(filename).replace(
@@ -4460,12 +4475,6 @@ function registerRenderRoutes(api, adapter) {
4460
4475
  }
4461
4476
  return c.json({ status: job.status });
4462
4477
  });
4463
- const RENDER_MIME = {
4464
- ".mp4": "video/mp4",
4465
- ".webm": "video/webm",
4466
- ".mov": "video/quicktime"
4467
- };
4468
- const RENDER_EXTENSIONS = Object.keys(RENDER_MIME);
4469
4478
  const isPlainFile = (path) => {
4470
4479
  try {
4471
4480
  return lstatSync3(path).isFile();
@@ -4511,17 +4520,22 @@ function registerRenderRoutes(api, adapter) {
4511
4520
  }
4512
4521
  });
4513
4522
  });
4514
- api.delete("/render/:jobId", (c) => {
4523
+ async function renderDirectories(jobId) {
4524
+ const outputPath = renderJobs.get(jobId)?.outputPath;
4525
+ if (outputPath) return [dirname5(outputPath)];
4526
+ const projects = await adapter.listProjects();
4527
+ return projects.map((project) => adapter.rendersDir(project));
4528
+ }
4529
+ api.delete("/render/:jobId", async (c) => {
4515
4530
  const { jobId } = c.req.param();
4516
- for (const [, state] of renderJobs) {
4517
- if (state.id === jobId && state.outputPath) {
4518
- const dir = state.outputPath.replace(/\/[^/]+$/, "");
4519
- for (const ext of [".mp4", ".webm", ".mov", ".meta.json"]) {
4520
- const fp = join10(dir, `${jobId}${ext}`);
4521
- if (existsSync6(fp)) unlinkSync3(fp);
4522
- }
4523
- break;
4524
- }
4531
+ if (jobId.includes("/") || jobId.includes("\\") || jobId.includes("\0")) {
4532
+ return c.json({ error: "forbidden" }, 403);
4533
+ }
4534
+ const directories = await renderDirectories(jobId);
4535
+ const artifacts = resolveRenderArtifacts(directories, jobId);
4536
+ if (!artifacts) return c.json({ error: "forbidden" }, 403);
4537
+ for (const path of artifacts) {
4538
+ if (existsSync6(path)) unlinkSync3(path);
4525
4539
  }
4526
4540
  renderJobs.delete(jobId);
4527
4541
  return c.json({ deleted: true });
@@ -5065,7 +5079,7 @@ function registerWaveformRoutes(api, adapter) {
5065
5079
  // src/routes/freezeFrame.ts
5066
5080
  import { execFile } from "child_process";
5067
5081
  import { readFileSync as readFileSync11, rmSync as rmSync5, statSync as statSync8 } from "fs";
5068
- import { basename as basename2, dirname as dirname5, join as join13, relative as relative4, sep as sep2 } from "path";
5082
+ import { basename as basename2, dirname as dirname6, join as join13, relative as relative4, sep as sep2 } from "path";
5069
5083
  import { findFfBinary as findFfBinary3 } from "@hyperframes/parsers/ff-binaries";
5070
5084
  import {
5071
5085
  atomicTempPath,
@@ -5312,14 +5326,14 @@ async function extractThenPublishWhole(extract, mediaPath, mediaTime, imagePath)
5312
5326
  }
5313
5327
  }
5314
5328
  async function extractStill(projectDir, absPath, source, playhead, tools) {
5315
- const fileDir = dirname5(absPath);
5329
+ const fileDir = dirname6(absPath);
5316
5330
  const mediaPath = pinWithinProject(projectDir, relative4(projectDir, join13(fileDir, source.src)));
5317
5331
  if (!mediaPath) return { error: `forbidden media path: ${source.src}`, status: 403 };
5318
5332
  const freezeDir = join13(projectDir, ...FREEZE_DIR);
5319
5333
  const fileName = freezeStillFileName(source.id, playhead, tools.stillToken());
5320
5334
  mkdirWithinProject(projectDir, freezeDir);
5321
5335
  const imagePath = pinWithinProject(projectDir, join13(...FREEZE_DIR, fileName));
5322
- if (!imagePath || dirname5(imagePath) !== freezeDir) {
5336
+ if (!imagePath || dirname6(imagePath) !== freezeDir) {
5323
5337
  return { error: `forbidden freeze path: ${fileName}`, status: 403 };
5324
5338
  }
5325
5339
  const failed = await extractThenPublishWhole(
@@ -5883,7 +5897,7 @@ function registerSelectionRoutes(api, adapter) {
5883
5897
  // src/routes/media.ts
5884
5898
  import { streamSSE as streamSSE2 } from "hono/streaming";
5885
5899
  import { existsSync as existsSync10 } from "fs";
5886
- import { basename as basename3, dirname as dirname6, extname, join as join15, posix } from "path";
5900
+ import { basename as basename3, dirname as dirname7, extname, join as join15, posix } from "path";
5887
5901
  var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
5888
5902
  ".mp4",
5889
5903
  ".mov",
@@ -6039,8 +6053,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
6039
6053
  return c.json({ error: "forbidden" }, 403);
6040
6054
  }
6041
6055
  }
6042
- mkdirWithinProject(project.dir, dirname6(outputPath));
6043
- if (backgroundOutputPath) mkdirWithinProject(project.dir, dirname6(backgroundOutputPath));
6056
+ mkdirWithinProject(project.dir, dirname7(outputPath));
6057
+ if (backgroundOutputPath) mkdirWithinProject(project.dir, dirname7(backgroundOutputPath));
6044
6058
  const jobId = makeJobId(project.id, mediaJobs);
6045
6059
  const state = adapter.startBackgroundRemoval({
6046
6060
  project,