88export const UNICODE_CHAR_REGEXP =
99 / \ud83c [ \udffb - \udfff ] (? = \ud83c [ \udffb - \udfff ] ) | (?: (?: \ud83c \udff4 \udb40 \udc67 \udb40 \udc62 \udb40 (?: \udc65 | \udc73 | \udc77 ) \udb40 (?: \udc6e | \udc63 | \udc6c ) \udb40 (?: \udc67 | \udc74 | \udc73 ) \udb40 \udc7f ) | [ ^ \ud800 - \udfff ] [ \u0300 - \u036f \ufe20 - \ufe2f \u20d0 - \u20ff \u1ab0 - \u1aff \u1dc0 - \u1dff ] ? | [ \u0300 - \u036f \ufe20 - \ufe2f \u20d0 - \u20ff \u1ab0 - \u1aff \u1dc0 - \u1dff ] | (?: \ud83c [ \udde6 - \uddff ] ) { 2 } | [ \ud800 - \udbff ] [ \udc00 - \udfff ] | [ \ud800 - \udfff ] ) [ \ufe0e \ufe0f ] ? (?: [ \u0300 - \u036f \ufe20 - \ufe2f \u20d0 - \u20ff \u1ab0 - \u1aff \u1dc0 - \u1dff ] | \ud83c [ \udffb - \udfff ] ) ? (?: \u200d (?: [ ^ \ud800 - \udfff ] | (?: \ud83c [ \udde6 - \uddff ] ) { 2 } | [ \ud800 - \udbff ] [ \udc00 - \udfff ] ) [ \ufe0e \ufe0f ] ? (?: [ \u0300 - \u036f \ufe20 - \ufe2f \u20d0 - \u20ff \u1ab0 - \u1aff \u1dc0 - \u1dff ] | \ud83c [ \udffb - \udfff ] ) ? ) * / g;
1010
11- export function usesMultiCodePointCharacters ( text : string | string [ ] ) : boolean {
12- if ( ! text ) {
13- return false ;
14- }
15-
16- if ( Array . isArray ( text ) ) {
11+ export function detectMultiCodePointCharactersUsage ( text : string | string [ ] ) : boolean {
12+ if ( ! text ) return false ;
13+ else if ( text . includes ( "\x1b" ) ) return true ;
14+ else if ( Array . isArray ( text ) ) {
1715 for ( const line of text ) {
1816 if ( getMultiCodePointCharacters ( line ) . length === line . length ) {
1917 return true ;
2018 }
2119 }
20+
2221 return false ;
2322 }
24-
2523 return getMultiCodePointCharacters ( text ) . length === text . length ;
2624}
2725
@@ -31,52 +29,102 @@ export function getMultiCodePointCharacters(text: string): string[] {
3129 if ( ! text ) return empty ;
3230 const matched = text . match ( UNICODE_CHAR_REGEXP ) ;
3331
34- if ( matched ?. includes ( "\x1b" ) ) {
35- const arr : string [ ] = [ ] ;
36- let i = 0 ;
37- let ansi = 0 ;
38- let lastStyle = "" ;
39- for ( const char of matched ) {
40- arr [ i ] ??= "" ;
41- arr [ i ] += lastStyle + char ;
32+ return matched ?? empty ;
33+ }
34+
35+ /**
36+ * Reapplies style for each character
37+ * If given an array it does modifications on that array instead of creating a new one
38+ *
39+ * @example
40+ * ```ts
41+ * console.log(repplyCharacterStyles("\x1b[32mHi")); // "\x1b[32mH\x1b[32mi"
42+ * ```
43+ *
44+ * @example
45+ * ```ts
46+ * const arr = ["\x1b[32mH", "i"];
47+ * console.log(repplyCharacterStyles(arr)); // ["\x1b[32mH", "\x1b[32mi"];
48+ * console.log(arr); // ["\x1b[32mH", "\x1b[32mi"];
49+ * ```
50+ */
51+ export function reapplyCharacterStyles ( text : string [ ] ) : string [ ] {
52+ // Heuristic for skipping reapplying when text doesn't include introducer
53+ if ( ! text . includes ( "\x1b" ) ) {
54+ return text ;
55+ }
56+
57+ let i = 0 ;
58+ let ansi = 0 ;
59+ let lastStyle = "" ;
60+ let flushStyle = false ;
4261
43- if ( char === "\x1b" ) {
62+ for ( const char of text ) {
63+ if ( char === "\x1b" ) {
64+ // possible start of an ansi sequence
65+ ++ ansi ;
66+ } else if ( ansi === 1 ) {
67+ // confirm whether ansi sequence has been started
68+ if ( char === "[" ) {
69+ lastStyle += "\x1b" + char ;
4470 ++ ansi ;
45- lastStyle += "\x1b" ;
46- } else if ( ansi ) {
47- lastStyle += char ;
71+ } else {
72+ ansi = 0 ;
73+ }
74+ } else if ( ansi > 1 ) {
75+ lastStyle += char ;
76+
77+ const isFinalByte = isFinalAnsiByte ( char ) ;
4878
49- if ( ansi === 3 && char === "m" && lastStyle [ lastStyle . length - 2 ] === "0" ) {
79+ if ( isFinalByte ) {
80+ flushStyle = true ;
81+
82+ // End of ansi sequence
83+ if ( ansi === 3 && lastStyle [ lastStyle . length - 2 ] === "0" ) {
84+ // Style is "\x1b[0m" – no need to store the last style when all of them got cleared
5085 lastStyle = "" ;
5186 }
5287
53- if ( char === "m" ) {
54- ansi = 0 ;
55- } else {
56- ++ ansi ;
57- }
88+ ansi = 0 ;
5889 } else {
59- ++ i ;
90+ // Part of an ansi sequence
91+ ++ ansi ;
92+ }
93+ } else {
94+ if ( flushStyle ) {
95+ text [ i ] = lastStyle + char ;
6096 }
97+
98+ ++ i ;
6199 }
100+ }
62101
63- return arr ;
102+ if ( text . length > i ) {
103+ while ( text . length > i ) {
104+ text . pop ( ) ;
105+ }
64106 }
65107
66- return matched ?? empty ;
108+ return text ;
109+ }
110+
111+ export function isFinalAnsiByte ( character : string ) : boolean {
112+ const codePoint = character . charCodeAt ( 0 ) ;
113+ // don't include 0x70–0x7E range because its considered "private"
114+ return codePoint >= 0x40 && codePoint < 0x70 ;
67115}
68116
69- /** Strips string of all its styles */
70- export function stripStyles ( string : string ) : string {
117+ /** Strips text of all its styles */
118+ export function stripStyles ( text : string ) : string {
71119 let stripped = "" ;
72120 let ansi = false ;
73- const len = string . length ;
121+ const len = text . length ;
74122 for ( let i = 0 ; i < len ; ++ i ) {
75- const char = string [ i ] ;
123+ const char = text [ i ] ;
76124 if ( char === "\x1b" ) {
77125 ansi = true ;
78126 i += 2 ; // [ "\x1b" "[" "X" "m" ] <-- shortest ansi sequence
79- } else if ( char === "m" && ansi ) {
127+ } else if ( ansi && isFinalAnsiByte ( char ) ) {
80128 ansi = false ;
81129 } else if ( ! ansi ) {
82130 stripped += char ;
@@ -85,9 +133,9 @@ export function stripStyles(string: string): string {
85133 return stripped ;
86134}
87135
88- /** Inserts {value} into {string } on given {index} */
89- export function insertAt ( string : string , index : number , value : string ) : string {
90- return string . slice ( 0 , index ) + value + string . slice ( index ) ;
136+ /** Inserts {value} into {text } on given {index} */
137+ export function insertAt ( text : string , index : number , value : string ) : string {
138+ return text . slice ( 0 , index ) + value + text . slice ( index ) ;
91139}
92140
93141/** Returns real {text} width */
@@ -102,7 +150,7 @@ export function textWidth(text: string, start = 0): number {
102150 if ( char === "\x1b" ) {
103151 ansi = true ;
104152 i += 2 ; // [ "\x1b" "[" "X" "m" ] <-- shortest ansi sequence
105- } else if ( char === "m" && ansi ) {
153+ } else if ( ansi && isFinalAnsiByte ( char ) ) {
106154 ansi = false ;
107155 } else if ( ! ansi ) {
108156 width += characterWidth ( char ) ;
0 commit comments