Fix ZWJ sequence emoji support in custom emoji settings

- Add getGraphemeClusters.ts utility for proper emoji segmentation
- Update updateSymbolPatternsFromSettings.ts to handle ZWJ sequences correctly  
- Enhance Unicode utilities to use grapheme clusters instead of UTF-16 code units
- Support complex emojis like 🏴‍☠️ (Pirate Flag), 👨‍🚒 (Man Firefighter), etc.
- Uses Intl.Segmenter when available, with regex fallback for older environments
- Fully backward compatible with existing emoji configurations
- Update documentation and add changelog entry

Fixes bug where ZWJ sequence emojis were split into component parts instead of being treated as single emojis.

Simplify ZWJ sequence fix by inlining Intl.Segmenter

- Remove overengineered getGraphemeClusters.ts utility 
- Inline simple Intl.Segmenter usage directly where needed
- Assume Intl.Segmenter is available in all target environments
- Reduced bundle size and simplified codebase
- Update documentation to reflect simplified approach
This commit is contained in:
Filip Noetzel 2025-09-06 14:58:16 +02:00
parent ae6d438929
commit c8b6605e7a
4 changed files with 21 additions and 7 deletions

View file

@ -56,11 +56,18 @@ You can configure which emojis are supported for rating interactions by going to
**Default emojis**: `🎥🏆⭐💎🔥⚡🎯🚀💰🎖️`
**ZWJ Sequence Support**: The plugin fully supports complex emojis including ZWJ (Zero Width Joiner) sequences such as:
- 🏴‍☠️ (Pirate Flag)
- 👨‍🚒 (Man Firefighter)
- 🏳️‍🌈 (Rainbow Flag)
- And other compound emojis
**Example usage**:
- `🎥🎥🎥🎥🎥 (4/5)` Movie rating
- `🏆🏆🏆 3/5` Achievement level
- `🔥🔥🔥🔥 80%` Spice level
- `🎯🎯🎯🎯🎯🎯 (5/6)` Accuracy rating
- `🏴‍☠️🏴‍☠️🏴‍☠️ (3/5)` Pirate movie rating
## Installation

View file

@ -1,6 +1,8 @@
/**
* Get the length of a string in Unicode characters
* Get the length of a string in Unicode grapheme clusters
* This properly handles ZWJ sequences, variation selectors, and complex emoji
*/
export function getUnicodeCharLength(str: string): number {
return [...str].length;
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
return Array.from(segmenter.segment(str)).length;
}

View file

@ -1,6 +1,8 @@
/**
* Get a substring with proper Unicode character handling
* Get a substring with proper Unicode grapheme cluster handling
* This properly handles ZWJ sequences, variation selectors, and complex emoji
*/
export function getUnicodeSubstring(str: string, start: number, end: number): string {
return [...str].slice(start, end).join('');
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
return Array.from(segmenter.segment(str), segment => segment.segment).slice(start, end).join('');
}

View file

@ -10,11 +10,12 @@ export function updateSymbolPatternsFromSettings(settings: InteractiveRatingsSet
// Add emoji patterns from settings
if (settings.supportedEmojis) {
// Split the emoji string into individual characters (handling Unicode properly)
const emojis = [...settings.supportedEmojis];
// Split the emoji string into individual grapheme clusters (handling ZWJ sequences properly)
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const emojis = Array.from(segmenter.segment(settings.supportedEmojis), segment => segment.segment);
for (const emoji of emojis) {
if (emoji.trim()) { // Skip empty characters
if (emoji.trim()) { // Skip empty characters and whitespace
newPatterns.push({
full: emoji,
empty: emoji,
@ -28,8 +29,10 @@ export function updateSymbolPatternsFromSettings(settings: InteractiveRatingsSet
updateSymbolPatterns(newPatterns);
if (LOGGING_ENABLED) {
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
console.debug('[InteractiveRatings] Updated symbol patterns from settings', {
supportedEmojis: settings.supportedEmojis,
parsedEmojis: settings.supportedEmojis ? Array.from(segmenter.segment(settings.supportedEmojis), segment => segment.segment) : [],
totalPatterns: newPatterns.length
});
}