From b8af7284fa493c8a13cee63150a8a6bc946d3481 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 07:44:15 +0000 Subject: [PATCH] fix(playlist): do not fold real flag emoji in auto-detect label folding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regional-indicator runs of exactly two are real flag emoji (πŸ‡³πŸ‡±); folding them glued letters onto adjacent text and could break a following label's word boundary. Only runs of three or more β€” fancy-text words like πŸ‡ΊπŸ‡ΈπŸ‡ͺπŸ‡· β€” fold to ASCII now. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4 --- .../provider-import-detection.util.spec.ts | 23 ++++++++++ .../src/lib/provider-import-scan.util.ts | 45 +++++++++++++++++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts b/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts index 08ca2562e..d4fed438a 100644 --- a/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts +++ b/libs/shared/interfaces/src/lib/provider-import-detection.util.spec.ts @@ -187,6 +187,29 @@ describe('detectProviderImportCandidates', () => { }); }); + it('folds regional-indicator fancy text but leaves real flag emoji alone', () => { + // πŸ‡ΊπŸ‡ΈπŸ‡ͺπŸ‡· / πŸ‡΅πŸ‡¦πŸ‡ΈπŸ‡Έ are runs of 3+ regional indicators β€” fancy text + // for USER / PASS. πŸ‡ΉπŸ‡· is a run of exactly two β€” a real flag that + // must not fold, or the glued "TR" would break the PORT label's + // word boundary. + const candidates = detectProviderImportCandidates( + [ + 'Server: iptv.example.tv', + 'πŸ‡ΉπŸ‡·PORT: 8080', + 'πŸ‡ΊπŸ‡ΈπŸ‡ͺπŸ‡· : carol', + 'πŸ‡΅πŸ‡¦πŸ‡ΈπŸ‡Έ : pa55', + ].join('\n') + ); + + const xtream = only(candidates, 'xtream'); + expect(xtream).toHaveLength(1); + expect(xtream[0]).toMatchObject({ + serverUrl: 'http://iptv.example.tv:8080', + username: 'carol', + password: 'pa55', + }); + }); + it('does not read a hyphenated word as a labeled value', () => { expect( detectProviderImportCandidates( diff --git a/libs/shared/interfaces/src/lib/provider-import-scan.util.ts b/libs/shared/interfaces/src/lib/provider-import-scan.util.ts index f45ffa4f8..d598e5a1f 100644 --- a/libs/shared/interfaces/src/lib/provider-import-scan.util.ts +++ b/libs/shared/interfaces/src/lib/provider-import-scan.util.ts @@ -98,19 +98,56 @@ const isHexIdentity = (value: string) => HEX_IDENTITY_PATTERN.test(value); * ASCII. */ export function foldDecorativeAlphabets(text: string): string { + const chars = Array.from(text); let result = ''; - for (const char of text) { - result += - foldDecorativeCodePoint(char.codePointAt(0) as number) ?? char; + for (let index = 0; index < chars.length; ) { + const code = chars[index].codePointAt(0) as number; + if (isRegionalIndicator(code)) { + // Regional indicators are ambiguous: a run of EXACTLY two is a + // real flag emoji (πŸ‡³πŸ‡±) that must survive untouched β€” folding it + // would glue "NL" onto whatever follows and break an adjacent + // label's word boundary. Fancy-text words (πŸ‡ΊπŸ‡ΈπŸ‡ͺπŸ‡·, πŸ‡ΊπŸ‡·πŸ‡±) are runs + // of three or more, so only those fold to letters. + let end = index; + while ( + end < chars.length && + isRegionalIndicator(chars[end].codePointAt(0) as number) + ) { + end += 1; + } + const run = chars.slice(index, end); + result += + run.length >= 3 + ? run + .map((char) => + String.fromCharCode( + 0x41 + + ((char.codePointAt(0) as number) - + REGIONAL_INDICATOR_A) + ) + ) + .join('') + : run.join(''); + index = end; + continue; + } + result += foldDecorativeCodePoint(code) ?? chars[index]; + index += 1; } return result; } +const REGIONAL_INDICATOR_A = 0x1f1e6; +const REGIONAL_INDICATOR_Z = 0x1f1ff; + +function isRegionalIndicator(code: number): boolean { + return code >= REGIONAL_INDICATOR_A && code <= REGIONAL_INDICATOR_Z; +} + const DECORATIVE_LATIN_RANGES: Array<[number, number]> = [ [0x1f130, 0x1f149], // πŸ„° squared A–Z [0x1f150, 0x1f169], // πŸ… negative circled A–Z [0x1f170, 0x1f189], // πŸ…° negative squared A–Z - [0x1f1e6, 0x1f1ff], // πŸ‡¦ regional indicator A–Z ]; const DECORATIVE_DIGIT_RANGES: Array<[number, number]> = [