{"version":3,"file":"util-8o642qKa.mjs","names":[],"sources":["../src/filter/util.ts"],"sourcesContent":["import type { SetFilterValue, TextMatchMode } from \"./filter.types\";\n\n/**\n * The sentinel a missing or empty value normalises to.\n *\n * `\"\"` is the right choice *because* it conflates empty-string with missing — that is the behaviour\n * a \"(Blank)\" checkbox is expected to have. A more distinctive sentinel would preserve a\n * distinction no filter UI has a way to express, and would stop the domain being JSON-safe.\n */\nexport const BLANK = \"\" as const;\n\n// Values reaching here are primitives in practice; the cast is what lets the type-aware linter\n// accept stringifying an `unknown`. Same idiom as `table/util.ts`.\nconst asString = (value: unknown): string => String(value as string);\n\n/**\n * Whether a value counts as missing/empty: `null`, `undefined`, `\"\"`, or an array that contributes\n * nothing once flattened (so a `tags: []` row is reachable through the \"(Blank)\" facet rather than\n * being unreachable).\n */\nexport const isBlank = (value: unknown): boolean => {\n  if (value === null || value === undefined || value === \"\") return true;\n  if (Array.isArray(value)) {\n    return (value as unknown[])\n      .flat(Number.POSITIVE_INFINITY)\n      .every((v) => v === null || v === undefined || v === \"\");\n  }\n  return false;\n};\n\n/**\n * The set of facet values one raw cell value contributes: arrays flattened, blanks dropped, and\n * `{ BLANK }` when nothing non-blank survives.\n *\n * This is the single definition of the blank rule, shared verbatim by `SetFilter.matches` and the\n * table's facet tally. If those two ever disagreed you would get a facet in the list that selects\n * no rows — which reads as a broken filter rather than as a normalisation bug, and is why this is\n * one exported function rather than a rule written twice.\n *\n * Non-primitives are stringified so the domain stays comparable by value and JSON-safe; see\n * {@link SetFilterValue}.\n */\nexport const facetValues = (value: unknown): Set<SetFilterValue> => {\n  const out = new Set<SetFilterValue>();\n  const raw: unknown[] = Array.isArray(value)\n    ? (value as unknown[]).flat(Number.POSITIVE_INFINITY)\n    : [value];\n\n  for (const v of raw) {\n    if (v === null || v === undefined || v === \"\") continue;\n    out.add(\n      typeof v === \"string\" || typeof v === \"number\" || typeof v === \"boolean\" ? v : asString(v),\n    );\n  }\n\n  if (out.size === 0) out.add(BLANK);\n  return out;\n};\n\n/**\n * Compare a query against one value as text. An empty query matches everything, which is what makes\n * \"no query typed\" a pass-through rather than a special case at every call site.\n *\n * Nothing is trimmed: a trailing space is a legitimate part of a \"contains\" query, and trimming here\n * but not there is how the two drift apart.\n */\nexport const textMatches = (\n  query: string,\n  value: unknown,\n  match: TextMatchMode = \"contains\",\n  caseSensitive = false,\n): boolean => {\n  if (query === \"\") return true;\n\n  let haystack = value === null || value === undefined ? \"\" : asString(value);\n  let needle = query;\n  if (!caseSensitive) {\n    haystack = haystack.toLowerCase();\n    needle = needle.toLowerCase();\n  }\n\n  if (match === \"equals\") return haystack === needle;\n  if (match === \"startsWith\") return haystack.startsWith(needle);\n  return haystack.includes(needle);\n};\n"],"mappings":";;;;;;;;AASA,MAAa,QAAQ;AAIrB,MAAM,YAAY,UAA2B,OAAO,KAAe;;;;;;AAOnE,MAAa,WAAW,UAA4B;CAClD,IAAI,UAAU,QAAQ,UAAU,UAAa,UAAU,IAAI,OAAO;CAClE,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAQ,MACL,KAAK,OAAO,iBAAiB,CAAC,CAC9B,OAAO,MAAM,MAAM,QAAQ,MAAM,UAAa,MAAM,EAAE;CAE3D,OAAO;AACT;;;;;;;;;;;;;AAcA,MAAa,eAAe,UAAwC;CAClE,MAAM,sBAAM,IAAI,IAAoB;CACpC,MAAM,MAAiB,MAAM,QAAQ,KAAK,IACrC,MAAoB,KAAK,OAAO,iBAAiB,IAClD,CAAC,KAAK;CAEV,KAAK,MAAM,KAAK,KAAK;EACnB,IAAI,MAAM,QAAQ,MAAM,UAAa,MAAM,IAAI;EAC/C,IAAI,IACF,OAAO,MAAM,YAAY,OAAO,MAAM,YAAY,OAAO,MAAM,YAAY,IAAI,SAAS,CAAC,CAC3F;CACF;CAEA,IAAI,IAAI,SAAS,GAAG,IAAI,MAAS;CACjC,OAAO;AACT;;;;;;;;AASA,MAAa,eACX,OACA,OACA,QAAuB,YACvB,gBAAgB,UACJ;CACZ,IAAI,UAAU,IAAI,OAAO;CAEzB,IAAI,WAAW,UAAU,QAAQ,UAAU,SAAY,KAAK,SAAS,KAAK;CAC1E,IAAI,SAAS;CACb,IAAI,CAAC,eAAe;EAClB,WAAW,SAAS,YAAY;EAChC,SAAS,OAAO,YAAY;CAC9B;CAEA,IAAI,UAAU,UAAU,OAAO,aAAa;CAC5C,IAAI,UAAU,cAAc,OAAO,SAAS,WAAW,MAAM;CAC7D,OAAO,SAAS,SAAS,MAAM;AACjC"}