@unhingged/vizu-core 0.1.2 → 0.1.3

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/dist/auto.js CHANGED
@@ -1,11 +1,3 @@
1
- import {
2
- findByFingerprint,
3
- findElementByFingerprint,
4
- fingerprint,
5
- fingerprintKey,
6
- fingerprintLabel
7
- } from "./chunk-OMIFOSQ2.js";
8
-
9
1
  // src/types.ts
10
2
  var SCHEMA_VERSION = 2;
11
3
 
@@ -365,18 +357,6 @@ var CloudStorageAdapter = class {
365
357
  async preflightAuth() {
366
358
  await this.resolveToken();
367
359
  }
368
- /**
369
- * Synchronous accessor for the currently cached bearer token, used
370
- * by sibling modules that need to authenticate against the same
371
- * backend (live-collab cross-origin polling). Returns null when no
372
- * valid token is cached — callers fall back to credentials:'include'
373
- * for same-origin or just disable themselves.
374
- */
375
- getCachedAuthToken() {
376
- if (!this.cachedToken) return null;
377
- if (this.isExpired(this.cachedToken)) return null;
378
- return this.cachedToken.token;
379
- }
380
360
  /* ─── StorageAdapter v2 ───────────────────────────────────────────── */
381
361
  async load(_namespace) {
382
362
  const out = [];
@@ -692,10 +672,297 @@ function apiErr(status, body) {
692
672
  return err;
693
673
  }
694
674
 
675
+ // src/fingerprint.ts
676
+ var TEXT_SNIPPET_MAX = 80;
677
+ var ANCESTOR_DEPTH = 4;
678
+ var ANCESTOR_MAX_SHIFT = 3;
679
+ var TEXT_RATIO_THRESHOLD = 0.75;
680
+ var CLASS_JACCARD_THRESHOLD = 0.6;
681
+ function buildSelector(el) {
682
+ const parts = [];
683
+ let current = el;
684
+ let depth = 0;
685
+ while (current && current !== document.documentElement && depth < 8) {
686
+ let part = current.tagName.toLowerCase();
687
+ if (current.id) {
688
+ parts.unshift("#" + CSS.escape(current.id));
689
+ break;
690
+ }
691
+ const cls = Array.from(current.classList).filter((c) => !c.startsWith("vz-") && !c.startsWith("vizu-")).slice(0, 2);
692
+ if (cls.length) part += "." + cls.map((c) => CSS.escape(c)).join(".");
693
+ const parent = current.parentElement;
694
+ if (parent) {
695
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
696
+ if (sameTag.length > 1) {
697
+ const idx = sameTag.indexOf(current);
698
+ part += ":nth-of-type(" + (idx + 1) + ")";
699
+ }
700
+ }
701
+ parts.unshift(part);
702
+ current = parent;
703
+ depth++;
704
+ }
705
+ return parts.join(" > ");
706
+ }
707
+ function fingerprint(el) {
708
+ const text = el.innerText || el.textContent || "";
709
+ const parent = el.parentElement;
710
+ let siblingIndex = 0;
711
+ if (parent) {
712
+ siblingIndex = Array.from(parent.children).indexOf(el);
713
+ }
714
+ return {
715
+ selector: buildSelector(el),
716
+ parentSelector: parent ? buildSelector(parent) : "",
717
+ tagName: el.tagName,
718
+ textSnippet: text.trim().slice(0, TEXT_SNIPPET_MAX),
719
+ siblingIndex,
720
+ attributes: {
721
+ id: el.id || void 0,
722
+ classList: Array.from(el.classList).filter((c) => !c.startsWith("vz-") && !c.startsWith("vizu-")),
723
+ role: el.getAttribute("role") || void 0,
724
+ ariaLabel: el.getAttribute("aria-label") || void 0,
725
+ dataKey: el.getAttribute("data-vizu-key") || void 0
726
+ },
727
+ ancestorChain: captureAncestorChain(el, ANCESTOR_DEPTH),
728
+ algorithmVersion: 2
729
+ };
730
+ }
731
+ function captureAncestorChain(el, depth) {
732
+ const steps = [];
733
+ let current = el.parentElement;
734
+ while (current && steps.length < depth) {
735
+ const parent = current.parentElement;
736
+ let nthOfType = 1;
737
+ if (parent) {
738
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
739
+ nthOfType = sameTag.indexOf(current) + 1;
740
+ }
741
+ steps.push({ tag: current.tagName, nthOfType });
742
+ current = parent;
743
+ }
744
+ return steps.length > 0 ? { steps } : void 0;
745
+ }
746
+ function findByFingerprint(fp, root = document) {
747
+ if (fp.attributes.dataKey) {
748
+ try {
749
+ const byKey = root.querySelector(`[data-vizu-key="${CSS.escape(fp.attributes.dataKey)}"]`);
750
+ if (byKey) return { element: byKey, confidence: "exact" };
751
+ } catch {
752
+ }
753
+ return { element: null, confidence: "orphaned" };
754
+ }
755
+ if (fp.attributes.id) {
756
+ try {
757
+ const byId = root.querySelector("#" + CSS.escape(fp.attributes.id));
758
+ if (byId) return { element: byId, confidence: "exact" };
759
+ } catch {
760
+ }
761
+ }
762
+ const candidates = [];
763
+ const classes = fp.attributes.classList;
764
+ if (classes && classes.length > 0) {
765
+ const m = findByClassSignature(root, fp.tagName, classes);
766
+ if (m?.element) candidates.push({ el: m.element, rung: "class" });
767
+ }
768
+ try {
769
+ const bySelector = root.querySelector(fp.selector);
770
+ if (bySelector) candidates.push({ el: bySelector, rung: "selector" });
771
+ } catch {
772
+ }
773
+ if (fp.ancestorChain && fp.ancestorChain.steps.length > 0) {
774
+ const m = findByAncestorChain(root, fp.tagName, fp.ancestorChain.steps);
775
+ if (m?.element) candidates.push({ el: m.element, rung: "chain" });
776
+ } else {
777
+ try {
778
+ const parent = fp.parentSelector ? root.querySelector(fp.parentSelector) : root;
779
+ if (parent) {
780
+ const candidate = parent.children[fp.siblingIndex];
781
+ if (candidate && candidate.tagName === fp.tagName) {
782
+ candidates.push({ el: candidate, rung: "chain" });
783
+ }
784
+ }
785
+ } catch {
786
+ }
787
+ }
788
+ if (fp.textSnippet) {
789
+ const m = findByTextSnippet(root, fp.tagName, fp.textSnippet);
790
+ if (m?.element) candidates.push({ el: m.element, rung: "text" });
791
+ }
792
+ if (candidates.length === 0) {
793
+ return { element: null, confidence: "orphaned" };
794
+ }
795
+ const votes = /* @__PURE__ */ new Map();
796
+ for (const c of candidates) {
797
+ let s = votes.get(c.el);
798
+ if (!s) {
799
+ s = /* @__PURE__ */ new Set();
800
+ votes.set(c.el, s);
801
+ }
802
+ s.add(c.rung);
803
+ }
804
+ let best = null;
805
+ for (const [el, rungs] of votes) {
806
+ if (!best || rungs.size > best.rungs.size) best = { el, rungs };
807
+ }
808
+ if (!best) return { element: null, confidence: "orphaned" };
809
+ if (best.rungs.size >= 3) return { element: best.el, confidence: "likely" };
810
+ if (best.rungs.size >= 2) return { element: best.el, confidence: "drifted" };
811
+ if (best.rungs.has("class") || best.rungs.has("chain")) {
812
+ return { element: best.el, confidence: "drifted" };
813
+ }
814
+ return { element: null, confidence: "orphaned" };
815
+ }
816
+ function findByClassSignature(root, tagName, needle) {
817
+ const needleSet = new Set(needle.filter((c) => !c.startsWith("vz-") && !c.startsWith("vizu-")));
818
+ if (needleSet.size === 0) return null;
819
+ const all = root.querySelectorAll(tagName.toLowerCase());
820
+ let best = null;
821
+ let secondScore = 0;
822
+ for (const el of all) {
823
+ const haystack = new Set(
824
+ Array.from(el.classList).filter((c) => !c.startsWith("vz-") && !c.startsWith("vizu-"))
825
+ );
826
+ if (haystack.size === 0) continue;
827
+ const score = jaccardSimilarity(needleSet, haystack);
828
+ if (score < CLASS_JACCARD_THRESHOLD) continue;
829
+ if (!best || score > best.score) {
830
+ secondScore = best?.score ?? 0;
831
+ best = { el, score };
832
+ } else if (score > secondScore) {
833
+ secondScore = score;
834
+ }
835
+ }
836
+ if (!best) return null;
837
+ if (best.score - secondScore < 0.1) return null;
838
+ return { element: best.el, confidence: "likely" };
839
+ }
840
+ function findByAncestorChain(root, tagName, steps) {
841
+ const all = root.querySelectorAll(tagName.toLowerCase());
842
+ let best = null;
843
+ for (const el of all) {
844
+ const ancestors = collectAncestorSteps(el, steps.length + ANCESTOR_MAX_SHIFT);
845
+ const score = scoreChainAgainstAncestors(ancestors, steps, ANCESTOR_MAX_SHIFT);
846
+ if (score < 2) continue;
847
+ if (!best || score > best.score) best = { el, score };
848
+ }
849
+ if (!best) return null;
850
+ return {
851
+ element: best.el,
852
+ confidence: best.score === steps.length ? "likely" : "drifted"
853
+ };
854
+ }
855
+ function collectAncestorSteps(el, limit) {
856
+ const steps = [];
857
+ let current = el.parentElement;
858
+ while (current && steps.length < limit) {
859
+ const parent = current.parentElement;
860
+ let nth = 1;
861
+ if (parent) {
862
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
863
+ nth = sameTag.indexOf(current) + 1;
864
+ }
865
+ steps.push({ tag: current.tagName, nthOfType: nth });
866
+ current = parent;
867
+ }
868
+ return steps;
869
+ }
870
+ function findByTextSnippet(root, tagName, snippet) {
871
+ const needle = normalizeText(snippet);
872
+ if (!needle) return null;
873
+ const all = root.querySelectorAll(tagName.toLowerCase());
874
+ let best = null;
875
+ for (const el of all) {
876
+ const raw = el.innerText || el.textContent || "";
877
+ const candidate = normalizeText(raw.slice(0, TEXT_SNIPPET_MAX * 2));
878
+ if (!candidate) continue;
879
+ const score = levenshteinRatio(needle, candidate);
880
+ if (score < TEXT_RATIO_THRESHOLD) continue;
881
+ if (!best || score > best.score) {
882
+ best = { el, score };
883
+ }
884
+ }
885
+ return best ? { element: best.el, confidence: "drifted" } : null;
886
+ }
887
+ function normalizeText(input) {
888
+ return input.toLowerCase().replace(/\s+/g, " ").trim();
889
+ }
890
+ function levenshteinRatio(a, b) {
891
+ if (a === b) return 1;
892
+ if (a.length === 0 || b.length === 0) return a.length === b.length ? 1 : 0;
893
+ const maxLen = Math.max(a.length, b.length);
894
+ const distance = levenshteinDistance(a, b);
895
+ return 1 - distance / maxLen;
896
+ }
897
+ function levenshteinDistance(a, b) {
898
+ if (a.length > b.length) {
899
+ const tmp = a;
900
+ a = b;
901
+ b = tmp;
902
+ }
903
+ const prev = new Array(a.length + 1);
904
+ const curr = new Array(a.length + 1);
905
+ for (let i = 0; i <= a.length; i++) prev[i] = i;
906
+ for (let j = 1; j <= b.length; j++) {
907
+ curr[0] = j;
908
+ for (let i = 1; i <= a.length; i++) {
909
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
910
+ curr[i] = Math.min(
911
+ curr[i - 1] + 1,
912
+ // insert
913
+ prev[i] + 1,
914
+ // delete
915
+ prev[i - 1] + cost
916
+ // substitute
917
+ );
918
+ }
919
+ for (let i = 0; i <= a.length; i++) prev[i] = curr[i];
920
+ }
921
+ return prev[a.length];
922
+ }
923
+ function scoreChainAgainstAncestors(actual, captured, maxShift) {
924
+ let best = 0;
925
+ for (let offset = 0; offset <= maxShift; offset++) {
926
+ let score = 0;
927
+ for (let i = 0; i < captured.length; i++) {
928
+ const a = actual[i + offset];
929
+ if (!a) break;
930
+ if (a.tag === captured[i].tag && a.nthOfType === captured[i].nthOfType) {
931
+ score++;
932
+ }
933
+ }
934
+ if (score > best) best = score;
935
+ }
936
+ return best;
937
+ }
938
+ function jaccardSimilarity(a, b) {
939
+ if (a.size === 0 && b.size === 0) return 1;
940
+ let intersection = 0;
941
+ for (const item of a) if (b.has(item)) intersection++;
942
+ const union = a.size + b.size - intersection;
943
+ return union === 0 ? 0 : intersection / union;
944
+ }
945
+ function findElementByFingerprint(fp, root) {
946
+ return findByFingerprint(fp, root).element;
947
+ }
948
+ function fingerprintKey(fp) {
949
+ return fp.selector + "|" + fp.textSnippet;
950
+ }
951
+ function fingerprintLabel(fp) {
952
+ const tag = fp.tagName.toLowerCase();
953
+ if (fp.textSnippet) {
954
+ const snippet = fp.textSnippet.length > 24 ? fp.textSnippet.slice(0, 24) + "\u2026" : fp.textSnippet;
955
+ return tag + ": " + snippet;
956
+ }
957
+ if (fp.attributes.id) return tag + "#" + fp.attributes.id;
958
+ if (fp.attributes.classList && fp.attributes.classList.length) return tag + "." + fp.attributes.classList[0];
959
+ return tag;
960
+ }
961
+
695
962
  // src/highlighter.ts
696
963
  var IGNORE_SELECTOR = "[data-vizu-root], [data-vizu-ignore], script, style, link, meta, head";
697
964
  var Highlighter = class {
698
- constructor(root, extraIgnore, onClick, onCurrentChange) {
965
+ constructor(root, extraIgnore, onClick) {
699
966
  this.current = null;
700
967
  this.active = false;
701
968
  this.paused = false;
@@ -730,7 +997,6 @@ var Highlighter = class {
730
997
  this.root = root;
731
998
  this.extraIgnore = extraIgnore;
732
999
  this.onClick = onClick;
733
- this.onCurrentChange = onCurrentChange ?? null;
734
1000
  this.overlay = document.createElement("div");
735
1001
  this.overlay.className = "vz-highlight";
736
1002
  this.overlay.style.display = "none";
@@ -739,7 +1005,6 @@ var Highlighter = class {
739
1005
  setCurrent(el) {
740
1006
  if (this.current === el) return;
741
1007
  this.current = el;
742
- this.onCurrentChange?.(el);
743
1008
  }
744
1009
  start() {
745
1010
  if (this.active) return;
@@ -2784,8 +3049,6 @@ var Vizu = class {
2784
3049
  this.selfHealedThisSession = /* @__PURE__ */ new Set();
2785
3050
  this.actions = [];
2786
3051
  this.user = null;
2787
- /** Live-collab module — non-null only when `opts.liveblocks` and `opts.cloud` are both set AND the plan carries `live_collab`. Stays null otherwise. */
2788
- this.liveCollab = null;
2789
3052
  this.enabled = false;
2790
3053
  this.rafScheduled = false;
2791
3054
  this.bus = new EventBus();
@@ -2857,31 +3120,6 @@ var Vizu = class {
2857
3120
  if (options.actions) this.actions = [...options.actions];
2858
3121
  if (options.onCommentAdded) this.bus.on("comment:added", (p) => options.onCommentAdded(p.comment));
2859
3122
  if (options.onCommentRemoved) this.bus.on("comment:removed", (p) => options.onCommentRemoved(p.id));
2860
- if (options.cloud) {
2861
- const cloud = options.cloud;
2862
- const apiUrl = (cloud.apiUrl ?? "https://vizu.unhingged.com").replace(/\/$/, "");
2863
- void import("./live-collab-BVLNJ5QI.js").then(({ LiveCollab }) => {
2864
- this.liveCollab = new LiveCollab({
2865
- workspaceSlug: cloud.workspace,
2866
- apiUrl,
2867
- getAuthHeader: async () => {
2868
- if (typeof window === "undefined") return null;
2869
- try {
2870
- if (new URL(apiUrl).origin === window.location.origin) return null;
2871
- } catch {
2872
- return null;
2873
- }
2874
- const adapter = this.storage;
2875
- if (adapter instanceof CloudStorageAdapter) {
2876
- const token = adapter.getCachedAuthToken();
2877
- if (token) return { Authorization: `Bearer ${token}` };
2878
- }
2879
- return null;
2880
- }
2881
- });
2882
- void this.liveCollab.start();
2883
- });
2884
- }
2885
3123
  if (typeof window !== "undefined") {
2886
3124
  window.addEventListener("keydown", this.onKeyDown);
2887
3125
  void this.loadComments().then(() => this.subscribeStorage());
@@ -2939,8 +3177,6 @@ var Vizu = class {
2939
3177
  this.unsubscribeStorage?.();
2940
3178
  this.unsubscribeStorage = null;
2941
3179
  this.selfHealedThisSession.clear();
2942
- this.liveCollab?.destroy();
2943
- this.liveCollab = null;
2944
3180
  this.bus.clear();
2945
3181
  }
2946
3182
  /* ===== Action API ===== */
@@ -3262,11 +3498,7 @@ var Vizu = class {
3262
3498
  this.highlighter = new Highlighter(
3263
3499
  this.root,
3264
3500
  this.opts.ignoreSelectors ?? [],
3265
- (el, e) => this.onElementClick(el, e),
3266
- (el) => {
3267
- if (!this.liveCollab) return;
3268
- this.liveCollab.setLocalSelection(el ? fingerprint(el) : null);
3269
- }
3501
+ (el, e) => this.onElementClick(el, e)
3270
3502
  );
3271
3503
  this.highlighter.start();
3272
3504
  this.renderAllMarkers();