-
-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathtest.js
More file actions
executable file
·749 lines (610 loc) · 25.9 KB
/
test.js
File metadata and controls
executable file
·749 lines (610 loc) · 25.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
import test from 'ava';
import chalk from 'chalk';
import stripAnsi from 'strip-ansi';
import randomItem from 'random-item';
import sliceAnsi from './index.js';
chalk.level = 1;
const fixture = chalk.red('the ') + chalk.green('quick ') + chalk.blue('brown ') + chalk.cyan('fox ') + chalk.yellow('jumped ');
const stripped = stripAnsi(fixture);
const ESCAPE = '\u001B';
const ANSI_BELL = '\u0007';
const ANSI_STRING_TERMINATOR = `${ESCAPE}\\`;
const C1_OSC = '\u009D';
const C1_STRING_TERMINATOR = '\u009C';
function generate(string) {
const random1 = randomItem(['rock', 'paper', 'scissors']);
const random2 = randomItem(['blue', 'green', 'yellow', 'red']);
return `${string}:${chalk[random2](random1)} `;
}
function createHyperlink(text, url, terminator = ANSI_BELL, closeTerminator = terminator) {
return `${ESCAPE}]8;;${url}${terminator}${text}${ESCAPE}]8;;${closeTerminator}`;
}
function stripOscHyperlinks(string) {
const hyperlinkPrefixes = [`${ESCAPE}]8;`, `${C1_OSC}8;`];
let output = '';
let index = 0;
while (index < string.length) {
const hyperlinkPrefix = hyperlinkPrefixes.find(prefix => string.startsWith(prefix, index));
if (!hyperlinkPrefix) {
output += string[index];
index++;
continue;
}
const uriStart = string.indexOf(';', index + hyperlinkPrefix.length);
if (uriStart === -1) {
break;
}
let sequenceIndex = uriStart + 1;
while (sequenceIndex < string.length) {
if (string[sequenceIndex] === ANSI_BELL) {
index = sequenceIndex + 1;
break;
}
if (
string[sequenceIndex] === ESCAPE
&& string[sequenceIndex + 1] === '\\'
) {
index = sequenceIndex + 2;
break;
}
if (string[sequenceIndex] === C1_STRING_TERMINATOR) {
index = sequenceIndex + 1;
break;
}
sequenceIndex++;
}
if (sequenceIndex >= string.length) {
break;
}
}
return output;
}
function stripForVisibleComparison(string) {
return stripAnsi(stripOscHyperlinks(string));
}
function assertVisibleSliceMatchesNative(t, input, start, end) {
const nativeSlice = stripForVisibleComparison(input).slice(start, end);
const ansiSlice = stripForVisibleComparison(sliceAnsi(input, start, end));
t.is(ansiSlice, nativeSlice);
}
function styleScalarAtIndex(string, scalarIndex, style) {
let output = '';
let index = 0;
for (const scalar of string) {
output += index === scalarIndex ? style(scalar) : scalar;
index++;
}
return output;
}
function hyperlinkScalarAtIndex(string, scalarIndex, url) {
let output = '';
let index = 0;
for (const scalar of string) {
output += index === scalarIndex ? createHyperlink(scalar, url) : scalar;
index++;
}
return output;
}
function assertSlicesMatchPlainReference(t, plain, styled, maximumIndex = 6) {
for (let start = 0; start <= maximumIndex; start++) {
for (let end = start; end <= maximumIndex; end++) {
const expected = stripForVisibleComparison(sliceAnsi(plain, start, end));
const actual = stripForVisibleComparison(sliceAnsi(styled, start, end));
t.is(actual, expected);
}
}
}
function createRandomInteger(maximum) {
return Math.floor(Math.random() * maximum);
}
function createRandomVisibleText() {
const parts = ['a', 'b', 'c', ' ', 'ß'];
const length = createRandomInteger(6) + 1;
let returnValue = '';
for (let index = 0; index < length; index++) {
returnValue += randomItem(parts);
}
return returnValue;
}
function createRandomHyperlinkText() {
const url = `https://example.com/${createRandomInteger(1000)}`;
const terminator = randomItem([ANSI_BELL, ANSI_STRING_TERMINATOR]);
return createHyperlink(createRandomVisibleText(), url, terminator);
}
function createRandomStyledText() {
const text = createRandomVisibleText();
const style = randomItem([
chalk.red,
chalk.green,
chalk.blue,
chalk.bold,
chalk.underline,
chalk.bgYellow.black,
]);
return style(text);
}
function createRandomValidAnsiText() {
const segmentCount = createRandomInteger(8) + 1;
let output = '';
for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex++) {
const type = randomItem(['plain', 'styled', 'hyperlink']);
if (type === 'plain') {
output += createRandomVisibleText();
} else if (type === 'styled') {
output += createRandomStyledText();
} else {
output += createRandomHyperlinkText();
}
}
return output;
}
test('main', t => {
// The slice should behave exactly as a regular JS slice behaves
for (let index = 0; index < 20; index++) {
for (let index2 = 19; index2 > index; index2--) {
const nativeSlice = stripped.slice(index, index2);
const ansiSlice = sliceAnsi(fixture, index, index2);
t.is(nativeSlice, stripAnsi(ansiSlice));
}
}
const a = JSON.stringify('\u001B[31mthe \u001B[39m\u001B[32mquick \u001B[39m');
const b = JSON.stringify('\u001B[34mbrown \u001B[39m\u001B[36mfox \u001B[39m');
const c = JSON.stringify('\u001B[31m \u001B[39m\u001B[32mquick \u001B[39m\u001B[34mbrown \u001B[39m\u001B[36mfox \u001B[39m');
t.is(JSON.stringify(sliceAnsi(fixture, 0, 10)), a);
t.is(JSON.stringify(sliceAnsi(fixture, 10, 20)), b);
t.is(JSON.stringify(sliceAnsi(fixture, 3, 20)), c);
const string = generate(1) + generate(2) + generate(3) + generate(4) + generate(5) + generate(6) + generate(7) + generate(8) + generate(9) + generate(10) + generate(11) + generate(12) + generate(13) + generate(14) + generate(15) + generate(1) + generate(2) + generate(3) + generate(4) + generate(5) + generate(6) + generate(7) + generate(8) + generate(9) + generate(10) + generate(11) + generate(12) + generate(13) + generate(14) + generate(15);
const native = stripAnsi(string).slice(0, 55);
const ansi = stripAnsi(sliceAnsi(string, 0, 55));
t.is(native, ansi);
});
test('supports fullwidth characters', t => {
t.is(sliceAnsi('안녕하세', 0, 4), '안녕');
});
test('supports unicode surrogate pairs', t => {
t.is(sliceAnsi('a\uD83C\uDE00BC', 0, 2), 'a\uD83C\uDE00');
});
test('does not split grapheme clusters with combining marks', t => {
const input = 'Ae\u0301B';
t.is(sliceAnsi(input, 1, 2), 'e\u0301');
t.is(sliceAnsi(input, 2, 3), 'B');
});
test('does not split ZWJ emoji grapheme clusters', t => {
const input = 'A👨👩👧👦B';
t.is(sliceAnsi(input, 1, 3), '👨👩👧👦');
t.is(sliceAnsi(input, 3, 4), 'B');
});
test('treats CRLF as a single grapheme cluster', t => {
const input = 'A\r\nB';
t.is(sliceAnsi(input, 1, 2), '\r\n');
t.is(sliceAnsi(input, 2, 3), 'B');
});
test('does not split styled grapheme clusters with combining marks', t => {
const input = '\u001B[31me\u0301\u001B[39m';
t.is(sliceAnsi(input, 0, 1), input);
t.is(sliceAnsi(input, 1, 2), '');
});
test('does not split grapheme clusters when styles appear inside combining sequence', t => {
const input = '\u001B[31me\u001B[39m\u0301B';
t.is(stripForVisibleComparison(sliceAnsi(input, 0, 1)), 'e\u0301');
t.is(stripForVisibleComparison(sliceAnsi(input, 1, 2)), 'B');
});
test('does not split Hangul Jamo grapheme clusters when styles appear inside sequence', t => {
const input = '\u001B[31mᄀ\u001B[39mᅡB';
t.is(stripForVisibleComparison(sliceAnsi(input, 0, 2)), '가');
t.is(stripForVisibleComparison(sliceAnsi(input, 2, 3)), 'B');
});
test('keeps style opens inside grapheme continuation past end boundary', t => {
const input = `e${chalk.red('\u0301')}B`;
t.is(sliceAnsi(input, 0, 1), `e${chalk.red('\u0301')}`);
});
test('keeps hyperlink opens inside grapheme continuation past end boundary', t => {
const open = `${ESCAPE}]8;;https://example.com${ANSI_BELL}`;
const close = `${ESCAPE}]8;;${ANSI_BELL}`;
const input = `e${open}\u0301${close}B`;
t.is(sliceAnsi(input, 0, 1), `e${open}\u0301${close}`);
});
test('doesn\'t add unnecessary escape codes', t => {
t.is(sliceAnsi('\u001B[31municorn\u001B[39m', 0, 3), '\u001B[31muni\u001B[39m');
});
test('can slice a normal character before a colored character', t => {
t.is(sliceAnsi('a\u001B[31mb\u001B[39m', 0, 1), 'a');
});
test('can slice a normal character after a colored character', t => {
t.is(sliceAnsi('\u001B[31ma\u001B[39mb', 1, 2), 'b');
});
// See https://github.com/chalk/slice-ansi/issues/22
test('can slice a string styled with both background and foreground', t => {
// Test string: `chalk.bgGreen.black('test');`
t.is(sliceAnsi('\u001B[42m\u001B[30mtest\u001B[39m\u001B[49m', 0, 1), '\u001B[42m\u001B[30mt\u001B[39m\u001B[49m');
});
test('can slice a string styled with modifier', t => {
// Test string: `chalk.underline('test');`
t.is(sliceAnsi('\u001B[4mtest\u001B[24m', 0, 1), '\u001B[4mt\u001B[24m');
});
test('can slice a string with unknown ANSI color', t => {
t.is(sliceAnsi('\u001B[20mTEST\u001B[49m', 0, 4), '\u001B[20mTEST\u001B[0m');
t.is(sliceAnsi('\u001B[1001mTEST\u001B[49m', 0, 3), '\u001B[1001mTES\u001B[0m');
t.is(sliceAnsi('\u001B[1001mTEST\u001B[49m', 0, 2), '\u001B[1001mTE\u001B[0m');
});
test('weird null issue', t => {
const s = '\u001B[1mautotune.flipCoin("easy as") ? 🎂 : 🍰 \u001B[33m★\u001B[39m\u001B[22m';
const result = sliceAnsi(s, 38);
t.false(result.includes('null'));
});
test('supports true color escape sequences', t => {
t.is(sliceAnsi('\u001B[1m\u001B[48;2;255;255;255m\u001B[38;2;255;0;0municorn\u001B[39m\u001B[49m\u001B[22m', 0, 3), '\u001B[1m\u001B[48;2;255;255;255m\u001B[38;2;255;0;0muni\u001B[39m\u001B[49m\u001B[22m');
});
test('supports colon-delimited truecolor SGR syntax', t => {
t.is(sliceAnsi('\u001B[38:2:255:0:0mred\u001B[39m', 0, 1), '\u001B[38:2:255:0:0mr\u001B[39m');
});
// See https://github.com/chalk/slice-ansi/issues/24
test('doesn\'t add extra escapes', t => {
const output = `${chalk.black.bgYellow(' RUNS ')} ${chalk.green('test')}`;
t.is(sliceAnsi(output, 0, 7), `${chalk.black.bgYellow(' RUNS ')} `);
t.is(sliceAnsi(output, 0, 8), `${chalk.black.bgYellow(' RUNS ')} `);
t.is(JSON.stringify(sliceAnsi('\u001B[31m' + output, 0, 4)), JSON.stringify(chalk.black.bgYellow(' RUN')));
});
// See https://github.com/chalk/slice-ansi/issues/26
test('does not lose fullwidth characters', t => {
t.is(sliceAnsi('古古test', 0), '古古test');
});
test('does not split regional-indicator flag graphemes', t => {
const input = 'A🇮🇱B';
t.is(sliceAnsi(input, 1, 2), '🇮🇱');
t.is(sliceAnsi(input, 2, 3), '');
});
test('does not split styled regional-indicator flag graphemes', t => {
const input = '\u001B[31m🇮🇱\u001B[39m';
t.is(sliceAnsi(input, 0, 1), input);
t.is(sliceAnsi(input, 1, 2), '');
});
test('counts emoji-style graphemes as fullwidth', t => {
t.is(sliceAnsi('A☺️B', 1, 3), '☺️');
t.is(sliceAnsi('A1️⃣B', 1, 3), '1️⃣');
t.is(sliceAnsi('A🇦B', 1, 3), '🇦');
});
test('does not treat text-presentation pictographs as fullwidth', t => {
t.is(sliceAnsi('A☺B', 2, 3), 'B');
t.is(sliceAnsi('A☂B', 2, 3), 'B');
});
test('can create empty slices', t => {
t.is(sliceAnsi('test', 0, 0), '');
});
test('slice links (issue #31)', t => {
const link = createHyperlink('Google', 'https://google.com');
t.is(sliceAnsi(link, 0, 6), link);
});
test('supports OSC 8 hyperlinks with ST terminator', t => {
const link = createHyperlink('Google', 'https://google.com', ANSI_STRING_TERMINATOR);
t.is(sliceAnsi(link, 0, 6), link);
});
test('supports OSC 8 hyperlinks with mixed close terminator', t => {
const link = createHyperlink('Google', 'https://google.com', ANSI_STRING_TERMINATOR, ANSI_BELL);
t.is(sliceAnsi(link, 0, 6), link);
});
test('supports OSC 8 hyperlinks with parameters', t => {
const link = `${ESCAPE}]8;id=abc;https://google.com${ANSI_BELL}Google${ESCAPE}]8;;${ANSI_BELL}`;
t.is(sliceAnsi(link, 0, 6), link);
t.is(sliceAnsi(link, 1, 4), `${ESCAPE}]8;id=abc;https://google.com${ANSI_BELL}oog${ESCAPE}]8;;${ANSI_BELL}`);
});
test('supports OSC 8 hyperlinks with parameters and ST terminator', t => {
const link = `${ESCAPE}]8;id=abc;https://google.com${ANSI_STRING_TERMINATOR}Google${ESCAPE}]8;;${ANSI_STRING_TERMINATOR}`;
t.is(sliceAnsi(link, 0, 6), link);
t.is(sliceAnsi(link, 2), `${ESCAPE}]8;id=abc;https://google.com${ANSI_STRING_TERMINATOR}ogle${ESCAPE}]8;;${ANSI_STRING_TERMINATOR}`);
});
test('supports ESC OSC 8 hyperlinks with C1 ST terminator', t => {
const link = `${ESCAPE}]8;;https://google.com${C1_STRING_TERMINATOR}Google${ESCAPE}]8;;${C1_STRING_TERMINATOR}`;
t.is(sliceAnsi(link, 0, 6), link);
t.is(sliceAnsi(link, 1, 4), `${ESCAPE}]8;;https://google.com${C1_STRING_TERMINATOR}oog${ESCAPE}]8;;${C1_STRING_TERMINATOR}`);
});
test('supports C1 OSC 8 hyperlinks with BEL terminator', t => {
const link = `${C1_OSC}8;;https://google.com${ANSI_BELL}Google${C1_OSC}8;;${ANSI_BELL}`;
t.is(sliceAnsi(link, 0, 6), link);
t.is(sliceAnsi(link, 1, 4), `${C1_OSC}8;;https://google.com${ANSI_BELL}oog${C1_OSC}8;;${ANSI_BELL}`);
});
test('supports C1 OSC 8 hyperlinks with C1 ST terminator', t => {
const link = `${C1_OSC}8;;https://google.com${C1_STRING_TERMINATOR}Google${C1_OSC}8;;${C1_STRING_TERMINATOR}`;
t.is(sliceAnsi(link, 0, 6), link);
t.is(sliceAnsi(link, 2), `${C1_OSC}8;;https://google.com${C1_STRING_TERMINATOR}ogle${C1_OSC}8;;${C1_STRING_TERMINATOR}`);
});
test('supports C1 OSC 8 hyperlinks with parameters and ESC ST terminator', t => {
const link = `${C1_OSC}8;id=abc;https://google.com${ANSI_STRING_TERMINATOR}Google${C1_OSC}8;;${ANSI_STRING_TERMINATOR}`;
t.is(sliceAnsi(link, 0, 6), link);
t.is(sliceAnsi(link, 1, 4), `${C1_OSC}8;id=abc;https://google.com${ANSI_STRING_TERMINATOR}oog${C1_OSC}8;;${ANSI_STRING_TERMINATOR}`);
});
test('can slice each visible character from hyperlink', t => {
const url = 'https://google.com';
const text = 'Google';
const link = createHyperlink(text, url);
for (let index = 0; index < text.length; index++) {
t.is(sliceAnsi(link, index, index + 1), createHyperlink(text.slice(index, index + 1), url));
}
});
test('can slice partial hyperlink text', t => {
const url = 'https://google.com';
const link = createHyperlink('Google', url);
t.is(sliceAnsi(link, 1, 4), createHyperlink('oog', url));
});
test('can create an empty slice inside hyperlink text', t => {
const link = createHyperlink('Google', 'https://google.com');
t.is(sliceAnsi(link, 2, 2), '');
});
test('keeps outer styles when slicing after hyperlink text', t => {
const input = chalk.red(`${createHyperlink('AB', 'https://example.com')}C`);
t.is(sliceAnsi(input, 2, 3), chalk.red('C'));
});
test('supports hyperlinks that close with non-empty parameters', t => {
const link = `${ESCAPE}]8;id=abc;https://google.com${ANSI_BELL}Google${ESCAPE}]8;id=abc;${ANSI_BELL}`;
t.is(sliceAnsi(link, 0, 6), link);
t.is(sliceAnsi(link, 0, 4), `${ESCAPE}]8;id=abc;https://google.com${ANSI_BELL}Goog${ESCAPE}]8;;${ANSI_BELL}`);
});
test('supports hyperlink slices with unicode surrogate pairs', t => {
const url = 'https://example.com';
const link = createHyperlink('a🙂b', url);
t.is(sliceAnsi(link, 1, 3), createHyperlink('🙂', url));
});
test('preserves grapheme clusters when slicing hyperlink text', t => {
const url = 'https://example.com';
const link = createHyperlink('A👨👩👧👦B', url);
t.is(sliceAnsi(link, 1, 3), createHyperlink('👨👩👧👦', url));
t.is(sliceAnsi(link, 2, 3), '');
});
test('does not split grapheme clusters when styles appear inside ZWJ sequence', t => {
const input = '\u001B[31m👨\u001B[39m👩👧👦B';
t.is(stripForVisibleComparison(sliceAnsi(input, 0, 2)), '👨👩👧👦');
t.is(stripForVisibleComparison(sliceAnsi(input, 2, 3)), 'B');
});
test('does not split grapheme clusters when styles appear between ZWJ and following pictograph', t => {
const input = `👨${chalk.red('👩👧👦')}B`;
t.is(stripForVisibleComparison(sliceAnsi(input, 0, 2)), '👨👩👧👦');
t.is(stripForVisibleComparison(sliceAnsi(input, 2, 3)), 'B');
});
test('keeps grapheme-safe boundaries with SGR inserted at internal scalar boundaries', t => {
const graphemes = [
'e\u0301',
'👨👩👧👦',
'👍🏽',
'1️⃣',
'☺️',
'🇮🇱',
'가',
'👨👩',
];
for (const grapheme of graphemes) {
const plain = `A${grapheme}B`;
const scalarCount = [...grapheme].length;
for (let scalarIndex = 0; scalarIndex < scalarCount; scalarIndex++) {
const styled = `A${styleScalarAtIndex(grapheme, scalarIndex, chalk.red)}B`;
assertSlicesMatchPlainReference(t, plain, styled);
}
}
});
test('keeps grapheme-safe boundaries with hyperlink tokens inserted at internal scalar boundaries', t => {
const graphemes = [
'e\u0301',
'👨👩👧👦',
'1️⃣',
'🇮🇱',
'가',
];
for (const grapheme of graphemes) {
const plain = `A${grapheme}B`;
const scalarCount = [...grapheme].length;
for (let scalarIndex = 0; scalarIndex < scalarCount; scalarIndex++) {
const styled = `A${hyperlinkScalarAtIndex(grapheme, scalarIndex, 'https://example.com')}B`;
assertSlicesMatchPlainReference(t, plain, styled);
}
}
});
test('can slice across plain text and hyperlink boundaries', t => {
const url = 'https://google.com';
const input = `A${createHyperlink('Google', url)}B`;
t.is(sliceAnsi(input, 0, 2), `A${createHyperlink('G', url)}`);
t.is(sliceAnsi(input, 6, 8), `${createHyperlink('e', url)}B`);
});
test('can slice a hyperlink that remains open to the end', t => {
const link = `${ESCAPE}]8;;https://google.com${ANSI_BELL}Google`;
t.is(sliceAnsi(link, 0, 6), createHyperlink('Google', 'https://google.com'));
});
test('can slice hyperlinks with nested style transitions', t => {
const url = 'https://example.com';
const input = createHyperlink(`${chalk.red('R')}${chalk.green('G')}${chalk.blue('B')}`, url);
assertVisibleSliceMatchesNative(t, input, 0, 3);
assertVisibleSliceMatchesNative(t, input, 1, 3);
assertVisibleSliceMatchesNative(t, input, 1, 2);
});
test('can slice styled hyperlink text without dropping styles', t => {
const url = 'https://example.com';
const input = chalk.bgGreen.black(createHyperlink(chalk.red('test'), url));
assertVisibleSliceMatchesNative(t, input, 0, 4);
assertVisibleSliceMatchesNative(t, input, 1, 3);
});
test('can slice multiple hyperlinks in one string', t => {
const input = `${createHyperlink('one', 'https://one.test')}-${createHyperlink('two', 'https://two.test')}`;
assertVisibleSliceMatchesNative(t, input, 0, 7);
assertVisibleSliceMatchesNative(t, input, 1, 6);
assertVisibleSliceMatchesNative(t, input, 3, 7);
});
test('can slice back-to-back hyperlinks', t => {
const input = `${createHyperlink('A', 'https://a.test')}${createHyperlink('B', 'https://b.test')}${createHyperlink('C', 'https://c.test')}`;
assertVisibleSliceMatchesNative(t, input, 0, 3);
assertVisibleSliceMatchesNative(t, input, 1, 3);
assertVisibleSliceMatchesNative(t, input, 0, 2);
});
test('can slice through link boundaries with mixed terminators', t => {
const input = `${createHyperlink('first', 'https://one.test', ANSI_STRING_TERMINATOR)} ${createHyperlink('second', 'https://two.test', ANSI_BELL, ANSI_STRING_TERMINATOR)}`;
assertVisibleSliceMatchesNative(t, input, 0, 8);
assertVisibleSliceMatchesNative(t, input, 2, 10);
assertVisibleSliceMatchesNative(t, input, 5, 11);
});
test('handles malformed OSC hyperlink input without throwing', t => {
const malformedOpen = `${ESCAPE}]8;;https://example.comGoogle`;
const malformedClose = `${ESCAPE}]8;;https://example.com${ANSI_BELL}Google${ESCAPE}]8;;`;
t.notThrows(() => {
sliceAnsi(malformedOpen, 0, 3);
});
t.notThrows(() => {
sliceAnsi(malformedClose, 0, 6);
});
t.false(sliceAnsi(malformedOpen, 0, 3).includes('null'));
t.false(sliceAnsi(malformedOpen, 0, 3).includes('undefined'));
t.false(sliceAnsi(malformedClose, 0, 6).includes('null'));
t.false(sliceAnsi(malformedClose, 0, 6).includes('undefined'));
});
test('randomized invariant: visible slice matches native slice', t => {
const iterations = 300;
for (let index = 0; index < iterations; index++) {
const input = createRandomValidAnsiText();
const visible = stripForVisibleComparison(input);
const start = createRandomInteger(visible.length + 1);
const end = start + createRandomInteger(visible.length - start + 1);
assertVisibleSliceMatchesNative(t, input, start, end);
}
});
test('randomized invariant: full-range slice preserves visible text', t => {
const iterations = 200;
for (let index = 0; index < iterations; index++) {
const input = createRandomValidAnsiText();
const output = sliceAnsi(input, 0);
t.is(stripForVisibleComparison(output), stripForVisibleComparison(input));
t.false(output.includes('null'));
t.false(output.includes('undefined'));
}
});
test('can slice hyperlink with omitted end', t => {
const link = createHyperlink('Google', 'https://google.com');
t.is(sliceAnsi(link, 0), link);
});
test('can slice from the middle of a hyperlink with omitted end', t => {
const url = 'https://google.com';
const link = createHyperlink('Google', url);
t.is(sliceAnsi(link, 2), createHyperlink('ogle', url));
});
test('does not include hyperlink escapes when slicing only outside linked text', t => {
const input = `prefix ${createHyperlink('Google', 'https://google.com')} suffix`;
t.is(sliceAnsi(input, 0, 3), 'pre');
t.is(sliceAnsi(input, 14, 19), 'suffi');
});
test('does not include styles that start after end', t => {
const input = `a${chalk.red('b')}`;
t.is(sliceAnsi(input, 0, 1), 'a');
});
test('keeps C1 SGR CSI behavior', t => {
const input = '\u009B31mred\u009B39m';
t.is(stripAnsi(sliceAnsi(input, 0, 3)), 'red');
t.is(sliceAnsi(input, 1, 2), '\u009B31me\u001B[39m');
});
test('treats non-canonical ESC CSI m sequences as non-visible control codes', t => {
const input = '\u001B[?25mA';
t.is(sliceAnsi(input, 0, 1), 'A');
});
test('treats non-canonical C1 CSI m sequences as non-visible control codes', t => {
const input = '\u009B?25mA';
t.is(sliceAnsi(input, 0, 1), 'A');
});
test('treats non-SGR CSI control sequences as non-visible control codes', t => {
const input = '\u001B[2KA';
t.is(sliceAnsi(input, 0, 1), 'A');
});
test('treats truncated CSI tails as non-visible control codes', t => {
t.is(sliceAnsi('\u001B[31', 0, 1), '');
t.is(sliceAnsi('\u009B31', 0, 1), '');
});
test('does not swallow visible text after malformed CSI bytes', t => {
const input = '\u001B[31ĀA';
t.is(sliceAnsi(input, 0, 1), 'Ā');
t.is(sliceAnsi(input, 1, 2), 'A');
});
test('does not swallow visible text after malformed CSI prefix', t => {
const input = '\u001B[ĀA';
t.is(sliceAnsi(input, 0, 1), 'Ā');
t.is(sliceAnsi(input, 1, 2), 'A');
});
test('does not swallow visible text after malformed C1 CSI prefix', t => {
const input = '\u009BĀA';
t.is(sliceAnsi(input, 0, 1), 'Ā');
t.is(sliceAnsi(input, 1, 2), 'A');
});
test('treats generic OSC control sequences as non-visible control codes', t => {
const input = '\u001B]0;title\u0007A';
t.is(sliceAnsi(input, 0, 1), 'A');
});
test('treats DCS control strings as non-visible control codes', t => {
const input = '\u001BP1;2;3+x\u001B\\A';
t.is(sliceAnsi(input, 0, 1), 'A');
});
test('treats C1 DCS control strings as non-visible control codes', t => {
const input = '\u0090payload\u009CA';
t.is(sliceAnsi(input, 0, 1), 'A');
});
test('treats SOS control strings as non-visible control codes', t => {
const input = '\u001BXpayload\u001B\\A';
t.is(sliceAnsi(input, 0, 1), 'A');
});
test('treats PM control strings as non-visible control codes', t => {
const input = '\u001B^payload\u001B\\A';
t.is(sliceAnsi(input, 0, 1), 'A');
});
test('treats C1 APC control strings as non-visible control codes', t => {
const input = '\u009Fpayload\u009CA';
t.is(sliceAnsi(input, 0, 1), 'A');
});
test('treats standalone ST control sequences as non-visible control codes', t => {
t.is(sliceAnsi('\u001B\\A', 0, 1), 'A');
t.is(sliceAnsi('\u009CA', 0, 1), 'A');
});
test('preserves style state across private CSI m control codes', t => {
const input = '\u001B[31mA\u001B[?25mB\u001B[39m';
t.is(sliceAnsi(input, 0, 2), input);
t.is(sliceAnsi(input, 1, 2), '\u001B[31mB\u001B[39m');
});
test('preserves visible indexing with control strings before styled text', t => {
const input = '\u001B]0;title\u0007\u001B[31mAB\u001B[39m';
t.is(sliceAnsi(input, 0, 1), '\u001B[31mA\u001B[39m');
t.is(sliceAnsi(input, 1, 2), '\u001B[31mB\u001B[39m');
});
test('preserves visible indexing with control strings between characters', t => {
const input = 'A\u001BP1;2;3+x\u001B\\B';
t.is(sliceAnsi(input, 0, 2), input);
t.is(sliceAnsi(input, 1, 2), 'B');
});
test('supports fullwidth slices inside hyperlinks', t => {
const link = createHyperlink('古古ab', 'https://example.com');
t.is(stripForVisibleComparison(sliceAnsi(link, 0, 2)), '古');
t.is(stripForVisibleComparison(sliceAnsi(link, 2, 4)), '古');
t.is(stripForVisibleComparison(sliceAnsi(link, 4, 6)), 'ab');
});
test('closes all styles from multi-parameter SGR code at slice end', t => {
const input = '\u001B[1;31mX';
t.is(sliceAnsi(input, 0, 1), '\u001B[1m\u001B[31mX\u001B[39m\u001B[22m');
});
test('preserves multi-parameter close codes after slice boundary', t => {
const input = '\u001B[31;42mX\u001B[39m\u001B[49m';
t.is(sliceAnsi(input, 0, 1), '\u001B[31m\u001B[42mX\u001B[39m\u001B[49m');
});
test('retains only background style after foreground closes from multi-parameter SGR', t => {
const input = '\u001B[31;42mX\u001B[39mY\u001B[49m';
t.is(sliceAnsi(input, 1, 2), '\u001B[42mY\u001B[49m');
});
test('overrides previous foreground styles cleanly', t => {
const input = '\u001B[31mA\u001B[32mB';
t.is(sliceAnsi(input, 0, 2), '\u001B[31mA\u001B[32mB\u001B[39m');
t.is(sliceAnsi(input, 1, 2), '\u001B[32mB\u001B[39m');
});
test('handles reset mixed with start in one SGR sequence', t => {
const input = '\u001B[32mA\u001B[0;31mB\u001B[39m';
t.is(sliceAnsi(input, 1, 2), '\u001B[31mB\u001B[39m');
});
test('does not include start codes from mixed SGR sequences after end boundary', t => {
const input = '\u001B[32mA\u001B[0;31mB\u001B[39m';
t.is(sliceAnsi(input, 0, 1), '\u001B[32mA\u001B[39m');
});
test('returns empty for out-of-range start with active hyperlink before it', t => {
const link = createHyperlink('Google', 'https://google.com');
t.is(sliceAnsi(link, 100), '');
});
test('treats malformed OSC tail as non-visible', t => {
const input = `${ESCAPE}]8;;https://example.com${ANSI_BELL}link${ESCAPE}]8;;broken plain`;
t.is(stripForVisibleComparison(sliceAnsi(input, 0)), 'link');
});