@@ -22,25 +22,24 @@ const leadingNonPrintingRegex = /^[\p{Default_Ignorable_Code_Point}\p{Control}\p
2222// RGI emoji sequences
2323const rgiEmojiRegex = / ^ \p{ RGI_Emoji} $ / v;
2424
25- // Detect minimally-qualified/unqualified emoji clusters (missing VS16)
26- // - ZWJ sequences with 2+ Extended_Pictographic (e.g., ❤🔥, 🏳🌈, ⛹♂)
27- // - Keycap sequences (e.g., #⃣, 0⃣)
28- const zwjRegex = / \u200D / ;
29- const validKeycapRegex = / ^ [ \d # * ] .* \u20E3 / ;
25+ // Detect minimally-qualified/unqualified emoji sequences (missing VS16 but still render as double-width)
26+ const unqualifiedKeycapRegex = / ^ [ \d # * ] \u20E3 $ / ;
3027const extendedPictographicRegex = / \p{ Extended_Pictographic} / gu;
3128
32- function isDoubleWidthEmojiCluster ( segment ) {
33- // Keycap sequences with valid base (0-9, #, *)
34- if ( validKeycapRegex . test ( segment ) ) {
29+ function isDoubleWidthNonRgiEmojiSequence ( segment ) {
30+ // Real emoji clusters are < 30 chars; guard against pathological input
31+ if ( segment . length > 50 ) {
32+ return false ;
33+ }
34+
35+ if ( unqualifiedKeycapRegex . test ( segment ) ) {
3536 return true ;
3637 }
3738
3839 // ZWJ sequences with 2+ Extended_Pictographic
39- if ( zwjRegex . test ( segment ) ) {
40- const matches = segment . match ( extendedPictographicRegex ) ;
41- if ( matches && matches . length >= 2 ) {
42- return true ;
43- }
40+ if ( segment . includes ( '\u200D' ) ) {
41+ const pictographics = segment . match ( extendedPictographicRegex ) ;
42+ return pictographics !== null && pictographics . length >= 2 ;
4443 }
4544
4645 return false ;
@@ -97,7 +96,7 @@ export default function stringWidth(input, options = {}) {
9796 }
9897
9998 // Emoji width logic
100- if ( rgiEmojiRegex . test ( segment ) || isDoubleWidthEmojiCluster ( segment ) ) {
99+ if ( rgiEmojiRegex . test ( segment ) || isDoubleWidthNonRgiEmojiSequence ( segment ) ) {
101100 width += 2 ;
102101 continue ;
103102 }
0 commit comments