SPB Git

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%
2.0 KB · 69 lines typescript
Raw Blame History
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(/&lt;/g, "<")16    .replace(/&gt;/g, ">")17    .replace(/&quot;/g, '"')18    .replace(/&#39;/g, "'")19    .replace(/&apos;/g, "'")20    .replace(/&amp;/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