fix(playlist): do not fold real flag emoji in auto-detect label folding

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
This commit is contained in:
Claude committed 2026-08-15 07:44:15 +00:00
1 parent 7b57b22e88
commit b8af7284fa
2 files changed
+64 -4

No files matched your search

@@ -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(
@@ -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]> = [