@comty.io/core 0.1.9 → 0.1.10

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,333 @@
1
+ // Generated from resilient-anchor.ts by sdk/scripts/build.mjs.
2
+ const generatedId = /^(?:(?:radix|base-ui)-)?(?:_r_[\w-]+_|:r[\w-]+:)$|^mat-|^cdk-|^ng-/i;
3
+ const anchorAttributes = [
4
+ "id",
5
+ "data-testid",
6
+ "data-test",
7
+ "data-cy",
8
+ "role",
9
+ "name",
10
+ "type",
11
+ "href",
12
+ "src",
13
+ "aria-label",
14
+ "alt",
15
+ "title",
16
+ "placeholder"
17
+ ];
18
+ function readAnchor(value) {
19
+ const record = (item) => item && typeof item === "object" && !Array.isArray(item) ? item : {};
20
+ const raw = record(value);
21
+ const saved = raw.resilient ?? raw.anchor ?? value;
22
+ if (isResilientAnchor(saved)) return saved;
23
+ if (raw.resilient !== void 0 || raw.anchor !== void 0 || "shape" in record(raw.target))
24
+ return null;
25
+ const context = record(raw.context);
26
+ const source = context.element ? context : raw;
27
+ const target = record(
28
+ source.element ?? source.target ?? (source.tag ? source : { text: source.text })
29
+ );
30
+ const string = (item) => typeof item === "string" ? item.trim().slice(0, 160) : "";
31
+ const convert = (item) => {
32
+ const stored = record(item.attributes);
33
+ const attributes = {};
34
+ for (const name of anchorAttributes) {
35
+ const alias = name === "aria-label" ? "ariaLabel" : name === "data-testid" ? "testId" : name;
36
+ const text = string(stored[name] ?? item[alias]);
37
+ if (text && !(name === "id" && generatedId.test(text)))
38
+ attributes[name] = text;
39
+ }
40
+ const tag = string(item.tag).toLowerCase();
41
+ return {
42
+ tag: /^[a-z][a-z0-9-]{0,79}$/.test(tag) ? tag : "*",
43
+ attributes,
44
+ classes: (Array.isArray(item.classes) ? item.classes : string(stored.class).split(/\s+/)).filter(
45
+ (name) => typeof name === "string" && !!name && name.length <= 80 && !/^(ng-|comty-|hph-)/.test(name)
46
+ ).slice(0, 8),
47
+ text: /^(input|textarea|select)$/.test(tag) ? "" : string(item.text ?? item.ownText),
48
+ heading: "",
49
+ shape: []
50
+ };
51
+ };
52
+ const ancestors = (Array.isArray(source.ancestors) ? source.ancestors : []).slice(0, 8).map((item) => convert(record(item))).filter((item) => item.tag !== "*" && !/^(body|html)$/.test(item.tag));
53
+ const inferredLayer = ancestors.find(
54
+ (item) => item.tag === "dialog" || /^(dialog|alertdialog)$/.test(item.attributes.role ?? "")
55
+ );
56
+ return {
57
+ version: 1,
58
+ target: convert(target),
59
+ ancestors,
60
+ layer: source.layer ? convert(record(source.layer)) : source.layer === null ? null : inferredLayer,
61
+ scope: -1,
62
+ group: -1,
63
+ twins: typeof source.candidateCount === "number" && Number.isInteger(source.candidateCount) && source.candidateCount > 1 ? source.candidateCount : 1
64
+ };
65
+ }
66
+ function createResilientAnchors(excluded, presentationLayer = (element) => element.closest('dialog, [role="dialog"], [role="alertdialog"], [popover]')) {
67
+ const editable = 'input, textarea, select, [contenteditable]:not([contenteditable="false"])';
68
+ const attributes = anchorAttributes;
69
+ const stable = /* @__PURE__ */ new Set([
70
+ "id",
71
+ "data-testid",
72
+ "data-test",
73
+ "data-cy",
74
+ "name",
75
+ "href"
76
+ ]);
77
+ let fingerprints = /* @__PURE__ */ new WeakMap();
78
+ function children(element) {
79
+ return [...element.children].filter((child) => !excluded(child));
80
+ }
81
+ function semantic(element) {
82
+ const role = element.getAttribute("role");
83
+ if (role) return role.slice(0, 80);
84
+ const tag = element.localName;
85
+ if (/^h[1-6]$/.test(tag)) return "heading";
86
+ if (tag === "svg" || tag === "img" || tag.endsWith("-icon")) return "image";
87
+ return /^(button|a|input|textarea|select|p|table|tr|td|th|ul|ol|li|dl|dt|dd|canvas|video)$/.test(
88
+ tag
89
+ ) ? tag : "";
90
+ }
91
+ function shape(element) {
92
+ const counts = /* @__PURE__ */ new Map();
93
+ const queue = [element];
94
+ let visited = 0;
95
+ while (queue.length && visited++ < 120) {
96
+ const node = queue.shift();
97
+ if (!node || excluded(node)) continue;
98
+ const token = semantic(node);
99
+ if (token) counts.set(token, Math.min(3, (counts.get(token) ?? 0) + 1));
100
+ if (token === "image" || node.matches(editable)) continue;
101
+ const descendants = children(node);
102
+ const patterns = /* @__PURE__ */ new Map();
103
+ for (const child of descendants.slice(0, 24)) {
104
+ const parts = children(child).map(semantic).filter(Boolean);
105
+ if (parts.length < 2) continue;
106
+ const key = parts.join("/");
107
+ patterns.set(key, (patterns.get(key) ?? 0) + 1);
108
+ }
109
+ if ([...patterns.values()].some((count) => count >= 2))
110
+ counts.set("records", 3);
111
+ queue.push(...descendants.slice(0, 40));
112
+ }
113
+ return [...counts].flatMap(([token, count]) => Array(count).fill(token)).sort();
114
+ }
115
+ function text(element) {
116
+ if (element.closest(editable)) return "";
117
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
118
+ let result = "", count = 0;
119
+ for (let node = walker.nextNode(); node && count++ < 100 && result.length < 160; node = walker.nextNode()) {
120
+ const parent = node.parentElement;
121
+ if (parent && !excluded(parent) && !parent.closest(`${editable}, script, style`))
122
+ result += ` ${node.textContent ?? ""}`;
123
+ }
124
+ return result.replace(/\s+/g, " ").trim().slice(0, 160);
125
+ }
126
+ function fingerprint(element) {
127
+ const cached = fingerprints.get(element);
128
+ if (cached) return cached;
129
+ const attrs = {};
130
+ for (const name of attributes) {
131
+ const value = element.getAttribute(name)?.trim();
132
+ if (value && !(name === "id" && generatedId.test(value)))
133
+ attrs[name] = value.slice(0, 160);
134
+ }
135
+ const result = {
136
+ tag: element.localName,
137
+ attributes: attrs,
138
+ classes: [...element.classList].filter((name) => name.length <= 80 && !/^(ng-|comty-|hph-)/.test(name)).slice(0, 8),
139
+ text: text(element),
140
+ heading: (() => {
141
+ const heading = element.querySelector("h1, h2, h3, h4, h5, h6, legend");
142
+ return heading && !excluded(heading) ? text(heading) : "";
143
+ })(),
144
+ shape: shape(element)
145
+ };
146
+ fingerprints.set(element, result);
147
+ return result;
148
+ }
149
+ function parents(element) {
150
+ const result = [];
151
+ for (let parent = element.parentElement; parent && parent !== document.body && result.length < 10; parent = parent.parentElement) {
152
+ if (!excluded(parent)) result.push(parent);
153
+ }
154
+ return result;
155
+ }
156
+ function overlap(left, right) {
157
+ const remaining = [...right];
158
+ let count = 0;
159
+ for (const item of left) {
160
+ const index = remaining.indexOf(item);
161
+ if (index >= 0) {
162
+ count++;
163
+ remaining.splice(index, 1);
164
+ }
165
+ }
166
+ return count;
167
+ }
168
+ function score(saved, current) {
169
+ if (saved.tag !== "*" && saved.tag !== current.tag) return -100;
170
+ let result = 20;
171
+ for (const [name, value] of Object.entries(saved.attributes)) {
172
+ if (current.attributes[name] === value)
173
+ result += stable.has(name) ? 45 : 12;
174
+ else if (stable.has(name) && current.attributes[name]) result -= 35;
175
+ }
176
+ result += Math.min(24, overlap(saved.classes, current.classes) * 12);
177
+ if (saved.text && saved.text === current.text) result += 36;
178
+ else if (saved.text && current.text) {
179
+ const left = [...new Set(saved.text.toLowerCase().split(/\s+/))];
180
+ const right = [...new Set(current.text.toLowerCase().split(/\s+/))];
181
+ if (overlap(left, right) / Math.max(left.length, right.length) >= 0.7)
182
+ result += 18;
183
+ }
184
+ if (saved.heading && saved.heading === current.heading) result += 24;
185
+ const total = Math.max(saved.shape.length, current.shape.length);
186
+ result += saved.shape.length ? 12 * overlap(saved.shape, current.shape) / total : 12;
187
+ return result;
188
+ }
189
+ function differentState(saved, current) {
190
+ if (saved.shape.length < 3 || current.shape.length < 3) return false;
191
+ const hasRecords = (tokens) => tokens.some((token) => /^(records|tr|li|row|listitem)$/.test(token));
192
+ if (hasRecords(saved.shape) !== hasRecords(current.shape) && saved.shape.includes("image") !== current.shape.includes("image"))
193
+ return true;
194
+ const common = overlap(saved.shape, current.shape);
195
+ return common / saved.shape.length < 0.45 && common / current.shape.length < 0.45;
196
+ }
197
+ function twins(element, saved, includeText = false) {
198
+ return (element.parentElement ? children(element.parentElement) : [element]).filter((child) => {
199
+ const candidate = fingerprint(child);
200
+ return candidate.tag === saved.tag && (!includeText || candidate.text === saved.text) && attributes.every(
201
+ (name) => candidate.attributes[name] === saved.attributes[name]
202
+ );
203
+ }).length;
204
+ }
205
+ function capture(element) {
206
+ fingerprints = /* @__PURE__ */ new WeakMap();
207
+ const target = fingerprint(element);
208
+ const ancestors = parents(element).slice(0, 8).map(fingerprint);
209
+ const layer = presentationLayer(element);
210
+ const scope = [target, ...ancestors.slice(0, 3)].findIndex(
211
+ (item) => !/^(main|body|html|nav)$/.test(item.tag) && item.shape.length >= 3 && item.shape.length <= 18
212
+ );
213
+ const group = [element, ...parents(element).slice(0, 8)].findIndex(
214
+ (node) => {
215
+ const saved = fingerprint(node);
216
+ return saved.shape.length >= 2 && node.parentElement && children(node.parentElement).filter((peer) => {
217
+ const other = fingerprint(peer);
218
+ return other.tag === saved.tag && overlap(saved.shape, other.shape) / Math.max(saved.shape.length, other.shape.length) > 0.8;
219
+ }).length > 1;
220
+ }
221
+ );
222
+ return {
223
+ version: 1,
224
+ target,
225
+ ancestors,
226
+ layer: layer ? fingerprint(layer) : null,
227
+ scope,
228
+ group,
229
+ twins: twins(element, target, true)
230
+ };
231
+ }
232
+ function resolve(anchor, previous = null, exact = null) {
233
+ fingerprints = /* @__PURE__ */ new WeakMap();
234
+ const candidates = (anchor.target.tag === "*" ? exact ? [exact] : [] : [...document.getElementsByTagName(anchor.target.tag)]).filter((element) => !excluded(element));
235
+ const ranked = [];
236
+ for (const element of candidates) {
237
+ const layer = presentationLayer(element);
238
+ if (anchor.layer !== void 0 && !!layer !== !!anchor.layer) continue;
239
+ if (layer && anchor.layer && score(anchor.layer, fingerprint(layer)) < 45)
240
+ continue;
241
+ const current = fingerprint(element);
242
+ if (Object.keys(anchor.target.attributes).some(
243
+ (name) => stable.has(name) && current.attributes[name] && current.attributes[name] !== anchor.target.attributes[name]
244
+ ))
245
+ continue;
246
+ if (anchor.target.attributes.id && !current.attributes.id && element !== previous && score(anchor.target, current) < 55)
247
+ continue;
248
+ let points = score(anchor.target, current);
249
+ const chain = parents(element);
250
+ let next = 0;
251
+ let scope = anchor.scope === 0 ? current : null;
252
+ let group = anchor.group === 0 ? element : null;
253
+ for (const [index, saved] of anchor.ancestors.entries()) {
254
+ let best2 = -1, bestScore = 0;
255
+ for (let i = next; i < Math.min(chain.length, next + 3); i++) {
256
+ const value = score(saved, fingerprint(chain[i]));
257
+ if (value > bestScore) {
258
+ best2 = i;
259
+ bestScore = value;
260
+ }
261
+ }
262
+ if (best2 < 0) continue;
263
+ next = best2 + 1;
264
+ points += Math.min(120, bestScore) * (index < 3 ? 0.6 : 0.4);
265
+ if (anchor.scope === index + 1) scope = fingerprint(chain[best2]);
266
+ if (anchor.group === index + 1) group = chain[best2];
267
+ }
268
+ if (anchor.scope >= 0 && !scope) continue;
269
+ const originalGroup = [anchor.target, ...anchor.ancestors][anchor.group];
270
+ if (originalGroup && element !== previous) {
271
+ if (!group) continue;
272
+ const peers = group.parentElement ? children(group.parentElement).filter(
273
+ (peer) => peer !== group && peer.localName === group.localName
274
+ ) : [];
275
+ const distinctClasses = originalGroup.classes.filter(
276
+ (name) => peers.length > 0 && peers.every((peer) => !peer.classList.contains(name))
277
+ );
278
+ const distinctAttributes = Object.fromEntries(
279
+ Object.entries(originalGroup.attributes).filter(
280
+ ([name, value]) => /^(id|data-testid|data-test|data-cy)$/.test(name) || peers.length > 0 && peers.every(
281
+ (peer) => fingerprint(peer).attributes[name] !== value
282
+ )
283
+ )
284
+ );
285
+ const witness = {
286
+ ...originalGroup,
287
+ classes: distinctClasses,
288
+ attributes: distinctAttributes
289
+ };
290
+ const currentGroup = fingerprint(group);
291
+ const points2 = score(witness, currentGroup) + (overlap(distinctClasses, currentGroup.classes) ? 12 : 0);
292
+ if (points2 < 55) continue;
293
+ }
294
+ const originalScope = [anchor.target, ...anchor.ancestors][anchor.scope];
295
+ const changed = !!(scope && originalScope && differentState(originalScope, scope)) || differentState(anchor.target, current);
296
+ if (anchor.twins > 1 && twins(element, anchor.target) < anchor.twins)
297
+ continue;
298
+ if (element === previous && previous.isConnected) points += 24;
299
+ if (candidates.length === 1) points += 24;
300
+ if (element === exact) points += anchor.target.tag === "*" ? 55 : 8;
301
+ ranked.push({ element, score: points, changed });
302
+ }
303
+ ranked.sort((a, b) => b.score - a.score);
304
+ const best = ranked[0];
305
+ if (!best)
306
+ return {
307
+ element: null,
308
+ status: anchor.layer ? "hidden" : anchor.twins > 1 && candidates.length ? "uncertain" : "missing"
309
+ };
310
+ if (best.score < 55 || ranked[1] && best.score - ranked[1].score < 14)
311
+ return { element: null, status: "uncertain" };
312
+ return best.changed ? { element: null, status: "hidden" } : { element: best.element, status: "matched" };
313
+ }
314
+ return { capture, resolve };
315
+ }
316
+ function isResilientAnchor(value) {
317
+ if (!value || typeof value !== "object") return false;
318
+ const anchor = value;
319
+ const strings = (items, max) => Array.isArray(items) && items.length <= max && items.every((item) => typeof item === "string" && item.length <= 160);
320
+ const fingerprint = (item) => {
321
+ if (!item || typeof item !== "object") return false;
322
+ const part = item;
323
+ return typeof part.tag === "string" && /^[a-z][a-z0-9-]{0,79}$/.test(part.tag) && typeof part.text === "string" && part.text.length <= 160 && typeof part.heading === "string" && part.heading.length <= 160 && strings(part.classes, 8) && strings(part.shape, 120) && !!part.attributes && typeof part.attributes === "object" && !Array.isArray(part.attributes) && Object.entries(part.attributes).length <= 13 && Object.entries(part.attributes).every(
324
+ ([key, val]) => key.length < 40 && typeof val === "string" && val.length <= 160
325
+ );
326
+ };
327
+ return anchor.version === 1 && fingerprint(anchor.target) && Array.isArray(anchor.ancestors) && anchor.ancestors.length <= 8 && anchor.ancestors.every(fingerprint) && (anchor.layer === null || fingerprint(anchor.layer)) && Number.isInteger(anchor.scope) && typeof anchor.scope === "number" && anchor.scope >= -1 && anchor.scope <= anchor.ancestors.length && Number.isInteger(anchor.group) && typeof anchor.group === "number" && anchor.group >= -1 && anchor.group <= anchor.ancestors.length && typeof anchor.twins === "number" && Number.isInteger(anchor.twins) && anchor.twins >= 1;
328
+ }
329
+ export {
330
+ createResilientAnchors,
331
+ isResilientAnchor,
332
+ readAnchor
333
+ };