@hyperframes/parsers 0.7.15

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.
@@ -0,0 +1,18 @@
1
+ declare const EXCLUDED_TAGS: Set<string>;
2
+ /**
3
+ * Collision tiebreak for byte-identical siblings: document-order dup counter
4
+ * (`hash(key#N)`). This IS order-dependent — two identical `<span></span>`
5
+ * get different ids based on which comes first in the DOM. This is unavoidable:
6
+ * unique ids for byte-identical elements require a positional signal.
7
+ *
8
+ * Why this is safe in practice: once `ensureHfIds` write-back persists
9
+ * `data-hf-id` to source the attribute is physically bound to its element.
10
+ * Reordering identical siblings carries the attribute along → zero
11
+ * order-dependence post-persist. `ensureHfIds` skips pinned elements
12
+ * (`if (el.getAttribute("data-hf-id")) continue`), so normal operation
13
+ * never re-exposes the ordering after first persist.
14
+ */
15
+ declare function mintHfId(el: Element, assigned: Set<string>): string;
16
+ declare function ensureHfIds(html: string): string;
17
+
18
+ export { EXCLUDED_TAGS, ensureHfIds, mintHfId };
package/dist/hfIds.js ADDED
@@ -0,0 +1,74 @@
1
+ // src/hfIds.ts
2
+ import { parseHTML } from "linkedom";
3
+ var EXCLUDED_TAGS = /* @__PURE__ */ new Set([
4
+ "script",
5
+ "style",
6
+ "template",
7
+ "meta",
8
+ "link",
9
+ "noscript",
10
+ "base"
11
+ ]);
12
+ function fnv1a(str) {
13
+ let h = 2166136261;
14
+ for (let i = 0; i < str.length; i++) {
15
+ h ^= str.charCodeAt(i);
16
+ h = Math.imul(h, 16777619);
17
+ }
18
+ return h >>> 0;
19
+ }
20
+ function toHfId(hash) {
21
+ const s = (hash >>> 0).toString(36);
22
+ const four = s.length >= 4 ? s.slice(-4) : s.padStart(4, "0");
23
+ return `hf-${four}`;
24
+ }
25
+ function ownText(el) {
26
+ let text = "";
27
+ el.childNodes.forEach((n) => {
28
+ if (n.nodeType === 3) text += n.nodeValue ?? "";
29
+ });
30
+ return text.trim();
31
+ }
32
+ function contentKey(el) {
33
+ const attrs = Array.from(el.attributes).filter((a) => !a.name.startsWith("data-hf-")).map((a) => `${a.name}\0${a.value}`).sort().join("");
34
+ return `${el.tagName.toLowerCase()}|${attrs}|${ownText(el)}`;
35
+ }
36
+ function mintHfId(el, assigned) {
37
+ const key = contentKey(el);
38
+ let id = toHfId(fnv1a(key));
39
+ let dup = 0;
40
+ while (assigned.has(id)) {
41
+ dup += 1;
42
+ if (dup > 1e4) {
43
+ id = `hf-${(fnv1a(key) >>> 0).toString(36)}-${dup}`;
44
+ break;
45
+ }
46
+ id = toHfId(fnv1a(`${key}#${dup}`));
47
+ }
48
+ assigned.add(id);
49
+ return id;
50
+ }
51
+ function ensureHfIds(html) {
52
+ const hasDocumentShell = /<!doctype|<html[\s>]/i.test(html);
53
+ const wrapped = !hasDocumentShell;
54
+ const { document } = wrapped ? parseHTML(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`) : parseHTML(html);
55
+ const body = document.body;
56
+ if (!body) return html;
57
+ const assigned = /* @__PURE__ */ new Set();
58
+ for (const el of Array.from(body.querySelectorAll("[data-hf-id]"))) {
59
+ const existing = el.getAttribute("data-hf-id");
60
+ if (existing) assigned.add(existing);
61
+ }
62
+ for (const el of Array.from(body.querySelectorAll("*"))) {
63
+ if (EXCLUDED_TAGS.has(el.tagName.toLowerCase())) continue;
64
+ if (el.getAttribute("data-hf-id")) continue;
65
+ el.setAttribute("data-hf-id", mintHfId(el, assigned));
66
+ }
67
+ return wrapped ? document.body.innerHTML || "" : document.toString();
68
+ }
69
+ export {
70
+ EXCLUDED_TAGS,
71
+ ensureHfIds,
72
+ mintHfId
73
+ };
74
+ //# sourceMappingURL=hfIds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hfIds.ts"],"sourcesContent":["/**\n * Stable hf- element id minting (R1). Node-safe (linkedom only, not browser DOM).\n *\n * Two surfaces share these helpers:\n * - ensureHfIds(html): node-id surface — mints data-hf-id on every element.\n * - mintHfId(el, assigned): shared by htmlParser for clip ids.\n *\n * Hash is CONTENT ONLY (tag + sorted attrs + own text) — no sibling position,\n * so inserting a non-identical sibling never shifts another element's id.\n */\nimport { parseHTML } from \"linkedom\";\n\n// Non-editable / non-visual elements that should never receive a stable id.\nexport const EXCLUDED_TAGS = new Set([\n \"script\",\n \"style\",\n \"template\",\n \"meta\",\n \"link\",\n \"noscript\",\n \"base\",\n]);\n\n// 32-bit FNV-1a. Pure, deterministic, no crypto, no Math.random.\nfunction fnv1a(str: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return h >>> 0;\n}\n\n// 4 base-36 chars · 36^4 ≈ 1.68M ids per document. Birthday-paradox collision\n// ≈ N²/(2·36^4): well under 1% per document after dup rehash at realistic\n// clip-model sizes (≤ a few hundred elements). The dup-rehash in mintHfId\n// resolves the rare collision; width is deliberately small for readable ids.\nfunction toHfId(hash: number): string {\n const s = (hash >>> 0).toString(36);\n // Use suffix (most-avalanched bits) for better distribution within the 4-char window.\n const four = s.length >= 4 ? s.slice(-4) : s.padStart(4, \"0\");\n return `hf-${four}`;\n}\n\n// Element's own direct text (TEXT_NODE children), not descendants'.\nfunction ownText(el: Element): string {\n let text = \"\";\n el.childNodes.forEach((n) => {\n if (n.nodeType === 3) text += (n as Text).nodeValue ?? \"\";\n });\n return text.trim();\n}\n\nfunction contentKey(el: Element): string {\n // Exclude all data-hf-* attrs (ids, studio state) — they must not influence the hash.\n // Use \\x00 / \\x01 separators (invalid in HTML attrs) to prevent ambiguous serialization.\n const attrs = Array.from(el.attributes)\n .filter((a) => !a.name.startsWith(\"data-hf-\"))\n .map((a) => `${a.name}\\x00${a.value}`)\n .sort()\n .join(\"\\x01\");\n return `${el.tagName.toLowerCase()}|${attrs}|${ownText(el)}`;\n}\n\n/**\n * Collision tiebreak for byte-identical siblings: document-order dup counter\n * (`hash(key#N)`). This IS order-dependent — two identical `<span></span>`\n * get different ids based on which comes first in the DOM. This is unavoidable:\n * unique ids for byte-identical elements require a positional signal.\n *\n * Why this is safe in practice: once `ensureHfIds` write-back persists\n * `data-hf-id` to source the attribute is physically bound to its element.\n * Reordering identical siblings carries the attribute along → zero\n * order-dependence post-persist. `ensureHfIds` skips pinned elements\n * (`if (el.getAttribute(\"data-hf-id\")) continue`), so normal operation\n * never re-exposes the ordering after first persist.\n */\n// WIRE CONTRACT: id minting is content-keyed (FNV1a of innerHTML + tag). R7's\n// preview route relies on mintHfId producing identical ids across mint contexts\n// (disk-persist pass vs. in-memory bundle pass) — see preview.test.ts\n// \"bundle returning untagged HTML gets same ids as disk\". Any change that adds\n// positional, session, or random input to the hash breaks that invariant and\n// makes hf- ids diverge between disk and served HTML, silently corrupting\n// drag-to-edit targeting.\nexport function mintHfId(el: Element, assigned: Set<string>): string {\n const key = contentKey(el);\n let id = toHfId(fnv1a(key));\n let dup = 0;\n while (assigned.has(id)) {\n dup += 1;\n // Graceful fallback instead of a hard throw: rehashing only fails to find a\n // free 4-char slot in a pathological document (~1.6M identical elements).\n // Rather than crash the whole parse, widen the id with the dup counter —\n // still deterministic and unique, just longer than the 4-char norm.\n if (dup > 10000) {\n id = `hf-${(fnv1a(key) >>> 0).toString(36)}-${dup}`;\n break;\n }\n id = toHfId(fnv1a(`${key}#${dup}`));\n }\n assigned.add(id);\n return id;\n}\n\nexport function ensureHfIds(html: string): string {\n // Mirror parseSourceDocument's fragment-wrapping so bare fragments don't land\n // outside <body> in linkedom, which would cause body.querySelectorAll to return [].\n const hasDocumentShell = /<!doctype|<html[\\s>]/i.test(html);\n const wrapped = !hasDocumentShell;\n const { document } = wrapped\n ? parseHTML(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`)\n : parseHTML(html);\n const body = document.body;\n if (!body) return html;\n\n const assigned = new Set<string>();\n // Seed with already-present ids (pin) so fresh mints never collide with them.\n // Scope to <body> to match the mint walk below — a stray data-hf-id in <head>\n // must not pin an id into the set that a body element would then be bumped off.\n for (const el of Array.from(body.querySelectorAll(\"[data-hf-id]\"))) {\n const existing = el.getAttribute(\"data-hf-id\");\n if (existing) assigned.add(existing);\n }\n\n for (const el of Array.from(body.querySelectorAll(\"*\"))) {\n if (EXCLUDED_TAGS.has(el.tagName.toLowerCase())) continue;\n if (el.getAttribute(\"data-hf-id\")) continue; // pinned\n el.setAttribute(\"data-hf-id\", mintHfId(el, assigned));\n }\n\n return wrapped ? document.body.innerHTML || \"\" : document.toString();\n}\n"],"mappings":";AAUA,SAAS,iBAAiB;AAGnB,IAAM,gBAAgB,oBAAI,IAAI;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGD,SAAS,MAAM,KAAqB;AAClC,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,SAAK,IAAI,WAAW,CAAC;AACrB,QAAI,KAAK,KAAK,GAAG,QAAU;AAAA,EAC7B;AACA,SAAO,MAAM;AACf;AAMA,SAAS,OAAO,MAAsB;AACpC,QAAM,KAAK,SAAS,GAAG,SAAS,EAAE;AAElC,QAAM,OAAO,EAAE,UAAU,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,GAAG;AAC5D,SAAO,MAAM,IAAI;AACnB;AAGA,SAAS,QAAQ,IAAqB;AACpC,MAAI,OAAO;AACX,KAAG,WAAW,QAAQ,CAAC,MAAM;AAC3B,QAAI,EAAE,aAAa,EAAG,SAAS,EAAW,aAAa;AAAA,EACzD,CAAC;AACD,SAAO,KAAK,KAAK;AACnB;AAEA,SAAS,WAAW,IAAqB;AAGvC,QAAM,QAAQ,MAAM,KAAK,GAAG,UAAU,EACnC,OAAO,CAAC,MAAM,CAAC,EAAE,KAAK,WAAW,UAAU,CAAC,EAC5C,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,KAAO,EAAE,KAAK,EAAE,EACpC,KAAK,EACL,KAAK,GAAM;AACd,SAAO,GAAG,GAAG,QAAQ,YAAY,CAAC,IAAI,KAAK,IAAI,QAAQ,EAAE,CAAC;AAC5D;AAsBO,SAAS,SAAS,IAAa,UAA+B;AACnE,QAAM,MAAM,WAAW,EAAE;AACzB,MAAI,KAAK,OAAO,MAAM,GAAG,CAAC;AAC1B,MAAI,MAAM;AACV,SAAO,SAAS,IAAI,EAAE,GAAG;AACvB,WAAO;AAKP,QAAI,MAAM,KAAO;AACf,WAAK,OAAO,MAAM,GAAG,MAAM,GAAG,SAAS,EAAE,CAAC,IAAI,GAAG;AACjD;AAAA,IACF;AACA,SAAK,OAAO,MAAM,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;AAAA,EACpC;AACA,WAAS,IAAI,EAAE;AACf,SAAO;AACT;AAEO,SAAS,YAAY,MAAsB;AAGhD,QAAM,mBAAmB,wBAAwB,KAAK,IAAI;AAC1D,QAAM,UAAU,CAAC;AACjB,QAAM,EAAE,SAAS,IAAI,UACjB,UAAU,2CAA2C,IAAI,gBAAgB,IACzE,UAAU,IAAI;AAClB,QAAM,OAAO,SAAS;AACtB,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,WAAW,oBAAI,IAAY;AAIjC,aAAW,MAAM,MAAM,KAAK,KAAK,iBAAiB,cAAc,CAAC,GAAG;AAClE,UAAM,WAAW,GAAG,aAAa,YAAY;AAC7C,QAAI,SAAU,UAAS,IAAI,QAAQ;AAAA,EACrC;AAEA,aAAW,MAAM,MAAM,KAAK,KAAK,iBAAiB,GAAG,CAAC,GAAG;AACvD,QAAI,cAAc,IAAI,GAAG,QAAQ,YAAY,CAAC,EAAG;AACjD,QAAI,GAAG,aAAa,YAAY,EAAG;AACnC,OAAG,aAAa,cAAc,SAAS,IAAI,QAAQ,CAAC;AAAA,EACtD;AAEA,SAAO,UAAU,SAAS,KAAK,aAAa,KAAK,SAAS,SAAS;AACrE;","names":[]}
@@ -0,0 +1,43 @@
1
+ import { C as CompositionVariable, T as TimelineElement, n as CanvasResolution, o as Keyframe, p as StageZoomKeyframe, V as ValidationResult } from './gsapSerialize-B_JRTCeV.js';
2
+ export { q as AddElementData, A as ArcPathConfig, a as ArcPathSegment, r as Asset, B as BooleanVariable, t as CANVAS_DIMENSIONS, u as COMPOSITION_VARIABLE_TYPES, w as ColorVariable, x as CompositionAPI, y as CompositionAsset, z as CompositionSpec, D as CompositionVariableBase, E as CompositionVariableType, F as DEFAULT_DURATIONS, H as ElementKeyframes, I as EnumVariable, J as FontVariable, G as GsapAnimation, b as GsapKeyframesData, c as GsapMethod, d as GsapPercentageKeyframe, e as GsapProvenance, f as GsapProvenanceKind, L as ImageVariable, K as KeyframeEditability, N as KeyframeProperties, O as MediaElementType, Q as MediaFile, R as NumberVariable, P as ParsedGsap, U as PlayerAPI, S as SplitAnimationsOptions, g as SplitAnimationsResult, W as StageZoom, X as StringVariable, Y as TIMELINE_COLORS, Z as TimelineCompositionElement, _ as TimelineElementBase, $ as TimelineElementType, a0 as TimelineMediaElement, a1 as TimelineTextElement, a2 as VALID_CANVAS_RESOLUTIONS, a3 as WaveformData, h as editabilityForProvenance, i as getAnimationsForElementId, a4 as getDefaultStageZoom, j as gsapAnimationsToKeyframes, a5 as isCompositionElement, a6 as isMediaElement, a7 as isTextElement, k as keyframesToGsapAnimations, a8 as normalizeResolutionFlag, s as serializeGsapAnimations, v as validateCompositionGsap } from './gsapSerialize-B_JRTCeV.js';
3
+ export { isStudioHoldSet } from './gsapParser.js';
4
+ export { PROPERTY_GROUPS, PropertyGroupName, SUPPORTED_EASES, SUPPORTED_PROPS, classifyPropertyGroup, classifyTweenPropertyGroup } from './gsapConstants.js';
5
+ export { SPRING_PRESETS, SpringPreset, generateSpringEaseData } from './springEase.js';
6
+ export { parseGsapScriptAcorn as parseGsapScript } from './gsapParserAcorn.js';
7
+ export { EXCLUDED_TAGS, ensureHfIds, mintHfId } from './hfIds.js';
8
+
9
+ interface ParsedHtml {
10
+ elements: TimelineElement[];
11
+ gsapScript: string | null;
12
+ styles: string | null;
13
+ resolution: CanvasResolution;
14
+ keyframes: Record<string, Keyframe[]>;
15
+ stageZoomKeyframes: StageZoomKeyframe[];
16
+ }
17
+ declare function parseHtml(html: string): ParsedHtml;
18
+ declare function updateElementInHtml(html: string, elementId: string, updates: Partial<TimelineElement>): string;
19
+ declare function addElementToHtml(html: string, element: Omit<TimelineElement, "id"> & {
20
+ id?: string;
21
+ }): {
22
+ html: string;
23
+ id: string;
24
+ };
25
+ declare function removeElementFromHtml(html: string, elementId: string): string;
26
+ interface CompositionMetadata {
27
+ compositionId: string | null;
28
+ compositionDuration: number | null;
29
+ variables: CompositionVariable[];
30
+ }
31
+ declare function extractCompositionMetadata(html: string): CompositionMetadata;
32
+ declare function validateCompositionHtml(html: string): ValidationResult;
33
+
34
+ /**
35
+ * Rewrite `script` so top-level helper calls / loops that build the timeline
36
+ * become explicit literal tweens. Returns the original script unchanged when
37
+ * there is nothing statically-resolvable to unroll.
38
+ */
39
+ declare function unrollComputedTimeline(script: string): string;
40
+
41
+ declare function queryByAttr(root: ParentNode, attr: string, value: string, tag?: string): Element | null;
42
+
43
+ export { CanvasResolution, type CompositionMetadata, CompositionVariable, Keyframe, type ParsedHtml, StageZoomKeyframe, TimelineElement, ValidationResult, addElementToHtml, extractCompositionMetadata, parseHtml, queryByAttr, removeElementFromHtml, unrollComputedTimeline, updateElementInHtml, validateCompositionHtml };