spb/tendril Public
Tendril — web ingestion platform (scrape/crawl/map/search) on macOS Apple Silicon: WebKit fidelity, authenticated pages, deterministic testable extraction. A self-hosted Firecrawl alternative.
JavaScript 82.6%
TypeScript 11.8%
HTML 5.3%
1// author: simon-pierre boucher <contact@spboucher.ai>2export interface SitemapUrl {3 readonly loc: string;4 readonly lastModified?: string;5}67export interface ParsedSitemap {8 readonly urls: SitemapUrl[];9 readonly childSitemaps: string[];10}1112function decodeXml(s: string): string {13 return s14 .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1")15 .replace(/</g, "<")16 .replace(/>/g, ">")17 .replace(/"/g, '"')18 .replace(/'/g, "'")19 .replace(/'/g, "'")20 .replace(/&/g, "&")21 .trim();22}2324function tag(block: string, name: string): string | undefined {25 const m = new RegExp(`<${name}[^>]*>([\\s\\S]*?)</${name}>`, "i").exec(block);26 return m?.[1] !== undefined ? decodeXml(m[1]) : undefined;27}2829function blocks(xml: string, name: string): string[] {30 const out: string[] = [];31 const re = new RegExp(`<${name}[^>]*>([\\s\\S]*?)</${name}>`, "gi");32 let m: RegExpExecArray | null;33 while ((m = re.exec(xml)) !== null) {34 if (m[1] !== undefined) out.push(m[1]);35 }36 return out;37}3839/**40 * Parse a sitemap or sitemap index (§14.3). Returns child sitemap URLs (for41 * recursion through indexes) and page URLs with optional lastmod.42 */43export function parseSitemap(xml: string): ParsedSitemap {44 const childSitemaps: string[] = [];45 for (const block of blocks(xml, "sitemap")) {46 const loc = tag(block, "loc");47 if (loc !== undefined && loc !== "") childSitemaps.push(loc);48 }4950 const urls: SitemapUrl[] = [];51 for (const block of blocks(xml, "url")) {52 const loc = tag(block, "loc");53 if (loc === undefined || loc === "") continue;54 const lastmod = tag(block, "lastmod");55 urls.push(lastmod !== undefined ? { loc, lastModified: lastmod } : { loc });56 }5758 if (urls.length === 0 && childSitemaps.length === 0) {59 const re = /<loc[^>]*>([\s\S]*?)<\/loc>/gi;60 let m: RegExpExecArray | null;61 while ((m = re.exec(xml)) !== null) {62 const loc = m[1] !== undefined ? decodeXml(m[1]) : "";63 if (loc !== "") urls.push({ loc });64 }65 }6667 return { urls, childSitemaps };68}69