Skip to content

Commit

Permalink
feat: add splitLines method
Browse files Browse the repository at this point in the history
  • Loading branch information
luciancooper committed Jun 23, 2021
1 parent 830433b commit 27eed36
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 0 deletions.
2 changes: 2 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const stringWidth = require('./stringWidth'),
charWidths = require('./charWidths'),
stripAnsi = require('./stripAnsi'),
codePointWidth = require('./codePoint'),
splitLines = require('./splitLines'),
{ sliceChars, sliceColumns } = require('./sliceString'),
wordWrap = require('./wordWrap');

Expand All @@ -14,6 +15,7 @@ module.exports = {
charWidths,
stripAnsi,
codePointWidth,
splitLines,
sliceChars,
sliceColumns,
wordWrap,
Expand Down
55 changes: 55 additions & 0 deletions lib/splitLines.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const parseAnsi = require('./parseAnsi'),
{ parseEscape, closeEscapes } = require('./utils');

/**
* Split a string with ANSI escape codes into an array of lines. Supports both `CRLF` and `LF` newlines.
* @param {string} string - string to split
* @returns {string[]} - array of lines in the input string
*/
module.exports = function splitLines(string) {
// ansi escapes stack, items in the form [seq, isLink, close, [i, j]]
const ansiStack = [],
// result array to hold processed lines
result = [],
// split input string into lines
lines = String(string).split(/\r?\n/g);
// iterate through each line in the input string
for (let i = 0, n = lines.length; i < n; i += 1) {
// the processed line
let line = '',
// intraline index
j = -1;
// match all ansi escape codes in the input line
for (const [chunk, isEscape] of parseAnsi(lines[i])) {
// check if chunk is an escape sequence
if (isEscape) {
// process this escape sequence
const closed = parseEscape(ansiStack, chunk, [i, j]);
if (closed && j >= 0) {
// append escape if it closes an active item in the stack
const [xi, xj] = closed;
if (xi < i || (xi === i && xj < j)) line += chunk;
} else if (closed === null) {
// escape is not a SGR/hyperlink escape
line += chunk;
}
continue;
}
// append any new escape sequences from the ansi stack
line += (j < 0 ? ansiStack : ansiStack.filter(([,,, [xi, xj]]) => (xi === i && xj === j)))
.map(([s]) => s)
.join('');
// append this chunk
line += chunk;
// increment the intraline index
j += 1;
}
// close open escape sequences if line is not empty
if (j >= 0) {
line += closeEscapes(ansiStack.filter(([,,, [xi, xj]]) => (xi < i || (xi === i && xj < j))));
}
// add proccessed line to the result array
result.push(line);
}
return result;
};
63 changes: 63 additions & 0 deletions test/splitLines.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const splitLines = require('../lib/splitLines');

describe('splitLines', () => {
test('supports both LF and CRLF newline types', () => {
expect(splitLines('AA\nBB\r\nCC')).toEqual(['AA', 'BB', 'CC']);
});

test('handles non-string inputs', () => {
expect(splitLines(10)).toEqual(['10']);
});

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',
]);
});

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',
]);
});

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',
]);
});

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

test('scrubs empty escape sequences', () => {
expect(splitLines('AA\u001b[41m\u001b[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']);
});

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',
]);
});

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',
'BB',
]);
});
});

0 comments on commit 27eed36

Please sign in to comment.