Skip to content

Commit

Permalink
style: make all escape sequences hexadecimal
Browse files Browse the repository at this point in the history
  • Loading branch information
luciancooper committed Apr 19, 2024
1 parent 6a0d82e commit a92b56a
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 212 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ const { splitLines } = require('tty-strings'),
chalk = require('chalk');

splitLines(chalk.green('foo\nbar'));
// > ['\u001b[32mfoo\u001b[39m', '\u001b[32mbar\u001b[39m']
// > ['\x1b[32mfoo\x1b[39m', '\x1b[32mbar\x1b[39m']
```

### `stripAnsi(string)`
Expand All @@ -235,7 +235,7 @@ This method is adapted from chalk's [`slice-ansi`](https://github.com/chalk/slic
```js
const { stripAnsi } = require('tty-strings');

stripAnsi('\u001b[32mfoo\u001b[39m');
stripAnsi('\x1b[32mfoo\x1b[39m');
// > 'foo'
```

Expand Down
2 changes: 1 addition & 1 deletion src/splitLines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { parseEscape, closeEscapes, type AnsiEscape } from './utils';
* import chalk from 'chalk';
*
* splitLines(chalk.green('foo\nbar'));
* // > ['\u001b[32mfoo\u001b[39m', '\u001b[32mbar\u001b[39m']
* // > ['\x1b[32mfoo\x1b[39m', '\x1b[32mbar\x1b[39m']
* ```
*
* @param string - Input string to split.
Expand Down
2 changes: 1 addition & 1 deletion src/stripAnsi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const regex = ansiRegex();
* ```ts
* import { stripAnsi } from 'tty-strings';
*
* const stripped = stripAnsi('\u001b[32mfoo\u001b[39m'); // 'foo'
* const stripped = stripAnsi('\x1b[32mfoo\x1b[39m'); // 'foo'
* ```
*
* @param string - Input string to strip.
Expand Down
4 changes: 2 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function parseEscape<T>(stack: AnsiEscape<T>[], seq: string, idx: T) {
}
} else {
// add this hyperlink escape to the stack
stack.push([seq, true, '\u001B]8;;\u0007', idx]);
stack.push([seq, true, '\x1b]8;;\x07', idx]);
}
} else {
// return null on a non SGR/hyperlink escape sequence
Expand All @@ -49,7 +49,7 @@ export function parseEscape<T>(stack: AnsiEscape<T>[], seq: string, idx: T) {
}

export function closeEscapes<T>(stack: AnsiEscape<T>[]) {
const escapes = stack.map(([, isLink, close]) => (isLink ? close : `\u001b[${close}m`));
const escapes = stack.map(([, isLink, close]) => (isLink ? close : `\x1b[${close}m`));
let result = '';
while (escapes.length) {
const esc = escapes.pop();
Expand Down
96 changes: 48 additions & 48 deletions test/sliceString.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,110 +25,110 @@ describe('sliceChars', () => {
});

test('slices ansi escape sequences', () => {
expect(sliceChars('fo\u001b[31mob\u001b[39mar', 0, 2)).toBe('fo');
expect(sliceChars('fo\u001b[31mob\u001b[39mar', 4, 6)).toBe('ar');
expect(sliceChars('fo\u001b[31mob\u001b[39mar', 1, 5)).toBe('o\u001b[31mob\u001b[39ma');
expect(sliceChars('\u001b[31mfoo\u001b[39m\u001b[32mbar\u001b[39m', 1, 5))
.toBe('\u001b[31moo\u001b[39m\u001b[32mba\u001b[39m');
expect(sliceChars('fo\x1b[31mob\x1b[39mar', 0, 2)).toBe('fo');
expect(sliceChars('fo\x1b[31mob\x1b[39mar', 4, 6)).toBe('ar');
expect(sliceChars('fo\x1b[31mob\x1b[39mar', 1, 5)).toBe('o\x1b[31mob\x1b[39ma');
expect(sliceChars('\x1b[31mfoo\x1b[39m\x1b[32mbar\x1b[39m', 1, 5))
.toBe('\x1b[31moo\x1b[39m\x1b[32mba\x1b[39m');
});

test('slices strings with both foreground & background styling', () => {
expect(sliceChars('\u001b[41m\u001b[33mfoobar\u001b[39m\u001b[49m', 1, 5))
.toBe('\u001b[41m\u001b[33mooba\u001b[39m\u001b[49m');
expect(sliceChars('\u001b[41mf\u001b[33mooba\u001b[39mr\u001b[49m', 2, 4))
.toBe('\u001b[41m\u001b[33mob\u001b[39m\u001b[49m');
expect(sliceChars('fo\u001b[41m\u001b[33mob\u001b[39m\u001b[49mar', 1, 5))
.toBe('o\u001b[41m\u001b[33mob\u001b[39m\u001b[49ma');
expect(sliceChars('\x1b[41m\x1b[33mfoobar\x1b[39m\x1b[49m', 1, 5))
.toBe('\x1b[41m\x1b[33mooba\x1b[39m\x1b[49m');
expect(sliceChars('\x1b[41mf\x1b[33mooba\x1b[39mr\x1b[49m', 2, 4))
.toBe('\x1b[41m\x1b[33mob\x1b[39m\x1b[49m');
expect(sliceChars('fo\x1b[41m\x1b[33mob\x1b[39m\x1b[49mar', 1, 5))
.toBe('o\x1b[41m\x1b[33mob\x1b[39m\x1b[49ma');
});

test('ignores empty escape sequences', () => {
// slice begins immediately after an empty escape sequence
expect(sliceChars('fo\u001b[42m\u001b[49m\u001b[41mob\u001b[49mar', 1, 5)).toBe('o\u001b[41mob\u001b[49ma');
expect(sliceChars('fo\x1b[42m\x1b[49m\x1b[41mob\x1b[49mar', 1, 5)).toBe('o\x1b[41mob\x1b[49ma');
// slice ends immediately before an empty escape sequence
expect(sliceChars('fo\u001b[41mob\u001b[49ma\u001b[42m\u001b[49mr', 1, 5)).toBe('o\u001b[41mob\u001b[49ma');
expect(sliceChars('fo\x1b[41mob\x1b[49ma\x1b[42m\x1b[49mr', 1, 5)).toBe('o\x1b[41mob\x1b[49ma');
// slice spans an empty escape sequence
expect(sliceChars('fo\u001b[41mo\u001b[32m\u001b[39mb\u001b[49mar', 1, 5)).toBe('o\u001b[41mob\u001b[49ma');
expect(sliceChars('fo\x1b[41mo\x1b[32m\x1b[39mb\x1b[49mar', 1, 5)).toBe('o\x1b[41mob\x1b[49ma');
// string ends in an empty escape sequence
expect(sliceChars('fo\u001b[41mob\u001b[49mar\u001b[32m\u001b[39m', 1)).toBe('o\u001b[41mob\u001b[49mar');
expect(sliceChars('fo\x1b[41mob\x1b[49mar\x1b[32m\x1b[39m', 1)).toBe('o\x1b[41mob\x1b[49mar');
});

test('handles unclosed escape sequences', () => {
// inputs contain unclosed foreground codes
expect(sliceChars('\u001b[32mfoobar')).toBe('\u001b[32mfoobar\u001b[39m');
expect(sliceChars('\u001b[32mfoobar', 1, 5)).toBe('\u001b[32mooba\u001b[39m');
expect(sliceChars('\x1b[32mfoobar')).toBe('\x1b[32mfoobar\x1b[39m');
expect(sliceChars('\x1b[32mfoobar', 1, 5)).toBe('\x1b[32mooba\x1b[39m');
// inputs contain unclosed foreground & background codes
expect(sliceChars('\u001b[41m\u001b[32mfoobar')).toBe('\u001b[41m\u001b[32mfoobar\u001b[39m\u001b[49m');
expect(sliceChars('\u001b[41m\u001b[32mfoobar', 1, 5)).toBe('\u001b[41m\u001b[32mooba\u001b[39m\u001b[49m');
expect(sliceChars('\x1b[41m\x1b[32mfoobar')).toBe('\x1b[41m\x1b[32mfoobar\x1b[39m\x1b[49m');
expect(sliceChars('\x1b[41m\x1b[32mfoobar', 1, 5)).toBe('\x1b[41m\x1b[32mooba\x1b[39m\x1b[49m');
});

test('ignores unnecessary opening escape sequences', () => {
// string ends with an opening 31m code, which should not appear in the result
expect(sliceChars('foo\u001b[32mbar\u001b[39m\u001b[31m', 4)).toBe('\u001b[32mar\u001b[39m');
expect(sliceChars('foo\x1b[32mbar\x1b[39m\x1b[31m', 4)).toBe('\x1b[32mar\x1b[39m');
// the slice range ends on an opening 31m code, which should not appear in the result
expect(sliceChars('foo\u001b[32mba\u001b[31mr\u001b[39m', 3, 5)).toBe('\u001b[32mba\u001b[39m');
expect(sliceChars('foo\x1b[32mba\x1b[31mr\x1b[39m', 3, 5)).toBe('\x1b[32mba\x1b[39m');
});

test('does not add unnecessary closing escape sequences', () => {
// all slice should have 2 foreground color opening codes and be closed with a single `\u001b[39m`
expect(sliceChars('\u001b[31m\u001b[32mfoo', 1)).toBe('\u001b[31m\u001b[32moo\u001b[39m');
expect(sliceChars('\u001b[31m\u001b[33mfoo\u001b[39mbar', 0, 4)).toBe('\u001b[31m\u001b[33mfoo\u001b[39mb');
expect(sliceChars('\u001b[31mf\u001b[33moo\u001b[39mbar', 1, 4)).toBe('\u001b[31m\u001b[33moo\u001b[39mb');
// all slice should have 2 foreground color opening codes and be closed with a single `\x1b[39m`
expect(sliceChars('\x1b[31m\x1b[32mfoo', 1)).toBe('\x1b[31m\x1b[32moo\x1b[39m');
expect(sliceChars('\x1b[31m\x1b[33mfoo\x1b[39mbar', 0, 4)).toBe('\x1b[31m\x1b[33mfoo\x1b[39mb');
expect(sliceChars('\x1b[31mf\x1b[33moo\x1b[39mbar', 1, 4)).toBe('\x1b[31m\x1b[33moo\x1b[39mb');
});

test('handles unknown ansi style codes', () => {
// unknown escape sequence \u001b[1001m should be closed by a reset sequence \u001b[0m
expect(sliceChars('\u001b[1001mfoobar\u001b[49m', 0, 3)).toBe('\u001b[1001mfoo\u001b[0m');
// unknown escape sequence \x1b[1001m should be closed by a reset sequence \x1b[0m
expect(sliceChars('\x1b[1001mfoobar\x1b[49m', 0, 3)).toBe('\x1b[1001mfoo\x1b[0m');
});

test('handles reset escape sequences', () => {
// both foreground and background are closed by a reset sequence (\u001b[0m)
expect(sliceChars('\u001b[32mfoo\u001b[41mbar\u001b[0m', 2)).toBe('\u001b[32mo\u001b[41mbar\u001b[0m');
// both foreground and background are closed by a reset sequence (\x1b[0m)
expect(sliceChars('\x1b[32mfoo\x1b[41mbar\x1b[0m', 2)).toBe('\x1b[32mo\x1b[41mbar\x1b[0m');
});

test('slices hyperlink escape sequences', () => {
// slice within a link
expect(sliceChars('\u001b]8;;link\u0007foobar\u001b]8;;\u0007', 0, 3))
.toBe('\u001b]8;;link\u0007foo\u001b]8;;\u0007');
expect(sliceChars('\x1b]8;;link\x07foobar\x1b]8;;\x07', 0, 3))
.toBe('\x1b]8;;link\x07foo\x1b]8;;\x07');
// slice begins outside link and ends inside link
expect(sliceChars('fo\u001b]8;;link\u0007ob\u001b]8;;\u0007ar', 0, 3))
.toBe('fo\u001b]8;;link\u0007o\u001b]8;;\u0007');
expect(sliceChars('fo\x1b]8;;link\x07ob\x1b]8;;\x07ar', 0, 3))
.toBe('fo\x1b]8;;link\x07o\x1b]8;;\x07');
// slice spans the link
expect(sliceChars('fo\u001b]8;;link\u0007ob\u001b]8;;\u0007ar', 1, 5))
.toBe('o\u001b]8;;link\u0007ob\u001b]8;;\u0007a');
expect(sliceChars('fo\x1b]8;;link\x07ob\x1b]8;;\x07ar', 1, 5))
.toBe('o\x1b]8;;link\x07ob\x1b]8;;\x07a');
// slice adjacent hyperlinks
expect(sliceChars('\u001b]8;;link1\u0007foo\u001b]8;;\u0007\u001b]8;;link2\u0007bar\u001b]8;;\u0007', 3, 5))
.toBe('\u001b]8;;link2\u0007ba\u001b]8;;\u0007');
expect(sliceChars('\x1b]8;;link1\x07foo\x1b]8;;\x07\x1b]8;;link2\x07bar\x1b]8;;\x07', 3, 5))
.toBe('\x1b]8;;link2\x07ba\x1b]8;;\x07');
});

test('slices hyperlink escape sequences that contain style sequences', () => {
// color styling contained in link text
expect(sliceChars('\u001b]8;;link\u0007\u001b[41mfoo\u001b[49mbar\u001b]8;;\u0007', 2, 6))
.toBe('\u001b]8;;link\u0007\u001b[41mo\u001b[49mbar\u001b]8;;\u0007');
expect(sliceChars('\x1b]8;;link\x07\x1b[41mfoo\x1b[49mbar\x1b]8;;\x07', 2, 6))
.toBe('\x1b]8;;link\x07\x1b[41mo\x1b[49mbar\x1b]8;;\x07');
// link and color styling overlap
expect(sliceChars('\u001b]8;;link\u0007fo\u001b[32mob\u001b]8;;\u0007ar\u001b[39m', 2))
.toBe('\u001b]8;;link\u0007\u001b[32mob\u001b]8;;\u0007ar\u001b[39m');
expect(sliceChars('\x1b]8;;link\x07fo\x1b[32mob\x1b]8;;\x07ar\x1b[39m', 2))
.toBe('\x1b]8;;link\x07\x1b[32mob\x1b]8;;\x07ar\x1b[39m');
});

test('handles unclosed hyperlink escape sequences', () => {
expect(sliceChars('\u001b]8;;link1\u0007foo\u001b]8;;link2\u0007bar', 3))
.toBe('\u001b]8;;link1\u0007\u001b]8;;link2\u0007bar\u001b]8;;\u0007');
expect(sliceChars('\x1b]8;;link1\x07foo\x1b]8;;link2\x07bar', 3))
.toBe('\x1b]8;;link1\x07\x1b]8;;link2\x07bar\x1b]8;;\x07');
});

test('supports 8 bit color escape sequences', () => {
// foreground 55 & background 176
expect(sliceChars('\u001b[38;5;55m\u001b[48;5;176mfoobar\u001b[49m\u001b[39m', 0, 3))
.toBe('\u001b[38;5;55m\u001b[48;5;176mfoo\u001b[49m\u001b[39m');
expect(sliceChars('\x1b[38;5;55m\x1b[48;5;176mfoobar\x1b[49m\x1b[39m', 0, 3))
.toBe('\x1b[38;5;55m\x1b[48;5;176mfoo\x1b[49m\x1b[39m');
});

test('supports 24 bit color escape sequences', () => {
// foreground #6134eb & background #ccc0f0
expect(sliceChars('\u001b[38;2;97;52;235m\u001b[48;2;204;192;240mfoobar\u001b[49m\u001b[39m', 0, 3))
.toBe('\u001b[38;2;97;52;235m\u001b[48;2;204;192;240mfoo\u001b[49m\u001b[39m');
expect(sliceChars('\x1b[38;2;97;52;235m\x1b[48;2;204;192;240mfoobar\x1b[49m\x1b[39m', 0, 3))
.toBe('\x1b[38;2;97;52;235m\x1b[48;2;204;192;240mfoo\x1b[49m\x1b[39m');
});

test('ignores escape sequences that are not styles or hyperlinks', () => {
// contains a window title escape sequence
expect(sliceChars('foo\u001B]0;window_title\u0007bar')).toBe('foobar');
expect(sliceChars('foo\x1b]0;window_title\x07bar')).toBe('foobar');
});
});

Expand Down
38 changes: 19 additions & 19 deletions test/splitLines.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,53 +10,53 @@ describe('splitLines', () => {
});

test('splits style escapes that span multiple lines', () => {
expect(splitLines('\u001b[41mAAA\u001b[33mBBB\nCCC\u001b[39mDDD\u001b[49m')).toEqual([
'\u001b[41mAAA\u001b[33mBBB\u001b[39m\u001b[49m',
'\u001b[41m\u001b[33mCCC\u001b[39mDDD\u001b[49m',
expect(splitLines('\x1b[41mAAA\x1b[33mBBB\nCCC\x1b[39mDDD\x1b[49m')).toEqual([
'\x1b[41mAAA\x1b[33mBBB\x1b[39m\x1b[49m',
'\x1b[41m\x1b[33mCCC\x1b[39mDDD\x1b[49m',
]);
});

test('splits style escapes that span a single line', () => {
expect(splitLines('\u001b[41mAAAA\u001b[49m\u001b[33m\nBBBB\u001b[39m')).toEqual([
'\u001b[41mAAAA\u001b[49m',
'\u001b[33mBBBB\u001b[39m',
expect(splitLines('\x1b[41mAAAA\x1b[49m\x1b[33m\nBBBB\x1b[39m')).toEqual([
'\x1b[41mAAAA\x1b[49m',
'\x1b[33mBBBB\x1b[39m',
]);
});

test('splits style escape sequences that overlap across a line break', () => {
expect(splitLines('\u001b[41mAAAA\u001b[33m\n\u001b[49mBBBB\u001b[39m')).toEqual([
'\u001b[41mAAAA\u001b[49m',
'\u001b[33mBBBB\u001b[39m',
expect(splitLines('\x1b[41mAAAA\x1b[33m\n\x1b[49mBBBB\x1b[39m')).toEqual([
'\x1b[41mAAAA\x1b[49m',
'\x1b[33mBBBB\x1b[39m',
]);
});

test('splits style escape sequences that span empty lines', () => {
expect(splitLines('\u001b[41m\nAAA\n\nBBB\u001b[49m')).toEqual([
expect(splitLines('\x1b[41m\nAAA\n\nBBB\x1b[49m')).toEqual([
'',
'\u001b[41mAAA\u001b[49m',
'\x1b[41mAAA\x1b[49m',
'',
'\u001b[41mBBB\u001b[49m',
'\x1b[41mBBB\x1b[49m',
]);
});

test('scrubs empty escape sequences', () => {
expect(splitLines('AA\u001b[41m\u001b[49mA\nBB')).toEqual(['AAA', 'BB']);
expect(splitLines('AA\x1b[41m\x1b[49mA\nBB')).toEqual(['AAA', 'BB']);
});

test('scrubs escape sequences that span only line breaks', () => {
expect(splitLines('AAAA\u001b[41m\n\u001b[49mBBBB')).toEqual(['AAAA', 'BBBB']);
expect(splitLines('AAAA\x1b[41m\n\x1b[49mBBBB')).toEqual(['AAAA', 'BBBB']);
});

test('supports ansi hyperlink escapes', () => {
expect(splitLines('\u001b]8;;link\u0007AA\nB\u001b]8;;\u0007b')).toEqual([
'\u001b]8;;link\u0007AA\u001b]8;;\u0007',
'\u001b]8;;link\u0007B\u001b]8;;\u0007b',
expect(splitLines('\x1b]8;;link\x07AA\nB\x1b]8;;\x07b')).toEqual([
'\x1b]8;;link\x07AA\x1b]8;;\x07',
'\x1b]8;;link\x07B\x1b]8;;\x07b',
]);
});

test('handles non-SGR/non-hyperlink ansi escape sequences', () => {
expect(splitLines('AA\u001B]0;window_title\u0007\nBB')).toEqual([
'AA\u001B]0;window_title\u0007',
expect(splitLines('AA\x1b]0;window_title\x07\nBB')).toEqual([
'AA\x1b]0;window_title\x07',
'BB',
]);
});
Expand Down
6 changes: 3 additions & 3 deletions test/stringLength.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ describe('stringLength', () => {
test('returns 0 on empty strings', () => {
expect(stringLength('')).toBe(0);
// empty string with escape sequences
expect(stringLength('\u001b[31m\u001b[39m')).toBe(0);
expect(stringLength('\x1b[31m\x1b[39m')).toBe(0);
});

test('returns 0 on non-string inputs', () => {
Expand All @@ -19,11 +19,11 @@ describe('stringLength', () => {
});

test('ignores ansi escape sequences', () => {
expect(stringLength('\u001B[31mfoo\u001B[39m')).toBe(3);
expect(stringLength('\x1b[31mfoo\x1b[39m')).toBe(3);
});

test('ignores ansi hyperlink escape sequences', () => {
expect(stringLength('\u001B]8;;https://foo.com\u0007bar\u001B]8;;\u0007')).toBe(3);
expect(stringLength('\x1b]8;;https://foo.com\x07bar\x1b]8;;\x07')).toBe(3);
});

test('counts latin letters with combining diacritical marks', () => {
Expand Down
6 changes: 3 additions & 3 deletions test/stringWidth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ describe('stringWidth', () => {
});

test('ignores ansi escape sequences', () => {
expect(stringWidth('\u001B[31mfoo\u001B[39m')).toBe(3);
expect(stringWidth('\x1b[31mfoo\x1b[39m')).toBe(3);
});

test('ignores ansi hyperlinks', () => {
expect(stringWidth('\u001B]8;;https://foo.com\u0007bar\u001B]8;;\u0007')).toBe(3);
expect(stringWidth('\x1b]8;;https://foo.com\x07bar\x1b]8;;\x07')).toBe(3);
});

test('returns 0 on non-string inputs', () => {
Expand All @@ -35,7 +35,7 @@ describe('stringWidth', () => {
test('returns 0 on empty strings', () => {
expect(stringWidth('')).toBe(0);
// empty string with escape sequences
expect(stringWidth('\u001b[31m\u001b[39m')).toBe(0);
expect(stringWidth('\x1b[31m\x1b[39m')).toBe(0);
});

describe('measures emoji', () => {
Expand Down
4 changes: 2 additions & 2 deletions test/stripAnsi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { stripAnsi } from '../src';

describe('stripAnsi', () => {
test('removes ansi sequences from strings', () => {
expect(stripAnsi('\u001b[32m\u001b[46mfoo\u001b[49m\u001b[39m')).toBe('foo');
expect(stripAnsi('\x1b[32m\x1b[46mfoo\x1b[49m\x1b[39m')).toBe('foo');
});

test('removes hyperlinks from strings', () => {
expect(stripAnsi('\u001B]8;;https://github.com\u0007foo\u001B]8;;\u0007')).toBe('foo');
expect(stripAnsi('\x1b]8;;https://github.com\x07foo\x1b]8;;\x07')).toBe('foo');
});

test('handles non string inputs', () => {
Expand Down
Loading

0 comments on commit a92b56a

Please sign in to comment.