@detiq/crawler 1.0.5 → 1.0.7

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.
@@ -83,11 +83,18 @@ function extractMetadata(html, url) {
83
83
  ogImage: null,
84
84
  ogUrl: null,
85
85
  ogType: null,
86
+ ogSiteName: null,
87
+ ogLocale: null,
86
88
  twitterCard: null,
87
89
  twitterTitle: null,
88
90
  twitterDescription: null,
91
+ twitterImage: null,
92
+ twitterSite: null,
89
93
  canonical: null,
90
94
  robots: null,
95
+ hreflang: [],
96
+ themeColor: null,
97
+ favicon: null,
91
98
  jsonLd: [],
92
99
  schemaTypes: [],
93
100
  lang: null
@@ -107,14 +114,27 @@ function extractMetadata(html, url) {
107
114
  if (name === "twitter:card") meta.twitterCard = content;
108
115
  if (name === "twitter:title") meta.twitterTitle = content;
109
116
  if (name === "twitter:description") meta.twitterDescription = content;
117
+ if (name === "twitter:image") meta.twitterImage = content;
118
+ if (name === "twitter:site") meta.twitterSite = content;
119
+ if (name === "theme-color") meta.themeColor = content;
110
120
  if (prop === "og:title") meta.ogTitle = content;
111
121
  if (prop === "og:description") meta.ogDescription = content;
112
122
  if (prop === "og:image") meta.ogImage = content;
113
123
  if (prop === "og:url") meta.ogUrl = content;
114
124
  if (prop === "og:type") meta.ogType = content;
125
+ if (prop === "og:site_name") meta.ogSiteName = content;
126
+ if (prop === "og:locale") meta.ogLocale = content;
115
127
  }
116
128
  const canonical = document.querySelector('link[rel="canonical"]');
117
129
  if (canonical) meta.canonical = canonical.getAttribute("href") ?? null;
130
+ const faviconEl = document.querySelector('link[rel="icon"], link[rel="shortcut icon"]');
131
+ if (faviconEl) meta.favicon = faviconEl.getAttribute("href") ?? null;
132
+ const hreflangEls = [...document.querySelectorAll('link[rel="alternate"][hreflang]')];
133
+ for (const el of hreflangEls) {
134
+ const lang = el.getAttribute("hreflang") ?? "";
135
+ const href = el.getAttribute("href") ?? "";
136
+ if (lang && href) meta.hreflang.push({ lang, href });
137
+ }
118
138
  const jsonLdScripts = [...document.querySelectorAll('script[type="application/ld+json"]')];
119
139
  for (const s of jsonLdScripts) {
120
140
  try {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/extractors/advanced.ts"],
4
- "sourcesContent": ["import { parseHTML } from 'linkedom';\n\nexport interface ExtractionResult {\n matches: string[];\n count: number;\n method: string;\n}\n\nexport type TableRow = Record<string, string>;\n\nexport interface PageMetadata {\n title: string | null;\n description: string | null;\n keywords: string | null;\n ogTitle: string | null;\n ogDescription: string | null;\n ogImage: string | null;\n ogUrl: string | null;\n ogType: string | null;\n twitterCard: string | null;\n twitterTitle: string | null;\n twitterDescription: string | null;\n canonical: string | null;\n robots: string | null;\n jsonLd: object[];\n schemaTypes: string[];\n lang: string | null;\n}\n\nexport function extractByCssSelector(html: string, selector: string, attr?: string): ExtractionResult {\n try {\n const { document } = parseHTML(html);\n const nodes = Array.from(document.querySelectorAll(selector));\n const matches = nodes.map((n: any) => {\n if (attr) return (n as any).getAttribute(attr) ?? '';\n return (n as any).textContent?.trim() ?? '';\n }).filter(Boolean);\n return { matches, count: matches.length, method: 'css-selector' };\n } catch (e) {\n return { matches: [], count: 0, method: 'css-selector-error' };\n }\n}\n\nexport function extractByXPath(html: string, xpath: string): ExtractionResult {\n try {\n const { document, XPathResult } = parseHTML(html) as any;\n const result = document.evaluate(xpath, document, null, XPathResult?.ANY_TYPE ?? 0, null);\n const matches: string[] = [];\n let node = result.iterateNext?.();\n while (node) {\n const text = (node as any).textContent?.trim() ?? (node as any).nodeValue?.trim() ?? '';\n if (text) matches.push(text);\n node = result.iterateNext?.();\n }\n return { matches, count: matches.length, method: 'xpath' };\n } catch (e) {\n return { matches: [], count: 0, method: 'xpath-error' };\n }\n}\n\nexport function extractByRegex(text: string, pattern: string, flags?: string): ExtractionResult {\n try {\n const re = new RegExp(pattern, (flags ?? 'g').includes('g') ? flags : (flags ?? '') + 'g');\n const matches: string[] = [];\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n matches.push(m[1] ?? m[0]);\n if (!re.global) break;\n }\n return { matches, count: matches.length, method: 'regex' };\n } catch {\n return { matches: [], count: 0, method: 'regex-error' };\n }\n}\n\nexport function extractTables(html: string): TableRow[][] {\n try {\n const { document } = parseHTML(html);\n const tables = Array.from(document.querySelectorAll('table'));\n return tables.map((table: any) => {\n const headers: string[] = [];\n const rows: TableRow[] = [];\n const ths = [...table.querySelectorAll('thead th, thead td, tr:first-child th')];\n if (ths.length > 0) {\n ths.forEach((th: any, i: number) => headers.push(th.textContent?.trim() || `col${i + 1}`));\n }\n const trs = [...table.querySelectorAll('tbody tr, tr')];\n for (const tr of trs) {\n const cells = [...(tr as any).querySelectorAll('td, th')];\n if (cells.length === 0) continue;\n if (headers.length === 0) {\n cells.forEach((_: any, i: number) => headers.push(`col${i + 1}`));\n }\n const row: TableRow = {};\n cells.forEach((cell: any, i: number) => {\n const key = headers[i] || `col${i + 1}`;\n row[key] = cell.textContent?.trim() ?? '';\n });\n if (Object.values(row).some(v => v)) rows.push(row);\n }\n return rows;\n }).filter(t => t.length > 0);\n } catch {\n return [];\n }\n}\n\nexport function extractMetadata(html: string, url: string): PageMetadata {\n const meta: PageMetadata = {\n title: null, description: null, keywords: null,\n ogTitle: null, ogDescription: null, ogImage: null, ogUrl: null, ogType: null,\n twitterCard: null, twitterTitle: null, twitterDescription: null,\n canonical: null, robots: null, jsonLd: [], schemaTypes: [], lang: null,\n };\n try {\n const { document } = parseHTML(html);\n meta.title = (document as any).title ?? (document as any).querySelector('title')?.textContent?.trim() ?? null;\n meta.lang = (document as any).documentElement?.getAttribute('lang') ?? null;\n\n const metas = [...(document as any).querySelectorAll('meta')];\n for (const m of metas) {\n const name = (m as any).getAttribute('name')?.toLowerCase() ?? '';\n const prop = (m as any).getAttribute('property')?.toLowerCase() ?? '';\n const content = (m as any).getAttribute('content') ?? '';\n if (name === 'description') meta.description = content;\n if (name === 'keywords') meta.keywords = content;\n if (name === 'robots') meta.robots = content;\n if (name === 'twitter:card') meta.twitterCard = content;\n if (name === 'twitter:title') meta.twitterTitle = content;\n if (name === 'twitter:description') meta.twitterDescription = content;\n if (prop === 'og:title') meta.ogTitle = content;\n if (prop === 'og:description') meta.ogDescription = content;\n if (prop === 'og:image') meta.ogImage = content;\n if (prop === 'og:url') meta.ogUrl = content;\n if (prop === 'og:type') meta.ogType = content;\n }\n\n const canonical = (document as any).querySelector('link[rel=\"canonical\"]');\n if (canonical) meta.canonical = canonical.getAttribute('href') ?? null;\n\n const jsonLdScripts = [...(document as any).querySelectorAll('script[type=\"application/ld+json\"]')];\n for (const s of jsonLdScripts) {\n try {\n const parsed = JSON.parse((s as any).textContent ?? '{}');\n meta.jsonLd.push(parsed);\n const types = Array.isArray(parsed) ? parsed.map((p: any) => p['@type']).flat() : [parsed['@type']];\n meta.schemaTypes.push(...types.filter(Boolean));\n } catch { /* skip invalid JSON-LD */ }\n }\n } catch { /* non-fatal */ }\n return meta;\n}\n\nexport interface ProductData {\n name: string | null;\n description: string | null;\n brand: string | null;\n sku: string | null;\n price: number | null;\n priceCurrency: string | null;\n availability: string | null;\n rating: number | null;\n reviewCount: number | null;\n image: string | null;\n url: string | null;\n}\n\nexport function extractStructuredProduct(jsonLd: object[]): ProductData | null {\n for (const item of jsonLd) {\n try {\n const type = (item as any)['@type'];\n const isProduct = Array.isArray(type)\n ? type.some((t: any) => typeof t === 'string' && t.toLowerCase() === 'product')\n : typeof type === 'string' && type.toLowerCase() === 'product';\n if (!isProduct) continue;\n\n const i = item as any;\n const offers = Array.isArray(i.offers) ? i.offers[0] : i.offers;\n\n const rawPrice = offers?.price;\n const price = rawPrice != null ? parseFloat(rawPrice) : NaN;\n\n const rawRating = i.aggregateRating?.ratingValue;\n const rating = rawRating != null ? parseFloat(rawRating) : NaN;\n\n const rawReviewCount = i.aggregateRating?.reviewCount;\n const reviewCount = rawReviewCount != null ? parseInt(rawReviewCount, 10) : NaN;\n\n const rawAvailability: string | undefined = offers?.availability;\n const availability = rawAvailability\n ? rawAvailability.replace(/^https?:\\/\\/schema\\.org\\//, '')\n : null;\n\n const rawImage = i.image;\n const image = Array.isArray(rawImage) ? (rawImage[0] ?? null) : (rawImage ?? null);\n\n const brand = i.brand?.name ?? i.brand ?? null;\n\n return {\n name: i.name ?? null,\n description: i.description ?? null,\n brand: typeof brand === 'string' ? brand : null,\n sku: i.sku ?? null,\n price: isNaN(price) ? null : price,\n priceCurrency: offers?.priceCurrency ?? null,\n availability,\n rating: isNaN(rating) ? null : rating,\n reviewCount: isNaN(reviewCount) ? null : reviewCount,\n image,\n url: i.url ?? null,\n };\n } catch { /* skip malformed item */ }\n }\n return null;\n}\n\nexport function extractImages(html: string, baseUrl: string): Array<{ src: string; alt: string; width?: number; height?: number }> {\n try {\n const { document } = parseHTML(html);\n const base = new URL(baseUrl);\n return Array.from((document as any).querySelectorAll('img'))\n .map((img: any) => {\n const src = img.getAttribute('src') ?? img.getAttribute('data-src') ?? '';\n if (!src) return null;\n try {\n return {\n src: new URL(src, base).href,\n alt: (img.getAttribute('alt') ?? '') as string,\n width: img.getAttribute('width') ? parseInt(img.getAttribute('width')) : undefined,\n height: img.getAttribute('height') ? parseInt(img.getAttribute('height')) : undefined,\n } as { src: string; alt: string; width?: number; height?: number };\n } catch { return null; }\n })\n .filter((x): x is { src: string; alt: string; width?: number; height?: number } => x !== null);\n } catch {\n return [];\n }\n}\n\nexport function extractMediaFiles(html: string, baseUrl: string): Array<{ url: string; type: 'image' | 'video' | 'audio' | 'document'; tag: string }> {\n const IMAGE_EXT = /\\.(png|jpe?g|gif|webp|svg|ico|avif)(\\?.*)?$/i;\n const VIDEO_EXT = /\\.(mp4|webm|ogv|mov|avi)(\\?.*)?$/i;\n const AUDIO_EXT = /\\.(mp3|ogg|wav|flac|aac)(\\?.*)?$/i;\n const DOC_EXT = /\\.(pdf|docx?|xlsx?|pptx?|csv|zip)(\\?.*)?$/i;\n\n function classify(url: string, tag: string): 'image' | 'video' | 'audio' | 'document' {\n if (tag === 'img' || IMAGE_EXT.test(url)) return 'image';\n if (tag === 'video' || VIDEO_EXT.test(url)) return 'video';\n if (tag === 'audio' || AUDIO_EXT.test(url)) return 'audio';\n if (DOC_EXT.test(url)) return 'document';\n return 'document';\n }\n\n try {\n const { document } = parseHTML(html);\n const base = new URL(baseUrl);\n const results: Array<{ url: string; type: 'image' | 'video' | 'audio' | 'document'; tag: string }> = [];\n const seen = new Set<string>();\n\n const selectors: [string, string] = ['img[src],video[src],audio[src],source[src]', 'src'];\n const attrPairs: Array<[string, string]> = [\n ['img', 'src'], ['video', 'src'], ['audio', 'src'], ['source', 'src'],\n ['a', 'href'],\n ];\n\n for (const [sel, attr] of attrPairs) {\n for (const el of [...(document as any).querySelectorAll(sel)]) {\n const raw = (el as any).getAttribute(attr) ?? '';\n if (!raw) continue;\n try {\n const resolved = new URL(raw, base).href;\n if (seen.has(resolved)) continue;\n if (sel === 'a' && !DOC_EXT.test(resolved) && !IMAGE_EXT.test(resolved) && !VIDEO_EXT.test(resolved) && !AUDIO_EXT.test(resolved)) continue;\n seen.add(resolved);\n results.push({ url: resolved, type: classify(resolved, sel), tag: sel });\n } catch { /* skip */ }\n }\n }\n return results;\n } catch {\n return [];\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { parseHTML } from 'linkedom';\n\nexport interface ExtractionResult {\n matches: string[];\n count: number;\n method: string;\n}\n\nexport type TableRow = Record<string, string>;\n\nexport interface PageMetadata {\n title: string | null;\n description: string | null;\n keywords: string | null;\n ogTitle: string | null;\n ogDescription: string | null;\n ogImage: string | null;\n ogUrl: string | null;\n ogType: string | null;\n ogSiteName: string | null;\n ogLocale: string | null;\n twitterCard: string | null;\n twitterTitle: string | null;\n twitterDescription: string | null;\n twitterImage: string | null;\n twitterSite: string | null;\n canonical: string | null;\n robots: string | null;\n hreflang: Array<{ lang: string; href: string }>;\n themeColor: string | null;\n favicon: string | null;\n jsonLd: object[];\n schemaTypes: string[];\n lang: string | null;\n}\n\nexport function extractByCssSelector(html: string, selector: string, attr?: string): ExtractionResult {\n try {\n const { document } = parseHTML(html);\n const nodes = Array.from(document.querySelectorAll(selector));\n const matches = nodes.map((n: any) => {\n if (attr) return (n as any).getAttribute(attr) ?? '';\n return (n as any).textContent?.trim() ?? '';\n }).filter(Boolean);\n return { matches, count: matches.length, method: 'css-selector' };\n } catch (e) {\n return { matches: [], count: 0, method: 'css-selector-error' };\n }\n}\n\nexport function extractByXPath(html: string, xpath: string): ExtractionResult {\n try {\n const { document, XPathResult } = parseHTML(html) as any;\n const result = document.evaluate(xpath, document, null, XPathResult?.ANY_TYPE ?? 0, null);\n const matches: string[] = [];\n let node = result.iterateNext?.();\n while (node) {\n const text = (node as any).textContent?.trim() ?? (node as any).nodeValue?.trim() ?? '';\n if (text) matches.push(text);\n node = result.iterateNext?.();\n }\n return { matches, count: matches.length, method: 'xpath' };\n } catch (e) {\n return { matches: [], count: 0, method: 'xpath-error' };\n }\n}\n\nexport function extractByRegex(text: string, pattern: string, flags?: string): ExtractionResult {\n try {\n const re = new RegExp(pattern, (flags ?? 'g').includes('g') ? flags : (flags ?? '') + 'g');\n const matches: string[] = [];\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n matches.push(m[1] ?? m[0]);\n if (!re.global) break;\n }\n return { matches, count: matches.length, method: 'regex' };\n } catch {\n return { matches: [], count: 0, method: 'regex-error' };\n }\n}\n\nexport function extractTables(html: string): TableRow[][] {\n try {\n const { document } = parseHTML(html);\n const tables = Array.from(document.querySelectorAll('table'));\n return tables.map((table: any) => {\n const headers: string[] = [];\n const rows: TableRow[] = [];\n const ths = [...table.querySelectorAll('thead th, thead td, tr:first-child th')];\n if (ths.length > 0) {\n ths.forEach((th: any, i: number) => headers.push(th.textContent?.trim() || `col${i + 1}`));\n }\n const trs = [...table.querySelectorAll('tbody tr, tr')];\n for (const tr of trs) {\n const cells = [...(tr as any).querySelectorAll('td, th')];\n if (cells.length === 0) continue;\n if (headers.length === 0) {\n cells.forEach((_: any, i: number) => headers.push(`col${i + 1}`));\n }\n const row: TableRow = {};\n cells.forEach((cell: any, i: number) => {\n const key = headers[i] || `col${i + 1}`;\n row[key] = cell.textContent?.trim() ?? '';\n });\n if (Object.values(row).some(v => v)) rows.push(row);\n }\n return rows;\n }).filter(t => t.length > 0);\n } catch {\n return [];\n }\n}\n\nexport function extractMetadata(html: string, url: string): PageMetadata {\n const meta: PageMetadata = {\n title: null, description: null, keywords: null,\n ogTitle: null, ogDescription: null, ogImage: null, ogUrl: null, ogType: null,\n ogSiteName: null, ogLocale: null,\n twitterCard: null, twitterTitle: null, twitterDescription: null,\n twitterImage: null, twitterSite: null,\n canonical: null, robots: null,\n hreflang: [], themeColor: null, favicon: null,\n jsonLd: [], schemaTypes: [], lang: null,\n };\n try {\n const { document } = parseHTML(html);\n meta.title = (document as any).title ?? (document as any).querySelector('title')?.textContent?.trim() ?? null;\n meta.lang = (document as any).documentElement?.getAttribute('lang') ?? null;\n\n const metas = [...(document as any).querySelectorAll('meta')];\n for (const m of metas) {\n const name = (m as any).getAttribute('name')?.toLowerCase() ?? '';\n const prop = (m as any).getAttribute('property')?.toLowerCase() ?? '';\n const content = (m as any).getAttribute('content') ?? '';\n if (name === 'description') meta.description = content;\n if (name === 'keywords') meta.keywords = content;\n if (name === 'robots') meta.robots = content;\n if (name === 'twitter:card') meta.twitterCard = content;\n if (name === 'twitter:title') meta.twitterTitle = content;\n if (name === 'twitter:description') meta.twitterDescription = content;\n if (name === 'twitter:image') meta.twitterImage = content;\n if (name === 'twitter:site') meta.twitterSite = content;\n if (name === 'theme-color') meta.themeColor = content;\n if (prop === 'og:title') meta.ogTitle = content;\n if (prop === 'og:description') meta.ogDescription = content;\n if (prop === 'og:image') meta.ogImage = content;\n if (prop === 'og:url') meta.ogUrl = content;\n if (prop === 'og:type') meta.ogType = content;\n if (prop === 'og:site_name') meta.ogSiteName = content;\n if (prop === 'og:locale') meta.ogLocale = content;\n }\n\n const canonical = (document as any).querySelector('link[rel=\"canonical\"]');\n if (canonical) meta.canonical = canonical.getAttribute('href') ?? null;\n\n const faviconEl = (document as any).querySelector('link[rel=\"icon\"], link[rel=\"shortcut icon\"]');\n if (faviconEl) meta.favicon = faviconEl.getAttribute('href') ?? null;\n\n const hreflangEls = [...(document as any).querySelectorAll('link[rel=\"alternate\"][hreflang]')];\n for (const el of hreflangEls) {\n const lang = (el as any).getAttribute('hreflang') ?? '';\n const href = (el as any).getAttribute('href') ?? '';\n if (lang && href) meta.hreflang.push({ lang, href });\n }\n\n const jsonLdScripts = [...(document as any).querySelectorAll('script[type=\"application/ld+json\"]')];\n for (const s of jsonLdScripts) {\n try {\n const parsed = JSON.parse((s as any).textContent ?? '{}');\n meta.jsonLd.push(parsed);\n const types = Array.isArray(parsed) ? parsed.map((p: any) => p['@type']).flat() : [parsed['@type']];\n meta.schemaTypes.push(...types.filter(Boolean));\n } catch { /* skip invalid JSON-LD */ }\n }\n } catch { /* non-fatal */ }\n return meta;\n}\n\nexport interface ProductData {\n name: string | null;\n description: string | null;\n brand: string | null;\n sku: string | null;\n price: number | null;\n priceCurrency: string | null;\n availability: string | null;\n rating: number | null;\n reviewCount: number | null;\n image: string | null;\n url: string | null;\n}\n\nexport function extractStructuredProduct(jsonLd: object[]): ProductData | null {\n for (const item of jsonLd) {\n try {\n const type = (item as any)['@type'];\n const isProduct = Array.isArray(type)\n ? type.some((t: any) => typeof t === 'string' && t.toLowerCase() === 'product')\n : typeof type === 'string' && type.toLowerCase() === 'product';\n if (!isProduct) continue;\n\n const i = item as any;\n const offers = Array.isArray(i.offers) ? i.offers[0] : i.offers;\n\n const rawPrice = offers?.price;\n const price = rawPrice != null ? parseFloat(rawPrice) : NaN;\n\n const rawRating = i.aggregateRating?.ratingValue;\n const rating = rawRating != null ? parseFloat(rawRating) : NaN;\n\n const rawReviewCount = i.aggregateRating?.reviewCount;\n const reviewCount = rawReviewCount != null ? parseInt(rawReviewCount, 10) : NaN;\n\n const rawAvailability: string | undefined = offers?.availability;\n const availability = rawAvailability\n ? rawAvailability.replace(/^https?:\\/\\/schema\\.org\\//, '')\n : null;\n\n const rawImage = i.image;\n const image = Array.isArray(rawImage) ? (rawImage[0] ?? null) : (rawImage ?? null);\n\n const brand = i.brand?.name ?? i.brand ?? null;\n\n return {\n name: i.name ?? null,\n description: i.description ?? null,\n brand: typeof brand === 'string' ? brand : null,\n sku: i.sku ?? null,\n price: isNaN(price) ? null : price,\n priceCurrency: offers?.priceCurrency ?? null,\n availability,\n rating: isNaN(rating) ? null : rating,\n reviewCount: isNaN(reviewCount) ? null : reviewCount,\n image,\n url: i.url ?? null,\n };\n } catch { /* skip malformed item */ }\n }\n return null;\n}\n\nexport function extractImages(html: string, baseUrl: string): Array<{ src: string; alt: string; width?: number; height?: number }> {\n try {\n const { document } = parseHTML(html);\n const base = new URL(baseUrl);\n return Array.from((document as any).querySelectorAll('img'))\n .map((img: any) => {\n const src = img.getAttribute('src') ?? img.getAttribute('data-src') ?? '';\n if (!src) return null;\n try {\n return {\n src: new URL(src, base).href,\n alt: (img.getAttribute('alt') ?? '') as string,\n width: img.getAttribute('width') ? parseInt(img.getAttribute('width')) : undefined,\n height: img.getAttribute('height') ? parseInt(img.getAttribute('height')) : undefined,\n } as { src: string; alt: string; width?: number; height?: number };\n } catch { return null; }\n })\n .filter((x): x is { src: string; alt: string; width?: number; height?: number } => x !== null);\n } catch {\n return [];\n }\n}\n\nexport function extractMediaFiles(html: string, baseUrl: string): Array<{ url: string; type: 'image' | 'video' | 'audio' | 'document'; tag: string }> {\n const IMAGE_EXT = /\\.(png|jpe?g|gif|webp|svg|ico|avif)(\\?.*)?$/i;\n const VIDEO_EXT = /\\.(mp4|webm|ogv|mov|avi)(\\?.*)?$/i;\n const AUDIO_EXT = /\\.(mp3|ogg|wav|flac|aac)(\\?.*)?$/i;\n const DOC_EXT = /\\.(pdf|docx?|xlsx?|pptx?|csv|zip)(\\?.*)?$/i;\n\n function classify(url: string, tag: string): 'image' | 'video' | 'audio' | 'document' {\n if (tag === 'img' || IMAGE_EXT.test(url)) return 'image';\n if (tag === 'video' || VIDEO_EXT.test(url)) return 'video';\n if (tag === 'audio' || AUDIO_EXT.test(url)) return 'audio';\n if (DOC_EXT.test(url)) return 'document';\n return 'document';\n }\n\n try {\n const { document } = parseHTML(html);\n const base = new URL(baseUrl);\n const results: Array<{ url: string; type: 'image' | 'video' | 'audio' | 'document'; tag: string }> = [];\n const seen = new Set<string>();\n\n const selectors: [string, string] = ['img[src],video[src],audio[src],source[src]', 'src'];\n const attrPairs: Array<[string, string]> = [\n ['img', 'src'], ['video', 'src'], ['audio', 'src'], ['source', 'src'],\n ['a', 'href'],\n ];\n\n for (const [sel, attr] of attrPairs) {\n for (const el of [...(document as any).querySelectorAll(sel)]) {\n const raw = (el as any).getAttribute(attr) ?? '';\n if (!raw) continue;\n try {\n const resolved = new URL(raw, base).href;\n if (seen.has(resolved)) continue;\n if (sel === 'a' && !DOC_EXT.test(resolved) && !IMAGE_EXT.test(resolved) && !VIDEO_EXT.test(resolved) && !AUDIO_EXT.test(resolved)) continue;\n seen.add(resolved);\n results.push({ url: resolved, type: classify(resolved, sel), tag: sel });\n } catch { /* skip */ }\n }\n }\n return results;\n } catch {\n return [];\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -106,7 +106,7 @@ async function findModalTriggers(page) {
106
106
  p = p.parentElement;
107
107
  }
108
108
  return true;
109
- }).slice(0, 8).map((el) => {
109
+ }).slice(0, 20).map((el) => {
110
110
  const label = el.innerText?.trim() || el.getAttribute("aria-label") || "button";
111
111
  return { selector: `[aria-label="${label}"]`, label: label.slice(0, 60) };
112
112
  });
@@ -161,7 +161,20 @@ async function probeScreen(page, screenId, url, elements) {
161
161
  await submitBtn.click({ timeout: 2e3 });
162
162
  await page.waitForTimeout(1500);
163
163
  const errors = await collectErrors(page);
164
- for (const errorText of errors) {
164
+ const ariaFieldErrors = await page.evaluate(
165
+ () => Array.from(document.querySelectorAll('[aria-invalid="true"]')).map((el) => {
166
+ const describedBy = el.getAttribute("aria-describedby");
167
+ const errorEl = describedBy ? document.getElementById(describedBy) : null;
168
+ const label = el.getAttribute("aria-label") || el.getAttribute("placeholder") || el.name || "";
169
+ return { label, errorText: errorEl?.textContent?.trim() || "" };
170
+ }).filter((f) => f.errorText)
171
+ );
172
+ const ariaMatched = /* @__PURE__ */ new Set();
173
+ for (const af of ariaFieldErrors) {
174
+ ariaMatched.add(af.errorText);
175
+ result.validationErrors.push({ field: af.label || "unknown", trigger: "empty_submit", errorText: af.errorText.slice(0, 150) });
176
+ }
177
+ for (const errorText of errors.filter((e) => !ariaMatched.has(e))) {
165
178
  const matchedField = formInputs.find(
166
179
  (el) => errorText.toLowerCase().includes(el.meaning.toLowerCase().split(" ")[0]) || el.meaning.toLowerCase().includes(errorText.toLowerCase().split(" ")[0])
167
180
  );
@@ -240,6 +253,28 @@ async function probeScreen(page, screenId, url, elements) {
240
253
  content: dialogText.content,
241
254
  fields: dialogText.fields.slice(0, 10)
242
255
  });
256
+ } else {
257
+ const accordionText = await page.evaluate(() => {
258
+ const panels = Array.from(document.querySelectorAll(
259
+ '[aria-expanded="true"] + *, details[open] > :not(summary), [data-state="open"]:not([role="dialog"]):not([class*="modal" i]), [data-headlessui-state="open"] > *'
260
+ )).filter((el) => {
261
+ const s = window.getComputedStyle(el);
262
+ return s.display !== "none" && s.visibility !== "hidden";
263
+ });
264
+ if (panels.length === 0) return null;
265
+ const panel = panels[0];
266
+ const heading = panel.querySelector("h2,h3,h4")?.textContent?.replace(/\s+/g, " ").trim() || "";
267
+ const content = panel.innerText?.replace(/\s+/g, " ").trim().slice(0, 400) || "";
268
+ return content.length > 15 ? { heading, content } : null;
269
+ });
270
+ if (accordionText) {
271
+ result.modals.push({
272
+ triggerElement: trigger.label,
273
+ title: accordionText.heading || `${trigger.label} (revealed)`,
274
+ content: accordionText.content,
275
+ fields: []
276
+ });
277
+ }
243
278
  }
244
279
  await page.keyboard.press("Escape");
245
280
  await page.waitForTimeout(400);
@@ -257,6 +292,34 @@ async function probeScreen(page, screenId, url, elements) {
257
292
  result.conditionals.push(...conditionals);
258
293
  } catch {
259
294
  }
295
+ try {
296
+ result.tabPanels = await probeTabPanels(page);
297
+ } catch {
298
+ }
299
+ try {
300
+ result.tooltips = await probeTooltips(page);
301
+ } catch {
302
+ }
303
+ try {
304
+ result.carouselSlides = await probeCarousels(page);
305
+ } catch {
306
+ }
307
+ try {
308
+ result.wizardSteps = await probeWizardSteps(page);
309
+ } catch {
310
+ }
311
+ try {
312
+ result.focusTraps = await detectFocusTraps(page);
313
+ } catch {
314
+ }
315
+ try {
316
+ result.skipNavIssues = await probeSkipNavigation(page);
317
+ } catch {
318
+ }
319
+ try {
320
+ result.contextMenus = await probeContextMenus(page);
321
+ } catch {
322
+ }
260
323
  return result;
261
324
  }
262
325
  function probeResultToObservations(result) {
@@ -276,6 +339,27 @@ function probeResultToObservations(result) {
276
339
  for (const lp of result.loadingPatterns) {
277
340
  obs.push({ elementMeaning: "(submit button)", observationType: "LOADING_STATE", observedText: lp, triggeredBy: "form_submit" });
278
341
  }
342
+ for (const tp of result.tabPanels ?? []) {
343
+ obs.push({ elementMeaning: tp.tabLabel, observationType: "TAB_PANEL", observedText: tp.panelContent, triggeredBy: `tab_click:${tp.tabLabel}` });
344
+ }
345
+ for (const tt of result.tooltips ?? []) {
346
+ obs.push({ elementMeaning: tt.triggerLabel, observationType: "TOOLTIP", observedText: tt.tooltipText, triggeredBy: "hover_or_title" });
347
+ }
348
+ for (const cs of result.carouselSlides ?? []) {
349
+ obs.push({ elementMeaning: `Slide ${cs.slideIndex + 1}`, observationType: "CAROUSEL_SLIDE", observedText: cs.content, triggeredBy: `carousel_next:${cs.slideIndex}` });
350
+ }
351
+ for (const ws of result.wizardSteps ?? []) {
352
+ obs.push({ elementMeaning: ws.stepLabel ?? `Step ${ws.stepIndex + 1}`, observationType: "WIZARD_STEP", observedText: ws.content, triggeredBy: `wizard_next:${ws.stepIndex}` });
353
+ }
354
+ for (const ft of result.focusTraps ?? []) {
355
+ obs.push({ elementMeaning: ft.modalTrigger, observationType: "FOCUS_TRAP", observedText: ft.focusTrapped ? "Focus correctly trapped inside dialog" : "FAIL: focus escaped dialog \u2014 WCAG 2.1.2 violation", triggeredBy: "tab_key_sequence" });
356
+ }
357
+ for (const issue of result.skipNavIssues ?? []) {
358
+ obs.push({ elementMeaning: "(page)", observationType: "SKIP_NAV_ISSUE", observedText: issue, triggeredBy: "skip_nav_audit" });
359
+ }
360
+ for (const cm of result.contextMenus ?? []) {
361
+ obs.push({ elementMeaning: cm.trigger, observationType: "CONTEXT_MENU", observedText: cm.items.join(" | "), triggeredBy: "right_click" });
362
+ }
279
363
  return obs;
280
364
  }
281
365
  function probeResultToMarkdown(result) {
@@ -302,6 +386,39 @@ function probeResultToMarkdown(result) {
302
386
  if (m.fields.length > 0) lines.push(` - Fields: ${m.fields.join(", ")}`);
303
387
  }
304
388
  }
389
+ if ((result.tabPanels ?? []).length > 0) {
390
+ lines.push("\n### Tab Panel Contents (observed)");
391
+ for (const tp of result.tabPanels) {
392
+ lines.push(`- **Tab: ${tp.tabLabel}**${tp.panelHeading ? ` \u2014 ${tp.panelHeading}` : ""}: ${tp.panelContent.slice(0, 200)}`);
393
+ }
394
+ }
395
+ if ((result.tooltips ?? []).length > 0) {
396
+ lines.push("\n### Tooltips (observed)");
397
+ for (const tt of result.tooltips) {
398
+ lines.push(`- **${tt.triggerLabel}**: ${tt.tooltipText}`);
399
+ }
400
+ }
401
+ if ((result.carouselSlides ?? []).length > 0) {
402
+ lines.push("\n### Carousel Slides (observed)");
403
+ for (const cs of result.carouselSlides) {
404
+ lines.push(`- **Slide ${cs.slideIndex + 1}**${cs.heading ? ` \u2014 ${cs.heading}` : ""}: ${cs.content.slice(0, 200)}`);
405
+ }
406
+ }
407
+ if ((result.wizardSteps ?? []).length > 0) {
408
+ lines.push("\n### Wizard / Multi-Step Form (observed)");
409
+ for (const ws of result.wizardSteps) {
410
+ const label = ws.stepLabel ? ` \u2014 ${ws.stepLabel}` : "";
411
+ lines.push(`- **Step ${ws.stepIndex + 1}${label}**: ${ws.content.slice(0, 200)}`);
412
+ if (ws.fields.length > 0) lines.push(` - Fields: ${ws.fields.join(", ")}`);
413
+ }
414
+ }
415
+ if ((result.focusTraps ?? []).length > 0) {
416
+ lines.push("\n### Focus-Trap Audit (observed)");
417
+ for (const ft of result.focusTraps) {
418
+ const verdict = ft.focusTrapped ? "\u2705 PASS" : "\u274C FAIL (WCAG 2.1.2)";
419
+ lines.push(`- **${ft.modalTrigger}**: ${verdict} \u2014 focused: [${ft.sampledFocused.slice(0, 4).join(", ")}]`);
420
+ }
421
+ }
305
422
  if (result.conditionals.length > 0) {
306
423
  lines.push("\n### Conditional UI (observed)");
307
424
  for (const c of result.conditionals) {
@@ -383,6 +500,221 @@ ${interactionChunks.join("\n\n")}`;
383
500
  await onProgress?.(98, "Interactive probe complete");
384
501
  console.log(`[probe] Probed ${probeable.length} screens. Errors: ${errorCatalogueChunks.length}, Interactions: ${interactionChunks.length}`);
385
502
  }
503
+ async function probeSkipNavigation(page) {
504
+ const issues = [];
505
+ try {
506
+ const skipLinks = await page.evaluate(
507
+ () => Array.from(document.querySelectorAll('a[href^="#"]')).filter((el) => /skip|jump to|main content|content|nav/i.test(el.innerText?.toLowerCase().trim() || "")).map((el) => ({
508
+ text: el.innerText?.trim() || "",
509
+ href: el.getAttribute("href") || "",
510
+ hasTarget: !!document.querySelector(el.getAttribute("href") || "#__invalid__")
511
+ }))
512
+ );
513
+ if (skipLinks.length === 0) {
514
+ issues.push("No skip navigation links found (WCAG 2.4.1 \u2014 keyboard users cannot bypass nav blocks)");
515
+ }
516
+ for (const link of skipLinks) {
517
+ if (!link.hasTarget) issues.push(`Skip link "${link.text}" \u2192 "${link.href}" target not found in DOM`);
518
+ }
519
+ } catch {
520
+ }
521
+ return issues;
522
+ }
523
+ async function probeContextMenus(page) {
524
+ const results = [];
525
+ try {
526
+ const targets = await page.$$('canvas, [data-contextmenu], [oncontextmenu], [class*="chart" i]:not(script)');
527
+ for (const t of targets.slice(0, 4)) {
528
+ try {
529
+ await t.click({ button: "right", timeout: 1e3 });
530
+ await page.waitForTimeout(350);
531
+ const items = await page.evaluate(
532
+ () => Array.from(document.querySelectorAll('[role="menu"] [role="menuitem"], .context-menu-item, [class*="contextmenu" i] li')).map((el) => el.innerText?.trim()).filter(Boolean).slice(0, 12)
533
+ );
534
+ if (items.length > 0) {
535
+ const trigger = await t.evaluate((el) => el.getAttribute("aria-label") || el.className.slice(0, 40) || el.tagName).catch(() => "");
536
+ results.push({ trigger, items });
537
+ }
538
+ await page.keyboard.press("Escape").catch(() => {
539
+ });
540
+ } catch {
541
+ }
542
+ }
543
+ } catch {
544
+ }
545
+ return results;
546
+ }
547
+ async function probeCarousels(page) {
548
+ const results = [];
549
+ try {
550
+ const CAROUSEL_SEL = '[class*="carousel" i], [class*="slider" i], [class*="swiper" i], [role="region"][aria-roledescription*="carousel" i]';
551
+ const hasCarousel = await page.evaluate((sel) => !!document.querySelector(sel), CAROUSEL_SEL);
552
+ if (!hasCarousel) return results;
553
+ const NEXT_SEL = '[aria-label*="next" i], [aria-label*="Next"], [class*="next" i]:not([class*="footnext"]):not(a), button[class*="arrow-right" i], button[class*="chevron-right" i]';
554
+ const slide0 = await page.evaluate((carSel) => {
555
+ const car = document.querySelector(carSel);
556
+ if (!car) return null;
557
+ const active = car.querySelector('[aria-hidden="false"], .active, [class*="active" i], [class*="current" i]');
558
+ const target = active ?? car;
559
+ return { heading: target.querySelector("h2,h3,h4")?.textContent?.trim() ?? "", content: target.innerText?.replace(/\s+/g, " ").trim().slice(0, 300) ?? "" };
560
+ }, CAROUSEL_SEL);
561
+ if (slide0?.content && slide0.content.length > 15) results.push({ slideIndex: 0, heading: slide0.heading || void 0, content: slide0.content });
562
+ const seen = new Set(slide0?.content ? [slide0.content] : []);
563
+ for (let i = 1; i < 8; i++) {
564
+ try {
565
+ const nextBtn = page.locator(NEXT_SEL).first();
566
+ const visible = await nextBtn.isVisible().catch(() => false);
567
+ if (!visible) break;
568
+ await nextBtn.click({ timeout: 1e3 });
569
+ await page.waitForTimeout(600);
570
+ const slide = await page.evaluate((carSel) => {
571
+ const car = document.querySelector(carSel);
572
+ if (!car) return null;
573
+ const active = car.querySelector('[aria-hidden="false"], .active, [class*="active" i], [class*="current" i]');
574
+ const target = active ?? car;
575
+ return { heading: target.querySelector("h2,h3,h4")?.textContent?.trim() ?? "", content: target.innerText?.replace(/\s+/g, " ").trim().slice(0, 300) ?? "" };
576
+ }, CAROUSEL_SEL);
577
+ if (!slide?.content || slide.content.length < 15 || seen.has(slide.content)) break;
578
+ seen.add(slide.content);
579
+ results.push({ slideIndex: i, heading: slide.heading || void 0, content: slide.content });
580
+ } catch {
581
+ break;
582
+ }
583
+ }
584
+ } catch {
585
+ }
586
+ return results;
587
+ }
588
+ async function probeWizardSteps(page) {
589
+ const results = [];
590
+ try {
591
+ const NEXT_LABELS = /^(next|continue|proceed|step \d|›|→|forward)/i;
592
+ const BACK_LABELS = /^(back|previous|prev|‹|←)/i;
593
+ const WIZARD_INDICATORS = '[role="progressbar"], [class*="stepper" i], [class*="wizard" i], [class*="progress-step" i], [aria-label*="step" i]';
594
+ const isWizard = await page.evaluate((sel) => !!document.querySelector(sel), WIZARD_INDICATORS);
595
+ if (!isWizard) return results;
596
+ const captureStep = async (stepIndex) => {
597
+ const data = await page.evaluate(() => {
598
+ const content = document.querySelector('main, [role="main"], form')?.innerText?.replace(/\s+/g, " ").trim().slice(0, 400) ?? "";
599
+ const stepLabel = document.querySelector('[aria-current="step"], [class*="step-label" i], [class*="stepper-label" i]')?.textContent?.trim() ?? "";
600
+ const fields = Array.from(document.querySelectorAll('input:not([type="hidden"]), select, textarea')).map((el) => el.getAttribute("aria-label") || el.placeholder || el.name || "").filter(Boolean).slice(0, 10);
601
+ return { content, stepLabel, fields };
602
+ });
603
+ return { stepIndex, stepLabel: data.stepLabel || void 0, content: data.content, fields: data.fields };
604
+ };
605
+ results.push(await captureStep(0));
606
+ for (let i = 1; i < 6; i++) {
607
+ try {
608
+ const nextLabel = await page.evaluate((nextRe) => {
609
+ const btns = Array.from(document.querySelectorAll('button, [role="button"]'));
610
+ const btn2 = btns.find((b) => new RegExp(nextRe).test(b.innerText?.trim() || ""));
611
+ return btn2?.innerText?.trim() || null;
612
+ }, NEXT_LABELS.source);
613
+ if (!nextLabel) break;
614
+ const btn = page.locator(`button:has-text("${nextLabel}")`).first();
615
+ await btn.click({ timeout: 1500 });
616
+ await page.waitForTimeout(800);
617
+ results.push(await captureStep(i));
618
+ } catch {
619
+ break;
620
+ }
621
+ }
622
+ } catch {
623
+ }
624
+ return results;
625
+ }
626
+ async function detectFocusTraps(page) {
627
+ const results = [];
628
+ try {
629
+ const openDialogs = await page.evaluate(() => {
630
+ return Array.from(document.querySelectorAll('[role="dialog"], [role="alertdialog"]')).filter((el) => window.getComputedStyle(el).display !== "none").map((el) => el.getAttribute("aria-label") || el.querySelector('[role="heading"]')?.textContent?.trim() || "dialog");
631
+ });
632
+ for (const dialogLabel of openDialogs.slice(0, 3)) {
633
+ const sampled = [];
634
+ let allInside = true;
635
+ for (let i = 0; i < 8; i++) {
636
+ await page.keyboard.press("Tab");
637
+ await page.waitForTimeout(80);
638
+ const focused = await page.evaluate(() => {
639
+ const el = document.activeElement;
640
+ const inDialog = !!el?.closest('[role="dialog"], [role="alertdialog"]');
641
+ return { label: el?.getAttribute("aria-label") || el?.textContent?.trim().slice(0, 40) || el?.tagName || "", inDialog };
642
+ });
643
+ sampled.push(focused.label);
644
+ if (!focused.inDialog) {
645
+ allInside = false;
646
+ break;
647
+ }
648
+ }
649
+ results.push({ modalTrigger: dialogLabel, focusTrapped: allInside, sampledFocused: sampled });
650
+ }
651
+ } catch {
652
+ }
653
+ return results;
654
+ }
655
+ async function probeTabPanels(page) {
656
+ const results = [];
657
+ try {
658
+ const tabs = await page.$$('[role="tab"]:not([aria-disabled="true"])');
659
+ for (const tab of tabs.slice(0, 10)) {
660
+ try {
661
+ const tabLabel = await tab.evaluate((el) => el.innerText?.trim() || el.getAttribute("aria-label") || "");
662
+ if (!tabLabel) continue;
663
+ await tab.click({ timeout: 1e3 });
664
+ await page.waitForTimeout(400);
665
+ const panelContent = await page.evaluate(() => {
666
+ const panel = document.querySelector('[role="tabpanel"]:not([hidden])');
667
+ if (!panel) return null;
668
+ const style = window.getComputedStyle(panel);
669
+ if (style.display === "none" || style.visibility === "hidden") return null;
670
+ return {
671
+ heading: panel.querySelector("h2,h3,h4")?.textContent?.trim() ?? "",
672
+ content: panel.innerText?.replace(/\s+/g, " ").trim().slice(0, 400) ?? ""
673
+ };
674
+ });
675
+ if (panelContent?.content && panelContent.content.length > 15) {
676
+ results.push({ tabLabel: tabLabel.slice(0, 60), panelContent: panelContent.content, panelHeading: panelContent.heading || void 0 });
677
+ }
678
+ } catch {
679
+ }
680
+ }
681
+ } catch {
682
+ }
683
+ return results;
684
+ }
685
+ async function probeTooltips(page) {
686
+ const results = [];
687
+ try {
688
+ const candidates = await page.evaluate(() => {
689
+ return Array.from(document.querySelectorAll('[title], [aria-describedby], [data-tooltip], [data-tip], [class*="tooltip" i]')).filter((el) => {
690
+ const style = window.getComputedStyle(el);
691
+ return style.display !== "none" && style.visibility !== "hidden";
692
+ }).slice(0, 8).map((el) => ({
693
+ label: el.innerText?.trim().slice(0, 60) || el.getAttribute("aria-label") || "",
694
+ title: el.getAttribute("title") || "",
695
+ describedBy: el.getAttribute("aria-describedby") || "",
696
+ selector: el.tagName.toLowerCase() + (el.id ? `#${el.id}` : "")
697
+ }));
698
+ });
699
+ for (const c of candidates) {
700
+ try {
701
+ if (c.title && c.title.length > 5) {
702
+ results.push({ triggerLabel: c.label || c.selector, tooltipText: c.title.slice(0, 200) });
703
+ continue;
704
+ }
705
+ if (c.describedBy) {
706
+ const tooltipText = await page.evaluate((id) => document.getElementById(id)?.textContent?.trim() ?? "", c.describedBy);
707
+ if (tooltipText && tooltipText.length > 5) {
708
+ results.push({ triggerLabel: c.label || c.selector, tooltipText: tooltipText.slice(0, 200) });
709
+ }
710
+ }
711
+ } catch {
712
+ }
713
+ }
714
+ } catch {
715
+ }
716
+ return results;
717
+ }
386
718
  async function probeKeyboardNav(page) {
387
719
  const sequence = [];
388
720
  const issues = [];