mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 14:16:16 -08:00
Turkish upper and lower case queries now return the same results everywhere a title can be searched. Lower-casing the dotted capital "İ" (U+0130) leaves a combining dot behind, so "İnş" and "inş" reached different search arms and different results. - Case folding is locale-invariant: `toLowerCase()`, never `toLocaleLowerCase()`, which under a Turkish or Azeri OS locale maps ASCII "I" to the dotless "ı". - The Electron content search composes to NFC and drops the leftover combining marks before tokenizing, and its LIKE/GLOB pattern builders additionally spell the `'tr'`-locale İ forms, since SQLite LIKE folds only ASCII. - A shared `foldSearchText` covers every in-memory filter: channel lists, the Xtream and Stalker catalogs, category filters, collections, the EPG guide, the command palette, sources, the playlist switcher and the download lists. - Composing before the strip keeps canonically equivalent spellings equal while the fold stays accent-sensitive; the Turkish I/ı pair is deliberately left alone, as the FTS index does not fold it either. Covered by a SQLite-backed spec over the real trigram index plus regression cases in the affected renderer specs. Closes #609. Co-Authored-By: Justin Willhite <5132924+thejdubb02@users.noreply.github.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
36 lines
1.7 KiB
TypeScript
36 lines
1.7 KiB
TypeScript
/**
|
||
* Locale-invariant case fold for in-memory search filters (channel lists,
|
||
* catalog and category filters, source and download lists). Pure function,
|
||
* no Angular/Node dependencies.
|
||
*
|
||
* `toLowerCase()` alone is not enough for one visible input: the Turkish
|
||
* dotted capital "İ" (U+0130) lower-cases to "i" plus a combining dot above
|
||
* (U+0307), so `"İnşaat".toLowerCase().includes("inş")` is `false` even
|
||
* though the two spellings are the same word (issue #609).
|
||
*
|
||
* The fold therefore runs `toLowerCase()`, re-composes the result to NFC and
|
||
* drops whatever combining marks are left. Composing first is what makes the
|
||
* fold agree on canonically equivalent text: a provider title stored
|
||
* decomposed ("e" + U+0301) and a query typed precomposed ("é") reach the
|
||
* same string, and a Greek "Ά" lower-cases to a form NFC maps onto the same
|
||
* "ά" the user types. Only marks that cannot compose survive to be stripped —
|
||
* the dotted I's leftover dot among them — so precomposed letters (ç, ş, ü,
|
||
* é, ё, й) are preserved and the fold stays accent-sensitive, exactly as the
|
||
* filters were before. A lower-cased string of printable ASCII is already NFC
|
||
* and carries no marks, which is the fast path taken by most titles.
|
||
*
|
||
* `toLocaleLowerCase()` is deliberately not used: under a Turkish or Azeri
|
||
* OS locale it maps ASCII "I" to the dotless "ı", so the same list would
|
||
* filter differently per machine.
|
||
*/
|
||
const COMBINING_MARKS_REGEXP = /[̀-ͯ]/g;
|
||
const NON_PRINTABLE_ASCII_REGEXP = /[^ -~]/;
|
||
|
||
export function foldSearchText(value: string): string {
|
||
const lowered = value.toLowerCase();
|
||
|
||
return NON_PRINTABLE_ASCII_REGEXP.test(lowered)
|
||
? lowered.normalize('NFC').replace(COMBINING_MARKS_REGEXP, '')
|
||
: lowered;
|
||
}
|