@unhingged/vizu-core 0.1.1 → 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
 
@@ -537,7 +529,7 @@ var CloudStorageAdapter = class {
537
529
  }
538
530
  }
539
531
  sessionKey() {
540
- return `vizu:cloud-token:${this.workspace}`;
532
+ return `vizu:cloud-token:v2:${this.workspace}`;
541
533
  }
542
534
  /**
543
535
  * Resolve a valid token, opening the popup if needed.
@@ -680,10 +672,297 @@ function apiErr(status, body) {
680
672
  return err;
681
673
  }
682
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
+
683
962
  // src/highlighter.ts
684
963
  var IGNORE_SELECTOR = "[data-vizu-root], [data-vizu-ignore], script, style, link, meta, head";
685
964
  var Highlighter = class {
686
- constructor(root, extraIgnore, onClick, onCurrentChange) {
965
+ constructor(root, extraIgnore, onClick) {
687
966
  this.current = null;
688
967
  this.active = false;
689
968
  this.paused = false;
@@ -718,7 +997,6 @@ var Highlighter = class {
718
997
  this.root = root;
719
998
  this.extraIgnore = extraIgnore;
720
999
  this.onClick = onClick;
721
- this.onCurrentChange = onCurrentChange ?? null;
722
1000
  this.overlay = document.createElement("div");
723
1001
  this.overlay.className = "vz-highlight";
724
1002
  this.overlay.style.display = "none";
@@ -727,7 +1005,6 @@ var Highlighter = class {
727
1005
  setCurrent(el) {
728
1006
  if (this.current === el) return;
729
1007
  this.current = el;
730
- this.onCurrentChange?.(el);
731
1008
  }
732
1009
  start() {
733
1010
  if (this.active) return;
@@ -2772,8 +3049,6 @@ var Vizu = class {
2772
3049
  this.selfHealedThisSession = /* @__PURE__ */ new Set();
2773
3050
  this.actions = [];
2774
3051
  this.user = null;
2775
- /** Live-collab module — non-null only when `opts.liveblocks` and `opts.cloud` are both set AND the plan carries `live_collab`. Stays null otherwise. */
2776
- this.liveCollab = null;
2777
3052
  this.enabled = false;
2778
3053
  this.rafScheduled = false;
2779
3054
  this.bus = new EventBus();
@@ -2845,18 +3120,6 @@ var Vizu = class {
2845
3120
  if (options.actions) this.actions = [...options.actions];
2846
3121
  if (options.onCommentAdded) this.bus.on("comment:added", (p) => options.onCommentAdded(p.comment));
2847
3122
  if (options.onCommentRemoved) this.bus.on("comment:removed", (p) => options.onCommentRemoved(p.id));
2848
- if (options.liveblocks && options.cloud) {
2849
- const live = options.liveblocks;
2850
- const cloud = options.cloud;
2851
- void import("./live-collab-IRUNFAPE.js").then(({ LiveCollab }) => {
2852
- this.liveCollab = new LiveCollab({
2853
- publicKey: live.publicKey,
2854
- authEndpoint: live.authEndpoint,
2855
- workspaceSlug: cloud.workspace
2856
- });
2857
- void this.liveCollab.start();
2858
- });
2859
- }
2860
3123
  if (typeof window !== "undefined") {
2861
3124
  window.addEventListener("keydown", this.onKeyDown);
2862
3125
  void this.loadComments().then(() => this.subscribeStorage());
@@ -2914,8 +3177,6 @@ var Vizu = class {
2914
3177
  this.unsubscribeStorage?.();
2915
3178
  this.unsubscribeStorage = null;
2916
3179
  this.selfHealedThisSession.clear();
2917
- this.liveCollab?.destroy();
2918
- this.liveCollab = null;
2919
3180
  this.bus.clear();
2920
3181
  }
2921
3182
  /* ===== Action API ===== */
@@ -3237,11 +3498,7 @@ var Vizu = class {
3237
3498
  this.highlighter = new Highlighter(
3238
3499
  this.root,
3239
3500
  this.opts.ignoreSelectors ?? [],
3240
- (el, e) => this.onElementClick(el, e),
3241
- (el) => {
3242
- if (!this.liveCollab) return;
3243
- this.liveCollab.setLocalSelection(el ? fingerprint(el) : null);
3244
- }
3501
+ (el, e) => this.onElementClick(el, e)
3245
3502
  );
3246
3503
  this.highlighter.start();
3247
3504
  this.renderAllMarkers();