@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.
@@ -1,919 +1,161 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- var __require = /* @__PURE__ */ ((x) =>
6
- typeof require !== 'undefined'
7
- ? require
8
- : typeof Proxy !== 'undefined'
9
- ? new Proxy(x, {
10
- get: (a, b) => (typeof require !== 'undefined' ? require : a)[b],
11
- })
12
- : x)(function (x) {
13
- if (typeof require !== 'undefined') return require.apply(this, arguments);
14
- throw Error(`Dynamic require of "${x}" is not supported`);
15
- });
16
- var __esm = (fn, res) =>
17
- function __init() {
18
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])((fn = 0))), res;
19
- };
20
- var __export = (target, all) => {
21
- for (var name in all) __defProp(target, name, { get: all[name], enumerable: true });
22
- };
23
- var __copyProps = (to, from, except, desc) => {
24
- if ((from && typeof from === 'object') || typeof from === 'function') {
25
- for (const key of __getOwnPropNames(from))
26
- if (!__hasOwnProp.call(to, key) && key !== except)
27
- __defProp(to, key, {
28
- get: () => from[key],
29
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable,
30
- });
31
- }
32
- return to;
33
- };
34
- var __toCommonJS = (mod) => __copyProps(__defProp({}, '__esModule', { value: true }), mod);
35
-
36
- // src/content-detection.ts
37
- var ContentDetection;
38
- var init_content_detection = __esm({
39
- 'src/content-detection.ts'() {
40
- ContentDetection = class {
41
- /**
42
- * Find the main content area of a page
43
- * Inspired by dom-to-semantic-markdown's approach
44
- */
45
- static findMainContent(doc) {
46
- const mainElement = doc.querySelector('main, [role="main"]');
47
- if (mainElement) {
48
- return mainElement;
49
- }
50
- if (!doc.body) {
51
- return doc.documentElement;
52
- }
53
- return this.detectMainContent(doc.body);
54
- }
55
- /**
56
- * Detect main content using scoring algorithm
57
- */
58
- static detectMainContent(rootElement) {
59
- const candidates = [];
60
- const minScore = 15;
61
- this.collectCandidates(rootElement, candidates, minScore);
62
- if (candidates.length === 0) {
63
- return rootElement;
64
- }
65
- candidates.sort((a, b) => this.calculateContentScore(b) - this.calculateContentScore(a));
66
- let bestCandidate = candidates[0];
67
- for (let i = 1; i < candidates.length; i++) {
68
- const isIndependent = !candidates.some(
69
- (other, j) => j !== i && other.contains(candidates[i])
70
- );
71
- if (
72
- isIndependent &&
73
- this.calculateContentScore(candidates[i]) > this.calculateContentScore(bestCandidate)
74
- ) {
75
- bestCandidate = candidates[i];
76
- }
77
- }
78
- return bestCandidate;
79
- }
80
- /**
81
- * Collect content candidates
82
- */
83
- static collectCandidates(element, candidates, minScore) {
84
- const score = this.calculateContentScore(element);
85
- if (score >= minScore) {
86
- candidates.push(element);
87
- }
88
- Array.from(element.children).forEach((child) => {
89
- this.collectCandidates(child, candidates, minScore);
90
- });
91
- }
92
- /**
93
- * Calculate content score for an element
94
- */
95
- static calculateContentScore(element) {
96
- let score = 0;
97
- const semanticClasses = [
98
- 'article',
99
- 'content',
100
- 'main-container',
101
- 'main',
102
- 'main-content',
103
- 'post',
104
- 'entry',
105
- ];
106
- const semanticIds = ['content', 'main', 'article', 'post', 'entry'];
107
- semanticClasses.forEach((cls) => {
108
- if (element.classList.contains(cls)) {
109
- score += 10;
110
- }
111
- });
112
- semanticIds.forEach((id) => {
113
- if (element.id?.toLowerCase().includes(id)) {
114
- score += 10;
115
- }
116
- });
117
- const tag = element.tagName.toLowerCase();
118
- const highValueTags = ['article', 'main', 'section'];
119
- if (highValueTags.includes(tag)) {
120
- score += 8;
121
- }
122
- const paragraphs = element.getElementsByTagName('p').length;
123
- score += Math.min(paragraphs * 2, 10);
124
- const headings = element.querySelectorAll('h1, h2, h3').length;
125
- score += Math.min(headings * 3, 9);
126
- const textLength = element.textContent?.trim().length || 0;
127
- if (textLength > 300) {
128
- score += Math.min(Math.floor(textLength / 300) * 2, 10);
129
- }
130
- const linkDensity = this.calculateLinkDensity(element);
131
- if (linkDensity < 0.3) {
132
- score += 5;
133
- } else if (linkDensity > 0.5) {
134
- score -= 5;
135
- }
136
- if (
137
- element.hasAttribute('data-main') ||
138
- element.hasAttribute('data-content') ||
139
- element.hasAttribute('itemprop')
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
- // src/markdown-formatter.ts
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.map((k) => ({ k, i: lower.indexOf(k) })).find((x) => x.i > -1);
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()} \u2026 ${t.slice(start, end).trim()}\u2026`;
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}\u2026`;
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 '\u{1F9ED}';
200
+ return '🧭';
957
201
  case 'navigation':
958
- return '\u{1F4D1}';
202
+ return '📑';
959
203
  case 'main':
960
- return '\u{1F4C4}';
204
+ return '📄';
961
205
  case 'sections':
962
- return '\u{1F5C2}\uFE0F';
206
+ return '🗂️';
963
207
  case 'sidebar':
964
- return '\u{1F4DA}';
208
+ return '📚';
965
209
  case 'footer':
966
- return '\u{1F53B}';
210
+ return '🔻';
967
211
  case 'modals':
968
- return '\u{1F4AC}';
212
+ return '💬';
969
213
  default:
970
- return '\u{1F539}';
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 = bestSelector(el);
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)'} \u2192 \`${sel}\`${actionText}`;
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
- const stablePct = Math.round((stable / total) * 100);
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(`\u201C${truncate(region.textPreview, 80)}\u201D`);
1041
- const statsLine = stats.length ? ` \u2014 ${stats.join(', ')}` : '';
1042
- return `${icon} ${label} \u2192 \`${region.selector}\` [${id}]${statsLine}`;
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
- const attrs = [
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, '&gt;')
1062
291
  .replace(/"/g, '&quot;');
1063
292
  }
1064
- function capitalize(s) {
1065
- return s.charAt(0).toUpperCase() + s.slice(1);
1066
- }
1067
- var MarkdownFormatter;
1068
- var init_markdown_formatter = __esm({
1069
- 'src/markdown-formatter.ts'() {
1070
- MarkdownFormatter = class {
1071
- static structure(overview, _opts, meta) {
1072
- const lines = [];
1073
- lines.push('# Page Outline');
1074
- if (meta?.title || meta?.url) {
1075
- lines.push(`Title: ${meta?.title ?? ''}`.trim());
1076
- lines.push(`URL: ${meta?.url ?? ''}`.trim());
1077
- }
1078
- lines.push('');
1079
- const regions = overview.regions;
1080
- const entries = [
1081
- ['header', regions.header],
1082
- ['navigation', regions.navigation],
1083
- ['main', regions.main],
1084
- ['sections', regions.sections],
1085
- ['sidebar', regions.sidebar],
1086
- ['footer', regions.footer],
1087
- ['modals', regions.modals],
1088
- ];
1089
- for (const [key, value] of entries) {
1090
- if (!value) continue;
1091
- const icon = iconForRegion(key);
1092
- if (Array.isArray(value)) {
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
- // src/progressive.ts
1210
- function resolveSmartDomReader() {
1211
- if (typeof window !== 'undefined') {
1212
- const globalWindow = window;
1213
- const direct = globalWindow.SmartDOMReader;
1214
- if (typeof direct === 'function') {
1215
- return direct;
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
- const namespace = globalWindow.SmartDOMReaderNamespace;
1218
- if (namespace && typeof namespace.SmartDOMReader === 'function') {
1219
- return namespace.SmartDOMReader;
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
- try {
1223
- if (typeof __require === 'function') {
1224
- const moduleExports = (init_index(), __toCommonJS(index_exports));
1225
- if (moduleExports && typeof moduleExports.SmartDOMReader === 'function') {
1226
- return moduleExports.SmartDOMReader;
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
- if (moduleExports && typeof moduleExports.default === 'function') {
1229
- return moduleExports.default;
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
- } catch {}
1233
- return void 0;
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
- var ProgressiveExtractor;
1236
- var init_progressive = __esm({
1237
- 'src/progressive.ts'() {
1238
- init_content_detection();
1239
- init_selectors();
1240
- init_traversal();
1241
- ProgressiveExtractor = class {
1242
- /**
1243
- * Step 1: Extract high-level structural overview
1244
- * This provides a "map" of the page for the AI to understand structure
1245
- */
1246
- static extractStructure(root) {
1247
- const regions = {};
1248
- const header = root.querySelector('header, [role="banner"], .header, #header');
1249
- if (header) {
1250
- regions.header = this.analyzeRegion(header);
1251
- }
1252
- const navs = root.querySelectorAll('nav, [role="navigation"], .nav, .navigation');
1253
- if (navs.length > 0) {
1254
- regions.navigation = Array.from(navs).map((nav) => this.analyzeRegion(nav));
1255
- }
1256
- if (root instanceof Document) {
1257
- const main = ContentDetection.findMainContent(root);
1258
- if (main) {
1259
- regions.main = this.analyzeRegion(main);
1260
- const sections = main.querySelectorAll('section, article, [role="region"]');
1261
- if (sections.length > 0) {
1262
- regions.sections = Array.from(sections)
1263
- .filter((section) => !section.closest('nav, header, footer'))
1264
- .map((section) => this.analyzeRegion(section));
1265
- }
1266
- }
1267
- } else {
1268
- regions.main = this.analyzeRegion(root);
1269
- const sections = root.querySelectorAll('section, article, [role="region"]');
1270
- if (sections.length > 0) {
1271
- regions.sections = Array.from(sections)
1272
- .filter((section) => !section.closest('nav, header, footer'))
1273
- .map((section) => this.analyzeRegion(section));
1274
- }
1275
- }
1276
- const sidebars = root.querySelectorAll('aside, [role="complementary"], .sidebar, #sidebar');
1277
- if (sidebars.length > 0) {
1278
- regions.sidebar = Array.from(sidebars).map((sidebar) => this.analyzeRegion(sidebar));
1279
- }
1280
- const footer = root.querySelector('footer, [role="contentinfo"], .footer, #footer');
1281
- if (footer) {
1282
- regions.footer = this.analyzeRegion(footer);
1283
- }
1284
- const modals = root.querySelectorAll('[role="dialog"], .modal, .popup, .overlay');
1285
- const visibleModals = Array.from(modals).filter((modal) => DOMTraversal.isVisible(modal));
1286
- if (visibleModals.length > 0) {
1287
- regions.modals = visibleModals.map((modal) => this.analyzeRegion(modal));
1288
- }
1289
- const forms = this.extractFormOverview(root);
1290
- const summary = this.calculateSummary(root, regions, forms);
1291
- const suggestions = this.generateSuggestions(regions, summary);
1292
- return { regions, forms, summary, suggestions };
1293
- }
1294
- /**
1295
- * Step 2: Extract detailed information from a specific region
1296
- */
1297
- static extractRegion(selector, doc, options, smartDomReaderCtor) {
1298
- const element = doc.querySelector(selector);
1299
- if (!element) return null;
1300
- const SmartDOMReaderCtor = smartDomReaderCtor ?? resolveSmartDomReader();
1301
- if (!SmartDOMReaderCtor) {
1302
- throw new Error(
1303
- 'SmartDOMReader is unavailable. Ensure the Smart DOM Reader module is loaded before calling extractRegion.'
1304
- );
1305
- }
1306
- const reader = new SmartDOMReaderCtor(options);
1307
- return reader.extract(element, options);
1308
- }
1309
- /**
1310
- * Step 3: Extract readable content from a region
1311
- */
1312
- static extractContent(selector, doc, options = {}) {
1313
- const element = doc.querySelector(selector);
1314
- if (!element) return null;
1315
- const result = {
1316
- selector,
1317
- text: {},
1318
- metadata: {
1319
- wordCount: 0,
1320
- hasInteractive: false,
1321
- },
1322
- };
1323
- if (options.includeHeadings !== false) {
1324
- const headings = element.querySelectorAll('h1, h2, h3, h4, h5, h6');
1325
- result.text.headings = Array.from(headings).map((h) => ({
1326
- level: Number.parseInt(h.tagName[1], 10),
1327
- text: this.getTextContent(h, options.maxTextLength),
1328
- }));
1329
- }
1330
- const paragraphs = element.querySelectorAll('p');
1331
- if (paragraphs.length > 0) {
1332
- result.text.paragraphs = Array.from(paragraphs)
1333
- .map((p) => this.getTextContent(p, options.maxTextLength))
1334
- .filter((text) => text.length > 0);
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
- * Extract overview of forms on the page
1436
- */
1437
- static extractFormOverview(root) {
1438
- const forms = root.querySelectorAll('form');
1439
- return Array.from(forms).map((form) => {
1440
- const inputs = form.querySelectorAll('input, textarea, select');
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
- * Calculate summary statistics
1484
- */
1485
- static calculateSummary(root, regions, forms) {
1486
- const allInteractive = root.querySelectorAll('button, a[href], input, textarea, select');
1487
- const allSections = root.querySelectorAll('section, article, [role="region"]');
1488
- const hasModals = (regions.modals?.length || 0) > 0;
1489
- const errorSelectors = ['.error', '.alert-danger', '[role="alert"]'];
1490
- const hasErrors = errorSelectors.some((sel) => {
1491
- const element = root.querySelector(sel);
1492
- return element ? DOMTraversal.isVisible(element) : false;
1493
- });
1494
- const loadingSelectors = ['.loading', '.spinner', '[aria-busy="true"]'];
1495
- const isLoading = loadingSelectors.some((sel) => {
1496
- const element = root.querySelector(sel);
1497
- return element ? DOMTraversal.isVisible(element) : false;
1498
- });
1499
- const mainContentSelector = regions.main?.selector;
1500
- return {
1501
- totalInteractive: allInteractive.length,
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
- * Generate AI-friendly suggestions
1512
- */
1513
- static generateSuggestions(regions, summary) {
1514
- const suggestions = [];
1515
- if (summary.hasErrors) {
1516
- suggestions.push('Page has error indicators - check error messages before interacting');
1517
- }
1518
- if (summary.isLoading) {
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
- * Get text content with optional truncation
1539
- */
1540
- static getTextContent(element, maxLength) {
1541
- const text = element.textContent?.trim() || '';
1542
- if (maxLength && text.length > maxLength) {
1543
- return `${text.substring(0, maxLength)}...`;
1544
- }
1545
- return text;
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
- // src/types.ts
1552
- var init_types = __esm({
1553
- 'src/types.ts'() {},
1554
- });
1555
-
1556
- // src/index.ts
1557
- var index_exports = {};
1558
- __export(index_exports, {
1559
- ContentDetection: () => ContentDetection,
1560
- MarkdownFormatter: () => MarkdownFormatter,
1561
- ProgressiveExtractor: () => ProgressiveExtractor,
1562
- SelectorGenerator: () => SelectorGenerator,
1563
- SmartDOMReader: () => SmartDOMReader,
1564
- default: () => index_default,
1565
- });
1566
- var SmartDOMReader;
1567
- var index_default;
1568
- var init_index = __esm({
1569
- 'src/index.ts'() {
1570
- init_content_detection();
1571
- init_selectors();
1572
- init_traversal();
1573
- init_content_detection();
1574
- init_markdown_formatter();
1575
- init_progressive();
1576
- init_selectors();
1577
- init_types();
1578
- SmartDOMReader = class _SmartDOMReader {
1579
- options;
1580
- constructor(options = {}) {
1581
- this.options = {
1582
- mode: options.mode || 'interactive',
1583
- maxDepth: options.maxDepth || 5,
1584
- includeHidden: options.includeHidden || false,
1585
- includeShadowDOM: options.includeShadowDOM || true,
1586
- includeIframes: options.includeIframes || false,
1587
- viewportOnly: options.viewportOnly || false,
1588
- mainContentOnly: options.mainContentOnly || false,
1589
- customSelectors: options.customSelectors || [],
1590
- attributeTruncateLength: options.attributeTruncateLength,
1591
- dataAttributeTruncateLength: options.dataAttributeTruncateLength,
1592
- textTruncateLength: options.textTruncateLength,
1593
- filter: options.filter,
1594
- };
1595
- }
1596
- /**
1597
- * Main extraction method - extracts all data in one pass
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
- * Extract form information
1714
- */
1715
- extractForms(container, options) {
1716
- const forms = [];
1717
- const formElements = container.querySelectorAll('form');
1718
- formElements.forEach((form) => {
1719
- if (!this.shouldIncludeElement(form, options)) return;
1720
- const formInputs = [];
1721
- const formButtons = [];
1722
- const inputs = form.querySelectorAll(
1723
- 'input:not([type="button"]):not([type="submit"]), textarea, select'
1724
- );
1725
- inputs.forEach((input) => {
1726
- const extracted = DOMTraversal.extractElement(input, options);
1727
- if (extracted) formInputs.push(extracted);
1728
- });
1729
- const buttons = form.querySelectorAll(
1730
- 'button, input[type="button"], input[type="submit"]'
1731
- );
1732
- buttons.forEach((button) => {
1733
- const extracted = DOMTraversal.extractElement(button, options);
1734
- if (extracted) formButtons.push(extracted);
1735
- });
1736
- forms.push({
1737
- selector: SelectorGenerator.generateSelectors(form).css,
1738
- action: form.getAttribute('action') || void 0,
1739
- method: form.getAttribute('method') || void 0,
1740
- inputs: formInputs,
1741
- buttons: formButtons,
1742
- });
1743
- });
1744
- return forms;
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
- * Extract semantic elements (full mode only)
1748
- */
1749
- extractSemanticElements(container, options) {
1750
- const headings = [];
1751
- const images = [];
1752
- const tables = [];
1753
- const lists = [];
1754
- const articles = [];
1755
- container.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((el) => {
1756
- if (this.shouldIncludeElement(el, options)) {
1757
- const extracted = DOMTraversal.extractElement(el, options);
1758
- if (extracted) headings.push(extracted);
1759
- }
1760
- });
1761
- container.querySelectorAll('img').forEach((el) => {
1762
- if (this.shouldIncludeElement(el, options)) {
1763
- const extracted = DOMTraversal.extractElement(el, options);
1764
- if (extracted) images.push(extracted);
1765
- }
1766
- });
1767
- container.querySelectorAll('table').forEach((el) => {
1768
- if (this.shouldIncludeElement(el, options)) {
1769
- const extracted = DOMTraversal.extractElement(el, options);
1770
- if (extracted) tables.push(extracted);
1771
- }
1772
- });
1773
- container.querySelectorAll('ul, ol').forEach((el) => {
1774
- if (this.shouldIncludeElement(el, options)) {
1775
- const extracted = DOMTraversal.extractElement(el, options);
1776
- if (extracted) lists.push(extracted);
1777
- }
1778
- });
1779
- container.querySelectorAll('article, [role="article"]').forEach((el) => {
1780
- if (this.shouldIncludeElement(el, options)) {
1781
- const extracted = DOMTraversal.extractElement(el, options);
1782
- if (extracted) articles.push(extracted);
1783
- }
1784
- });
1785
- return {
1786
- headings,
1787
- images,
1788
- tables,
1789
- lists,
1790
- articles,
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
- * Extract metadata
1795
- */
1796
- extractMetadata(doc, container, options) {
1797
- const allElements = container.querySelectorAll('*');
1798
- const extractedElements = container.querySelectorAll(
1799
- 'button, a, input, textarea, select, h1, h2, h3, h4, h5, h6, img, table, ul, ol, article'
1800
- ).length;
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
- totalElements: allElements.length,
1803
- extractedElements,
1804
- mainContent:
1805
- options.mainContentOnly && container instanceof Element
1806
- ? SelectorGenerator.generateSelectors(container).css
1807
- : void 0,
1808
- language: doc.documentElement.getAttribute('lang') || void 0,
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
- * Check if element should be included based on options
1813
- */
1814
- shouldIncludeElement(element, options) {
1815
- if (!options.includeHidden && !DOMTraversal.isVisible(element)) {
1816
- return false;
1817
- }
1818
- if (options.viewportOnly && !DOMTraversal.isInViewport(element)) {
1819
- return false;
1820
- }
1821
- if (options.filter && !DOMTraversal.passesFilter(element, options.filter)) {
1822
- return false;
1823
- }
1824
- return true;
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
- index_default = SmartDOMReader;
1906
- },
1907
- });
1908
- init_index();
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
- index_default as default,
1531
+ SmartDOMReader as default,
1916
1532
  };
1917
- //# sourceMappingURL=index.js.map
1533
+
1534
+ //# sourceMappingURL=index.mjs.map