(html: string)
| 174 | } |
| 175 | |
| 176 | export function parseHtml(html: string): ParsedHtml { |
| 177 | const withIds = ensureHfIds(html); |
| 178 | const parser = new DOMParser(); |
| 179 | const doc = parser.parseFromString(withIds, "text/html"); |
| 180 | |
| 181 | const elements: TimelineElement[] = []; |
| 182 | const keyframes: Record<string, Keyframe[]> = {}; |
| 183 | let idCounter = 0; |
| 184 | |
| 185 | const htmlEl = doc.documentElement; |
| 186 | if (!htmlEl) { |
| 187 | throw new CompositionHtmlParseError("parseHtml: input HTML is empty or could not be parsed"); |
| 188 | } |
| 189 | const customStylesAttr = htmlEl.getAttribute("data-custom-styles"); |
| 190 | let customStyles: string | null = null; |
| 191 | if (customStylesAttr) { |
| 192 | try { |
| 193 | customStyles = JSON.parse(customStylesAttr); |
| 194 | } catch { |
| 195 | customStyles = customStylesAttr; |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | const timedElements = doc.querySelectorAll("[data-start]"); |
| 200 | |
| 201 | timedElements.forEach((el) => { |
| 202 | const type = getElementType(el); |
| 203 | if (!type) return; |
| 204 | |
| 205 | const start = parseFloat(el.getAttribute("data-start") || "0"); |
| 206 | const dataEnd = el.getAttribute("data-end"); |
| 207 | |
| 208 | let duration: number; |
| 209 | if (dataEnd) { |
| 210 | duration = Math.max(0, parseFloat(dataEnd) - start); |
| 211 | } else { |
| 212 | duration = 5; |
| 213 | } |
| 214 | |
| 215 | // R1: stable hf- id minted by ensureHfIds above; clips just read it. |
| 216 | // Legacy/migration note: ensureHfIds pins a pre-existing `data-hf-id`, and |
| 217 | // the generator emits `data-hf-id="${element.id}"`. So a clip authored |
| 218 | // before R1 with `id="my-title"` round-trips as `data-hf-id="my-title"` — |
| 219 | // a non-`hf-`-shaped but still stable, exact-match handle. This is safe |
| 220 | // indefinitely: targeting uses exact `[data-hf-id="…"]` match (it does not |
| 221 | // require the hf- prefix). ensureHfIds skips elements that already carry |
| 222 | // data-hf-id, so legacy values are NOT re-minted automatically — they |
| 223 | // persist until the user re-saves the composition through Studio. Not a bug. |
| 224 | const id = el.getAttribute("data-hf-id") || el.id || `element-${++idCounter}`; |
| 225 | const name = getElementName(el); |
| 226 | const zIndex = getZIndex(el); |
| 227 | |
| 228 | // Parse data-keyframes attribute if present |
| 229 | const keyframesAttr = el.getAttribute("data-keyframes"); |
| 230 | if (keyframesAttr) { |
| 231 | try { |
| 232 | const parsedKeyframes = JSON.parse(keyframesAttr); |
| 233 | if (Array.isArray(parsedKeyframes) && parsedKeyframes.length > 0) { |
no test coverage detected