{"version":3,"sources":["../../src/runtime/BaseProvider.ts","../../src/runtime/html.ts","../../src/runtime/helpers.ts"],"names":["d","escapeRegex"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @jobo/scraper-sdk/runtime — Base Provider Classes\n *\n * Abstract base classes that provide common scaffolding for the two\n * scraper architectures. Extend one of these instead of implementing\n * {@link ScraperProvider} from scratch.\n *\n * - {@link BaseFullDetailsScraper} — For ATS platforms where the listing\n *   page or API returns all job details in one pass.\n *\n * - {@link BaseSplitScraper} — For ATS platforms where listings are\n *   minimal and a secondary detail fetch is required per job.\n */\n\nimport type {\n  ScraperProvider,\n  ValidationResult,\n} from '../types/provider';\nimport type { ScraperContext } from '../types/context';\nimport type {\n  JobListing,\n  JobDetails,\n  ListingsResult,\n  JobDetailsResult,\n  PaginationCursor,\n} from '../types/models';\n\n// ---------------------------------------------------------------------------\n// Base Full-Details Scraper\n// ---------------------------------------------------------------------------\n\n/**\n * Base class for **full-details scrapers**.\n *\n * Use this when the ATS listing page or API returns complete job details\n * (title, description, locations, etc.) in a single pass. The platform\n * will NOT call `scrapeJobDetails()`.\n *\n * Subclasses must implement:\n * - `validate(url)` — URL validation and company ID extraction.\n * - `scrapeListings(ctx, cursor?)` — Paginated listing scrape that returns\n *   full {@link JobDetails} objects (cast to {@link JobListing}).\n *\n * @example\n * ```ts\n * import { BaseFullDetailsScraper, ScraperContext, ListingsResult } from \"@jobo/scraper-sdk/runtime\";\n *\n * export class MyAtsScraper extends BaseFullDetailsScraper {\n *   async validate(url: string) {\n *     const match = url.match(/myats\\.com\\/company\\/(\\w+)/);\n *     return { isValid: !!match, companyId: match?.[1] };\n *   }\n *\n *   async scrapeListings(ctx: ScraperContext, cursor?: PaginationCursor): Promise<ListingsResult> {\n *     const page = cursor ? parseInt(cursor.value) : 0;\n *     const res = await ctx.http.get<ApiResponse>(`${ctx.baseUrl}/api/jobs?offset=${page * 20}`);\n *     return {\n *       listings: res.data.jobs.map(j => this.mapJob(j, ctx)),\n *       hasMore: res.data.jobs.length === 20,\n *       nextCursor: { type: \"offset\", value: String((page + 1) * 20) },\n *     };\n *   }\n * }\n * ```\n */\nexport abstract class BaseFullDetailsScraper implements ScraperProvider {\n  abstract validate(url: string): Promise<ValidationResult> | ValidationResult;\n  abstract scrapeListings(ctx: ScraperContext, cursor?: PaginationCursor): Promise<ListingsResult>;\n\n  /**\n   * Optional initialization hook. Override to perform one-time setup.\n   * Default implementation is a no-op.\n   */\n  async initialize(_ctx: ScraperContext): Promise<void> {\n    // No-op by default. Override in subclass if needed.\n  }\n\n  /**\n   * Not applicable for full-details scrapers.\n   * Throws an error if called — the platform should never call this.\n   */\n  async scrapeJobDetails(_ctx: ScraperContext, _listing: JobListing): Promise<JobDetailsResult> {\n    throw new Error(\n      'scrapeJobDetails() should not be called on a full-details scraper. ' +\n      'Ensure manifest.category is set to \"full\".',\n    );\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Base Split Scraper\n// ---------------------------------------------------------------------------\n\n/**\n * Base class for **split scrapers**.\n *\n * Use this when the ATS listing page only provides basic identifiers\n * (URL, ID, maybe title) and a secondary request is needed to fetch\n * the full job description, locations, and metadata.\n *\n * Subclasses must implement:\n * - `validate(url)` — URL validation and company ID extraction.\n * - `scrapeListings(ctx, cursor?)` — Paginated listing scrape returning\n *   minimal {@link JobListing} objects (at minimum: `externalId` + `applyUrl`).\n * - `scrapeJobDetails(ctx, listing)` — Fetch full details for one listing.\n *\n * @example\n * ```ts\n * import { BaseSplitScraper, ScraperContext, ListingsResult, JobDetailsResult } from \"@jobo/scraper-sdk/runtime\";\n *\n * export class MyAtsScraper extends BaseSplitScraper {\n *   async validate(url: string) {\n *     return { isValid: url.includes(\"myats.com\") };\n *   }\n *\n *   async scrapeListings(ctx: ScraperContext): Promise<ListingsResult> {\n *     const page = await ctx.browser.renderPage({ url: `${ctx.baseUrl}/jobs` });\n *     const links = extractLinks(page.html, /\\/jobs\\/\\d+/);\n *     return {\n *       listings: links.map(link => ({\n *         externalId: link.split(\"/\").pop()!,\n *         applyUrl: resolveUrl(link + \"/apply\", ctx.baseUrl),\n *         listingUrl: resolveUrl(link, ctx.baseUrl),\n *       })),\n *       hasMore: false,\n *     };\n *   }\n *\n *   async scrapeJobDetails(ctx: ScraperContext, listing: JobListing): Promise<JobDetailsResult> {\n *     const page = await ctx.browser.renderPage({ url: listing.listingUrl! });\n *     return {\n *       details: {\n *         ...listing,\n *         title: extractText(page.html, \"h1\")!,\n *         description: extractText(page.html, 'div class=\"description\"')!,\n *         locations: [{ text: extractText(page.html, 'span class=\"location\"')! }],\n *         listingUrl: listing.listingUrl!,\n *       },\n *     };\n *   }\n * }\n * ```\n */\nexport abstract class BaseSplitScraper implements ScraperProvider {\n  abstract validate(url: string): Promise<ValidationResult> | ValidationResult;\n  abstract scrapeListings(ctx: ScraperContext, cursor?: PaginationCursor): Promise<ListingsResult>;\n  abstract scrapeJobDetails(ctx: ScraperContext, listing: JobListing): Promise<JobDetailsResult>;\n\n  /**\n   * Optional initialization hook. Override to perform one-time setup.\n   * Default implementation is a no-op.\n   */\n  async initialize(_ctx: ScraperContext): Promise<void> {\n    // No-op by default. Override in subclass if needed.\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Helper: Build a complete JobDetails from partial data\n// ---------------------------------------------------------------------------\n\n/**\n * Merge a minimal {@link JobListing} with additional detail fields\n * to produce a complete {@link JobDetails} object.\n *\n * Throws if any required detail field is missing after the merge.\n *\n * @param listing — The base listing (from `scrapeListings()`).\n * @param details — Additional fields discovered during detail scraping.\n * @returns A validated {@link JobDetails} object.\n *\n * @example\n * ```ts\n * const details = mergeListingWithDetails(listing, {\n *   title: \"Senior Engineer\",\n *   description: \"<p>We are looking for...</p>\",\n *   locations: [{ text: \"NYC\" }],\n *   listingUrl: \"https://ats.example.com/jobs/123\",\n *   meta: { department: \"Engineering\" },\n * });\n * ```\n */\nexport function mergeListingWithDetails(\n  listing: JobListing,\n  details: Partial<JobDetails>,\n): JobDetails {\n  const merged: JobDetails = {\n    externalId: details.externalId ?? listing.externalId,\n    applyUrl: details.applyUrl ?? listing.applyUrl,\n    listingUrl: details.listingUrl ?? listing.listingUrl ?? listing.applyUrl,\n    title: details.title ?? listing.title ?? '',\n    description: details.description ?? listing.description ?? '',\n    locations: details.locations ?? listing.locations ?? [],\n    companyName: details.companyName ?? listing.companyName,\n    postedAt: details.postedAt ?? listing.postedAt,\n    closesAt: details.closesAt ?? listing.closesAt,\n    meta: { ...listing.meta, ...details.meta },\n  };\n\n  return merged;\n}\n","/**\n * @jobo/scraper-sdk/runtime — HTML Parsing Helpers\n *\n * Lightweight, zero-dependency utilities for extracting data from HTML strings.\n * These run in Node.js (not the browser) and operate on raw HTML text.\n *\n * For complex DOM manipulation, use `ctx.browser.renderPage()` and parse\n * the returned HTML. These helpers are designed for simple, fast extraction\n * from static HTML or API-returned HTML fragments.\n */\n\n// ---------------------------------------------------------------------------\n// Tag stripping & text extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Strip all HTML tags from a string, returning plain text.\n * Decodes common HTML entities and normalizes whitespace.\n *\n * @param html — Raw HTML string.\n * @returns Plain text with tags removed and entities decoded.\n *\n * @example\n * ```ts\n * stripHtml(\"<p>Hello <b>world</b> &amp; friends</p>\");\n * // → \"Hello world & friends\"\n * ```\n */\nexport function stripHtml(html: string): string {\n  if (!html) return '';\n\n  return html\n    // Remove script and style blocks entirely\n    .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n    .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n    // Replace block-level tags with newlines\n    .replace(/<\\/(p|div|h[1-6]|li|tr|br\\s*\\/?)>/gi, '\\n')\n    .replace(/<br\\s*\\/?>/gi, '\\n')\n    // Remove all remaining tags\n    .replace(/<[^>]+>/g, '')\n    // Decode common entities\n    .replace(/&amp;/g, '&')\n    .replace(/&lt;/g, '<')\n    .replace(/&gt;/g, '>')\n    .replace(/&quot;/g, '\"')\n    .replace(/&#39;/g, \"'\")\n    .replace(/&nbsp;/g, ' ')\n    .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(parseInt(code, 10)))\n    .replace(/&#x([0-9a-fA-F]+);/g, (_match, code) => String.fromCharCode(parseInt(code, 16)))\n    // Normalize whitespace\n    .replace(/[ \\t]+/g, ' ')\n    .replace(/\\n\\s*\\n/g, '\\n')\n    .trim();\n}\n\n/**\n * Strip HTML and truncate to a maximum length, appending \"...\" if truncated.\n *\n * @param html — Raw HTML string.\n * @param maxLength — Maximum character length of the output.\n * @returns Truncated plain text.\n */\nexport function stripHtmlAndTruncate(html: string, maxLength: number): string {\n  const text = stripHtml(html);\n  if (text.length <= maxLength) return text;\n  return text.substring(0, maxLength - 3).trimEnd() + '...';\n}\n\n// ---------------------------------------------------------------------------\n// Attribute & content extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the value of an attribute from the first matching HTML tag.\n *\n * @param html — Raw HTML string.\n * @param tagPattern — Regex pattern to match the opening tag (e.g. `<meta name=\"description\"`).\n * @param attribute — Attribute name to extract (e.g. `\"content\"`).\n * @returns The attribute value, or `null` if not found.\n *\n * @example\n * ```ts\n * extractAttribute(html, '<meta property=\"og:title\"', 'content');\n * // → \"Senior Engineer at Acme\"\n * ```\n */\nexport function extractAttribute(html: string, tagPattern: string, attribute: string): string | null {\n  // Escape special regex chars in the tag pattern, but keep it flexible\n  const escapedPattern = tagPattern.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n  const regex = new RegExp(\n    `${escapedPattern}[^>]*?\\\\b${attribute}\\\\s*=\\\\s*(?:\"([^\"]*)\"|'([^']*)')`,\n    'i',\n  );\n  const match = html.match(regex);\n  if (match) return match[1] ?? match[2] ?? null;\n\n  // Try reverse order (attribute before the tag pattern content)\n  const reverseRegex = new RegExp(\n    `<[^>]*?\\\\b${attribute}\\\\s*=\\\\s*(?:\"([^\"]*)\"|'([^']*)')\\\\s[^>]*?${tagPattern.replace(/^</, '').replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}`,\n    'i',\n  );\n  const reverseMatch = html.match(reverseRegex);\n  return reverseMatch ? (reverseMatch[1] ?? reverseMatch[2] ?? null) : null;\n}\n\n/**\n * Extract the text content between an opening and closing tag.\n *\n * @param html — Raw HTML string.\n * @param selector — Simple tag selector (e.g. `\"title\"`, `\"h1\"`, `\"h1 class=\\\"job-title\\\"\"`).\n * @returns The inner text (HTML stripped), or `null` if not found.\n *\n * @example\n * ```ts\n * extractText(html, \"title\");\n * // → \"Senior Engineer - Acme Corp\"\n *\n * extractText(html, 'h1 class=\"job-title\"');\n * // → \"Senior Engineer\"\n * ```\n */\nexport function extractText(html: string, selector: string): string | null {\n  const parts = selector.split(/\\s+/);\n  const tag = parts[0];\n  const attrPart = parts.length > 1 ? parts.slice(1).join(' ') : '';\n\n  let pattern: string;\n  if (attrPart) {\n    const escapedAttr = attrPart.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n    pattern = `<${tag}\\\\s[^>]*?${escapedAttr}[^>]*?>([\\\\s\\\\S]*?)<\\\\/${tag}>`;\n  } else {\n    pattern = `<${tag}(?:\\\\s[^>]*)?>([\\\\s\\\\S]*?)<\\\\/${tag}>`;\n  }\n\n  const regex = new RegExp(pattern, 'i');\n  const match = html.match(regex);\n  if (!match) return null;\n\n  return stripHtml(match[1]).trim() || null;\n}\n\n// ---------------------------------------------------------------------------\n// Meta tag helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract a `<meta>` tag value by its `name` attribute.\n *\n * @param html — Raw HTML string.\n * @param name — The `name` attribute value (e.g. `\"description\"`).\n * @returns The `content` attribute value, or `null`.\n *\n * @example\n * ```ts\n * extractMetaByName(html, \"description\");\n * // → \"We are hiring a senior engineer...\"\n * ```\n */\nexport function extractMetaByName(html: string, name: string): string | null {\n  const regex = new RegExp(\n    `<meta\\\\s[^>]*?name\\\\s*=\\\\s*[\"']${escapeRegex(name)}[\"'][^>]*?content\\\\s*=\\\\s*[\"']([^\"']*)[\"']|` +\n    `<meta\\\\s[^>]*?content\\\\s*=\\\\s*[\"']([^\"']*)[\"'][^>]*?name\\\\s*=\\\\s*[\"']${escapeRegex(name)}[\"']`,\n    'i',\n  );\n  const match = html.match(regex);\n  return match ? (match[1] ?? match[2] ?? null) : null;\n}\n\n/**\n * Extract a `<meta>` tag value by its `property` attribute (Open Graph, etc.).\n *\n * @param html — Raw HTML string.\n * @param property — The `property` attribute value (e.g. `\"og:title\"`).\n * @returns The `content` attribute value, or `null`.\n *\n * @example\n * ```ts\n * extractMetaByProperty(html, \"og:title\");\n * // → \"Senior Engineer at Acme\"\n * ```\n */\nexport function extractMetaByProperty(html: string, property: string): string | null {\n  const regex = new RegExp(\n    `<meta\\\\s[^>]*?property\\\\s*=\\\\s*[\"']${escapeRegex(property)}[\"'][^>]*?content\\\\s*=\\\\s*[\"']([^\"']*)[\"']|` +\n    `<meta\\\\s[^>]*?content\\\\s*=\\\\s*[\"']([^\"']*)[\"'][^>]*?property\\\\s*=\\\\s*[\"']${escapeRegex(property)}[\"']`,\n    'i',\n  );\n  const match = html.match(regex);\n  return match ? (match[1] ?? match[2] ?? null) : null;\n}\n\n// ---------------------------------------------------------------------------\n// JSON-LD extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Extract and parse all JSON-LD blocks (`<script type=\"application/ld+json\">`)\n * from an HTML document.\n *\n * @param html — Raw HTML string.\n * @returns Array of parsed JSON-LD objects. Invalid JSON blocks are silently skipped.\n *\n * @example\n * ```ts\n * const jsonLd = extractJsonLd(html);\n * const jobPosting = jsonLd.find(j => j[\"@type\"] === \"JobPosting\");\n * ```\n */\nexport function extractJsonLd(html: string): Record<string, unknown>[] {\n  const results: Record<string, unknown>[] = [];\n  const regex = /<script\\s[^>]*?type\\s*=\\s*[\"']application\\/ld\\+json[\"'][^>]*?>([\\s\\S]*?)<\\/script>/gi;\n\n  let match: RegExpExecArray | null;\n  while ((match = regex.exec(html)) !== null) {\n    try {\n      const parsed = JSON.parse(match[1].trim());\n      if (Array.isArray(parsed)) {\n        for (const item of parsed) {\n          if (item && typeof item === 'object') {\n            results.push(item as Record<string, unknown>);\n          }\n        }\n      } else if (parsed && typeof parsed === 'object') {\n        // Handle @graph arrays\n        const obj = parsed as Record<string, unknown>;\n        if (Array.isArray(obj['@graph'])) {\n          for (const item of obj['@graph'] as unknown[]) {\n            if (item && typeof item === 'object') {\n              results.push(item as Record<string, unknown>);\n            }\n          }\n        }\n        results.push(obj);\n      }\n    } catch {\n      // Silently skip malformed JSON-LD\n    }\n  }\n\n  return results;\n}\n\n/**\n * Find a JSON-LD object by its `@type` value.\n *\n * @param jsonLdItems — Array of parsed JSON-LD objects (from {@link extractJsonLd}).\n * @param type — The `@type` value to search for (e.g. `\"JobPosting\"`).\n * @returns The first matching object, or `null`.\n *\n * @example\n * ```ts\n * const items = extractJsonLd(html);\n * const job = findJsonLdByType(items, \"JobPosting\");\n * if (job) {\n *   console.log(job.title, job.description);\n * }\n * ```\n */\nexport function findJsonLdByType(\n  jsonLdItems: Record<string, unknown>[],\n  type: string,\n): Record<string, unknown> | null {\n  for (const item of jsonLdItems) {\n    const itemType = item['@type'];\n    if (itemType === type) return item;\n    if (Array.isArray(itemType) && itemType.includes(type)) return item;\n  }\n  return null;\n}\n\n// ---------------------------------------------------------------------------\n// Link extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Extract all `href` values from `<a>` tags matching an optional pattern.\n *\n * @param html — Raw HTML string.\n * @param hrefPattern — Optional regex pattern to filter hrefs (e.g. `/\\/jobs\\/\\d+/`).\n * @returns Array of matching href values.\n *\n * @example\n * ```ts\n * // All links\n * extractLinks(html);\n *\n * // Only job detail links\n * extractLinks(html, /\\/jobs\\/\\d+/);\n * ```\n */\nexport function extractLinks(html: string, hrefPattern?: RegExp): string[] {\n  const links: string[] = [];\n  const regex = /<a\\s[^>]*?href\\s*=\\s*[\"']([^\"']+)[\"'][^>]*?>/gi;\n\n  let match: RegExpExecArray | null;\n  while ((match = regex.exec(html)) !== null) {\n    const href = match[1];\n    if (!hrefPattern || hrefPattern.test(href)) {\n      links.push(href);\n    }\n  }\n\n  return [...new Set(links)]; // Deduplicate\n}\n\n// ---------------------------------------------------------------------------\n// URL helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a potentially relative URL against a base URL.\n *\n * @param url — The URL to resolve (may be relative).\n * @param baseUrl — The base URL to resolve against.\n * @returns The absolute URL.\n *\n * @example\n * ```ts\n * resolveUrl(\"/jobs/123\", \"https://ats.example.com/careers\");\n * // → \"https://ats.example.com/jobs/123\"\n *\n * resolveUrl(\"https://other.com/jobs/123\", \"https://ats.example.com\");\n * // → \"https://other.com/jobs/123\" (already absolute)\n * ```\n */\nexport function resolveUrl(url: string, baseUrl: string): string {\n  try {\n    return new URL(url, baseUrl).href;\n  } catch {\n    return url;\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction escapeRegex(str: string): string {\n  return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n","/**\n * @jobo/scraper-sdk/runtime — Common Scraping Helpers\n *\n * Utility functions for common scraping patterns: URL manipulation,\n * data normalization, pagination, and defensive parsing.\n */\n\nimport type {\n  JobLocation,\n  MetadataValue,\n  PaginationCursor,\n} from \"../types/models\";\n\nimport type { UrlFieldExtractor } from \"../types/provider\";\n\n// ---------------------------------------------------------------------------\n// Pagination helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Create an offset-based pagination cursor.\n *\n * @param offset — The offset value for the next page.\n * @returns A {@link PaginationCursor} with `type: \"offset\"`.\n *\n * @example\n * ```ts\n * return { listings, hasMore: true, nextCursor: offsetCursor(50) };\n * ```\n */\nexport function offsetCursor(offset: number): PaginationCursor {\n  return { type: \"offset\", value: String(offset) };\n}\n\n/**\n * Create a page-number pagination cursor.\n *\n * @param page — The page number for the next page (1-indexed).\n * @returns A {@link PaginationCursor} with `type: \"page\"`.\n */\nexport function pageCursor(page: number): PaginationCursor {\n  return { type: \"page\", value: String(page) };\n}\n\n/**\n * Create an opaque cursor-based pagination cursor.\n *\n * @param cursor — The opaque cursor string from the API response.\n * @returns A {@link PaginationCursor} with `type: \"cursor\"`.\n */\nexport function cursorPagination(cursor: string): PaginationCursor {\n  return { type: \"cursor\", value: cursor };\n}\n\n/**\n * Create a URL-based pagination cursor (next page URL).\n *\n * @param url — The full URL of the next page.\n * @returns A {@link PaginationCursor} with `type: \"url\"`.\n */\nexport function urlCursor(url: string): PaginationCursor {\n  return { type: \"url\", value: url };\n}\n\n/**\n * Parse the current page number from a cursor, defaulting to 1.\n *\n * @param cursor — The pagination cursor, or `undefined` for the first page.\n * @returns The page number as an integer.\n */\nexport function currentPage(cursor?: PaginationCursor): number {\n  if (!cursor) return 1;\n  const parsed = parseInt(cursor.value, 10);\n  return isNaN(parsed) ? 1 : parsed;\n}\n\n/**\n * Parse the current offset from a cursor, defaulting to 0.\n *\n * @param cursor — The pagination cursor, or `undefined` for the first page.\n * @returns The offset as an integer.\n */\nexport function currentOffset(cursor?: PaginationCursor): number {\n  if (!cursor) return 0;\n  const parsed = parseInt(cursor.value, 10);\n  return isNaN(parsed) ? 0 : parsed;\n}\n\n// ---------------------------------------------------------------------------\n// Location helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Parse a raw location string into a {@link JobLocation} object.\n *\n * Handles common formats:\n * - `\"San Francisco, CA\"` → `{ text, city, state }`\n * - `\"San Francisco, CA, US\"` → `{ text, city, state, country }`\n * - `\"Remote\"` → `{ text, isRemote: true }`\n * - `\"Remote (US)\"` → `{ text, isRemote: true, country: \"US\" }`\n * - `\"New York, NY 10001\"` → `{ text, city, state, postalCode }`\n *\n * This is a best-effort parser. The platform's location pipeline will\n * further normalize and geocode the result.\n *\n * @param text — Raw location string as displayed on the page.\n * @returns A {@link JobLocation} with as many fields populated as possible.\n */\nexport function parseLocation(text: string): JobLocation {\n  if (!text || !text.trim()) return { text: text || \"\" };\n\n  const trimmed = text.trim();\n  const location: JobLocation = { text: trimmed };\n\n  // Detect remote\n  if (/\\bremote\\b/i.test(trimmed)) {\n    location.isRemote = true;\n    // Extract country from \"Remote (US)\" or \"Remote - US\"\n    const remoteCountry = trimmed.match(\n      /remote\\s*[\\(\\-–—]\\s*([A-Z]{2,})\\s*\\)?/i,\n    );\n    if (remoteCountry) {\n      location.country = remoteCountry[1].toUpperCase();\n    }\n    return location;\n  }\n\n  // Try \"City, State ZIP, Country\" or \"City, State, Country\" or \"City, State\"\n  const parts = trimmed\n    .split(\",\")\n    .map((p) => p.trim())\n    .filter(Boolean);\n\n  if (parts.length >= 1) {\n    location.city = parts[0];\n  }\n\n  if (parts.length >= 2) {\n    // Second part might be \"State ZIP\" or just \"State\" or \"Country\"\n    const stateZip = parts[1].match(/^([A-Z]{2})\\s+(\\d{5}(?:-\\d{4})?)$/);\n    if (stateZip) {\n      location.state = stateZip[1];\n      location.postalCode = stateZip[2];\n    } else if (/^[A-Z]{2}$/.test(parts[1])) {\n      // Could be US state or country code — treat as state if there's a third part\n      if (parts.length >= 3) {\n        location.state = parts[1];\n      } else {\n        location.state = parts[1];\n      }\n    } else {\n      location.state = parts[1];\n    }\n  }\n\n  if (parts.length >= 3) {\n    location.country = parts[parts.length - 1];\n  }\n\n  return location;\n}\n\n/**\n * Parse multiple locations from a delimited string.\n *\n * Handles common multi-location formats:\n * - `\"San Francisco, CA; New York, NY\"` (semicolon-separated)\n * - `\"San Francisco, CA | New York, NY\"` (pipe-separated)\n * - `\"San Francisco, CA / New York, NY\"` (slash-separated)\n *\n * @param text — Raw multi-location string.\n * @param delimiter — Delimiter regex. Defaults to `; | /` with surrounding whitespace.\n * @returns Array of parsed {@link JobLocation} objects.\n */\nexport function parseLocations(\n  text: string,\n  delimiter?: RegExp,\n): JobLocation[] {\n  if (!text || !text.trim()) return [];\n\n  const delim = delimiter ?? /\\s*[;|/]\\s*/;\n  return text\n    .split(delim)\n    .map((s) => s.trim())\n    .filter(Boolean)\n    .map(parseLocation);\n}\n\n// ---------------------------------------------------------------------------\n// Data normalization\n// ---------------------------------------------------------------------------\n\n/**\n * Safely extract a string value from an unknown data source.\n * Returns `undefined` if the value is null, undefined, or not a string.\n *\n * @param value — The value to extract.\n * @returns The string value, or `undefined`.\n */\nexport function asString(value: unknown): string | undefined {\n  if (value === null || value === undefined) return undefined;\n  if (typeof value === \"string\") return value || undefined;\n  if (typeof value === \"number\" || typeof value === \"boolean\")\n    return String(value);\n  return undefined;\n}\n\n/**\n * Safely extract a number value from an unknown data source.\n *\n * @param value — The value to extract.\n * @returns The number value, or `undefined` if not a valid number.\n */\nexport function asNumber(value: unknown): number | undefined {\n  if (value === null || value === undefined) return undefined;\n  if (typeof value === \"number\") return isNaN(value) ? undefined : value;\n  if (typeof value === \"string\") {\n    const parsed = parseFloat(value);\n    return isNaN(parsed) ? undefined : parsed;\n  }\n  return undefined;\n}\n\n/**\n * Safely extract an array of strings from an unknown data source.\n *\n * @param value — The value to extract. Can be a string (split by comma) or an array.\n * @returns Array of non-empty strings.\n */\nexport function asStringArray(value: unknown): string[] {\n  if (!value) return [];\n  if (typeof value === \"string\")\n    return value\n      .split(\",\")\n      .map((s) => s.trim())\n      .filter(Boolean);\n  if (Array.isArray(value)) return value.map((v) => String(v)).filter(Boolean);\n  return [];\n}\n\n/**\n * Build a metadata map from key-value pairs, skipping null/undefined values.\n *\n * @param entries — Key-value pairs to include.\n * @returns A clean metadata map with only defined values.\n *\n * @example\n * ```ts\n * const meta = buildMeta(\n *   [\"employmentType\", job.type],\n *   [\"department\", job.dept],\n *   [\"salary\", undefined], // skipped\n * );\n * // → { employmentType: \"Full-time\", department: \"Engineering\" }\n * ```\n */\nexport function buildMeta(\n  ...entries: [string, MetadataValue | undefined][]\n): Record<string, MetadataValue> {\n  const result: Record<string, MetadataValue> = {};\n  for (const [key, value] of entries) {\n    if (value !== undefined && value !== null && value !== \"\") {\n      result[key] = value;\n    }\n  }\n  return result;\n}\n\n// ---------------------------------------------------------------------------\n// Date helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Attempt to parse a date string into an ISO 8601 string.\n * Returns `undefined` if the input cannot be parsed.\n *\n * Handles common formats:\n * - ISO 8601: `\"2025-01-15T00:00:00Z\"`\n * - US format: `\"01/15/2025\"`, `\"January 15, 2025\"`\n * - Relative: `\"3 days ago\"`, `\"Posted yesterday\"`\n *\n * @param dateStr — Raw date string from the ATS.\n * @returns ISO 8601 date string, or `undefined`.\n */\nexport function parseDate(\n  dateStr: string | null | undefined,\n): string | undefined {\n  if (!dateStr) return undefined;\n\n  const trimmed = dateStr.trim();\n  if (!trimmed) return undefined;\n\n  // Already ISO 8601\n  if (/^\\d{4}-\\d{2}-\\d{2}/.test(trimmed)) {\n    const d = new Date(trimmed);\n    return isNaN(d.getTime()) ? undefined : d.toISOString();\n  }\n\n  // Relative dates: \"X days ago\", \"yesterday\", \"today\"\n  const relativeMatch = trimmed.match(\n    /(\\d+)\\s+(day|hour|minute|week|month)s?\\s+ago/i,\n  );\n  if (relativeMatch) {\n    const amount = parseInt(relativeMatch[1], 10);\n    const unit = relativeMatch[2].toLowerCase();\n    const now = new Date();\n    switch (unit) {\n      case \"minute\":\n        now.setMinutes(now.getMinutes() - amount);\n        break;\n      case \"hour\":\n        now.setHours(now.getHours() - amount);\n        break;\n      case \"day\":\n        now.setDate(now.getDate() - amount);\n        break;\n      case \"week\":\n        now.setDate(now.getDate() - amount * 7);\n        break;\n      case \"month\":\n        now.setMonth(now.getMonth() - amount);\n        break;\n    }\n    return now.toISOString();\n  }\n\n  if (/yesterday/i.test(trimmed)) {\n    const d = new Date();\n    d.setDate(d.getDate() - 1);\n    return d.toISOString();\n  }\n\n  if (/today/i.test(trimmed)) {\n    return new Date().toISOString();\n  }\n\n  // Try native Date parsing as fallback\n  const d = new Date(trimmed);\n  return isNaN(d.getTime()) ? undefined : d.toISOString();\n}\n\n// ---------------------------------------------------------------------------\n// Deduplication\n// ---------------------------------------------------------------------------\n\n/**\n * Deduplicate an array of listings by `externalId`.\n * The first occurrence of each ID is kept.\n *\n * @param listings — Array of listings that may contain duplicates.\n * @returns Deduplicated array.\n */\nexport function deduplicateListings<T extends { externalId: string }>(\n  listings: T[],\n): T[] {\n  const seen = new Set<string>();\n  return listings.filter((listing) => {\n    if (seen.has(listing.externalId)) return false;\n    seen.add(listing.externalId);\n    return true;\n  });\n}\n\n// ---------------------------------------------------------------------------\n// URL helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Match a URL against a glob-style pattern.\n * Supports `*` (single path segment), `**` (any path), and `:param` (named segment).\n *\n * When the pattern does not contain `?`, query strings and fragments are\n * stripped from the URL before matching (mirrors C# UrlPatternMatcher).\n */\nexport function matchesUrlPattern(url: string, pattern: string): boolean {\n  try {\n    const urlToMatch = pattern.includes(\"?\") ? url : url.replace(/[?#].*$/, \"\");\n\n    const regexPattern =\n      \"^\" +\n      escapeRegex(pattern)\n        .replace(/\\\\\\*\\\\\\*/g, \"@@DOUBLESTAR@@\")\n        .replace(/\\\\\\*/g, \"[^/]*\")\n        .replace(/@@DOUBLESTAR@@/g, \".*\")\n        .replace(/\\\\:/g, \"[^/]+\") +\n      \"$\";\n    return new RegExp(regexPattern, \"i\").test(urlToMatch);\n  } catch {\n    return false;\n  }\n}\n\n/** Check if a URL matches any of the given patterns. */\nexport function matchesAnyUrlPattern(url: string, patterns: string[]): boolean {\n  return patterns.some((p) => matchesUrlPattern(url, p));\n}\n\n/**\n * Extract a specific named parameter from a URL using a single glob-style template.\n *\n * When the template does not explicitly handle query parameters (i.e. contains\n * no `?`), query strings and fragments are stripped from the URL before matching.\n * This mirrors the C# `UrlPatternMatcher.NormalizeUrl` behaviour and prevents\n * tracking parameters like `?source=jobright` or `?utm_source=...` from\n * breaking otherwise valid extractions.\n */\nexport function extractNamedParam(\n  url: string,\n  pattern: string,\n  paramName: string,\n): string | undefined {\n  // Strip query string / fragment when the template doesn't handle them,\n  // matching the C# UrlPatternMatcher.NormalizeUrl behaviour.\n  const urlToMatch = pattern.includes(\"?\") ? url : url.replace(/[?#].*$/, \"\");\n\n  const paramNames: string[] = [];\n  const regexStr =\n    \"^\" +\n    pattern\n      .replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n      .replace(/\\*\\*/g, \".*\")\n      .replace(/\\*/g, \"[^/]*\")\n      .replace(/:([a-zA-Z][a-zA-Z0-9_]*)/g, (_match, name) => {\n        paramNames.push(name);\n        return \"([^/?#&]+)\";\n      }) +\n    \"$\";\n\n  try {\n    const match = new RegExp(regexStr, \"i\").exec(urlToMatch);\n    if (!match) return undefined;\n    const idx = paramNames.indexOf(paramName);\n    if (idx === -1) return undefined;\n    return match[idx + 1];\n  } catch {\n    return undefined;\n  }\n}\n\n/**\n * Extract a named parameter from a URL by trying multiple glob-style templates.\n * Returns the first successful extraction.\n */\nexport function extractParamFromUrl(\n  url: string,\n  patterns: string[],\n  paramName: string,\n): string | undefined {\n  for (const pattern of patterns) {\n    const result = extractNamedParam(url, pattern, paramName);\n    if (result !== undefined) return result;\n  }\n  return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Structured field extraction (new extractor-based framework)\n// ---------------------------------------------------------------------------\n\n/** Default character class for :paramName captures in urlPattern. */\nconst DEFAULT_SEGMENT_CHARSET = \"a-zA-Z0-9_~%.-\";\n\n/**\n * Normalize extractor(s) to a flat array.\n * Accepts a single extractor, an array of extractors, or undefined.\n */\nexport function toExtractorArray(\n  v: UrlFieldExtractor | UrlFieldExtractor[] | undefined | null,\n): UrlFieldExtractor[] {\n  if (!v) return [];\n  return Array.isArray(v) ? v : [v];\n}\n\n/**\n * Match a Simplify-style path pattern against a URL pathname and return\n * all named captures. Returns undefined if no match.\n *\n * Pattern syntax:\n * - `:paramName` — Named capture (one path segment)\n * - `*` — Wildcard (one unnamed path segment)\n * - `**` — Globstar (zero or more path segments)\n * - `(/*)` — Optional trailing path (zero or more segments at end)\n *\n * @param pathname — The URL pathname to match (e.g. `/acme/jobs/123`).\n * @param pattern — The path pattern (e.g. `/:companyId/jobs/:jobId(/*)`).\n * @param options — Optional extraction options.\n * @param options.segmentCharset — Custom character class for the target param capture.\n * @param options.targetParam — Which named param gets the custom charset (others use default).\n * @returns Map of param names to captured values, or undefined if no match.\n */\nexport function matchPathPattern(\n  pathname: string,\n  pattern: string,\n  options?: { segmentCharset?: string; targetParam?: string },\n): Record<string, string> | undefined {\n  const defaultCaptureGroup = `([${DEFAULT_SEGMENT_CHARSET}]+)`;\n  const targetCharset = options?.segmentCharset || DEFAULT_SEGMENT_CHARSET;\n  const targetCaptureGroup = `([${targetCharset}]+)`;\n  const targetParam = options?.targetParam;\n\n  // Build regex from pattern\n  const paramNames: string[] = [];\n\n  // Handle optional trailing (/*)\n  let corePattern = pattern;\n  let optionalTrailing = false;\n  if (corePattern.endsWith(\"(/*)\") || corePattern.endsWith(\"(/**)\")) {\n    optionalTrailing = true;\n    corePattern = corePattern.replace(/\\(\\/\\*{1,2}\\)$/, \"\");\n  }\n\n  // Also handle trailing /** or /* after the optional suffix is removed\n  let trailingWild = \"\";\n  if (corePattern.endsWith(\"/**\")) {\n    trailingWild = \"/**\";\n    corePattern = corePattern.slice(0, -3);\n  } else if (corePattern.endsWith(\"/*\")) {\n    trailingWild = \"/*\";\n    corePattern = corePattern.slice(0, -2);\n  }\n\n  let regexStr =\n    \"^\" +\n    corePattern\n      // Escape regex special chars except * and :\n      .replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n      // Handle ** (globstar) — must be done before * (single wildcard)\n      .replace(/\\*\\*/g, \"@@DOUBLESTAR@@\")\n      // Handle * (single segment wildcard)\n      .replace(/\\*/g, \"[^/]+\")\n      .replace(/@@DOUBLESTAR@@/g, \"(?:.+)?\")\n      // Handle :paramName captures\n      .replace(/:([a-zA-Z][a-zA-Z0-9_]*)/g, (_match, name) => {\n        paramNames.push(name);\n        // Apply custom charset only to the target param; others use default.\n        return targetParam && name === targetParam\n          ? targetCaptureGroup\n          : defaultCaptureGroup;\n      });\n\n  // Add trailing wild portion\n  if (trailingWild === \"/**\") {\n    regexStr += \"(?:/.*)?\";\n  } else if (trailingWild === \"/*\") {\n    regexStr += \"(?:/[^/]+)?\";\n  }\n\n  // Add optional trailing\n  if (optionalTrailing) {\n    regexStr += \"(?:/.*)?\";\n  }\n\n  regexStr += \"$\";\n\n  try {\n    const match = new RegExp(regexStr, \"i\").exec(pathname);\n    if (!match) return undefined;\n\n    const result: Record<string, string> = {};\n    for (let i = 0; i < paramNames.length; i++) {\n      if (match[i + 1] !== undefined) {\n        result[paramNames[i]] = match[i + 1];\n      }\n    }\n    return result;\n  } catch {\n    return undefined;\n  }\n}\n\n/**\n * Extract a field value from a URL using a single extractor definition.\n *\n * @param url — The full URL to extract from.\n * @param extractor — The extractor definition.\n * @param defaultParam — Default param name to look for in path captures (e.g. \"companyId\").\n * @returns The extracted value, or undefined if extraction fails.\n */\nexport function applyExtractor(\n  url: string,\n  extractor: UrlFieldExtractor,\n  defaultParam: string,\n): string | undefined {\n  try {\n    const parsed = new URL(url);\n\n    // Mode 1: Query parameter extraction\n    if (extractor.searchParam) {\n      const value = parsed.searchParams.get(extractor.searchParam);\n      return value || undefined;\n    }\n\n    // Mode 2: Hostname segment extraction\n    if (extractor.hostSegment !== undefined) {\n      const segments = parsed.hostname.split(\".\");\n      const value = segments[extractor.hostSegment];\n      return value || undefined;\n    }\n\n    // Mode 3: Path pattern extraction\n    if (extractor.urlPattern) {\n      const paramName = extractor.param || defaultParam;\n      const captures = matchPathPattern(parsed.pathname, extractor.urlPattern, {\n        segmentCharset: extractor.segmentCharset,\n        targetParam: paramName,\n      });\n      if (!captures) return undefined;\n      return captures[paramName] || undefined;\n    }\n\n    return undefined;\n  } catch {\n    return undefined;\n  }\n}\n\n/**\n * Extract a field value from a URL by trying extractors in order.\n * Returns the first successful extraction.\n *\n * @param url — The full URL to extract from.\n * @param extractors — Array of extractor definitions (tried in order).\n * @param defaultParam — Default param name (e.g. \"companyId\" or \"jobId\").\n * @returns The extracted value, or null if no extractor matches.\n */\nexport function extractFieldFromUrl(\n  url: string,\n  extractors: UrlFieldExtractor[],\n  defaultParam: string,\n): string | undefined {\n  for (const extractor of extractors) {\n    const result = applyExtractor(url, extractor, defaultParam);\n    if (result !== undefined) return result;\n  }\n  return undefined;\n}\n\n/**\n * Build a canonical listing URL from a job/detail URL using extractors.\n *\n * For path-based extractors: reconstructs the URL using only the path up to\n * the companyId capture (strips trailing job-specific path).\n *\n * For query/subdomain extractors: returns the origin (scheme + host) as the\n * listing base.\n *\n * @param url — The full URL to derive the listing URL from.\n * @param extractors — The companyId extractors.\n * @returns The canonical listing URL, or null.\n */\nexport function buildListingUrlFromExtractors(\n  url: string,\n  extractors: UrlFieldExtractor[],\n): string | null {\n  try {\n    const parsed = new URL(url);\n    let anyMatched = false;\n\n    for (const extractor of extractors) {\n      // Collect all named captures from the extractor match\n      let captures: Record<string, string> = {};\n\n      if (extractor.urlPattern) {\n        const paramName = extractor.param || \"companyId\";\n        const result = matchPathPattern(parsed.pathname, extractor.urlPattern, {\n          segmentCharset: extractor.segmentCharset,\n          targetParam: paramName,\n        });\n        if (!result) continue;\n        captures = result;\n        if (!captures[paramName]) continue;\n      } else if (extractor.hostSegment !== undefined) {\n        const value = applyExtractor(url, extractor, \"companyId\");\n        if (!value) continue;\n        captures = { companyId: value };\n      } else if (extractor.searchParam) {\n        const value = parsed.searchParams.get(extractor.searchParam);\n        if (!value) continue;\n        captures = { companyId: value };\n      } else {\n        continue;\n      }\n\n      anyMatched = true;\n\n      // Use the explicit template if provided\n      if (extractor.listingUrlTemplate !== undefined) {\n        const filled = extractor.listingUrlTemplate.replace(\n          /:([a-zA-Z][a-zA-Z0-9_]*)/g,\n          (_, name) =>\n            captures[name] !== undefined ? captures[name] : `:${name}`,\n        );\n        return `${parsed.origin}${filled}`;\n      }\n\n      // No template on this extractor — try the next one\n    }\n\n    // Fallback: if anything matched but no template found, return origin\n    return anyMatched ? parsed.origin : null;\n  } catch {\n    return null;\n  }\n}\n\n// ---------------------------------------------------------------------------\n// High-level URL info extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Result of extracting provider info from a URL using manifest patterns.\n * Pure pattern-matching — no HTTP requests or scraping.\n */\nexport interface UrlExtractionResult {\n  /** Whether the URL matches any of the provider's urlPatterns. */\n  urlPatternMatched: boolean;\n  /** Extracted job ID from jobIdExtractor, or null. */\n  jobId: string | null;\n  /** Extracted company ID from companyIdExtractor, or null. */\n  companyId: string | null;\n  /** Canonical listing URL reconstructed from companyIdExtractor, or null. */\n  listingUrl: string | null;\n}\n\n/**\n * Extract provider info (companyId, jobId, listingUrl) from a URL using\n * structured extractors. Pure pattern-matching — no HTTP requests or scraping.\n *\n * @param url — The URL to extract from (e.g. a job listing URL).\n * @param urlPatterns — Glob patterns that identify the provider (from manifest `urlPatterns`).\n * @param companyIdExtractors — Structured extractors for company ID (from manifest `companyIdExtractor`).\n * @param jobIdExtractors — Structured extractors for job ID (from manifest `jobIdExtractor`).\n * @returns Extraction result with matched flag, companyId, jobId, and canonical listingUrl.\n *\n * @example\n * ```ts\n * const result = extractUrlInfo(\n *   \"https://jobs.ashbyhq.com/ramp/abc-def-123\",\n *   [\"https://jobs.ashbyhq.com/**\"],\n *   [{ urlPattern: \"/:companyId(/**)\" }],\n *   [{ urlPattern: \"/:companyId/:jobId(/*)\" }],\n * );\n * // → { urlPatternMatched: true, companyId: \"ramp\", jobId: \"abc-def-123\",\n * //     listingUrl: \"https://jobs.ashbyhq.com/ramp\" }\n * ```\n */\nexport function extractUrlInfo(\n  url: string,\n  urlPatterns: string[],\n  companyIdExtractors: UrlFieldExtractor[],\n  jobIdExtractors: UrlFieldExtractor[],\n  excludeUrlPatterns?: string[],\n): UrlExtractionResult {\n  if (\n    excludeUrlPatterns &&\n    excludeUrlPatterns.length > 0 &&\n    matchesAnyUrlPattern(url, excludeUrlPatterns)\n  ) {\n    return {\n      urlPatternMatched: false,\n      jobId: null,\n      companyId: null,\n      listingUrl: null,\n    };\n  }\n\n  const companyId =\n    extractFieldFromUrl(url, companyIdExtractors, \"companyId\") ?? null;\n  const jobId = extractFieldFromUrl(url, jobIdExtractors, \"jobId\") ?? null;\n  const listingUrl = buildListingUrlFromExtractors(url, companyIdExtractors);\n\n  return {\n    urlPatternMatched: matchesAnyUrlPattern(url, urlPatterns),\n    jobId,\n    companyId,\n    listingUrl,\n  };\n}\n\n/**\n * Extract a path segment from a URL by position (0-indexed from the path root).\n *\n * @param url — The URL to extract from.\n * @param index — The segment index (0 = first path segment).\n * @returns The path segment, or `undefined` if the index is out of bounds.\n *\n * @example\n * ```ts\n * extractPathSegment(\"https://boards.greenhouse.io/acme/jobs/123\", 0);\n * // → \"acme\"\n *\n * extractPathSegment(\"https://boards.greenhouse.io/acme/jobs/123\", 2);\n * // → \"123\"\n * ```\n */\nexport function extractPathSegment(\n  url: string,\n  index: number,\n): string | undefined {\n  try {\n    const segments = new URL(url).pathname.split(\"/\").filter(Boolean);\n    return segments[index];\n  } catch {\n    return undefined;\n  }\n}\n\n/**\n * Build a URL from a base and path segments, with optional query parameters.\n *\n * @param base — Base URL (e.g. `\"https://api.ats.com\"`).\n * @param path — Path segments to join (e.g. `[\"v1\", \"jobs\"]`).\n * @param params — Optional query parameters.\n * @returns The constructed URL string.\n *\n * @example\n * ```ts\n * buildUrl(\"https://api.ats.com\", [\"v1\", \"jobs\"], { page: \"2\", limit: \"20\" });\n * // → \"https://api.ats.com/v1/jobs?page=2&limit=20\"\n * ```\n */\nexport function buildUrl(\n  base: string,\n  path: string[],\n  params?: Record<string, string | number | boolean | undefined>,\n): string {\n  const cleanBase = base.replace(/\\/+$/, \"\");\n  const cleanPath = path.map((s) => encodeURIComponent(s)).join(\"/\");\n  let url = `${cleanBase}/${cleanPath}`;\n\n  if (params) {\n    const searchParams = new URLSearchParams();\n    for (const [key, value] of Object.entries(params)) {\n      if (value !== undefined && value !== null) {\n        searchParams.set(key, String(value));\n      }\n    }\n    const qs = searchParams.toString();\n    if (qs) url += `?${qs}`;\n  }\n\n  return url;\n}\n\nfunction escapeRegex(str: string): string {\n  return str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n"]}