{"version":3,"file":"fuzzy.mjs","names":[],"sources":["../src/fuzzy.ts"],"sourcesContent":["/**\n * Browser-safe fuzzy search helper. Exported through the\n * `@unpunnyfuns/swatchbook-core/fuzzy` subpath so the MCP server\n * + future browser consumers can use it without pulling in the\n * Terrazzo parser. Single transitive dep (`@leeoniya/ufuzzy`),\n * zero Node API usage.\n */\nimport uFuzzy from '@leeoniya/ufuzzy';\n\nexport interface FuzzyOptions {\n  /** Cap the returned list. Applied after ranking. */\n  limit?: number;\n  /**\n   * Allow terms to match out of authored order. `false` means `\"bg prim\"`\n   * only matches when `bg` appears before `prim` in the haystack entry;\n   * `true` also accepts `\"prim bg\"`. Defaults to `true` — token paths read\n   * left-to-right in DTCG convention, but user queries often don't.\n   */\n  outOfOrder?: boolean;\n}\n\nconst matcher = new uFuzzy({ intraMode: 1, interIns: 8 });\n\n/**\n * Rank-ordered fuzzy filter. Empty / whitespace-only queries return the\n * items unchanged in their input order. Non-empty queries return only\n * matching items, ranked by uFuzzy's default scoring (boundary / prefix\n * quality, tighter matches first). Ties fall back to input order.\n *\n * The matcher is configured for short identifier-style haystacks — token\n * paths, CSS variable names — with single-character typo tolerance\n * (`intraMode: SingleError`) and generous allowance for extra characters\n * between the needle's terms (`interIns: 8`), which covers multi-segment\n * paths like `color.surface.default` searched with `\"surf def\"`.\n */\nexport function fuzzyFilter<T>(\n  items: readonly T[],\n  query: string,\n  key: (item: T) => string,\n  options: FuzzyOptions = {},\n): T[] {\n  const needle = query.trim();\n  if (needle === '') return items.slice(0, options.limit);\n  if (items.length === 0) return [];\n\n  const haystack = items.map(key);\n  const outOfOrder = options.outOfOrder ?? true;\n  const [idxs, _info, order] = matcher.search(haystack, needle, outOfOrder ? 8 : 0);\n  if (idxs === null) return [];\n\n  const ranked = order\n    ? order.flatMap((oi): number[] => {\n        const v = idxs[oi];\n        return v === undefined ? [] : [v];\n      })\n    : idxs;\n  const out: T[] = [];\n  const cap = options.limit ?? ranked.length;\n  for (let i = 0; i < ranked.length && out.length < cap; i += 1) {\n    const rankIndex = ranked[i];\n    if (rankIndex === undefined) continue;\n    const item = items[rankIndex];\n    if (item !== undefined) out.push(item);\n  }\n  return out;\n}\n\n/**\n * Test a single string against a query. Convenience wrapper for\n * single-value predicates (e.g. tree pruning where each leaf is tested\n * independently). Empty query matches everything.\n */\nexport function fuzzyMatches(haystack: string, query: string): boolean {\n  const needle = query.trim();\n  if (needle === '') return true;\n  const [idxs] = matcher.search([haystack], needle, 8);\n  return idxs !== null && idxs.length > 0;\n}\n"],"mappings":";;;;;;;;;AAqBA,MAAM,UAAU,IAAI,OAAO;CAAE,WAAW;CAAG,UAAU;CAAG,CAAC;;;;;;;;;;;;;AAczD,SAAgB,YACd,OACA,OACA,KACA,UAAwB,EAAE,EACrB;CACL,MAAM,SAAS,MAAM,MAAM;AAC3B,KAAI,WAAW,GAAI,QAAO,MAAM,MAAM,GAAG,QAAQ,MAAM;AACvD,KAAI,MAAM,WAAW,EAAG,QAAO,EAAE;CAEjC,MAAM,WAAW,MAAM,IAAI,IAAI;CAC/B,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,CAAC,MAAM,OAAO,SAAS,QAAQ,OAAO,UAAU,QAAQ,aAAa,IAAI,EAAE;AACjF,KAAI,SAAS,KAAM,QAAO,EAAE;CAE5B,MAAM,SAAS,QACX,MAAM,SAAS,OAAiB;EAC9B,MAAM,IAAI,KAAK;AACf,SAAO,MAAM,KAAA,IAAY,EAAE,GAAG,CAAC,EAAE;GACjC,GACF;CACJ,MAAM,MAAW,EAAE;CACnB,MAAM,MAAM,QAAQ,SAAS,OAAO;AACpC,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,UAAU,IAAI,SAAS,KAAK,KAAK,GAAG;EAC7D,MAAM,YAAY,OAAO;AACzB,MAAI,cAAc,KAAA,EAAW;EAC7B,MAAM,OAAO,MAAM;AACnB,MAAI,SAAS,KAAA,EAAW,KAAI,KAAK,KAAK;;AAExC,QAAO;;;;;;;AAQT,SAAgB,aAAa,UAAkB,OAAwB;CACrE,MAAM,SAAS,MAAM,MAAM;AAC3B,KAAI,WAAW,GAAI,QAAO;CAC1B,MAAM,CAAC,QAAQ,QAAQ,OAAO,CAAC,SAAS,EAAE,QAAQ,EAAE;AACpD,QAAO,SAAS,QAAQ,KAAK,SAAS"}