@mcp-b/smart-dom-reader-server 0.2.0 → 5.1.0
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/lib/smart-dom-reader.bundle.js +1394 -1777
- package/package.json +22 -10
- package/src/index.ts +167 -178
|
@@ -1,919 +1,161 @@
|
|
|
1
|
-
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
for (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
ContentDetection
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
) {
|
|
141
|
-
score += 8;
|
|
142
|
-
}
|
|
143
|
-
const role = element.getAttribute('role');
|
|
144
|
-
if (role === 'main' || role === 'article') {
|
|
145
|
-
score += 10;
|
|
146
|
-
}
|
|
147
|
-
if (
|
|
148
|
-
element.matches(
|
|
149
|
-
'aside, nav, header, footer, .sidebar, .navigation, .menu, .ad, .advertisement'
|
|
150
|
-
)
|
|
151
|
-
) {
|
|
152
|
-
score -= 10;
|
|
153
|
-
}
|
|
154
|
-
const forms = element.getElementsByTagName('form').length;
|
|
155
|
-
if (forms > 2) {
|
|
156
|
-
score -= 5;
|
|
157
|
-
}
|
|
158
|
-
return Math.max(0, score);
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Calculate link density in an element
|
|
162
|
-
*/
|
|
163
|
-
static calculateLinkDensity(element) {
|
|
164
|
-
const links = element.getElementsByTagName('a');
|
|
165
|
-
let linkTextLength = 0;
|
|
166
|
-
for (const link of Array.from(links)) {
|
|
167
|
-
linkTextLength += link.textContent?.length || 0;
|
|
168
|
-
}
|
|
169
|
-
const totalTextLength = element.textContent?.length || 1;
|
|
170
|
-
return linkTextLength / totalTextLength;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Check if an element is likely navigation
|
|
174
|
-
*/
|
|
175
|
-
static isNavigation(element) {
|
|
176
|
-
const tag = element.tagName.toLowerCase();
|
|
177
|
-
if (tag === 'nav' || element.getAttribute('role') === 'navigation') {
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
const navPatterns = [/nav/i, /menu/i, /sidebar/i, /toolbar/i];
|
|
181
|
-
const classesAndId = `${element.className} ${element.id}`.toLowerCase();
|
|
182
|
-
return navPatterns.some((pattern) => pattern.test(classesAndId));
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Check if element is likely supplementary content
|
|
186
|
-
*/
|
|
187
|
-
static isSupplementary(element) {
|
|
188
|
-
const tag = element.tagName.toLowerCase();
|
|
189
|
-
if (tag === 'aside' || element.getAttribute('role') === 'complementary') {
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
const supplementaryPatterns = [
|
|
193
|
-
/sidebar/i,
|
|
194
|
-
/widget/i,
|
|
195
|
-
/related/i,
|
|
196
|
-
/advertisement/i,
|
|
197
|
-
/social/i,
|
|
198
|
-
];
|
|
199
|
-
const classesAndId = `${element.className} ${element.id}`.toLowerCase();
|
|
200
|
-
return supplementaryPatterns.some((pattern) => pattern.test(classesAndId));
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Detect page landmarks
|
|
204
|
-
*/
|
|
205
|
-
static detectLandmarks(doc) {
|
|
206
|
-
const landmarks = {
|
|
207
|
-
navigation: [],
|
|
208
|
-
main: [],
|
|
209
|
-
complementary: [],
|
|
210
|
-
contentinfo: [],
|
|
211
|
-
banner: [],
|
|
212
|
-
search: [],
|
|
213
|
-
form: [],
|
|
214
|
-
region: [],
|
|
215
|
-
};
|
|
216
|
-
const landmarkSelectors = {
|
|
217
|
-
navigation: 'nav, [role="navigation"]',
|
|
218
|
-
main: 'main, [role="main"]',
|
|
219
|
-
complementary: 'aside, [role="complementary"]',
|
|
220
|
-
contentinfo: 'footer, [role="contentinfo"]',
|
|
221
|
-
banner: 'header, [role="banner"]',
|
|
222
|
-
search: '[role="search"]',
|
|
223
|
-
form: 'form[aria-label], form[aria-labelledby], [role="form"]',
|
|
224
|
-
region: 'section[aria-label], section[aria-labelledby], [role="region"]',
|
|
225
|
-
};
|
|
226
|
-
for (const [landmark, selector] of Object.entries(landmarkSelectors)) {
|
|
227
|
-
const elements = doc.querySelectorAll(selector);
|
|
228
|
-
landmarks[landmark] = Array.from(elements);
|
|
229
|
-
}
|
|
230
|
-
return landmarks;
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// src/selectors.ts
|
|
237
|
-
var SelectorGenerator;
|
|
238
|
-
var init_selectors = __esm({
|
|
239
|
-
'src/selectors.ts'() {
|
|
240
|
-
SelectorGenerator = class {
|
|
241
|
-
/**
|
|
242
|
-
* Generate multiple selector strategies for an element
|
|
243
|
-
*/
|
|
244
|
-
static generateSelectors(element) {
|
|
245
|
-
const doc = element.ownerDocument || document;
|
|
246
|
-
const candidates = [];
|
|
247
|
-
if (element.id && this.isUniqueId(element.id, doc)) {
|
|
248
|
-
candidates.push({ type: 'id', value: `#${CSS.escape(element.id)}`, score: 100 });
|
|
249
|
-
}
|
|
250
|
-
const testId = this.getDataTestId(element);
|
|
251
|
-
if (testId) {
|
|
252
|
-
const v = `[data-testid="${CSS.escape(testId)}"]`;
|
|
253
|
-
candidates.push({
|
|
254
|
-
type: 'data-testid',
|
|
255
|
-
value: v,
|
|
256
|
-
score: 90 + (this.isUniqueSelectorSafe(v, doc) ? 5 : 0),
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
const role = element.getAttribute('role');
|
|
260
|
-
const aria = element.getAttribute('aria-label');
|
|
261
|
-
if (role && aria) {
|
|
262
|
-
const v = `[role="${CSS.escape(role)}"][aria-label="${CSS.escape(aria)}"]`;
|
|
263
|
-
candidates.push({
|
|
264
|
-
type: 'role-aria',
|
|
265
|
-
value: v,
|
|
266
|
-
score: 85 + (this.isUniqueSelectorSafe(v, doc) ? 5 : 0),
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
const nameAttr = element.getAttribute('name');
|
|
270
|
-
if (nameAttr) {
|
|
271
|
-
const v = `[name="${CSS.escape(nameAttr)}"]`;
|
|
272
|
-
candidates.push({
|
|
273
|
-
type: 'name',
|
|
274
|
-
value: v,
|
|
275
|
-
score: 78 + (this.isUniqueSelectorSafe(v, doc) ? 5 : 0),
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
const pathCss = this.generateCSSSelector(element, doc);
|
|
279
|
-
const structuralPenalty = (pathCss.match(/:nth-child\(/g) || []).length * 10;
|
|
280
|
-
const classBonus = pathCss.includes('.') ? 8 : 0;
|
|
281
|
-
const pathScore = Math.max(0, 70 + classBonus - structuralPenalty);
|
|
282
|
-
candidates.push({ type: 'class-path', value: pathCss, score: pathScore });
|
|
283
|
-
const xpath = this.generateXPath(element, doc);
|
|
284
|
-
candidates.push({ type: 'xpath', value: xpath, score: 40 });
|
|
285
|
-
const textBased = this.generateTextBasedSelector(element);
|
|
286
|
-
if (textBased) candidates.push({ type: 'text', value: textBased, score: 30 });
|
|
287
|
-
candidates.sort((a, b) => b.score - a.score);
|
|
288
|
-
const bestCss =
|
|
289
|
-
candidates.find((c) => c.type !== 'xpath' && c.type !== 'text')?.value || pathCss;
|
|
290
|
-
return {
|
|
291
|
-
css: bestCss,
|
|
292
|
-
xpath,
|
|
293
|
-
textBased,
|
|
294
|
-
dataTestId: testId || void 0,
|
|
295
|
-
ariaLabel: aria || void 0,
|
|
296
|
-
candidates,
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Generate a unique CSS selector for an element
|
|
301
|
-
*/
|
|
302
|
-
static generateCSSSelector(element, doc) {
|
|
303
|
-
if (element.id && this.isUniqueId(element.id, doc)) {
|
|
304
|
-
return `#${CSS.escape(element.id)}`;
|
|
305
|
-
}
|
|
306
|
-
const testId = this.getDataTestId(element);
|
|
307
|
-
if (testId) {
|
|
308
|
-
return `[data-testid="${CSS.escape(testId)}"]`;
|
|
309
|
-
}
|
|
310
|
-
const path = [];
|
|
311
|
-
let current = element;
|
|
312
|
-
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
313
|
-
let selector = current.nodeName.toLowerCase();
|
|
314
|
-
if (current.id && this.isUniqueId(current.id, doc)) {
|
|
315
|
-
selector = `#${CSS.escape(current.id)}`;
|
|
316
|
-
path.unshift(selector);
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
const classes = this.getMeaningfulClasses(current);
|
|
320
|
-
if (classes.length > 0) {
|
|
321
|
-
selector += `.${classes.map((c) => CSS.escape(c)).join('.')}`;
|
|
322
|
-
}
|
|
323
|
-
const siblings = current.parentElement?.children;
|
|
324
|
-
if (siblings && siblings.length > 1) {
|
|
325
|
-
const index = Array.from(siblings).indexOf(current);
|
|
326
|
-
if (index > 0 || !this.isUniqueSelector(selector, current.parentElement)) {
|
|
327
|
-
selector += `:nth-child(${index + 1})`;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
path.unshift(selector);
|
|
331
|
-
current = current.parentElement;
|
|
332
|
-
}
|
|
333
|
-
return this.optimizePath(path, element, doc);
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Generate XPath for an element
|
|
337
|
-
*/
|
|
338
|
-
static generateXPath(element, doc) {
|
|
339
|
-
if (element.id && this.isUniqueId(element.id, doc)) {
|
|
340
|
-
return `//*[@id="${element.id}"]`;
|
|
341
|
-
}
|
|
342
|
-
const path = [];
|
|
343
|
-
let current = element;
|
|
344
|
-
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
345
|
-
const tagName = current.nodeName.toLowerCase();
|
|
346
|
-
if (current.id && this.isUniqueId(current.id, doc)) {
|
|
347
|
-
path.unshift(`//*[@id="${current.id}"]`);
|
|
348
|
-
break;
|
|
349
|
-
}
|
|
350
|
-
let xpath = tagName;
|
|
351
|
-
const siblings = current.parentElement?.children;
|
|
352
|
-
if (siblings) {
|
|
353
|
-
const sameTagSiblings = Array.from(siblings).filter(
|
|
354
|
-
(s) => s.nodeName.toLowerCase() === tagName
|
|
355
|
-
);
|
|
356
|
-
if (sameTagSiblings.length > 1) {
|
|
357
|
-
const index = sameTagSiblings.indexOf(current) + 1;
|
|
358
|
-
xpath += `[${index}]`;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
path.unshift(xpath);
|
|
362
|
-
current = current.parentElement;
|
|
363
|
-
}
|
|
364
|
-
return `//${path.join('/')}`;
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* Generate a text-based selector for buttons and links
|
|
368
|
-
*/
|
|
369
|
-
static generateTextBasedSelector(element) {
|
|
370
|
-
const text = element.textContent?.trim();
|
|
371
|
-
if (!text || text.length > 50) return void 0;
|
|
372
|
-
const tag = element.nodeName.toLowerCase();
|
|
373
|
-
if (['button', 'a', 'label'].includes(tag)) {
|
|
374
|
-
const escapedText = text.replace(/['"\\]/g, '\\$&');
|
|
375
|
-
return `${tag}:contains("${escapedText}")`;
|
|
376
|
-
}
|
|
377
|
-
return void 0;
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* Get data-testid or similar attributes
|
|
381
|
-
*/
|
|
382
|
-
static getDataTestId(element) {
|
|
383
|
-
return (
|
|
384
|
-
element.getAttribute('data-testid') ||
|
|
385
|
-
element.getAttribute('data-test-id') ||
|
|
386
|
-
element.getAttribute('data-test') ||
|
|
387
|
-
element.getAttribute('data-cy') ||
|
|
388
|
-
void 0
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Check if an ID is unique in the document
|
|
393
|
-
*/
|
|
394
|
-
static isUniqueId(id, doc) {
|
|
395
|
-
return doc.querySelectorAll(`#${CSS.escape(id)}`).length === 1;
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* Check if a selector is unique within a container
|
|
399
|
-
*/
|
|
400
|
-
static isUniqueSelector(selector, container) {
|
|
401
|
-
try {
|
|
402
|
-
return container.querySelectorAll(selector).length === 1;
|
|
403
|
-
} catch {
|
|
404
|
-
return false;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
static isUniqueSelectorSafe(selector, doc) {
|
|
408
|
-
try {
|
|
409
|
-
return doc.querySelectorAll(selector).length === 1;
|
|
410
|
-
} catch {
|
|
411
|
-
return false;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
/**
|
|
415
|
-
* Get meaningful classes (filtering out utility classes)
|
|
416
|
-
*/
|
|
417
|
-
static getMeaningfulClasses(element) {
|
|
418
|
-
const classes = Array.from(element.classList);
|
|
419
|
-
const utilityPatterns = [
|
|
420
|
-
/^(p|m|w|h|text|bg|border|flex|grid|col|row)-/,
|
|
421
|
-
/^(xs|sm|md|lg|xl|2xl):/,
|
|
422
|
-
/^(hover|focus|active|disabled|checked):/,
|
|
423
|
-
/^js-/,
|
|
424
|
-
/^is-/,
|
|
425
|
-
/^has-/,
|
|
426
|
-
];
|
|
427
|
-
return classes
|
|
428
|
-
.filter((cls) => {
|
|
429
|
-
if (cls.length < 3) return false;
|
|
430
|
-
return !utilityPatterns.some((pattern) => pattern.test(cls));
|
|
431
|
-
})
|
|
432
|
-
.slice(0, 2);
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Optimize the selector path by removing unnecessary parts
|
|
436
|
-
*/
|
|
437
|
-
static optimizePath(path, element, doc) {
|
|
438
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
439
|
-
const shortPath = path.slice(i).join(' > ');
|
|
440
|
-
try {
|
|
441
|
-
const matches = doc.querySelectorAll(shortPath);
|
|
442
|
-
if (matches.length === 1 && matches[0] === element) {
|
|
443
|
-
return shortPath;
|
|
444
|
-
}
|
|
445
|
-
} catch {}
|
|
446
|
-
}
|
|
447
|
-
return path.join(' > ');
|
|
448
|
-
}
|
|
449
|
-
/**
|
|
450
|
-
* Get a human-readable path description
|
|
451
|
-
*/
|
|
452
|
-
static getContextPath(element) {
|
|
453
|
-
const path = [];
|
|
454
|
-
let current = element;
|
|
455
|
-
let depth = 0;
|
|
456
|
-
const maxDepth = 5;
|
|
457
|
-
while (current && current !== element.ownerDocument?.body && depth < maxDepth) {
|
|
458
|
-
const tag = current.nodeName.toLowerCase();
|
|
459
|
-
let descriptor = tag;
|
|
460
|
-
if (current.id) {
|
|
461
|
-
descriptor = `${tag}#${current.id}`;
|
|
462
|
-
} else if (current.className && typeof current.className === 'string') {
|
|
463
|
-
const firstClass = current.className.split(' ')[0];
|
|
464
|
-
if (firstClass) {
|
|
465
|
-
descriptor = `${tag}.${firstClass}`;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
const role = current.getAttribute('role');
|
|
469
|
-
if (role) {
|
|
470
|
-
descriptor += `[role="${role}"]`;
|
|
471
|
-
}
|
|
472
|
-
path.unshift(descriptor);
|
|
473
|
-
current = current.parentElement;
|
|
474
|
-
depth++;
|
|
475
|
-
}
|
|
476
|
-
return path;
|
|
477
|
-
}
|
|
478
|
-
};
|
|
479
|
-
},
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
// src/traversal.ts
|
|
483
|
-
var DOMTraversal;
|
|
484
|
-
var init_traversal = __esm({
|
|
485
|
-
'src/traversal.ts'() {
|
|
486
|
-
init_selectors();
|
|
487
|
-
DOMTraversal = class {
|
|
488
|
-
static INTERACTIVE_SELECTORS = [
|
|
489
|
-
'button',
|
|
490
|
-
'a[href]',
|
|
491
|
-
'input:not([type="hidden"])',
|
|
492
|
-
'textarea',
|
|
493
|
-
'select',
|
|
494
|
-
'[role="button"]',
|
|
495
|
-
'[onclick]',
|
|
496
|
-
'[contenteditable="true"]',
|
|
497
|
-
'summary',
|
|
498
|
-
'[tabindex]:not([tabindex="-1"])',
|
|
499
|
-
];
|
|
500
|
-
static SEMANTIC_SELECTORS = [
|
|
501
|
-
'h1',
|
|
502
|
-
'h2',
|
|
503
|
-
'h3',
|
|
504
|
-
'h4',
|
|
505
|
-
'h5',
|
|
506
|
-
'h6',
|
|
507
|
-
'article',
|
|
508
|
-
'section',
|
|
509
|
-
'nav',
|
|
510
|
-
'aside',
|
|
511
|
-
'main',
|
|
512
|
-
'header',
|
|
513
|
-
'footer',
|
|
514
|
-
'form',
|
|
515
|
-
'table',
|
|
516
|
-
'ul',
|
|
517
|
-
'ol',
|
|
518
|
-
'img[alt]',
|
|
519
|
-
'figure',
|
|
520
|
-
'video',
|
|
521
|
-
'audio',
|
|
522
|
-
'[role="navigation"]',
|
|
523
|
-
'[role="main"]',
|
|
524
|
-
'[role="complementary"]',
|
|
525
|
-
'[role="contentinfo"]',
|
|
526
|
-
];
|
|
527
|
-
/**
|
|
528
|
-
* Check if element is visible
|
|
529
|
-
*/
|
|
530
|
-
static isVisible(element, computedStyle) {
|
|
531
|
-
const rect = element.getBoundingClientRect();
|
|
532
|
-
const style =
|
|
533
|
-
computedStyle || element.ownerDocument?.defaultView?.getComputedStyle(element);
|
|
534
|
-
if (!style) return false;
|
|
535
|
-
return !!(
|
|
536
|
-
rect.width > 0 &&
|
|
537
|
-
rect.height > 0 &&
|
|
538
|
-
style.display !== 'none' &&
|
|
539
|
-
style.visibility !== 'hidden' &&
|
|
540
|
-
style.opacity !== '0' &&
|
|
541
|
-
element.offsetParent !== null
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
/**
|
|
545
|
-
* Check if element is in viewport
|
|
546
|
-
*/
|
|
547
|
-
static isInViewport(element, viewport) {
|
|
548
|
-
const rect = element.getBoundingClientRect();
|
|
549
|
-
const view = viewport || {
|
|
550
|
-
width: element.ownerDocument?.defaultView?.innerWidth || 0,
|
|
551
|
-
height: element.ownerDocument?.defaultView?.innerHeight || 0,
|
|
552
|
-
};
|
|
553
|
-
return (
|
|
554
|
-
rect.top < view.height && rect.bottom > 0 && rect.left < view.width && rect.right > 0
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
/**
|
|
558
|
-
* Check if element passes filter criteria
|
|
559
|
-
*/
|
|
560
|
-
static passesFilter(element, filter) {
|
|
561
|
-
if (!filter) return true;
|
|
562
|
-
const htmlElement = element;
|
|
563
|
-
if (filter.excludeSelectors?.length) {
|
|
564
|
-
for (const selector of filter.excludeSelectors) {
|
|
565
|
-
if (element.matches(selector)) return false;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
if (filter.includeSelectors?.length) {
|
|
569
|
-
let matches = false;
|
|
570
|
-
for (const selector of filter.includeSelectors) {
|
|
571
|
-
if (element.matches(selector)) {
|
|
572
|
-
matches = true;
|
|
573
|
-
break;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
if (!matches) return false;
|
|
577
|
-
}
|
|
578
|
-
if (filter.tags?.length && !filter.tags.includes(element.tagName.toLowerCase())) {
|
|
579
|
-
return false;
|
|
580
|
-
}
|
|
581
|
-
const textContent = htmlElement.textContent?.toLowerCase() || '';
|
|
582
|
-
if (filter.textContains?.length) {
|
|
583
|
-
let hasText = false;
|
|
584
|
-
for (const text of filter.textContains) {
|
|
585
|
-
if (textContent.includes(text.toLowerCase())) {
|
|
586
|
-
hasText = true;
|
|
587
|
-
break;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
if (!hasText) return false;
|
|
591
|
-
}
|
|
592
|
-
if (filter.textMatches?.length) {
|
|
593
|
-
let matches = false;
|
|
594
|
-
for (const pattern of filter.textMatches) {
|
|
595
|
-
if (pattern.test(textContent)) {
|
|
596
|
-
matches = true;
|
|
597
|
-
break;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
if (!matches) return false;
|
|
601
|
-
}
|
|
602
|
-
if (filter.hasAttributes?.length) {
|
|
603
|
-
for (const attr of filter.hasAttributes) {
|
|
604
|
-
if (!element.hasAttribute(attr)) return false;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
if (filter.attributeValues) {
|
|
608
|
-
for (const [attr, value] of Object.entries(filter.attributeValues)) {
|
|
609
|
-
const attrValue = element.getAttribute(attr);
|
|
610
|
-
if (!attrValue) return false;
|
|
611
|
-
if (typeof value === 'string') {
|
|
612
|
-
if (attrValue !== value) return false;
|
|
613
|
-
} else if (value instanceof RegExp) {
|
|
614
|
-
if (!value.test(attrValue)) return false;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
if (filter.withinSelectors?.length) {
|
|
619
|
-
let isWithin = false;
|
|
620
|
-
for (const selector of filter.withinSelectors) {
|
|
621
|
-
if (element.closest(selector)) {
|
|
622
|
-
isWithin = true;
|
|
623
|
-
break;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
if (!isWithin) return false;
|
|
627
|
-
}
|
|
628
|
-
if (filter.interactionTypes?.length) {
|
|
629
|
-
const interaction = this.getInteractionInfo(element);
|
|
630
|
-
let hasInteraction = false;
|
|
631
|
-
for (const type of filter.interactionTypes) {
|
|
632
|
-
if (interaction[type]) {
|
|
633
|
-
hasInteraction = true;
|
|
634
|
-
break;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
if (!hasInteraction) return false;
|
|
638
|
-
}
|
|
639
|
-
if (filter.nearText) {
|
|
640
|
-
const parent = element.parentElement;
|
|
641
|
-
if (
|
|
642
|
-
!parent ||
|
|
643
|
-
!parent.textContent?.toLowerCase().includes(filter.nearText.toLowerCase())
|
|
644
|
-
) {
|
|
645
|
-
return false;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
return true;
|
|
649
|
-
}
|
|
650
|
-
/**
|
|
651
|
-
* Extract element information
|
|
652
|
-
*/
|
|
653
|
-
static extractElement(element, options, depth = 0) {
|
|
654
|
-
if (options.maxDepth && depth > options.maxDepth) {
|
|
655
|
-
return null;
|
|
656
|
-
}
|
|
657
|
-
if (!options.includeHidden && !this.isVisible(element)) {
|
|
658
|
-
return null;
|
|
659
|
-
}
|
|
660
|
-
if (options.viewportOnly && !this.isInViewport(element)) {
|
|
661
|
-
return null;
|
|
662
|
-
}
|
|
663
|
-
if (!this.passesFilter(element, options.filter)) {
|
|
664
|
-
return null;
|
|
665
|
-
}
|
|
666
|
-
const htmlElement = element;
|
|
667
|
-
const extracted = {
|
|
668
|
-
tag: element.tagName.toLowerCase(),
|
|
669
|
-
text: this.getElementText(element, options),
|
|
670
|
-
selector: SelectorGenerator.generateSelectors(element),
|
|
671
|
-
attributes: this.getRelevantAttributes(element, options),
|
|
672
|
-
context: this.getElementContext(element),
|
|
673
|
-
interaction: this.getInteractionInfo(element),
|
|
674
|
-
// bounds removed to save tokens
|
|
675
|
-
};
|
|
676
|
-
if (options.mode === 'full' && this.isSemanticContainer(element)) {
|
|
677
|
-
const children = [];
|
|
678
|
-
if (options.includeShadowDOM && htmlElement.shadowRoot) {
|
|
679
|
-
const shadowChildren = this.extractChildren(htmlElement.shadowRoot, options, depth + 1);
|
|
680
|
-
children.push(...shadowChildren);
|
|
681
|
-
}
|
|
682
|
-
const regularChildren = this.extractChildren(element, options, depth + 1);
|
|
683
|
-
children.push(...regularChildren);
|
|
684
|
-
if (children.length > 0) {
|
|
685
|
-
extracted.children = children;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
return extracted;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Extract children elements
|
|
692
|
-
*/
|
|
693
|
-
static extractChildren(container, options, depth) {
|
|
694
|
-
const children = [];
|
|
695
|
-
const elements = container.querySelectorAll('*');
|
|
696
|
-
for (const child of Array.from(elements)) {
|
|
697
|
-
if (this.hasExtractedAncestor(child, elements)) {
|
|
698
|
-
continue;
|
|
699
|
-
}
|
|
700
|
-
const extracted = this.extractElement(child, options, depth);
|
|
701
|
-
if (extracted) {
|
|
702
|
-
children.push(extracted);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
return children;
|
|
706
|
-
}
|
|
707
|
-
/**
|
|
708
|
-
* Check if element has an ancestor that was already extracted
|
|
709
|
-
*/
|
|
710
|
-
static hasExtractedAncestor(element, extractedElements) {
|
|
711
|
-
let parent = element.parentElement;
|
|
712
|
-
while (parent) {
|
|
713
|
-
if (Array.from(extractedElements).includes(parent)) {
|
|
714
|
-
return true;
|
|
715
|
-
}
|
|
716
|
-
parent = parent.parentElement;
|
|
717
|
-
}
|
|
718
|
-
return false;
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
|
-
* Get relevant attributes for an element
|
|
722
|
-
*/
|
|
723
|
-
static getRelevantAttributes(element, options) {
|
|
724
|
-
const relevant = [
|
|
725
|
-
'id',
|
|
726
|
-
'class',
|
|
727
|
-
'name',
|
|
728
|
-
'type',
|
|
729
|
-
'value',
|
|
730
|
-
'placeholder',
|
|
731
|
-
'href',
|
|
732
|
-
'src',
|
|
733
|
-
'alt',
|
|
734
|
-
'title',
|
|
735
|
-
'action',
|
|
736
|
-
'method',
|
|
737
|
-
'aria-label',
|
|
738
|
-
'aria-describedby',
|
|
739
|
-
'aria-controls',
|
|
740
|
-
'role',
|
|
741
|
-
'disabled',
|
|
742
|
-
'readonly',
|
|
743
|
-
'required',
|
|
744
|
-
'checked',
|
|
745
|
-
'min',
|
|
746
|
-
'max',
|
|
747
|
-
'pattern',
|
|
748
|
-
'step',
|
|
749
|
-
'autocomplete',
|
|
750
|
-
'data-testid',
|
|
751
|
-
'data-test',
|
|
752
|
-
'data-cy',
|
|
753
|
-
];
|
|
754
|
-
const attributes = {};
|
|
755
|
-
const attrTruncate = options.attributeTruncateLength ?? 100;
|
|
756
|
-
const dataAttrTruncate = options.dataAttributeTruncateLength ?? 50;
|
|
757
|
-
for (const attr of relevant) {
|
|
758
|
-
const value = element.getAttribute(attr);
|
|
759
|
-
if (value) {
|
|
760
|
-
attributes[attr] =
|
|
761
|
-
value.length > attrTruncate ? `${value.substring(0, attrTruncate)}...` : value;
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
for (const attr of element.attributes) {
|
|
765
|
-
if (attr.name.startsWith('data-') && !relevant.includes(attr.name)) {
|
|
766
|
-
attributes[attr.name] =
|
|
767
|
-
attr.value.length > dataAttrTruncate
|
|
768
|
-
? `${attr.value.substring(0, dataAttrTruncate)}...`
|
|
769
|
-
: attr.value;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
return attributes;
|
|
773
|
-
}
|
|
774
|
-
/**
|
|
775
|
-
* Get element context information
|
|
776
|
-
*/
|
|
777
|
-
static getElementContext(element) {
|
|
778
|
-
const context = {
|
|
779
|
-
parentChain: SelectorGenerator.getContextPath(element),
|
|
780
|
-
};
|
|
781
|
-
const form = element.closest('form');
|
|
782
|
-
if (form) {
|
|
783
|
-
context.nearestForm = SelectorGenerator.generateSelectors(form).css;
|
|
784
|
-
}
|
|
785
|
-
const section = element.closest('section, [role="region"]');
|
|
786
|
-
if (section) {
|
|
787
|
-
context.nearestSection = SelectorGenerator.generateSelectors(section).css;
|
|
788
|
-
}
|
|
789
|
-
const main = element.closest('main, [role="main"]');
|
|
790
|
-
if (main) {
|
|
791
|
-
context.nearestMain = SelectorGenerator.generateSelectors(main).css;
|
|
792
|
-
}
|
|
793
|
-
const nav = element.closest('nav, [role="navigation"]');
|
|
794
|
-
if (nav) {
|
|
795
|
-
context.nearestNav = SelectorGenerator.generateSelectors(nav).css;
|
|
796
|
-
}
|
|
797
|
-
return context;
|
|
798
|
-
}
|
|
799
|
-
/**
|
|
800
|
-
* Get interaction information for an element (compact format)
|
|
801
|
-
*/
|
|
802
|
-
static getInteractionInfo(element) {
|
|
803
|
-
const htmlElement = element;
|
|
804
|
-
const interaction = {};
|
|
805
|
-
const hasClickHandler = !!(
|
|
806
|
-
htmlElement.onclick ||
|
|
807
|
-
element.getAttribute('onclick') ||
|
|
808
|
-
element.matches('button, a[href], [role="button"], [tabindex]:not([tabindex="-1"])')
|
|
809
|
-
);
|
|
810
|
-
if (hasClickHandler) interaction.click = true;
|
|
811
|
-
const hasChangeHandler = !!(
|
|
812
|
-
htmlElement.onchange ||
|
|
813
|
-
element.getAttribute('onchange') ||
|
|
814
|
-
element.matches('input, select, textarea')
|
|
815
|
-
);
|
|
816
|
-
if (hasChangeHandler) interaction.change = true;
|
|
817
|
-
const hasSubmitHandler = !!(
|
|
818
|
-
htmlElement.onsubmit ||
|
|
819
|
-
element.getAttribute('onsubmit') ||
|
|
820
|
-
element.matches('form')
|
|
821
|
-
);
|
|
822
|
-
if (hasSubmitHandler) interaction.submit = true;
|
|
823
|
-
const triggersNavigation = element.matches('a[href], button[type="submit"]');
|
|
824
|
-
if (triggersNavigation) interaction.nav = true;
|
|
825
|
-
const isDisabled =
|
|
826
|
-
htmlElement.hasAttribute('disabled') ||
|
|
827
|
-
htmlElement.getAttribute('aria-disabled') === 'true';
|
|
828
|
-
if (isDisabled) interaction.disabled = true;
|
|
829
|
-
const isHidden = !this.isVisible(element);
|
|
830
|
-
if (isHidden) interaction.hidden = true;
|
|
831
|
-
const ariaRole = element.getAttribute('role');
|
|
832
|
-
if (ariaRole) interaction.role = ariaRole;
|
|
833
|
-
if (element.matches('input, textarea, select, button')) {
|
|
834
|
-
const form = element.form || element.closest('form');
|
|
835
|
-
if (form) {
|
|
836
|
-
interaction.form = SelectorGenerator.generateSelectors(form).css;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
return interaction;
|
|
840
|
-
}
|
|
841
|
-
/**
|
|
842
|
-
* Get text content of an element (limited length)
|
|
843
|
-
*/
|
|
844
|
-
static getElementText(element, options) {
|
|
845
|
-
if (element.matches('input, textarea')) {
|
|
846
|
-
const input = element;
|
|
847
|
-
return input.value || input.placeholder || '';
|
|
848
|
-
}
|
|
849
|
-
if (element.matches('img')) {
|
|
850
|
-
return element.alt || '';
|
|
851
|
-
}
|
|
852
|
-
const text = element.textContent?.trim() || '';
|
|
853
|
-
const maxLength = options?.textTruncateLength;
|
|
854
|
-
if (maxLength && text.length > maxLength) {
|
|
855
|
-
return `${text.substring(0, maxLength)}...`;
|
|
856
|
-
}
|
|
857
|
-
return text;
|
|
858
|
-
}
|
|
859
|
-
/**
|
|
860
|
-
* Check if element is a semantic container
|
|
861
|
-
*/
|
|
862
|
-
static isSemanticContainer(element) {
|
|
863
|
-
return element.matches(
|
|
864
|
-
'article, section, nav, aside, main, header, footer, form, table, ul, ol, dl, figure, details, dialog, [role="region"], [role="navigation"], [role="main"], [role="complementary"]'
|
|
865
|
-
);
|
|
866
|
-
}
|
|
867
|
-
/**
|
|
868
|
-
* Get interactive elements
|
|
869
|
-
*/
|
|
870
|
-
static getInteractiveElements(container, options) {
|
|
871
|
-
const elements = [];
|
|
872
|
-
const selector = this.INTERACTIVE_SELECTORS.join(', ');
|
|
873
|
-
const found = container.querySelectorAll(selector);
|
|
874
|
-
for (const element of Array.from(found)) {
|
|
875
|
-
const extracted = this.extractElement(element, options);
|
|
876
|
-
if (extracted) {
|
|
877
|
-
elements.push(extracted);
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
if (options.customSelectors) {
|
|
881
|
-
for (const customSelector of options.customSelectors) {
|
|
882
|
-
try {
|
|
883
|
-
const customFound = container.querySelectorAll(customSelector);
|
|
884
|
-
for (const element of Array.from(customFound)) {
|
|
885
|
-
const extracted = this.extractElement(element, options);
|
|
886
|
-
if (extracted) {
|
|
887
|
-
elements.push(extracted);
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
} catch (_e) {
|
|
891
|
-
console.warn(`Invalid custom selector: ${customSelector}`);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
return elements;
|
|
896
|
-
}
|
|
897
|
-
/**
|
|
898
|
-
* Get semantic elements (for full mode)
|
|
899
|
-
*/
|
|
900
|
-
static getSemanticElements(container, options) {
|
|
901
|
-
const elements = [];
|
|
902
|
-
const selector = this.SEMANTIC_SELECTORS.join(', ');
|
|
903
|
-
const found = container.querySelectorAll(selector);
|
|
904
|
-
for (const element of Array.from(found)) {
|
|
905
|
-
const extracted = this.extractElement(element, options);
|
|
906
|
-
if (extracted) {
|
|
907
|
-
elements.push(extracted);
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
return elements;
|
|
911
|
-
}
|
|
1
|
+
//#region src/content-detection.ts
|
|
2
|
+
var ContentDetection = class ContentDetection {
|
|
3
|
+
/**
|
|
4
|
+
* Find the main content area of a page
|
|
5
|
+
* Inspired by dom-to-semantic-markdown's approach
|
|
6
|
+
*/
|
|
7
|
+
static findMainContent(doc) {
|
|
8
|
+
const mainElement = doc.querySelector('main, [role="main"]');
|
|
9
|
+
if (mainElement) return mainElement;
|
|
10
|
+
if (!doc.body) return doc.documentElement;
|
|
11
|
+
return ContentDetection.detectMainContent(doc.body);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Detect main content using scoring algorithm
|
|
15
|
+
*/
|
|
16
|
+
static detectMainContent(rootElement) {
|
|
17
|
+
const candidates = [];
|
|
18
|
+
ContentDetection.collectCandidates(rootElement, candidates, 15);
|
|
19
|
+
if (candidates.length === 0) return rootElement;
|
|
20
|
+
candidates.sort(
|
|
21
|
+
(a, b) =>
|
|
22
|
+
ContentDetection.calculateContentScore(b) - ContentDetection.calculateContentScore(a)
|
|
23
|
+
);
|
|
24
|
+
let bestCandidate = candidates[0];
|
|
25
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
26
|
+
const candidate = candidates[i];
|
|
27
|
+
if (
|
|
28
|
+
!candidates.some((other, j) => j !== i && other.contains(candidate)) &&
|
|
29
|
+
ContentDetection.calculateContentScore(candidate) >
|
|
30
|
+
ContentDetection.calculateContentScore(bestCandidate)
|
|
31
|
+
)
|
|
32
|
+
bestCandidate = candidate;
|
|
33
|
+
}
|
|
34
|
+
return bestCandidate;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Collect content candidates
|
|
38
|
+
*/
|
|
39
|
+
static collectCandidates(element, candidates, minScore) {
|
|
40
|
+
if (ContentDetection.calculateContentScore(element) >= minScore) candidates.push(element);
|
|
41
|
+
Array.from(element.children).forEach((child) => {
|
|
42
|
+
ContentDetection.collectCandidates(child, candidates, minScore);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Calculate content score for an element
|
|
47
|
+
*/
|
|
48
|
+
static calculateContentScore(element) {
|
|
49
|
+
let score = 0;
|
|
50
|
+
const semanticClasses = [
|
|
51
|
+
'article',
|
|
52
|
+
'content',
|
|
53
|
+
'main-container',
|
|
54
|
+
'main',
|
|
55
|
+
'main-content',
|
|
56
|
+
'post',
|
|
57
|
+
'entry',
|
|
58
|
+
];
|
|
59
|
+
const semanticIds = ['content', 'main', 'article', 'post', 'entry'];
|
|
60
|
+
semanticClasses.forEach((cls) => {
|
|
61
|
+
if (element.classList.contains(cls)) score += 10;
|
|
62
|
+
});
|
|
63
|
+
semanticIds.forEach((id) => {
|
|
64
|
+
if (element.id?.toLowerCase().includes(id)) score += 10;
|
|
65
|
+
});
|
|
66
|
+
const tag = element.tagName.toLowerCase();
|
|
67
|
+
if (['article', 'main', 'section'].includes(tag)) score += 8;
|
|
68
|
+
const paragraphs = element.getElementsByTagName('p').length;
|
|
69
|
+
score += Math.min(paragraphs * 2, 10);
|
|
70
|
+
const headings = element.querySelectorAll('h1, h2, h3').length;
|
|
71
|
+
score += Math.min(headings * 3, 9);
|
|
72
|
+
const textLength = element.textContent?.trim().length || 0;
|
|
73
|
+
if (textLength > 300) score += Math.min(Math.floor(textLength / 300) * 2, 10);
|
|
74
|
+
const linkDensity = ContentDetection.calculateLinkDensity(element);
|
|
75
|
+
if (linkDensity < 0.3) score += 5;
|
|
76
|
+
else if (linkDensity > 0.5) score -= 5;
|
|
77
|
+
if (
|
|
78
|
+
element.hasAttribute('data-main') ||
|
|
79
|
+
element.hasAttribute('data-content') ||
|
|
80
|
+
element.hasAttribute('itemprop')
|
|
81
|
+
)
|
|
82
|
+
score += 8;
|
|
83
|
+
const role = element.getAttribute('role');
|
|
84
|
+
if (role === 'main' || role === 'article') score += 10;
|
|
85
|
+
if (
|
|
86
|
+
element.matches(
|
|
87
|
+
'aside, nav, header, footer, .sidebar, .navigation, .menu, .ad, .advertisement'
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
score -= 10;
|
|
91
|
+
if (element.getElementsByTagName('form').length > 2) score -= 5;
|
|
92
|
+
return Math.max(0, score);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Calculate link density in an element
|
|
96
|
+
*/
|
|
97
|
+
static calculateLinkDensity(element) {
|
|
98
|
+
const links = element.getElementsByTagName('a');
|
|
99
|
+
let linkTextLength = 0;
|
|
100
|
+
for (const link of Array.from(links)) linkTextLength += link.textContent?.length || 0;
|
|
101
|
+
const totalTextLength = element.textContent?.length || 1;
|
|
102
|
+
return linkTextLength / totalTextLength;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if an element is likely navigation
|
|
106
|
+
*/
|
|
107
|
+
static isNavigation(element) {
|
|
108
|
+
if (element.tagName.toLowerCase() === 'nav' || element.getAttribute('role') === 'navigation')
|
|
109
|
+
return true;
|
|
110
|
+
const navPatterns = [/nav/i, /menu/i, /sidebar/i, /toolbar/i];
|
|
111
|
+
const classesAndId = `${element.className} ${element.id}`.toLowerCase();
|
|
112
|
+
return navPatterns.some((pattern) => pattern.test(classesAndId));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Check if element is likely supplementary content
|
|
116
|
+
*/
|
|
117
|
+
static isSupplementary(element) {
|
|
118
|
+
if (
|
|
119
|
+
element.tagName.toLowerCase() === 'aside' ||
|
|
120
|
+
element.getAttribute('role') === 'complementary'
|
|
121
|
+
)
|
|
122
|
+
return true;
|
|
123
|
+
const supplementaryPatterns = [/sidebar/i, /widget/i, /related/i, /advertisement/i, /social/i];
|
|
124
|
+
const classesAndId = `${element.className} ${element.id}`.toLowerCase();
|
|
125
|
+
return supplementaryPatterns.some((pattern) => pattern.test(classesAndId));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Detect page landmarks
|
|
129
|
+
*/
|
|
130
|
+
static detectLandmarks(doc) {
|
|
131
|
+
const landmarks = {
|
|
132
|
+
navigation: [],
|
|
133
|
+
main: [],
|
|
134
|
+
complementary: [],
|
|
135
|
+
contentinfo: [],
|
|
136
|
+
banner: [],
|
|
137
|
+
search: [],
|
|
138
|
+
form: [],
|
|
139
|
+
region: [],
|
|
912
140
|
};
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
141
|
+
for (const [landmark, selector] of Object.entries({
|
|
142
|
+
navigation: 'nav, [role="navigation"]',
|
|
143
|
+
main: 'main, [role="main"]',
|
|
144
|
+
complementary: 'aside, [role="complementary"]',
|
|
145
|
+
contentinfo: 'footer, [role="contentinfo"]',
|
|
146
|
+
banner: 'header, [role="banner"]',
|
|
147
|
+
search: '[role="search"]',
|
|
148
|
+
form: 'form[aria-label], form[aria-labelledby], [role="form"]',
|
|
149
|
+
region: 'section[aria-label], section[aria-labelledby], [role="region"]',
|
|
150
|
+
})) {
|
|
151
|
+
const elements = doc.querySelectorAll(selector);
|
|
152
|
+
landmarks[landmark] = Array.from(elements);
|
|
153
|
+
}
|
|
154
|
+
return landmarks;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/markdown-formatter.ts
|
|
917
159
|
function truncate(text, len) {
|
|
918
160
|
const t = (text ?? '').trim();
|
|
919
161
|
if (!len || t.length <= len) return t;
|
|
@@ -930,20 +172,22 @@ function truncate(text, len) {
|
|
|
930
172
|
'continue',
|
|
931
173
|
];
|
|
932
174
|
const lower = t.toLowerCase();
|
|
933
|
-
const hit = keywords
|
|
175
|
+
const hit = keywords
|
|
176
|
+
.map((k) => ({
|
|
177
|
+
k,
|
|
178
|
+
i: lower.indexOf(k),
|
|
179
|
+
}))
|
|
180
|
+
.find((x) => x.i > -1);
|
|
934
181
|
const head = Math.max(0, Math.floor(len * 0.66));
|
|
935
182
|
if (hit && hit.i > head) {
|
|
936
183
|
const tailWindow = Math.max(12, len - head - 5);
|
|
937
184
|
const start = Math.max(0, hit.i - Math.floor(tailWindow / 2));
|
|
938
185
|
const end = Math.min(t.length, start + tailWindow);
|
|
939
|
-
return `${t.slice(0, head).trimEnd()}
|
|
186
|
+
return `${t.slice(0, head).trimEnd()} … ${t.slice(start, end).trim()}…`;
|
|
940
187
|
}
|
|
941
188
|
const slice = t.slice(0, len);
|
|
942
189
|
const lastSpace = slice.lastIndexOf(' ');
|
|
943
|
-
return `${lastSpace > 32 ? slice.slice(0, lastSpace) : slice}
|
|
944
|
-
}
|
|
945
|
-
function bestSelector(el) {
|
|
946
|
-
return el.selector?.css || '';
|
|
190
|
+
return `${lastSpace > 32 ? slice.slice(0, lastSpace) : slice}…`;
|
|
947
191
|
}
|
|
948
192
|
function hashId(input) {
|
|
949
193
|
let h = 5381;
|
|
@@ -953,26 +197,26 @@ function hashId(input) {
|
|
|
953
197
|
function iconForRegion(key) {
|
|
954
198
|
switch (key) {
|
|
955
199
|
case 'header':
|
|
956
|
-
return '
|
|
200
|
+
return '🧭';
|
|
957
201
|
case 'navigation':
|
|
958
|
-
return '
|
|
202
|
+
return '📑';
|
|
959
203
|
case 'main':
|
|
960
|
-
return '
|
|
204
|
+
return '📄';
|
|
961
205
|
case 'sections':
|
|
962
|
-
return '
|
|
206
|
+
return '🗂️';
|
|
963
207
|
case 'sidebar':
|
|
964
|
-
return '
|
|
208
|
+
return '📚';
|
|
965
209
|
case 'footer':
|
|
966
|
-
return '
|
|
210
|
+
return '🔻';
|
|
967
211
|
case 'modals':
|
|
968
|
-
return '
|
|
212
|
+
return '💬';
|
|
969
213
|
default:
|
|
970
|
-
return '
|
|
214
|
+
return '🔹';
|
|
971
215
|
}
|
|
972
216
|
}
|
|
973
217
|
function elementLine(el, opts) {
|
|
974
218
|
const txt = truncate(el.text || el.attributes?.ariaLabel, opts?.maxTextLength ?? 80);
|
|
975
|
-
const sel =
|
|
219
|
+
const sel = el.selector?.css || '';
|
|
976
220
|
const tag = el.tag.toLowerCase();
|
|
977
221
|
const action = el.interaction?.submit
|
|
978
222
|
? 'submit'
|
|
@@ -982,7 +226,7 @@ function elementLine(el, opts) {
|
|
|
982
226
|
? 'change'
|
|
983
227
|
: void 0;
|
|
984
228
|
const actionText = action ? ` (${action})` : '';
|
|
985
|
-
return `- ${tag.toUpperCase()}: ${txt || '(no text)'}
|
|
229
|
+
return `- ${tag.toUpperCase()}: ${txt || '(no text)'} → \`${sel}\`${actionText}`;
|
|
986
230
|
}
|
|
987
231
|
function selectorQualitySummary(inter) {
|
|
988
232
|
const all = [];
|
|
@@ -995,9 +239,7 @@ function selectorQualitySummary(inter) {
|
|
|
995
239
|
const testIdCount = all.filter((s) => /\[data-testid=/.test(s)).length;
|
|
996
240
|
const nthCount = all.filter((s) => /:nth-child\(/.test(s)).length;
|
|
997
241
|
const stable = idCount + testIdCount;
|
|
998
|
-
|
|
999
|
-
const nthPct = Math.round((nthCount / total) * 100);
|
|
1000
|
-
return `Selector quality: ${stablePct}% stable (ID/data-testid), ${nthPct}% structural (:nth-child)`;
|
|
242
|
+
return `Selector quality: ${Math.round((stable / total) * 100)}% stable (ID/data-testid), ${Math.round((nthCount / total) * 100)}% structural (:nth-child)`;
|
|
1001
243
|
}
|
|
1002
244
|
function renderInteractive(inter, opts) {
|
|
1003
245
|
const parts = [];
|
|
@@ -1021,11 +263,8 @@ function renderInteractive(inter, opts) {
|
|
|
1021
263
|
}
|
|
1022
264
|
if (inter.forms.length) {
|
|
1023
265
|
parts.push('Forms:');
|
|
1024
|
-
for (const f of limit(inter.forms))
|
|
1025
|
-
parts.push(
|
|
1026
|
-
`- FORM: action=${f.action ?? '-'} method=${f.method ?? '-'} \u2192 \`${f.selector}\``
|
|
1027
|
-
);
|
|
1028
|
-
}
|
|
266
|
+
for (const f of limit(inter.forms))
|
|
267
|
+
parts.push(`- FORM: action=${f.action ?? '-'} method=${f.method ?? '-'} → \`${f.selector}\``);
|
|
1029
268
|
}
|
|
1030
269
|
return parts.join('\n');
|
|
1031
270
|
}
|
|
@@ -1037,22 +276,12 @@ function renderRegionInfo(region) {
|
|
|
1037
276
|
if (region.buttonCount) stats.push(`${region.buttonCount} buttons`);
|
|
1038
277
|
if (region.linkCount) stats.push(`${region.linkCount} links`);
|
|
1039
278
|
if (region.inputCount) stats.push(`${region.inputCount} inputs`);
|
|
1040
|
-
if (region.textPreview) stats.push(
|
|
1041
|
-
const statsLine = stats.length ? `
|
|
1042
|
-
return `${icon} ${label}
|
|
279
|
+
if (region.textPreview) stats.push(`“${truncate(region.textPreview, 80)}”`);
|
|
280
|
+
const statsLine = stats.length ? ` — ${stats.join(', ')}` : '';
|
|
281
|
+
return `${icon} ${label} → \`${region.selector}\` [${id}]${statsLine}`;
|
|
1043
282
|
}
|
|
1044
283
|
function wrapXml(body, meta, type = 'section') {
|
|
1045
|
-
|
|
1046
|
-
meta?.title ? `title="${escapeXml(meta.title)}"` : null,
|
|
1047
|
-
meta?.url ? `url="${escapeXml(meta.url)}"` : null,
|
|
1048
|
-
]
|
|
1049
|
-
.filter(Boolean)
|
|
1050
|
-
.join(' ');
|
|
1051
|
-
return `<page ${attrs}>
|
|
1052
|
-
<${type}><![CDATA[
|
|
1053
|
-
${body}
|
|
1054
|
-
]]></${type}>
|
|
1055
|
-
</page>`;
|
|
284
|
+
return `<page ${[meta?.title ? `title="${escapeXml(meta.title)}"` : null, meta?.url ? `url="${escapeXml(meta.url)}"` : null].filter(Boolean).join(' ')}>\n <${type}><![CDATA[\n${body}\n]]></${type}>\n</page>`;
|
|
1056
285
|
}
|
|
1057
286
|
function escapeXml(s) {
|
|
1058
287
|
return s
|
|
@@ -1061,857 +290,1245 @@ function escapeXml(s) {
|
|
|
1061
290
|
.replace(/>/g, '>')
|
|
1062
291
|
.replace(/"/g, '"');
|
|
1063
292
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
for (const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
if (!value.length) continue;
|
|
1094
|
-
lines.push(`## ${icon} ${capitalize(key)}`);
|
|
1095
|
-
for (const region of value) lines.push(renderRegionInfo(region));
|
|
1096
|
-
} else {
|
|
1097
|
-
lines.push(`## ${icon} ${capitalize(key)}`);
|
|
1098
|
-
lines.push(renderRegionInfo(value));
|
|
1099
|
-
}
|
|
1100
|
-
lines.push('');
|
|
1101
|
-
}
|
|
1102
|
-
if (overview.suggestions?.length) {
|
|
1103
|
-
lines.push('## Suggestions');
|
|
1104
|
-
for (const s of overview.suggestions) lines.push(`- ${s}`);
|
|
1105
|
-
lines.push('');
|
|
1106
|
-
}
|
|
1107
|
-
lines.push(
|
|
1108
|
-
'Next: choose a region (by selector or [sectionId]) and call dom_extract_region for actionable details.'
|
|
1109
|
-
);
|
|
1110
|
-
const body = lines.join('\n');
|
|
1111
|
-
return wrapXml(body, meta, 'outline');
|
|
1112
|
-
}
|
|
1113
|
-
static region(result, opts, meta) {
|
|
1114
|
-
const lines = [];
|
|
1115
|
-
lines.push('# Region Details');
|
|
1116
|
-
if (meta?.title || meta?.url) {
|
|
1117
|
-
lines.push(`Title: ${meta?.title ?? ''}`.trim());
|
|
1118
|
-
lines.push(`URL: ${meta?.url ?? ''}`.trim());
|
|
1119
|
-
}
|
|
1120
|
-
lines.push('');
|
|
1121
|
-
const inter = result.interactive;
|
|
1122
|
-
if (result.page) {
|
|
1123
|
-
const ps = [
|
|
1124
|
-
result.page.hasErrors ? 'errors: yes' : 'errors: no',
|
|
1125
|
-
result.page.isLoading ? 'loading: yes' : 'loading: no',
|
|
1126
|
-
result.page.hasModals ? 'modals: yes' : 'modals: no',
|
|
1127
|
-
];
|
|
1128
|
-
lines.push(`Page state: ${ps.join(', ')}`);
|
|
1129
|
-
}
|
|
1130
|
-
const summary = [];
|
|
1131
|
-
const count = (arr) => (arr ? arr.length : 0);
|
|
1132
|
-
summary.push(`${count(inter.buttons)} buttons`);
|
|
1133
|
-
summary.push(`${count(inter.links)} links`);
|
|
1134
|
-
summary.push(`${count(inter.inputs)} inputs`);
|
|
1135
|
-
if (inter.forms?.length) summary.push(`${count(inter.forms)} forms`);
|
|
1136
|
-
lines.push(`Summary: ${summary.join(', ')}`);
|
|
1137
|
-
lines.push(selectorQualitySummary(inter));
|
|
1138
|
-
lines.push('');
|
|
1139
|
-
lines.push(renderInteractive(inter, opts));
|
|
1140
|
-
lines.push('');
|
|
1141
|
-
lines.push(
|
|
1142
|
-
'Next: write a script using the most stable selectors above. If selectors look unstable, rerun dom_extract_region with higher detail or call dom_extract_content for text context.'
|
|
1143
|
-
);
|
|
1144
|
-
const body = lines.join('\n');
|
|
1145
|
-
return wrapXml(body, meta, 'section');
|
|
1146
|
-
}
|
|
1147
|
-
static content(content, opts, meta) {
|
|
1148
|
-
const lines = [];
|
|
1149
|
-
lines.push('# Content');
|
|
1150
|
-
lines.push(`Selector: \`${content.selector}\``);
|
|
1151
|
-
lines.push('');
|
|
1152
|
-
if (content.text.headings?.length) {
|
|
1153
|
-
lines.push('Headings:');
|
|
1154
|
-
for (const h of content.text.headings)
|
|
1155
|
-
lines.push(`- H${h.level}: ${truncate(h.text, opts.maxTextLength ?? 120)}`);
|
|
1156
|
-
lines.push('');
|
|
1157
|
-
}
|
|
1158
|
-
if (content.text.paragraphs?.length) {
|
|
1159
|
-
const limit =
|
|
1160
|
-
typeof opts.maxElements === 'number'
|
|
1161
|
-
? opts.maxElements
|
|
1162
|
-
: content.text.paragraphs.length;
|
|
1163
|
-
lines.push('Paragraphs:');
|
|
1164
|
-
for (const p of content.text.paragraphs.slice(0, limit))
|
|
1165
|
-
lines.push(`- ${truncate(p, opts.maxTextLength ?? 200)}`);
|
|
1166
|
-
lines.push('');
|
|
1167
|
-
}
|
|
1168
|
-
if (content.text.lists?.length) {
|
|
1169
|
-
lines.push('Lists:');
|
|
1170
|
-
for (const list of content.text.lists) {
|
|
1171
|
-
lines.push(`- ${list.type.toUpperCase()}:`);
|
|
1172
|
-
const limit =
|
|
1173
|
-
typeof opts.maxElements === 'number' ? opts.maxElements : list.items.length;
|
|
1174
|
-
for (const item of list.items.slice(0, limit))
|
|
1175
|
-
lines.push(` - ${truncate(item, opts.maxTextLength ?? 120)}`);
|
|
1176
|
-
}
|
|
1177
|
-
lines.push('');
|
|
1178
|
-
}
|
|
1179
|
-
if (content.tables?.length) {
|
|
1180
|
-
lines.push('Tables:');
|
|
1181
|
-
for (const t of content.tables) {
|
|
1182
|
-
lines.push(`- Headers: ${t.headers.join(' | ')}`);
|
|
1183
|
-
const limit = typeof opts.maxElements === 'number' ? opts.maxElements : t.rows.length;
|
|
1184
|
-
for (const row of t.rows.slice(0, limit)) lines.push(` - ${row.join(' | ')}`);
|
|
1185
|
-
}
|
|
1186
|
-
lines.push('');
|
|
1187
|
-
}
|
|
1188
|
-
if (content.media?.length) {
|
|
1189
|
-
lines.push('Media:');
|
|
1190
|
-
const limit =
|
|
1191
|
-
typeof opts.maxElements === 'number' ? opts.maxElements : content.media.length;
|
|
1192
|
-
for (const m of content.media.slice(0, limit)) {
|
|
1193
|
-
lines.push(
|
|
1194
|
-
`- ${m.type.toUpperCase()}: ${m.alt ?? ''} ${m.src ? `\u2192 ${m.src}` : ''}`.trim()
|
|
1195
|
-
);
|
|
1196
|
-
}
|
|
1197
|
-
lines.push('');
|
|
1198
|
-
}
|
|
1199
|
-
lines.push(
|
|
1200
|
-
'Next: if text is insufficient for targeting, call dom_extract_region for interactive selectors.'
|
|
1201
|
-
);
|
|
1202
|
-
const body = lines.join('\n');
|
|
1203
|
-
return wrapXml(body, meta, 'content');
|
|
293
|
+
var MarkdownFormatter = class {
|
|
294
|
+
static structure(overview, _opts = {}, meta) {
|
|
295
|
+
const lines = [];
|
|
296
|
+
lines.push('# Page Outline');
|
|
297
|
+
if (meta?.title || meta?.url) {
|
|
298
|
+
lines.push(`Title: ${meta?.title ?? ''}`.trim());
|
|
299
|
+
lines.push(`URL: ${meta?.url ?? ''}`.trim());
|
|
300
|
+
}
|
|
301
|
+
lines.push('');
|
|
302
|
+
const regions = overview.regions;
|
|
303
|
+
const entries = [
|
|
304
|
+
['header', regions.header],
|
|
305
|
+
['navigation', regions.navigation],
|
|
306
|
+
['main', regions.main],
|
|
307
|
+
['sections', regions.sections],
|
|
308
|
+
['sidebar', regions.sidebar],
|
|
309
|
+
['footer', regions.footer],
|
|
310
|
+
['modals', regions.modals],
|
|
311
|
+
];
|
|
312
|
+
for (const [key, value] of entries) {
|
|
313
|
+
if (!value) continue;
|
|
314
|
+
const icon = iconForRegion(key);
|
|
315
|
+
if (Array.isArray(value)) {
|
|
316
|
+
if (!value.length) continue;
|
|
317
|
+
lines.push(`## ${icon} ${capitalize(key)}`);
|
|
318
|
+
for (const region of value) lines.push(renderRegionInfo(region));
|
|
319
|
+
} else {
|
|
320
|
+
lines.push(`## ${icon} ${capitalize(key)}`);
|
|
321
|
+
lines.push(renderRegionInfo(value));
|
|
1204
322
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
323
|
+
lines.push('');
|
|
324
|
+
}
|
|
325
|
+
if (overview.suggestions?.length) {
|
|
326
|
+
lines.push('## Suggestions');
|
|
327
|
+
for (const s of overview.suggestions) lines.push(`- ${s}`);
|
|
328
|
+
lines.push('');
|
|
329
|
+
}
|
|
330
|
+
lines.push(
|
|
331
|
+
'Next: choose a region (by selector or [sectionId]) and call dom_extract_region for actionable details.'
|
|
332
|
+
);
|
|
333
|
+
return wrapXml(lines.join('\n'), meta, 'outline');
|
|
334
|
+
}
|
|
335
|
+
static region(result, opts = {}, meta) {
|
|
336
|
+
const lines = [];
|
|
337
|
+
lines.push('# Region Details');
|
|
338
|
+
if (meta?.title || meta?.url) {
|
|
339
|
+
lines.push(`Title: ${meta?.title ?? ''}`.trim());
|
|
340
|
+
lines.push(`URL: ${meta?.url ?? ''}`.trim());
|
|
1216
341
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
342
|
+
lines.push('');
|
|
343
|
+
const inter = result.interactive;
|
|
344
|
+
if (result.page) {
|
|
345
|
+
const ps = [
|
|
346
|
+
result.page.hasErrors ? 'errors: yes' : 'errors: no',
|
|
347
|
+
result.page.isLoading ? 'loading: yes' : 'loading: no',
|
|
348
|
+
result.page.hasModals ? 'modals: yes' : 'modals: no',
|
|
349
|
+
];
|
|
350
|
+
lines.push(`Page state: ${ps.join(', ')}`);
|
|
1220
351
|
}
|
|
352
|
+
const summary = [];
|
|
353
|
+
const count = (arr) => (arr ? arr.length : 0);
|
|
354
|
+
summary.push(`${count(inter.buttons)} buttons`);
|
|
355
|
+
summary.push(`${count(inter.links)} links`);
|
|
356
|
+
summary.push(`${count(inter.inputs)} inputs`);
|
|
357
|
+
if (inter.forms?.length) summary.push(`${count(inter.forms)} forms`);
|
|
358
|
+
lines.push(`Summary: ${summary.join(', ')}`);
|
|
359
|
+
lines.push(selectorQualitySummary(inter));
|
|
360
|
+
lines.push('');
|
|
361
|
+
lines.push(renderInteractive(inter, opts));
|
|
362
|
+
lines.push('');
|
|
363
|
+
lines.push(
|
|
364
|
+
'Next: write a script using the most stable selectors above. If selectors look unstable, rerun dom_extract_region with higher detail or call dom_extract_content for text context.'
|
|
365
|
+
);
|
|
366
|
+
return wrapXml(lines.join('\n'), meta, 'section');
|
|
1221
367
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
368
|
+
static content(content, opts = {}, meta) {
|
|
369
|
+
const lines = [];
|
|
370
|
+
lines.push('# Content');
|
|
371
|
+
lines.push(`Selector: \`${content.selector}\``);
|
|
372
|
+
lines.push('');
|
|
373
|
+
if (content.text.headings?.length) {
|
|
374
|
+
lines.push('Headings:');
|
|
375
|
+
for (const h of content.text.headings)
|
|
376
|
+
lines.push(`- H${h.level}: ${truncate(h.text, opts.maxTextLength ?? 120)}`);
|
|
377
|
+
lines.push('');
|
|
378
|
+
}
|
|
379
|
+
if (content.text.paragraphs?.length) {
|
|
380
|
+
const limit =
|
|
381
|
+
typeof opts.maxElements === 'number' ? opts.maxElements : content.text.paragraphs.length;
|
|
382
|
+
lines.push('Paragraphs:');
|
|
383
|
+
for (const p of content.text.paragraphs.slice(0, limit))
|
|
384
|
+
lines.push(`- ${truncate(p, opts.maxTextLength ?? 200)}`);
|
|
385
|
+
lines.push('');
|
|
386
|
+
}
|
|
387
|
+
if (content.text.lists?.length) {
|
|
388
|
+
lines.push('Lists:');
|
|
389
|
+
for (const list of content.text.lists) {
|
|
390
|
+
lines.push(`- ${list.type.toUpperCase()}:`);
|
|
391
|
+
const limit = typeof opts.maxElements === 'number' ? opts.maxElements : list.items.length;
|
|
392
|
+
for (const item of list.items.slice(0, limit))
|
|
393
|
+
lines.push(` - ${truncate(item, opts.maxTextLength ?? 120)}`);
|
|
1227
394
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
395
|
+
lines.push('');
|
|
396
|
+
}
|
|
397
|
+
if (content.tables?.length) {
|
|
398
|
+
lines.push('Tables:');
|
|
399
|
+
for (const t of content.tables) {
|
|
400
|
+
lines.push(`- Headers: ${t.headers.join(' | ')}`);
|
|
401
|
+
const limit = typeof opts.maxElements === 'number' ? opts.maxElements : t.rows.length;
|
|
402
|
+
for (const row of t.rows.slice(0, limit)) lines.push(` - ${row.join(' | ')}`);
|
|
1230
403
|
}
|
|
404
|
+
lines.push('');
|
|
1231
405
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
406
|
+
if (content.media?.length) {
|
|
407
|
+
lines.push('Media:');
|
|
408
|
+
const limit = typeof opts.maxElements === 'number' ? opts.maxElements : content.media.length;
|
|
409
|
+
for (const m of content.media.slice(0, limit))
|
|
410
|
+
lines.push(`- ${m.type.toUpperCase()}: ${m.alt ?? ''} ${m.src ? `→ ${m.src}` : ''}`.trim());
|
|
411
|
+
lines.push('');
|
|
412
|
+
}
|
|
413
|
+
lines.push(
|
|
414
|
+
'Next: if text is insufficient for targeting, call dom_extract_region for interactive selectors.'
|
|
415
|
+
);
|
|
416
|
+
return wrapXml(lines.join('\n'), meta, 'content');
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
function capitalize(s) {
|
|
420
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1234
421
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
)
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
if (options.includeLists !== false) {
|
|
1337
|
-
const lists = element.querySelectorAll('ul, ol');
|
|
1338
|
-
result.text.lists = Array.from(lists).map((list) => ({
|
|
1339
|
-
type: list.tagName.toLowerCase(),
|
|
1340
|
-
items: Array.from(list.querySelectorAll('li')).map((li) =>
|
|
1341
|
-
this.getTextContent(li, options.maxTextLength)
|
|
1342
|
-
),
|
|
1343
|
-
}));
|
|
1344
|
-
}
|
|
1345
|
-
if (options.includeTables !== false) {
|
|
1346
|
-
const tables = element.querySelectorAll('table');
|
|
1347
|
-
result.tables = Array.from(tables).map((table) => {
|
|
1348
|
-
const headers = Array.from(table.querySelectorAll('th')).map((th) =>
|
|
1349
|
-
this.getTextContent(th)
|
|
1350
|
-
);
|
|
1351
|
-
const rows = Array.from(table.querySelectorAll('tr'))
|
|
1352
|
-
.filter((tr) => tr.querySelector('td'))
|
|
1353
|
-
.map((tr) =>
|
|
1354
|
-
Array.from(tr.querySelectorAll('td')).map((td) => this.getTextContent(td))
|
|
1355
|
-
);
|
|
1356
|
-
return { headers, rows };
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
if (options.includeMedia !== false) {
|
|
1360
|
-
const images = element.querySelectorAll('img');
|
|
1361
|
-
const videos = element.querySelectorAll('video');
|
|
1362
|
-
const audios = element.querySelectorAll('audio');
|
|
1363
|
-
result.media = [
|
|
1364
|
-
...Array.from(images).map((img) => ({
|
|
1365
|
-
type: 'img',
|
|
1366
|
-
alt: img.getAttribute('alt') || void 0,
|
|
1367
|
-
src: img.getAttribute('src') || void 0,
|
|
1368
|
-
})),
|
|
1369
|
-
...Array.from(videos).map((video) => ({
|
|
1370
|
-
type: 'video',
|
|
1371
|
-
src: video.getAttribute('src') || void 0,
|
|
1372
|
-
})),
|
|
1373
|
-
...Array.from(audios).map((audio) => ({
|
|
1374
|
-
type: 'audio',
|
|
1375
|
-
src: audio.getAttribute('src') || void 0,
|
|
1376
|
-
})),
|
|
1377
|
-
];
|
|
1378
|
-
}
|
|
1379
|
-
const allText = element.textContent || '';
|
|
1380
|
-
result.metadata.wordCount = allText.trim().split(/\s+/).length;
|
|
1381
|
-
result.metadata.hasInteractive =
|
|
1382
|
-
element.querySelectorAll('button, a, input, textarea, select').length > 0;
|
|
1383
|
-
return result;
|
|
1384
|
-
}
|
|
1385
|
-
/**
|
|
1386
|
-
* Analyze a region and extract summary information
|
|
1387
|
-
*/
|
|
1388
|
-
static analyzeRegion(element) {
|
|
1389
|
-
const selector = SelectorGenerator.generateSelectors(element).css;
|
|
1390
|
-
const buttons = element.querySelectorAll('button, [role="button"]');
|
|
1391
|
-
const links = element.querySelectorAll('a[href]');
|
|
1392
|
-
const inputs = element.querySelectorAll('input, textarea, select');
|
|
1393
|
-
const forms = element.querySelectorAll('form');
|
|
1394
|
-
const lists = element.querySelectorAll('ul, ol');
|
|
1395
|
-
const tables = element.querySelectorAll('table');
|
|
1396
|
-
const media = element.querySelectorAll('img, video, audio');
|
|
1397
|
-
const interactiveCount = buttons.length + links.length + inputs.length;
|
|
1398
|
-
let label;
|
|
1399
|
-
const ariaLabel = element.getAttribute('aria-label');
|
|
1400
|
-
if (ariaLabel) {
|
|
1401
|
-
label = ariaLabel;
|
|
1402
|
-
} else if (element.getAttribute('aria-labelledby')) {
|
|
1403
|
-
const labelId = element.getAttribute('aria-labelledby');
|
|
1404
|
-
if (labelId) {
|
|
1405
|
-
const labelElement = element.ownerDocument?.getElementById(labelId);
|
|
1406
|
-
if (labelElement) {
|
|
1407
|
-
label = labelElement.textContent?.trim();
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
} else {
|
|
1411
|
-
const heading = element.querySelector('h1, h2, h3');
|
|
1412
|
-
if (heading) {
|
|
1413
|
-
label = heading.textContent?.trim();
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
const textContent = element.textContent?.trim() || '';
|
|
1417
|
-
const textPreview =
|
|
1418
|
-
textContent.length > 50 ? `${textContent.substring(0, 50)}...` : textContent;
|
|
1419
|
-
return {
|
|
1420
|
-
selector,
|
|
1421
|
-
label,
|
|
1422
|
-
role: element.getAttribute('role') || void 0,
|
|
1423
|
-
interactiveCount,
|
|
1424
|
-
hasForm: forms.length > 0,
|
|
1425
|
-
hasList: lists.length > 0,
|
|
1426
|
-
hasTable: tables.length > 0,
|
|
1427
|
-
hasMedia: media.length > 0,
|
|
1428
|
-
buttonCount: buttons.length > 0 ? buttons.length : void 0,
|
|
1429
|
-
linkCount: links.length > 0 ? links.length : void 0,
|
|
1430
|
-
inputCount: inputs.length > 0 ? inputs.length : void 0,
|
|
1431
|
-
textPreview: textPreview.length > 0 ? textPreview : void 0,
|
|
1432
|
-
};
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/selectors.ts
|
|
424
|
+
var SelectorGenerator = class SelectorGenerator {
|
|
425
|
+
/**
|
|
426
|
+
* Generate multiple selector strategies for an element
|
|
427
|
+
*/
|
|
428
|
+
static generateSelectors(element) {
|
|
429
|
+
const doc = element.ownerDocument || document;
|
|
430
|
+
const candidates = [];
|
|
431
|
+
if (element.id && SelectorGenerator.isUniqueId(element.id, doc))
|
|
432
|
+
candidates.push({
|
|
433
|
+
type: 'id',
|
|
434
|
+
value: `#${CSS.escape(element.id)}`,
|
|
435
|
+
score: 100,
|
|
436
|
+
});
|
|
437
|
+
const testId = SelectorGenerator.getDataTestId(element);
|
|
438
|
+
if (testId) {
|
|
439
|
+
const v = `[${testId.name}="${CSS.escape(testId.value)}"]`;
|
|
440
|
+
candidates.push({
|
|
441
|
+
type: 'data-testid',
|
|
442
|
+
value: v,
|
|
443
|
+
score: 90 + (SelectorGenerator.isUniqueSelector(v, doc) ? 5 : 0),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
const role = element.getAttribute('role');
|
|
447
|
+
const aria = element.getAttribute('aria-label');
|
|
448
|
+
if (role && aria) {
|
|
449
|
+
const v = `[role="${CSS.escape(role)}"][aria-label="${CSS.escape(aria)}"]`;
|
|
450
|
+
candidates.push({
|
|
451
|
+
type: 'role-aria',
|
|
452
|
+
value: v,
|
|
453
|
+
score: 85 + (SelectorGenerator.isUniqueSelector(v, doc) ? 5 : 0),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
const nameAttr = element.getAttribute('name');
|
|
457
|
+
if (nameAttr) {
|
|
458
|
+
const v = `[name="${CSS.escape(nameAttr)}"]`;
|
|
459
|
+
candidates.push({
|
|
460
|
+
type: 'name',
|
|
461
|
+
value: v,
|
|
462
|
+
score: 78 + (SelectorGenerator.isUniqueSelector(v, doc) ? 5 : 0),
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
const pathCss = SelectorGenerator.generateCSSSelector(element, doc);
|
|
466
|
+
const structuralPenalty = (pathCss.match(/:nth-child\(/g) || []).length * 10;
|
|
467
|
+
const classBonus = pathCss.includes('.') ? 8 : 0;
|
|
468
|
+
const pathScore = Math.max(0, 70 + classBonus - structuralPenalty);
|
|
469
|
+
candidates.push({
|
|
470
|
+
type: 'class-path',
|
|
471
|
+
value: pathCss,
|
|
472
|
+
score: pathScore,
|
|
473
|
+
});
|
|
474
|
+
const xpath = SelectorGenerator.generateXPath(element, doc);
|
|
475
|
+
candidates.push({
|
|
476
|
+
type: 'xpath',
|
|
477
|
+
value: xpath,
|
|
478
|
+
score: 40,
|
|
479
|
+
});
|
|
480
|
+
const textBased = SelectorGenerator.generateTextBasedSelector(element);
|
|
481
|
+
if (textBased)
|
|
482
|
+
candidates.push({
|
|
483
|
+
type: 'text',
|
|
484
|
+
value: textBased,
|
|
485
|
+
score: 30,
|
|
486
|
+
});
|
|
487
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
488
|
+
const selector = {
|
|
489
|
+
css:
|
|
490
|
+
candidates.find(
|
|
491
|
+
(c) =>
|
|
492
|
+
c.type !== 'xpath' &&
|
|
493
|
+
c.type !== 'text' &&
|
|
494
|
+
SelectorGenerator.isUniqueSelector(c.value, doc, element)
|
|
495
|
+
)?.value || pathCss,
|
|
496
|
+
xpath,
|
|
497
|
+
candidates,
|
|
498
|
+
};
|
|
499
|
+
if (textBased) selector.textBased = textBased;
|
|
500
|
+
if (testId) selector.dataTestId = testId.value;
|
|
501
|
+
if (aria) selector.ariaLabel = aria;
|
|
502
|
+
return selector;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Generate a unique CSS selector for an element
|
|
506
|
+
*/
|
|
507
|
+
static generateCSSSelector(element, doc) {
|
|
508
|
+
if (element.id && SelectorGenerator.isUniqueId(element.id, doc))
|
|
509
|
+
return `#${CSS.escape(element.id)}`;
|
|
510
|
+
const testId = SelectorGenerator.getDataTestId(element);
|
|
511
|
+
if (testId) {
|
|
512
|
+
const selector = `[${testId.name}="${CSS.escape(testId.value)}"]`;
|
|
513
|
+
if (SelectorGenerator.isUniqueSelector(selector, doc, element)) return selector;
|
|
514
|
+
}
|
|
515
|
+
const path = [];
|
|
516
|
+
let current = element;
|
|
517
|
+
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
518
|
+
let selector = current.nodeName.toLowerCase();
|
|
519
|
+
if (current.id && SelectorGenerator.isUniqueId(current.id, doc)) {
|
|
520
|
+
selector = `#${CSS.escape(current.id)}`;
|
|
521
|
+
path.unshift(selector);
|
|
522
|
+
break;
|
|
1433
523
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
const selector = SelectorGenerator.generateSelectors(form).css;
|
|
1442
|
-
let location = 'unknown';
|
|
1443
|
-
if (form.closest('header, [role="banner"]')) {
|
|
1444
|
-
location = 'header';
|
|
1445
|
-
} else if (form.closest('nav, [role="navigation"]')) {
|
|
1446
|
-
location = 'navigation';
|
|
1447
|
-
} else if (form.closest('main, [role="main"]')) {
|
|
1448
|
-
location = 'main';
|
|
1449
|
-
} else if (form.closest('aside, [role="complementary"]')) {
|
|
1450
|
-
location = 'sidebar';
|
|
1451
|
-
} else if (form.closest('footer, [role="contentinfo"]')) {
|
|
1452
|
-
location = 'footer';
|
|
1453
|
-
}
|
|
1454
|
-
let purpose;
|
|
1455
|
-
const formId = form.getAttribute('id')?.toLowerCase();
|
|
1456
|
-
const formClass = form.getAttribute('class')?.toLowerCase();
|
|
1457
|
-
const formAction = form.getAttribute('action')?.toLowerCase();
|
|
1458
|
-
const hasEmail = form.querySelector('input[type="email"]');
|
|
1459
|
-
const hasPassword = form.querySelector('input[type="password"]');
|
|
1460
|
-
const hasSearch = form.querySelector('input[type="search"]');
|
|
1461
|
-
if (hasSearch || formId?.includes('search') || formClass?.includes('search')) {
|
|
1462
|
-
purpose = 'search';
|
|
1463
|
-
} else if (hasPassword && hasEmail) {
|
|
1464
|
-
purpose = 'login';
|
|
1465
|
-
} else if (hasPassword) {
|
|
1466
|
-
purpose = 'authentication';
|
|
1467
|
-
} else if (formId?.includes('contact') || formClass?.includes('contact')) {
|
|
1468
|
-
purpose = 'contact';
|
|
1469
|
-
} else if (formId?.includes('subscribe') || formClass?.includes('subscribe')) {
|
|
1470
|
-
purpose = 'subscription';
|
|
1471
|
-
} else if (formAction?.includes('checkout') || formClass?.includes('checkout')) {
|
|
1472
|
-
purpose = 'checkout';
|
|
1473
|
-
}
|
|
1474
|
-
return {
|
|
1475
|
-
selector,
|
|
1476
|
-
location,
|
|
1477
|
-
inputCount: inputs.length,
|
|
1478
|
-
purpose,
|
|
1479
|
-
};
|
|
1480
|
-
});
|
|
524
|
+
const classes = SelectorGenerator.getMeaningfulClasses(current);
|
|
525
|
+
if (classes.length > 0) selector += `.${classes.map((c) => CSS.escape(c)).join('.')}`;
|
|
526
|
+
const siblings = current.parentElement?.children;
|
|
527
|
+
if (siblings && siblings.length > 1) {
|
|
528
|
+
const index = Array.from(siblings).indexOf(current);
|
|
529
|
+
if (index > 0 || !SelectorGenerator.isUniqueSelector(selector, current.parentElement))
|
|
530
|
+
selector += `:nth-child(${index + 1})`;
|
|
1481
531
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
totalForms: forms.length,
|
|
1503
|
-
totalSections: allSections.length,
|
|
1504
|
-
hasModals,
|
|
1505
|
-
hasErrors,
|
|
1506
|
-
isLoading,
|
|
1507
|
-
mainContentSelector,
|
|
1508
|
-
};
|
|
532
|
+
path.unshift(selector);
|
|
533
|
+
current = current.parentElement;
|
|
534
|
+
}
|
|
535
|
+
return SelectorGenerator.optimizePath(path, element, doc);
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Generate XPath for an element
|
|
539
|
+
*/
|
|
540
|
+
static generateXPath(element, doc) {
|
|
541
|
+
const path = [];
|
|
542
|
+
let current = element;
|
|
543
|
+
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
544
|
+
const tagName = current.nodeName.toLowerCase();
|
|
545
|
+
if (
|
|
546
|
+
current.id &&
|
|
547
|
+
!current.id.includes('"') &&
|
|
548
|
+
SelectorGenerator.isUniqueId(current.id, doc)
|
|
549
|
+
) {
|
|
550
|
+
path.unshift(`*[@id="${current.id}"]`);
|
|
551
|
+
break;
|
|
1509
552
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
suggestions.push('Page appears to be loading - wait or check loading state');
|
|
1520
|
-
}
|
|
1521
|
-
if (summary.hasModals) {
|
|
1522
|
-
suggestions.push('Modal/dialog is open - may need to interact with or close it first');
|
|
1523
|
-
}
|
|
1524
|
-
if (regions.main && regions.main.interactiveCount > 10) {
|
|
1525
|
-
suggestions.push(
|
|
1526
|
-
`Main content has ${regions.main.interactiveCount} interactive elements - consider filtering`
|
|
1527
|
-
);
|
|
1528
|
-
}
|
|
1529
|
-
if (summary.totalForms > 0) {
|
|
1530
|
-
suggestions.push(`Found ${summary.totalForms} form(s) on the page`);
|
|
1531
|
-
}
|
|
1532
|
-
if (!regions.main) {
|
|
1533
|
-
suggestions.push('No clear main content area detected - may need to explore regions');
|
|
553
|
+
let xpath = tagName;
|
|
554
|
+
const siblings = current.parentElement?.children;
|
|
555
|
+
if (siblings) {
|
|
556
|
+
const sameTagSiblings = Array.from(siblings).filter(
|
|
557
|
+
(s) => s.nodeName.toLowerCase() === tagName
|
|
558
|
+
);
|
|
559
|
+
if (sameTagSiblings.length > 1) {
|
|
560
|
+
const index = sameTagSiblings.indexOf(current) + 1;
|
|
561
|
+
xpath += `[${index}]`;
|
|
1534
562
|
}
|
|
1535
|
-
return suggestions;
|
|
1536
563
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
564
|
+
path.unshift(xpath);
|
|
565
|
+
current = current.parentElement;
|
|
566
|
+
}
|
|
567
|
+
return `//${path.join('/')}`;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Generate a text-based selector for buttons and links
|
|
571
|
+
*/
|
|
572
|
+
static generateTextBasedSelector(element) {
|
|
573
|
+
const text = element.textContent?.trim();
|
|
574
|
+
if (!text || text.length > 50) return void 0;
|
|
575
|
+
const tag = element.nodeName.toLowerCase();
|
|
576
|
+
if (['button', 'a', 'label'].includes(tag))
|
|
577
|
+
return `${tag}:contains("${text.replace(/['"\\]/g, '\\$&')}")`;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Get data-testid or similar attributes
|
|
581
|
+
*/
|
|
582
|
+
static getDataTestId(element) {
|
|
583
|
+
for (const name of ['data-testid', 'data-test-id', 'data-test', 'data-cy']) {
|
|
584
|
+
const attribute = element.getAttributeNode(name);
|
|
585
|
+
if (attribute?.value) return attribute;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Check if an ID is unique in the document
|
|
590
|
+
*/
|
|
591
|
+
static isUniqueId(id, doc) {
|
|
592
|
+
return doc.querySelectorAll(`#${CSS.escape(id)}`).length === 1;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Check if a selector is unique within a container
|
|
596
|
+
*/
|
|
597
|
+
static isUniqueSelector(selector, container, element) {
|
|
598
|
+
try {
|
|
599
|
+
const matches = container.querySelectorAll(selector);
|
|
600
|
+
return matches.length === 1 && (element === void 0 || matches[0] === element);
|
|
601
|
+
} catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Get meaningful classes (filtering out utility classes)
|
|
607
|
+
*/
|
|
608
|
+
static getMeaningfulClasses(element) {
|
|
609
|
+
const classes = Array.from(element.classList);
|
|
610
|
+
const utilityPatterns = [
|
|
611
|
+
/^(p|m|w|h|text|bg|border|flex|grid|col|row)-/,
|
|
612
|
+
/^(xs|sm|md|lg|xl|2xl):/,
|
|
613
|
+
/^(hover|focus|active|disabled|checked):/,
|
|
614
|
+
/^js-/,
|
|
615
|
+
/^is-/,
|
|
616
|
+
/^has-/,
|
|
617
|
+
];
|
|
618
|
+
return classes
|
|
619
|
+
.filter((cls) => {
|
|
620
|
+
if (cls.length < 3) return false;
|
|
621
|
+
return !utilityPatterns.some((pattern) => pattern.test(cls));
|
|
622
|
+
})
|
|
623
|
+
.slice(0, 2);
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Optimize the selector path by removing unnecessary parts
|
|
627
|
+
*/
|
|
628
|
+
static optimizePath(path, element, doc) {
|
|
629
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
630
|
+
const shortPath = path.slice(i).join(' > ');
|
|
631
|
+
try {
|
|
632
|
+
const matches = doc.querySelectorAll(shortPath);
|
|
633
|
+
if (matches.length === 1 && matches[0] === element) return shortPath;
|
|
634
|
+
} catch {}
|
|
635
|
+
}
|
|
636
|
+
return path.join(' > ');
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Get a human-readable path description
|
|
640
|
+
*/
|
|
641
|
+
static getContextPath(element) {
|
|
642
|
+
const path = [];
|
|
643
|
+
let current = element;
|
|
644
|
+
let depth = 0;
|
|
645
|
+
const maxDepth = 5;
|
|
646
|
+
while (current && current !== element.ownerDocument?.body && depth < maxDepth) {
|
|
647
|
+
const tag = current.nodeName.toLowerCase();
|
|
648
|
+
let descriptor = tag;
|
|
649
|
+
if (current.id) descriptor = `${tag}#${current.id}`;
|
|
650
|
+
else if (current.className && typeof current.className === 'string') {
|
|
651
|
+
const firstClass = current.className.split(' ')[0];
|
|
652
|
+
if (firstClass) descriptor = `${tag}.${firstClass}`;
|
|
1546
653
|
}
|
|
654
|
+
const role = current.getAttribute('role');
|
|
655
|
+
if (role) descriptor += `[role="${role}"]`;
|
|
656
|
+
path.unshift(descriptor);
|
|
657
|
+
current = current.parentElement;
|
|
658
|
+
depth++;
|
|
659
|
+
}
|
|
660
|
+
return path;
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/traversal.ts
|
|
665
|
+
var DOMTraversal = class DOMTraversal {
|
|
666
|
+
/**
|
|
667
|
+
* Check if a node is a Document.
|
|
668
|
+
*
|
|
669
|
+
* `instanceof Document` tests the *calling* realm's constructor, so a
|
|
670
|
+
* document reached through an iframe (frameSelector) always fails it.
|
|
671
|
+
* nodeType is realm-independent — use this everywhere instead.
|
|
672
|
+
*/
|
|
673
|
+
static isDocument(node) {
|
|
674
|
+
return node.nodeType === Node.DOCUMENT_NODE;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Check if element is visible
|
|
678
|
+
*/
|
|
679
|
+
static isVisible(element) {
|
|
680
|
+
const rect = element.getBoundingClientRect();
|
|
681
|
+
const style = element.ownerDocument?.defaultView?.getComputedStyle(element);
|
|
682
|
+
if (!style) return false;
|
|
683
|
+
return !!(
|
|
684
|
+
rect.width > 0 &&
|
|
685
|
+
rect.height > 0 &&
|
|
686
|
+
style.display !== 'none' &&
|
|
687
|
+
style.visibility !== 'hidden' &&
|
|
688
|
+
style.opacity !== '0'
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Check if element is in viewport
|
|
693
|
+
*/
|
|
694
|
+
static isInViewport(element) {
|
|
695
|
+
const rect = element.getBoundingClientRect();
|
|
696
|
+
const view = {
|
|
697
|
+
width: element.ownerDocument?.defaultView?.innerWidth || 0,
|
|
698
|
+
height: element.ownerDocument?.defaultView?.innerHeight || 0,
|
|
1547
699
|
};
|
|
1548
|
-
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
* @param rootElement The document or element to extract from
|
|
1599
|
-
* @param runtimeOptions Options to override constructor options
|
|
1600
|
-
*/
|
|
1601
|
-
extract(rootElement, runtimeOptions) {
|
|
1602
|
-
const startTime = Date.now();
|
|
1603
|
-
const doc = rootElement instanceof Document ? rootElement : rootElement.ownerDocument;
|
|
1604
|
-
const options = { ...this.options, ...runtimeOptions };
|
|
1605
|
-
let container = rootElement instanceof Document ? doc : rootElement;
|
|
1606
|
-
if (options.mainContentOnly && rootElement instanceof Document) {
|
|
1607
|
-
container = ContentDetection.findMainContent(doc);
|
|
1608
|
-
}
|
|
1609
|
-
const pageState = this.extractPageState(doc);
|
|
1610
|
-
const landmarks = this.extractLandmarks(doc);
|
|
1611
|
-
const interactive = this.extractInteractiveElements(container, options);
|
|
1612
|
-
const result = {
|
|
1613
|
-
mode: options.mode,
|
|
1614
|
-
timestamp: startTime,
|
|
1615
|
-
page: pageState,
|
|
1616
|
-
landmarks,
|
|
1617
|
-
interactive,
|
|
1618
|
-
};
|
|
1619
|
-
if (options.mode === 'full') {
|
|
1620
|
-
result.semantic = this.extractSemanticElements(container, options);
|
|
1621
|
-
result.metadata = this.extractMetadata(doc, container, options);
|
|
1622
|
-
}
|
|
1623
|
-
return result;
|
|
1624
|
-
}
|
|
1625
|
-
/**
|
|
1626
|
-
* Extract page state information
|
|
1627
|
-
*/
|
|
1628
|
-
extractPageState(doc) {
|
|
1629
|
-
return {
|
|
1630
|
-
url: doc.location?.href || '',
|
|
1631
|
-
title: doc.title || '',
|
|
1632
|
-
hasErrors: this.detectErrors(doc),
|
|
1633
|
-
isLoading: this.detectLoading(doc),
|
|
1634
|
-
hasModals: this.detectModals(doc),
|
|
1635
|
-
hasFocus: this.getFocusedElement(doc),
|
|
1636
|
-
};
|
|
1637
|
-
}
|
|
1638
|
-
/**
|
|
1639
|
-
* Extract page landmarks
|
|
1640
|
-
*/
|
|
1641
|
-
extractLandmarks(doc) {
|
|
1642
|
-
const detected = ContentDetection.detectLandmarks(doc);
|
|
1643
|
-
return {
|
|
1644
|
-
navigation: this.elementsToSelectors(detected.navigation || []),
|
|
1645
|
-
main: this.elementsToSelectors(detected.main || []),
|
|
1646
|
-
forms: this.elementsToSelectors(detected.form || []),
|
|
1647
|
-
headers: this.elementsToSelectors(detected.banner || []),
|
|
1648
|
-
footers: this.elementsToSelectors(detected.contentinfo || []),
|
|
1649
|
-
articles: this.elementsToSelectors(detected.region || []),
|
|
1650
|
-
sections: this.elementsToSelectors(detected.region || []),
|
|
1651
|
-
};
|
|
1652
|
-
}
|
|
1653
|
-
/**
|
|
1654
|
-
* Convert elements to selector strings
|
|
1655
|
-
*/
|
|
1656
|
-
elementsToSelectors(elements) {
|
|
1657
|
-
return elements.map((el) => SelectorGenerator.generateSelectors(el).css);
|
|
1658
|
-
}
|
|
1659
|
-
/**
|
|
1660
|
-
* Extract interactive elements
|
|
1661
|
-
*/
|
|
1662
|
-
extractInteractiveElements(container, options) {
|
|
1663
|
-
const buttons = [];
|
|
1664
|
-
const links = [];
|
|
1665
|
-
const inputs = [];
|
|
1666
|
-
const clickable = [];
|
|
1667
|
-
const buttonElements = container.querySelectorAll(
|
|
1668
|
-
'button, [role="button"], input[type="button"], input[type="submit"]'
|
|
1669
|
-
);
|
|
1670
|
-
buttonElements.forEach((el) => {
|
|
1671
|
-
if (this.shouldIncludeElement(el, options)) {
|
|
1672
|
-
const extracted = DOMTraversal.extractElement(el, options);
|
|
1673
|
-
if (extracted) buttons.push(extracted);
|
|
1674
|
-
}
|
|
1675
|
-
});
|
|
1676
|
-
const linkElements = container.querySelectorAll('a[href]');
|
|
1677
|
-
linkElements.forEach((el) => {
|
|
1678
|
-
if (this.shouldIncludeElement(el, options)) {
|
|
1679
|
-
const extracted = DOMTraversal.extractElement(el, options);
|
|
1680
|
-
if (extracted) links.push(extracted);
|
|
1681
|
-
}
|
|
1682
|
-
});
|
|
1683
|
-
const inputElements = container.querySelectorAll(
|
|
1684
|
-
'input:not([type="button"]):not([type="submit"]), textarea, select'
|
|
1685
|
-
);
|
|
1686
|
-
inputElements.forEach((el) => {
|
|
1687
|
-
if (this.shouldIncludeElement(el, options)) {
|
|
1688
|
-
const extracted = DOMTraversal.extractElement(el, options);
|
|
1689
|
-
if (extracted) inputs.push(extracted);
|
|
1690
|
-
}
|
|
1691
|
-
});
|
|
1692
|
-
if (options.customSelectors) {
|
|
1693
|
-
options.customSelectors.forEach((selector) => {
|
|
1694
|
-
const elements = container.querySelectorAll(selector);
|
|
1695
|
-
elements.forEach((el) => {
|
|
1696
|
-
if (this.shouldIncludeElement(el, options)) {
|
|
1697
|
-
const extracted = DOMTraversal.extractElement(el, options);
|
|
1698
|
-
if (extracted) clickable.push(extracted);
|
|
1699
|
-
}
|
|
1700
|
-
});
|
|
1701
|
-
});
|
|
700
|
+
return rect.top < view.height && rect.bottom > 0 && rect.left < view.width && rect.right > 0;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Check if element passes filter criteria
|
|
704
|
+
*/
|
|
705
|
+
static passesFilter(element, filter) {
|
|
706
|
+
if (!filter) return true;
|
|
707
|
+
if (filter.excludeSelectors?.length) {
|
|
708
|
+
for (const selector of filter.excludeSelectors) if (element.matches(selector)) return false;
|
|
709
|
+
}
|
|
710
|
+
if (filter.includeSelectors?.length) {
|
|
711
|
+
let matches = false;
|
|
712
|
+
for (const selector of filter.includeSelectors)
|
|
713
|
+
if (element.matches(selector)) {
|
|
714
|
+
matches = true;
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
if (!matches) return false;
|
|
718
|
+
}
|
|
719
|
+
if (filter.tags?.length && !filter.tags.includes(element.tagName.toLowerCase())) return false;
|
|
720
|
+
const textContent = element.textContent?.toLowerCase() || '';
|
|
721
|
+
if (filter.textContains?.length) {
|
|
722
|
+
let hasText = false;
|
|
723
|
+
for (const text of filter.textContains)
|
|
724
|
+
if (textContent.includes(text.toLowerCase())) {
|
|
725
|
+
hasText = true;
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
if (!hasText) return false;
|
|
729
|
+
}
|
|
730
|
+
if (filter.textMatches?.length) {
|
|
731
|
+
let matches = false;
|
|
732
|
+
for (const pattern of filter.textMatches)
|
|
733
|
+
if (pattern.test(textContent)) {
|
|
734
|
+
matches = true;
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
if (!matches) return false;
|
|
738
|
+
}
|
|
739
|
+
if (filter.hasAttributes?.length) {
|
|
740
|
+
for (const attr of filter.hasAttributes) if (!element.hasAttribute(attr)) return false;
|
|
741
|
+
}
|
|
742
|
+
if (filter.attributeValues)
|
|
743
|
+
for (const [attr, value] of Object.entries(filter.attributeValues)) {
|
|
744
|
+
const attrValue = element.getAttribute(attr);
|
|
745
|
+
if (!attrValue) return false;
|
|
746
|
+
if (typeof value === 'string') {
|
|
747
|
+
if (attrValue !== value) return false;
|
|
748
|
+
} else if (value instanceof RegExp) {
|
|
749
|
+
if (!value.test(attrValue)) return false;
|
|
1702
750
|
}
|
|
1703
|
-
const forms = this.extractForms(container, options);
|
|
1704
|
-
return {
|
|
1705
|
-
buttons,
|
|
1706
|
-
links,
|
|
1707
|
-
inputs,
|
|
1708
|
-
forms,
|
|
1709
|
-
clickable,
|
|
1710
|
-
};
|
|
1711
751
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
752
|
+
if (filter.withinSelectors?.length) {
|
|
753
|
+
let isWithin = false;
|
|
754
|
+
for (const selector of filter.withinSelectors)
|
|
755
|
+
if (element.closest(selector)) {
|
|
756
|
+
isWithin = true;
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
if (!isWithin) return false;
|
|
760
|
+
}
|
|
761
|
+
if (filter.interactionTypes?.length) {
|
|
762
|
+
const interaction = DOMTraversal.getInteractionInfo(element);
|
|
763
|
+
let hasInteraction = false;
|
|
764
|
+
for (const type of filter.interactionTypes)
|
|
765
|
+
if (interaction[type]) {
|
|
766
|
+
hasInteraction = true;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
if (!hasInteraction) return false;
|
|
770
|
+
}
|
|
771
|
+
if (filter.nearText) {
|
|
772
|
+
const parent = element.parentElement;
|
|
773
|
+
if (!parent || !parent.textContent?.toLowerCase().includes(filter.nearText.toLowerCase()))
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Extract element information
|
|
780
|
+
*/
|
|
781
|
+
static extractElement(element, options, depth = 0) {
|
|
782
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) return null;
|
|
783
|
+
if (!options.includeHidden && !DOMTraversal.isVisible(element)) return null;
|
|
784
|
+
if (options.viewportOnly && !DOMTraversal.isInViewport(element)) return null;
|
|
785
|
+
if (!DOMTraversal.passesFilter(element, options.filter)) return null;
|
|
786
|
+
const extracted = {
|
|
787
|
+
tag: element.tagName.toLowerCase(),
|
|
788
|
+
text: DOMTraversal.getElementText(element, options),
|
|
789
|
+
selector: SelectorGenerator.generateSelectors(element),
|
|
790
|
+
attributes: DOMTraversal.getRelevantAttributes(element, options),
|
|
791
|
+
context: DOMTraversal.getElementContext(element),
|
|
792
|
+
interaction: DOMTraversal.getInteractionInfo(element),
|
|
793
|
+
};
|
|
794
|
+
if (options.mode === 'full' && DOMTraversal.isSemanticContainer(element)) {
|
|
795
|
+
const children = [];
|
|
796
|
+
if (options.includeShadowDOM && element.shadowRoot) {
|
|
797
|
+
const shadowChildren = DOMTraversal.extractChildren(element.shadowRoot, options, depth + 1);
|
|
798
|
+
children.push(...shadowChildren);
|
|
1745
799
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
800
|
+
const regularChildren = DOMTraversal.extractChildren(element, options, depth + 1);
|
|
801
|
+
children.push(...regularChildren);
|
|
802
|
+
if (children.length > 0) extracted.children = children;
|
|
803
|
+
}
|
|
804
|
+
return extracted;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Extract children elements
|
|
808
|
+
*/
|
|
809
|
+
static extractChildren(container, options, depth) {
|
|
810
|
+
const children = [];
|
|
811
|
+
for (const child of Array.from(container.children)) {
|
|
812
|
+
const extracted = DOMTraversal.extractElement(child, options, depth);
|
|
813
|
+
if (extracted) children.push(extracted);
|
|
814
|
+
}
|
|
815
|
+
return children;
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Get relevant attributes for an element
|
|
819
|
+
*/
|
|
820
|
+
static getRelevantAttributes(element, options) {
|
|
821
|
+
const relevant = [
|
|
822
|
+
'id',
|
|
823
|
+
'class',
|
|
824
|
+
'name',
|
|
825
|
+
'type',
|
|
826
|
+
'value',
|
|
827
|
+
'placeholder',
|
|
828
|
+
'href',
|
|
829
|
+
'src',
|
|
830
|
+
'alt',
|
|
831
|
+
'title',
|
|
832
|
+
'action',
|
|
833
|
+
'method',
|
|
834
|
+
'aria-label',
|
|
835
|
+
'aria-describedby',
|
|
836
|
+
'aria-controls',
|
|
837
|
+
'role',
|
|
838
|
+
'disabled',
|
|
839
|
+
'readonly',
|
|
840
|
+
'required',
|
|
841
|
+
'checked',
|
|
842
|
+
'min',
|
|
843
|
+
'max',
|
|
844
|
+
'pattern',
|
|
845
|
+
'step',
|
|
846
|
+
'autocomplete',
|
|
847
|
+
'data-testid',
|
|
848
|
+
'data-test',
|
|
849
|
+
'data-cy',
|
|
850
|
+
];
|
|
851
|
+
const attributes = {};
|
|
852
|
+
const attrTruncate = options.attributeTruncateLength ?? 100;
|
|
853
|
+
const dataAttrTruncate = options.dataAttributeTruncateLength ?? 50;
|
|
854
|
+
for (const attr of relevant) {
|
|
855
|
+
const value = element.getAttribute(attr);
|
|
856
|
+
if (value)
|
|
857
|
+
attributes[attr] =
|
|
858
|
+
value.length > attrTruncate ? `${value.substring(0, attrTruncate)}...` : value;
|
|
859
|
+
}
|
|
860
|
+
for (const attr of element.attributes)
|
|
861
|
+
if (attr.name.startsWith('data-') && !relevant.includes(attr.name))
|
|
862
|
+
attributes[attr.name] =
|
|
863
|
+
attr.value.length > dataAttrTruncate
|
|
864
|
+
? `${attr.value.substring(0, dataAttrTruncate)}...`
|
|
865
|
+
: attr.value;
|
|
866
|
+
return attributes;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Get element context information
|
|
870
|
+
*/
|
|
871
|
+
static getElementContext(element) {
|
|
872
|
+
const context = { parentChain: SelectorGenerator.getContextPath(element) };
|
|
873
|
+
const form = element.closest('form');
|
|
874
|
+
if (form) context.nearestForm = SelectorGenerator.generateSelectors(form).css;
|
|
875
|
+
const section = element.closest('section, [role="region"]');
|
|
876
|
+
if (section) context.nearestSection = SelectorGenerator.generateSelectors(section).css;
|
|
877
|
+
const main = element.closest('main, [role="main"]');
|
|
878
|
+
if (main) context.nearestMain = SelectorGenerator.generateSelectors(main).css;
|
|
879
|
+
const nav = element.closest('nav, [role="navigation"]');
|
|
880
|
+
if (nav) context.nearestNav = SelectorGenerator.generateSelectors(nav).css;
|
|
881
|
+
return context;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Get interaction information for an element (compact format)
|
|
885
|
+
*/
|
|
886
|
+
static getInteractionInfo(element) {
|
|
887
|
+
const htmlElement = element;
|
|
888
|
+
const interaction = {};
|
|
889
|
+
if (
|
|
890
|
+
htmlElement.onclick ||
|
|
891
|
+
element.getAttribute('onclick') ||
|
|
892
|
+
element.matches('button, a[href], [role="button"], [tabindex]:not([tabindex="-1"])')
|
|
893
|
+
)
|
|
894
|
+
interaction.click = true;
|
|
895
|
+
if (
|
|
896
|
+
htmlElement.onchange ||
|
|
897
|
+
element.getAttribute('onchange') ||
|
|
898
|
+
element.matches('input, select, textarea')
|
|
899
|
+
)
|
|
900
|
+
interaction.change = true;
|
|
901
|
+
if (htmlElement.onsubmit || element.getAttribute('onsubmit') || element.matches('form'))
|
|
902
|
+
interaction.submit = true;
|
|
903
|
+
if (element.matches('a[href], button[type="submit"]')) interaction.nav = true;
|
|
904
|
+
if (element.hasAttribute('disabled') || element.getAttribute('aria-disabled') === 'true')
|
|
905
|
+
interaction.disabled = true;
|
|
906
|
+
if (!DOMTraversal.isVisible(element)) interaction.hidden = true;
|
|
907
|
+
const ariaRole = element.getAttribute('role');
|
|
908
|
+
if (ariaRole) interaction.role = ariaRole;
|
|
909
|
+
if (element.matches('input, textarea, select, button')) {
|
|
910
|
+
const form = element.form || element.closest('form');
|
|
911
|
+
if (form) interaction.form = SelectorGenerator.generateSelectors(form).css;
|
|
912
|
+
}
|
|
913
|
+
return interaction;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Get text content of an element (limited length)
|
|
917
|
+
*/
|
|
918
|
+
static getElementText(element, options) {
|
|
919
|
+
if (element.matches('input, textarea')) {
|
|
920
|
+
const input = element;
|
|
921
|
+
return input.value || input.placeholder || '';
|
|
922
|
+
}
|
|
923
|
+
if (element.matches('img')) return element.alt || '';
|
|
924
|
+
const text = element.textContent?.trim() || '';
|
|
925
|
+
const maxLength = options?.textTruncateLength;
|
|
926
|
+
if (maxLength && text.length > maxLength) return `${text.substring(0, maxLength)}...`;
|
|
927
|
+
return text;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Check if element is a semantic container
|
|
931
|
+
*/
|
|
932
|
+
static isSemanticContainer(element) {
|
|
933
|
+
return element.matches(
|
|
934
|
+
'article, section, nav, aside, main, header, footer, form, table, ul, ol, dl, figure, details, dialog, [role="region"], [role="navigation"], [role="main"], [role="complementary"]'
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
//#endregion
|
|
939
|
+
//#region src/smart-dom-reader.ts
|
|
940
|
+
/**
|
|
941
|
+
* Smart DOM Reader - Full Extraction Approach
|
|
942
|
+
*
|
|
943
|
+
* This class provides complete DOM extraction in a single pass.
|
|
944
|
+
* Use this when you need all information upfront and have sufficient
|
|
945
|
+
* token budget for processing the complete output.
|
|
946
|
+
*
|
|
947
|
+
* Features:
|
|
948
|
+
* - Single-pass extraction of all elements
|
|
949
|
+
* - Two modes: 'interactive' (UI elements) or 'full' (includes content)
|
|
950
|
+
* - Efficient for automation and testing scenarios
|
|
951
|
+
* - Returns complete structured data immediately
|
|
952
|
+
*/
|
|
953
|
+
var SmartDOMReader = class SmartDOMReader {
|
|
954
|
+
options;
|
|
955
|
+
constructor(options = {}) {
|
|
956
|
+
this.options = {
|
|
957
|
+
mode: options.mode || 'interactive',
|
|
958
|
+
maxDepth: options.maxDepth ?? 5,
|
|
959
|
+
includeHidden: options.includeHidden || false,
|
|
960
|
+
includeShadowDOM: options.includeShadowDOM ?? true,
|
|
961
|
+
includeIframes: options.includeIframes || false,
|
|
962
|
+
viewportOnly: options.viewportOnly || false,
|
|
963
|
+
mainContentOnly: options.mainContentOnly || false,
|
|
964
|
+
customSelectors: options.customSelectors || [],
|
|
965
|
+
...(options.attributeTruncateLength !== void 0 && {
|
|
966
|
+
attributeTruncateLength: options.attributeTruncateLength,
|
|
967
|
+
}),
|
|
968
|
+
...(options.dataAttributeTruncateLength !== void 0 && {
|
|
969
|
+
dataAttributeTruncateLength: options.dataAttributeTruncateLength,
|
|
970
|
+
}),
|
|
971
|
+
...(options.textTruncateLength !== void 0 && {
|
|
972
|
+
textTruncateLength: options.textTruncateLength,
|
|
973
|
+
}),
|
|
974
|
+
...(options.filter !== void 0 && { filter: options.filter }),
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Main extraction method - extracts all data in one pass
|
|
979
|
+
* @param rootElement The document or element to extract from
|
|
980
|
+
* @param runtimeOptions Options to override constructor options
|
|
981
|
+
*/
|
|
982
|
+
extract(rootElement = document, runtimeOptions) {
|
|
983
|
+
const startTime = Date.now();
|
|
984
|
+
const rootIsDocument = DOMTraversal.isDocument(rootElement);
|
|
985
|
+
const doc = rootIsDocument ? rootElement : rootElement.ownerDocument;
|
|
986
|
+
const options = {
|
|
987
|
+
...this.options,
|
|
988
|
+
...runtimeOptions,
|
|
989
|
+
};
|
|
990
|
+
let container = rootIsDocument ? doc : rootElement;
|
|
991
|
+
if (options.mainContentOnly && rootIsDocument)
|
|
992
|
+
container = ContentDetection.findMainContent(doc);
|
|
993
|
+
const pageState = this.extractPageState(doc);
|
|
994
|
+
const landmarks = this.extractLandmarks(doc);
|
|
995
|
+
const interactive = this.extractInteractiveElements(container, options);
|
|
996
|
+
const result = {
|
|
997
|
+
mode: options.mode,
|
|
998
|
+
timestamp: startTime,
|
|
999
|
+
page: pageState,
|
|
1000
|
+
landmarks,
|
|
1001
|
+
interactive,
|
|
1002
|
+
};
|
|
1003
|
+
if (options.mode === 'full') {
|
|
1004
|
+
const semantic = this.extractSemanticElements(container, options);
|
|
1005
|
+
const metadata = this.extractMetadata(doc, container, options);
|
|
1006
|
+
return {
|
|
1007
|
+
...result,
|
|
1008
|
+
semantic,
|
|
1009
|
+
metadata,
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
return result;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Extract page state information
|
|
1016
|
+
*/
|
|
1017
|
+
extractPageState(doc) {
|
|
1018
|
+
const hasFocus = this.getFocusedElement(doc);
|
|
1019
|
+
return {
|
|
1020
|
+
url: doc.location?.href || '',
|
|
1021
|
+
title: doc.title || '',
|
|
1022
|
+
hasErrors: this.detectErrors(doc),
|
|
1023
|
+
isLoading: this.detectLoading(doc),
|
|
1024
|
+
hasModals: this.detectModals(doc),
|
|
1025
|
+
...(hasFocus !== void 0 && { hasFocus }),
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Extract page landmarks
|
|
1030
|
+
*/
|
|
1031
|
+
extractLandmarks(doc) {
|
|
1032
|
+
const detected = ContentDetection.detectLandmarks(doc);
|
|
1033
|
+
return {
|
|
1034
|
+
navigation: this.elementsToSelectors(detected.navigation || []),
|
|
1035
|
+
main: this.elementsToSelectors(detected.main || []),
|
|
1036
|
+
forms: this.elementsToSelectors(detected.form || []),
|
|
1037
|
+
headers: this.elementsToSelectors(detected.banner || []),
|
|
1038
|
+
footers: this.elementsToSelectors(detected.contentinfo || []),
|
|
1039
|
+
articles: this.elementsToSelectors(detected.region || []),
|
|
1040
|
+
sections: this.elementsToSelectors(detected.region || []),
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Convert elements to selector strings
|
|
1045
|
+
*/
|
|
1046
|
+
elementsToSelectors(elements) {
|
|
1047
|
+
return elements.map((el) => SelectorGenerator.generateSelectors(el).css);
|
|
1048
|
+
}
|
|
1049
|
+
querySelectorAll(container, selector, includeShadowDOM) {
|
|
1050
|
+
const matches = [...container.querySelectorAll(selector)];
|
|
1051
|
+
if (!includeShadowDOM) return matches;
|
|
1052
|
+
if ('shadowRoot' in container && container.shadowRoot)
|
|
1053
|
+
matches.push(...this.querySelectorAll(container.shadowRoot, selector, true));
|
|
1054
|
+
for (const element of container.querySelectorAll('*'))
|
|
1055
|
+
if (element.shadowRoot)
|
|
1056
|
+
matches.push(...this.querySelectorAll(element.shadowRoot, selector, true));
|
|
1057
|
+
return matches;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Extract every element matching a selector.
|
|
1061
|
+
*
|
|
1062
|
+
* DOMTraversal.extractElement already applies the visibility, viewport and
|
|
1063
|
+
* filter guards, so pre-filtering here would just repeat its
|
|
1064
|
+
* getBoundingClientRect/getComputedStyle work for every element.
|
|
1065
|
+
*/
|
|
1066
|
+
extractAll(container, selector, options) {
|
|
1067
|
+
const extracted = [];
|
|
1068
|
+
for (const el of this.querySelectorAll(container, selector, options.includeShadowDOM)) {
|
|
1069
|
+
const element = DOMTraversal.extractElement(el, options);
|
|
1070
|
+
if (element) extracted.push(element);
|
|
1071
|
+
}
|
|
1072
|
+
return extracted;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Extract interactive elements
|
|
1076
|
+
*/
|
|
1077
|
+
extractInteractiveElements(container, options) {
|
|
1078
|
+
const clickable = [];
|
|
1079
|
+
for (const selector of options.customSelectors ?? [])
|
|
1080
|
+
clickable.push(...this.extractAll(container, selector, options));
|
|
1081
|
+
return {
|
|
1082
|
+
buttons: this.extractAll(
|
|
1083
|
+
container,
|
|
1084
|
+
'button, [role="button"], input[type="button"], input[type="submit"]',
|
|
1085
|
+
options
|
|
1086
|
+
),
|
|
1087
|
+
links: this.extractAll(container, 'a[href]', options),
|
|
1088
|
+
inputs: this.extractAll(
|
|
1089
|
+
container,
|
|
1090
|
+
'input:not([type="button"]):not([type="submit"]), textarea, select',
|
|
1091
|
+
options
|
|
1092
|
+
),
|
|
1093
|
+
forms: this.extractForms(container, options),
|
|
1094
|
+
clickable,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Extract form information
|
|
1099
|
+
*/
|
|
1100
|
+
extractForms(container, options) {
|
|
1101
|
+
const forms = [];
|
|
1102
|
+
this.querySelectorAll(container, 'form', options.includeShadowDOM).forEach((form) => {
|
|
1103
|
+
if (!this.shouldIncludeElement(form, options)) return;
|
|
1104
|
+
const action = form.getAttribute('action');
|
|
1105
|
+
const method = form.getAttribute('method');
|
|
1106
|
+
const formInfo = {
|
|
1107
|
+
selector: SelectorGenerator.generateSelectors(form).css,
|
|
1108
|
+
inputs: this.extractAll(
|
|
1109
|
+
form,
|
|
1110
|
+
'input:not([type="button"]):not([type="submit"]), textarea, select',
|
|
1111
|
+
options
|
|
1112
|
+
),
|
|
1113
|
+
buttons: this.extractAll(
|
|
1114
|
+
form,
|
|
1115
|
+
'button, input[type="button"], input[type="submit"]',
|
|
1116
|
+
options
|
|
1117
|
+
),
|
|
1118
|
+
};
|
|
1119
|
+
if (action) formInfo.action = action;
|
|
1120
|
+
if (method) formInfo.method = method;
|
|
1121
|
+
forms.push(formInfo);
|
|
1122
|
+
});
|
|
1123
|
+
return forms;
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Extract semantic elements (full mode only)
|
|
1127
|
+
*/
|
|
1128
|
+
extractSemanticElements(container, options) {
|
|
1129
|
+
return {
|
|
1130
|
+
headings: this.extractAll(container, 'h1, h2, h3, h4, h5, h6', options),
|
|
1131
|
+
images: this.extractAll(container, 'img', options),
|
|
1132
|
+
tables: this.extractAll(container, 'table', options),
|
|
1133
|
+
lists: this.extractAll(container, 'ul, ol', options),
|
|
1134
|
+
articles: this.extractAll(container, 'article, [role="article"]', options),
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Extract metadata
|
|
1139
|
+
*/
|
|
1140
|
+
extractMetadata(doc, container, options) {
|
|
1141
|
+
const allElements = this.querySelectorAll(container, '*', options.includeShadowDOM);
|
|
1142
|
+
const extractedElements = this.querySelectorAll(
|
|
1143
|
+
container,
|
|
1144
|
+
'button, a, input, textarea, select, h1, h2, h3, h4, h5, h6, img, table, ul, ol, article',
|
|
1145
|
+
options.includeShadowDOM
|
|
1146
|
+
).length;
|
|
1147
|
+
const metadata = {
|
|
1148
|
+
totalElements: allElements.length,
|
|
1149
|
+
extractedElements,
|
|
1150
|
+
};
|
|
1151
|
+
if (options.mainContentOnly && !DOMTraversal.isDocument(container))
|
|
1152
|
+
metadata.mainContent = SelectorGenerator.generateSelectors(container).css;
|
|
1153
|
+
const language = doc.documentElement.getAttribute('lang');
|
|
1154
|
+
if (language) metadata.language = language;
|
|
1155
|
+
return metadata;
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Check if element should be included based on options
|
|
1159
|
+
*/
|
|
1160
|
+
shouldIncludeElement(element, options) {
|
|
1161
|
+
if (!options.includeHidden && !DOMTraversal.isVisible(element)) return false;
|
|
1162
|
+
if (options.viewportOnly && !DOMTraversal.isInViewport(element)) return false;
|
|
1163
|
+
if (options.filter && !DOMTraversal.passesFilter(element, options.filter)) return false;
|
|
1164
|
+
return true;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Detect errors on the page
|
|
1168
|
+
*/
|
|
1169
|
+
detectErrors(doc) {
|
|
1170
|
+
return ['.error', '.alert-danger', '[role="alert"]', '.error-message'].some((sel) => {
|
|
1171
|
+
const element = doc.querySelector(sel);
|
|
1172
|
+
return element ? DOMTraversal.isVisible(element) : false;
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Detect if page is loading
|
|
1177
|
+
*/
|
|
1178
|
+
detectLoading(doc) {
|
|
1179
|
+
return ['.loading', '.spinner', '[aria-busy="true"]', '.loader'].some((sel) => {
|
|
1180
|
+
const element = doc.querySelector(sel);
|
|
1181
|
+
return element ? DOMTraversal.isVisible(element) : false;
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Detect modal dialogs
|
|
1186
|
+
*/
|
|
1187
|
+
detectModals(doc) {
|
|
1188
|
+
return ['[role="dialog"]', '.modal', '.popup', '.overlay'].some((sel) => {
|
|
1189
|
+
const element = doc.querySelector(sel);
|
|
1190
|
+
return element ? DOMTraversal.isVisible(element) : false;
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Get currently focused element
|
|
1195
|
+
*/
|
|
1196
|
+
getFocusedElement(doc) {
|
|
1197
|
+
const focused = doc.activeElement;
|
|
1198
|
+
if (focused && focused !== doc.body) return SelectorGenerator.generateSelectors(focused).css;
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Quick extraction for interactive elements only
|
|
1202
|
+
* @param doc The document to extract from
|
|
1203
|
+
* @param options Extraction options
|
|
1204
|
+
*/
|
|
1205
|
+
static extractInteractive(doc, options = {}) {
|
|
1206
|
+
return new SmartDOMReader({
|
|
1207
|
+
...options,
|
|
1208
|
+
mode: 'interactive',
|
|
1209
|
+
}).extract(doc);
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Quick extraction for full content
|
|
1213
|
+
* @param doc The document to extract from
|
|
1214
|
+
* @param options Extraction options
|
|
1215
|
+
*/
|
|
1216
|
+
static extractFull(doc, options = {}) {
|
|
1217
|
+
return new SmartDOMReader({
|
|
1218
|
+
...options,
|
|
1219
|
+
mode: 'full',
|
|
1220
|
+
}).extract(doc);
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Extract from a specific element
|
|
1224
|
+
* @param element The element to extract from
|
|
1225
|
+
* @param mode The extraction mode
|
|
1226
|
+
* @param options Additional options
|
|
1227
|
+
*/
|
|
1228
|
+
static extractFromElement(element, mode = 'interactive', options = {}) {
|
|
1229
|
+
return new SmartDOMReader({
|
|
1230
|
+
...options,
|
|
1231
|
+
mode,
|
|
1232
|
+
}).extract(element);
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
//#endregion
|
|
1236
|
+
//#region src/progressive.ts
|
|
1237
|
+
var ProgressiveExtractor = class ProgressiveExtractor {
|
|
1238
|
+
/**
|
|
1239
|
+
* Step 1: Extract high-level structural overview
|
|
1240
|
+
* This provides a "map" of the page for the AI to understand structure
|
|
1241
|
+
*/
|
|
1242
|
+
static extractStructure(root) {
|
|
1243
|
+
const regions = {};
|
|
1244
|
+
const header = root.querySelector('header, [role="banner"], .header, #header');
|
|
1245
|
+
if (header) regions.header = ProgressiveExtractor.analyzeRegion(header);
|
|
1246
|
+
const navs = root.querySelectorAll('nav, [role="navigation"], .nav, .navigation');
|
|
1247
|
+
if (navs.length > 0)
|
|
1248
|
+
regions.navigation = Array.from(navs).map((nav) => ProgressiveExtractor.analyzeRegion(nav));
|
|
1249
|
+
if (DOMTraversal.isDocument(root)) {
|
|
1250
|
+
const main = ContentDetection.findMainContent(root);
|
|
1251
|
+
if (main) {
|
|
1252
|
+
regions.main = ProgressiveExtractor.analyzeRegion(main);
|
|
1253
|
+
const sections = main.querySelectorAll('section, article, [role="region"]');
|
|
1254
|
+
if (sections.length > 0)
|
|
1255
|
+
regions.sections = Array.from(sections)
|
|
1256
|
+
.filter((section) => !section.closest('nav, header, footer'))
|
|
1257
|
+
.map((section) => ProgressiveExtractor.analyzeRegion(section));
|
|
1792
1258
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1259
|
+
} else {
|
|
1260
|
+
regions.main = ProgressiveExtractor.analyzeRegion(root);
|
|
1261
|
+
const sections = root.querySelectorAll('section, article, [role="region"]');
|
|
1262
|
+
if (sections.length > 0)
|
|
1263
|
+
regions.sections = Array.from(sections)
|
|
1264
|
+
.filter((section) => !section.closest('nav, header, footer'))
|
|
1265
|
+
.map((section) => ProgressiveExtractor.analyzeRegion(section));
|
|
1266
|
+
}
|
|
1267
|
+
const sidebars = root.querySelectorAll('aside, [role="complementary"], .sidebar, #sidebar');
|
|
1268
|
+
if (sidebars.length > 0)
|
|
1269
|
+
regions.sidebar = Array.from(sidebars).map((sidebar) =>
|
|
1270
|
+
ProgressiveExtractor.analyzeRegion(sidebar)
|
|
1271
|
+
);
|
|
1272
|
+
const footer = root.querySelector('footer, [role="contentinfo"], .footer, #footer');
|
|
1273
|
+
if (footer) regions.footer = ProgressiveExtractor.analyzeRegion(footer);
|
|
1274
|
+
const modals = root.querySelectorAll('[role="dialog"], .modal, .popup, .overlay');
|
|
1275
|
+
const visibleModals = Array.from(modals).filter((modal) => DOMTraversal.isVisible(modal));
|
|
1276
|
+
if (visibleModals.length > 0)
|
|
1277
|
+
regions.modals = visibleModals.map((modal) => ProgressiveExtractor.analyzeRegion(modal));
|
|
1278
|
+
const forms = ProgressiveExtractor.extractFormOverview(root);
|
|
1279
|
+
const summary = ProgressiveExtractor.calculateSummary(root, regions, forms);
|
|
1280
|
+
return {
|
|
1281
|
+
regions,
|
|
1282
|
+
forms,
|
|
1283
|
+
summary,
|
|
1284
|
+
suggestions: ProgressiveExtractor.generateSuggestions(regions, summary),
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Step 2: Extract detailed information from a specific region
|
|
1289
|
+
*/
|
|
1290
|
+
static extractRegion(selector, doc, options = {}) {
|
|
1291
|
+
const element = doc.querySelector(selector);
|
|
1292
|
+
if (!element) return null;
|
|
1293
|
+
return new SmartDOMReader(options).extract(element, options);
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Step 3: Extract readable content from a region
|
|
1297
|
+
*/
|
|
1298
|
+
static extractContent(selector, doc, options = {}) {
|
|
1299
|
+
const element = doc.querySelector(selector);
|
|
1300
|
+
if (!element) return null;
|
|
1301
|
+
const result = {
|
|
1302
|
+
selector,
|
|
1303
|
+
text: {},
|
|
1304
|
+
metadata: {
|
|
1305
|
+
wordCount: 0,
|
|
1306
|
+
hasInteractive: false,
|
|
1307
|
+
},
|
|
1308
|
+
};
|
|
1309
|
+
if (options.includeHeadings !== false) {
|
|
1310
|
+
const headings = element.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
1311
|
+
result.text.headings = Array.from(headings).map((h) => ({
|
|
1312
|
+
level: Number.parseInt(h.tagName[1], 10),
|
|
1313
|
+
text: ProgressiveExtractor.getTextContent(h, options.maxTextLength),
|
|
1314
|
+
}));
|
|
1315
|
+
}
|
|
1316
|
+
const paragraphs = element.querySelectorAll('p');
|
|
1317
|
+
if (paragraphs.length > 0)
|
|
1318
|
+
result.text.paragraphs = Array.from(paragraphs)
|
|
1319
|
+
.map((p) => ProgressiveExtractor.getTextContent(p, options.maxTextLength))
|
|
1320
|
+
.filter((text) => text.length > 0);
|
|
1321
|
+
if (options.includeLists !== false) {
|
|
1322
|
+
const lists = element.querySelectorAll('ul, ol');
|
|
1323
|
+
result.text.lists = Array.from(lists).map((list) => ({
|
|
1324
|
+
type: list.tagName.toLowerCase(),
|
|
1325
|
+
items: Array.from(list.querySelectorAll('li')).map((li) =>
|
|
1326
|
+
ProgressiveExtractor.getTextContent(li, options.maxTextLength)
|
|
1327
|
+
),
|
|
1328
|
+
}));
|
|
1329
|
+
}
|
|
1330
|
+
if (options.includeTables !== false) {
|
|
1331
|
+
const tables = element.querySelectorAll('table');
|
|
1332
|
+
result.tables = Array.from(tables).map((table) => {
|
|
1801
1333
|
return {
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1334
|
+
headers: Array.from(table.querySelectorAll('th')).map((th) =>
|
|
1335
|
+
ProgressiveExtractor.getTextContent(th)
|
|
1336
|
+
),
|
|
1337
|
+
rows: Array.from(table.querySelectorAll('tr'))
|
|
1338
|
+
.filter((tr) => tr.querySelector('td'))
|
|
1339
|
+
.map((tr) =>
|
|
1340
|
+
Array.from(tr.querySelectorAll('td')).map((td) =>
|
|
1341
|
+
ProgressiveExtractor.getTextContent(td)
|
|
1342
|
+
)
|
|
1343
|
+
),
|
|
1809
1344
|
};
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
if (options.includeMedia !== false) {
|
|
1348
|
+
const images = element.querySelectorAll('img');
|
|
1349
|
+
const videos = element.querySelectorAll('video');
|
|
1350
|
+
const audios = element.querySelectorAll('audio');
|
|
1351
|
+
result.media = [
|
|
1352
|
+
...Array.from(images).map((img) => {
|
|
1353
|
+
const item = { type: 'img' };
|
|
1354
|
+
const alt = img.getAttribute('alt');
|
|
1355
|
+
const src = img.getAttribute('src');
|
|
1356
|
+
if (alt) item.alt = alt;
|
|
1357
|
+
if (src) item.src = src;
|
|
1358
|
+
return item;
|
|
1359
|
+
}),
|
|
1360
|
+
...Array.from(videos).map((video) => {
|
|
1361
|
+
const item = { type: 'video' };
|
|
1362
|
+
const src = video.getAttribute('src');
|
|
1363
|
+
if (src) item.src = src;
|
|
1364
|
+
return item;
|
|
1365
|
+
}),
|
|
1366
|
+
...Array.from(audios).map((audio) => {
|
|
1367
|
+
const item = { type: 'audio' };
|
|
1368
|
+
const src = audio.getAttribute('src');
|
|
1369
|
+
if (src) item.src = src;
|
|
1370
|
+
return item;
|
|
1371
|
+
}),
|
|
1372
|
+
];
|
|
1373
|
+
}
|
|
1374
|
+
const allText = element.textContent || '';
|
|
1375
|
+
result.metadata.wordCount = allText.trim().split(/\s+/).length;
|
|
1376
|
+
result.metadata.hasInteractive =
|
|
1377
|
+
element.querySelectorAll('button, a, input, textarea, select').length > 0;
|
|
1378
|
+
return result;
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Analyze a region and extract summary information
|
|
1382
|
+
*/
|
|
1383
|
+
static analyzeRegion(element) {
|
|
1384
|
+
const selector = SelectorGenerator.generateSelectors(element).css;
|
|
1385
|
+
const buttons = element.querySelectorAll('button, [role="button"]');
|
|
1386
|
+
const links = element.querySelectorAll('a[href]');
|
|
1387
|
+
const inputs = element.querySelectorAll('input, textarea, select');
|
|
1388
|
+
const forms = element.querySelectorAll('form');
|
|
1389
|
+
const lists = element.querySelectorAll('ul, ol');
|
|
1390
|
+
const tables = element.querySelectorAll('table');
|
|
1391
|
+
const media = element.querySelectorAll('img, video, audio');
|
|
1392
|
+
const interactiveCount = buttons.length + links.length + inputs.length;
|
|
1393
|
+
let label;
|
|
1394
|
+
const ariaLabel = element.getAttribute('aria-label');
|
|
1395
|
+
if (ariaLabel) label = ariaLabel;
|
|
1396
|
+
else if (element.getAttribute('aria-labelledby')) {
|
|
1397
|
+
const labelId = element.getAttribute('aria-labelledby');
|
|
1398
|
+
if (labelId) {
|
|
1399
|
+
const labelElement = element.ownerDocument?.getElementById(labelId);
|
|
1400
|
+
if (labelElement) label = labelElement.textContent?.trim();
|
|
1810
1401
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
}
|
|
1826
|
-
/**
|
|
1827
|
-
* Detect errors on the page
|
|
1828
|
-
*/
|
|
1829
|
-
detectErrors(doc) {
|
|
1830
|
-
const errorSelectors = ['.error', '.alert-danger', '[role="alert"]', '.error-message'];
|
|
1831
|
-
return errorSelectors.some((sel) => {
|
|
1832
|
-
const element = doc.querySelector(sel);
|
|
1833
|
-
return element ? DOMTraversal.isVisible(element) : false;
|
|
1834
|
-
});
|
|
1835
|
-
}
|
|
1836
|
-
/**
|
|
1837
|
-
* Detect if page is loading
|
|
1838
|
-
*/
|
|
1839
|
-
detectLoading(doc) {
|
|
1840
|
-
const loadingSelectors = ['.loading', '.spinner', '[aria-busy="true"]', '.loader'];
|
|
1841
|
-
return loadingSelectors.some((sel) => {
|
|
1842
|
-
const element = doc.querySelector(sel);
|
|
1843
|
-
return element ? DOMTraversal.isVisible(element) : false;
|
|
1844
|
-
});
|
|
1845
|
-
}
|
|
1846
|
-
/**
|
|
1847
|
-
* Detect modal dialogs
|
|
1848
|
-
*/
|
|
1849
|
-
detectModals(doc) {
|
|
1850
|
-
const modalSelectors = ['[role="dialog"]', '.modal', '.popup', '.overlay'];
|
|
1851
|
-
return modalSelectors.some((sel) => {
|
|
1852
|
-
const element = doc.querySelector(sel);
|
|
1853
|
-
return element ? DOMTraversal.isVisible(element) : false;
|
|
1854
|
-
});
|
|
1855
|
-
}
|
|
1856
|
-
/**
|
|
1857
|
-
* Get currently focused element
|
|
1858
|
-
*/
|
|
1859
|
-
getFocusedElement(doc) {
|
|
1860
|
-
const focused = doc.activeElement;
|
|
1861
|
-
if (focused && focused !== doc.body) {
|
|
1862
|
-
return SelectorGenerator.generateSelectors(focused).css;
|
|
1863
|
-
}
|
|
1864
|
-
return void 0;
|
|
1865
|
-
}
|
|
1866
|
-
// ===== Static convenience methods =====
|
|
1867
|
-
/**
|
|
1868
|
-
* Quick extraction for interactive elements only
|
|
1869
|
-
* @param doc The document to extract from
|
|
1870
|
-
* @param options Extraction options
|
|
1871
|
-
*/
|
|
1872
|
-
static extractInteractive(doc, options = {}) {
|
|
1873
|
-
const reader = new _SmartDOMReader({
|
|
1874
|
-
...options,
|
|
1875
|
-
mode: 'interactive',
|
|
1876
|
-
});
|
|
1877
|
-
return reader.extract(doc);
|
|
1878
|
-
}
|
|
1879
|
-
/**
|
|
1880
|
-
* Quick extraction for full content
|
|
1881
|
-
* @param doc The document to extract from
|
|
1882
|
-
* @param options Extraction options
|
|
1883
|
-
*/
|
|
1884
|
-
static extractFull(doc, options = {}) {
|
|
1885
|
-
const reader = new _SmartDOMReader({
|
|
1886
|
-
...options,
|
|
1887
|
-
mode: 'full',
|
|
1888
|
-
});
|
|
1889
|
-
return reader.extract(doc);
|
|
1890
|
-
}
|
|
1891
|
-
/**
|
|
1892
|
-
* Extract from a specific element
|
|
1893
|
-
* @param element The element to extract from
|
|
1894
|
-
* @param mode The extraction mode
|
|
1895
|
-
* @param options Additional options
|
|
1896
|
-
*/
|
|
1897
|
-
static extractFromElement(element, mode = 'interactive', options = {}) {
|
|
1898
|
-
const reader = new _SmartDOMReader({
|
|
1899
|
-
...options,
|
|
1900
|
-
mode,
|
|
1901
|
-
});
|
|
1902
|
-
return reader.extract(element);
|
|
1903
|
-
}
|
|
1402
|
+
} else {
|
|
1403
|
+
const heading = element.querySelector('h1, h2, h3');
|
|
1404
|
+
if (heading) label = heading.textContent?.trim();
|
|
1405
|
+
}
|
|
1406
|
+
const textContent = element.textContent?.trim() || '';
|
|
1407
|
+
const textPreview =
|
|
1408
|
+
textContent.length > 50 ? `${textContent.substring(0, 50)}...` : textContent;
|
|
1409
|
+
const regionInfo = {
|
|
1410
|
+
selector,
|
|
1411
|
+
interactiveCount,
|
|
1412
|
+
hasForm: forms.length > 0,
|
|
1413
|
+
hasList: lists.length > 0,
|
|
1414
|
+
hasTable: tables.length > 0,
|
|
1415
|
+
hasMedia: media.length > 0,
|
|
1904
1416
|
};
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1417
|
+
if (label) regionInfo.label = label;
|
|
1418
|
+
const role = element.getAttribute('role');
|
|
1419
|
+
if (role) regionInfo.role = role;
|
|
1420
|
+
if (buttons.length > 0) regionInfo.buttonCount = buttons.length;
|
|
1421
|
+
if (links.length > 0) regionInfo.linkCount = links.length;
|
|
1422
|
+
if (inputs.length > 0) regionInfo.inputCount = inputs.length;
|
|
1423
|
+
if (textPreview.length > 0) regionInfo.textPreview = textPreview;
|
|
1424
|
+
return regionInfo;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Extract overview of forms on the page
|
|
1428
|
+
*/
|
|
1429
|
+
static extractFormOverview(root) {
|
|
1430
|
+
const forms = root.querySelectorAll('form');
|
|
1431
|
+
return Array.from(forms).map((form) => {
|
|
1432
|
+
const inputs = form.querySelectorAll('input, textarea, select');
|
|
1433
|
+
const selector = SelectorGenerator.generateSelectors(form).css;
|
|
1434
|
+
let location = 'unknown';
|
|
1435
|
+
if (form.closest('header, [role="banner"]')) location = 'header';
|
|
1436
|
+
else if (form.closest('nav, [role="navigation"]')) location = 'navigation';
|
|
1437
|
+
else if (form.closest('main, [role="main"]')) location = 'main';
|
|
1438
|
+
else if (form.closest('aside, [role="complementary"]')) location = 'sidebar';
|
|
1439
|
+
else if (form.closest('footer, [role="contentinfo"]')) location = 'footer';
|
|
1440
|
+
let purpose;
|
|
1441
|
+
const formId = form.getAttribute('id')?.toLowerCase();
|
|
1442
|
+
const formClass = form.getAttribute('class')?.toLowerCase();
|
|
1443
|
+
const formAction = form.getAttribute('action')?.toLowerCase();
|
|
1444
|
+
const hasEmail = form.querySelector('input[type="email"]');
|
|
1445
|
+
const hasPassword = form.querySelector('input[type="password"]');
|
|
1446
|
+
if (
|
|
1447
|
+
form.querySelector('input[type="search"]') ||
|
|
1448
|
+
formId?.includes('search') ||
|
|
1449
|
+
formClass?.includes('search')
|
|
1450
|
+
)
|
|
1451
|
+
purpose = 'search';
|
|
1452
|
+
else if (hasPassword && hasEmail) purpose = 'login';
|
|
1453
|
+
else if (hasPassword) purpose = 'authentication';
|
|
1454
|
+
else if (formId?.includes('contact') || formClass?.includes('contact')) purpose = 'contact';
|
|
1455
|
+
else if (formId?.includes('subscribe') || formClass?.includes('subscribe'))
|
|
1456
|
+
purpose = 'subscription';
|
|
1457
|
+
else if (formAction?.includes('checkout') || formClass?.includes('checkout'))
|
|
1458
|
+
purpose = 'checkout';
|
|
1459
|
+
const formOverview = {
|
|
1460
|
+
selector,
|
|
1461
|
+
location,
|
|
1462
|
+
inputCount: inputs.length,
|
|
1463
|
+
};
|
|
1464
|
+
if (purpose) formOverview.purpose = purpose;
|
|
1465
|
+
return formOverview;
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Calculate summary statistics
|
|
1470
|
+
*/
|
|
1471
|
+
static calculateSummary(root, regions, forms) {
|
|
1472
|
+
const allInteractive = root.querySelectorAll('button, a[href], input, textarea, select');
|
|
1473
|
+
const allSections = root.querySelectorAll('section, article, [role="region"]');
|
|
1474
|
+
const hasModals = (regions.modals?.length || 0) > 0;
|
|
1475
|
+
const hasErrors = ['.error', '.alert-danger', '[role="alert"]'].some((sel) => {
|
|
1476
|
+
const element = root.querySelector(sel);
|
|
1477
|
+
return element ? DOMTraversal.isVisible(element) : false;
|
|
1478
|
+
});
|
|
1479
|
+
const isLoading = ['.loading', '.spinner', '[aria-busy="true"]'].some((sel) => {
|
|
1480
|
+
const element = root.querySelector(sel);
|
|
1481
|
+
return element ? DOMTraversal.isVisible(element) : false;
|
|
1482
|
+
});
|
|
1483
|
+
const summary = {
|
|
1484
|
+
totalInteractive: allInteractive.length,
|
|
1485
|
+
totalForms: forms.length,
|
|
1486
|
+
totalSections: allSections.length,
|
|
1487
|
+
hasModals,
|
|
1488
|
+
hasErrors,
|
|
1489
|
+
isLoading,
|
|
1490
|
+
};
|
|
1491
|
+
const mainContentSelector = regions.main?.selector;
|
|
1492
|
+
if (mainContentSelector) summary.mainContentSelector = mainContentSelector;
|
|
1493
|
+
return summary;
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Generate AI-friendly suggestions
|
|
1497
|
+
*/
|
|
1498
|
+
static generateSuggestions(regions, summary) {
|
|
1499
|
+
const suggestions = [];
|
|
1500
|
+
if (summary.hasErrors)
|
|
1501
|
+
suggestions.push('Page has error indicators - check error messages before interacting');
|
|
1502
|
+
if (summary.isLoading)
|
|
1503
|
+
suggestions.push('Page appears to be loading - wait or check loading state');
|
|
1504
|
+
if (summary.hasModals)
|
|
1505
|
+
suggestions.push('Modal/dialog is open - may need to interact with or close it first');
|
|
1506
|
+
if (regions.main && regions.main.interactiveCount > 10)
|
|
1507
|
+
suggestions.push(
|
|
1508
|
+
`Main content has ${regions.main.interactiveCount} interactive elements - consider filtering`
|
|
1509
|
+
);
|
|
1510
|
+
if (summary.totalForms > 0) suggestions.push(`Found ${summary.totalForms} form(s) on the page`);
|
|
1511
|
+
if (!regions.main)
|
|
1512
|
+
suggestions.push('No clear main content area detected - may need to explore regions');
|
|
1513
|
+
return suggestions;
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Get text content with optional truncation
|
|
1517
|
+
*/
|
|
1518
|
+
static getTextContent(element, maxLength) {
|
|
1519
|
+
const text = element.textContent?.trim() || '';
|
|
1520
|
+
if (maxLength && text.length > maxLength) return `${text.substring(0, maxLength)}...`;
|
|
1521
|
+
return text;
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
//#endregion
|
|
1909
1525
|
export {
|
|
1910
1526
|
ContentDetection,
|
|
1911
1527
|
MarkdownFormatter,
|
|
1912
1528
|
ProgressiveExtractor,
|
|
1913
1529
|
SelectorGenerator,
|
|
1914
1530
|
SmartDOMReader,
|
|
1915
|
-
|
|
1531
|
+
SmartDOMReader as default,
|
|
1916
1532
|
};
|
|
1917
|
-
|
|
1533
|
+
|
|
1534
|
+
//# sourceMappingURL=index.mjs.map
|