(html: string, limit: number)
| 83 | |
| 84 | /** Parse the lite.duckduckgo.com/lite/ table-based results page (result-link / result-snippet). */ |
| 85 | export function parseDuckDuckGoLiteHtml(html: string, limit: number): WebSearchResult[] { |
| 86 | const results: WebSearchResult[] = []; |
| 87 | const linkRegex = /<a[^>]*class="[^"]*\bresult-link\b[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g; |
| 88 | const snipRegex = /<td[^>]*class="[^"]*\bresult-snippet\b[^"]*"[^>]*>([\s\S]*?)<\/td>/g; |
| 89 | |
| 90 | const links: Array<{ href: string; text: string; offset: number }> = []; |
| 91 | for (const m of html.matchAll(linkRegex)) { |
| 92 | links.push({ href: unwrapDdgRedirect(m[1] ?? ''), text: stripTags(m[2] ?? ''), offset: m.index ?? 0 }); |
| 93 | } |
| 94 | const snips: Array<{ text: string; offset: number }> = []; |
| 95 | for (const m of html.matchAll(snipRegex)) { |
| 96 | snips.push({ text: stripTags(m[1] ?? ''), offset: m.index ?? 0 }); |
| 97 | } |
| 98 | |
| 99 | let sIdx = 0; |
| 100 | for (const l of links) { |
| 101 | if (results.length >= limit) break; |
| 102 | while (sIdx < snips.length && snips[sIdx]!.offset < l.offset) sIdx++; |
| 103 | const snippetText = sIdx < snips.length ? snips[sIdx]!.text : ''; |
| 104 | if (!l.href || !l.text) continue; |
| 105 | results.push({ title: l.text, url: l.href, snippet: snippetText }); |
| 106 | } |
| 107 | return results; |
| 108 | } |
| 109 | |
| 110 | /** Unwrap DuckDuckGo's `//duckduckgo.com/l/?uddg=<encoded>` tracking redirect. */ |
| 111 | export function unwrapDdgRedirect(url: string): string { |
no test coverage detected