Skip to content

Commit

Permalink
feat: wrap word support ansi (#155)
Browse files Browse the repository at this point in the history
* Wrap word support ansi

* Refactor

* Implement split ansi string

Co-authored-by: Nam Hoang Le <nam.hoang.le@mgm-tp.com>
  • Loading branch information
nam-hle and Nam Hoang Le committed Apr 23, 2021
1 parent 6b899f8 commit 07285ae
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 21 deletions.
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -10,7 +10,8 @@
"lodash.flatten": "^4.4.0",
"lodash.truncate": "^4.4.2",
"slice-ansi": "^4.0.0",
"string-width": "^4.2.0"
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0"
},
"description": "Formats data into a string table.",
"devDependencies": {
Expand Down
22 changes: 21 additions & 1 deletion src/wrapCell.ts
@@ -1,6 +1,26 @@
import slice from 'slice-ansi';
import stripAnsi from 'strip-ansi';
import wrapString from './wrapString';
import wrapWord from './wrapWord';

const splitAnsi = (input: string) => {
const lengths = stripAnsi(input).split('\n').map(({length}) => {
return length;
});

const result: string[] = [];
let startIndex = 0;

lengths.forEach((length) => {
result.push(length === 0 ? '' : slice(input, startIndex, startIndex + length));

// Plus 1 for the newline character itself
startIndex += length + 1;
});

return result;
};

/**
* Wrap a single cell value into a list of lines
*
Expand All @@ -10,7 +30,7 @@ import wrapWord from './wrapWord';
*/
export default (cellValue: string, columnWidth: number, useWrapWord: boolean): string[] => {
// First split on literal newlines
const cellLines = cellValue.split('\n');
const cellLines = splitAnsi(cellValue);

// Then iterate over the list and word-wrap every remaining line if necessary.
for (let lineNr = 0; lineNr < cellLines.length;) {
Expand Down
46 changes: 31 additions & 15 deletions src/wrapWord.ts
@@ -1,32 +1,48 @@
import slice from 'slice-ansi';
import stringWidth from 'string-width';
import stripAnsi from 'strip-ansi';

export default (input: string, size: number): string[] => {
let subject = input;
const calculateStringLengths = (input: string, size: number): Array<[Length:number, Offset: number]> => {
let subject = stripAnsi(input);

const chunks = [];
const chunks: Array<[number, number]> = [];

// https://regex101.com/r/gY5kZ1/1
const re = new RegExp('(^.{1,' + String(size) + '}(\\s+|$))|(^.{1,' + String(size - 1) + '}(\\\\|/|_|\\.|,|;|-))');

do {
let chunk;
let chunk: string;

const match = re.exec(subject);

chunk = re.exec(subject);
if (match) {
chunk = match[0];

if (chunk) {
chunk = chunk[0];
subject = subject.slice(chunk.length);

subject = slice(subject, stringWidth(chunk));
const trimmedLength = chunk.trim().length;
const offset = chunk.length - trimmedLength;

chunk = chunk.trim();
chunks.push([trimmedLength, offset]);
} else {
chunk = slice(subject, 0, size);
subject = slice(subject, size);
}
chunk = subject.slice(0, size);
subject = subject.slice(size);

chunks.push(chunk);
} while (stringWidth(subject));
chunks.push([chunk.length, 0]);
}
} while (subject.length);

return chunks;
};

export default (input: string, size: number): string[] => {
const result: string[] = [];

let startIndex = 0;
calculateStringLengths(input, size).forEach(([length, offset]) => {
result.push(slice(input, startIndex, startIndex + length));

startIndex += length + offset;
});

return result;
};
12 changes: 12 additions & 0 deletions test/utils.ts
@@ -0,0 +1,12 @@
export const openRed = '\u001b[31m';
export const closeRed = '\u001b[39m';

export const stringToRed = (string: string) => {
return openRed + string + closeRed;
};

export const arrayToRed = (array: string[]) => {
return array.map((string) => {
return string === '' ? '' : stringToRed(string);
});
};
33 changes: 29 additions & 4 deletions test/wrapCell.ts
Expand Up @@ -6,6 +6,10 @@ import {
import wrapCell from '../src/wrapCell';
import wrapString from '../src/wrapString';
import wrapWord from '../src/wrapWord';
import {
arrayToRed,
stringToRed,
} from './utils';

describe('wrapCell', () => {
const strings = ['aa bb cc', 'a a bb cccc', 'aaabbcc', 'a\\bb', 'a_bb', 'a-bb', 'a.bb', 'a,bb', 'a;bb'];
Expand All @@ -15,6 +19,7 @@ describe('wrapCell', () => {
it('returns the same output as wrapWord\'s', () => {
for (const string of strings) {
expect(wrapCell(string, 3, true)).to.deep.equal(wrapWord(string, 3));
expect(wrapCell(stringToRed(string), 3, true)).to.deep.equal(arrayToRed(wrapWord(string, 3)));
}
});
});
Expand All @@ -28,31 +33,51 @@ describe('wrapCell', () => {
expect(wrapCell('\na\n', 5, true)).to.deep.equal(['', 'a', '']);
expect(wrapCell('a\na', 5, true)).to.deep.equal(['a', 'a']);
expect(wrapCell('a \na', 5, true)).to.deep.equal(['a', 'a']);

expect(wrapCell('\n\n', 5, true)).to.deep.equal(['', '', '']);
expect(wrapCell('a\n\n', 5, true)).to.deep.equal(['a', '', '']);
expect(wrapCell('\n\na', 5, true)).to.deep.equal(['', '', 'a']);
expect(wrapCell('a\n\nb', 5, true)).to.deep.equal(['a', '', 'b']);
expect(wrapCell('a\n\n\nb', 5, true)).to.deep.equal(['a', '', '', 'b']);

expect(wrapCell(stringToRed('\n'), 5, true)).to.deep.equal(arrayToRed(['', '']));
expect(wrapCell(stringToRed('a\n'), 5, true)).to.deep.equal(arrayToRed(['a', '']));
expect(wrapCell(stringToRed('\na'), 5, true)).to.deep.equal(arrayToRed(['', 'a']));
expect(wrapCell(stringToRed('\na\n'), 5, true)).to.deep.equal(arrayToRed(['', 'a', '']));
expect(wrapCell(stringToRed('a\na'), 5, true)).to.deep.equal(arrayToRed(['a', 'a']));
expect(wrapCell(stringToRed('a \na'), 5, true)).to.deep.equal(arrayToRed(['a', 'a']));
expect(wrapCell(stringToRed('\n\n'), 5, true)).to.deep.equal(arrayToRed(['', '', '']));
expect(wrapCell(stringToRed('a\n\n'), 5, true)).to.deep.equal(arrayToRed(['a', '', '']));
expect(wrapCell(stringToRed('\n\na'), 5, true)).to.deep.equal(arrayToRed(['', '', 'a']));
expect(wrapCell(stringToRed('a\n\nb'), 5, true)).to.deep.equal(arrayToRed(['a', '', 'b']));
expect(wrapCell(stringToRed('a\n\n\nb'), 5, true)).to.deep.equal(arrayToRed(['a', '', '', 'b']));
});
});

context('the length of lineChunk is longer than the length of container', () => {
it('continues cut the word by wrapWord function', () => {
expect(wrapCell('aaa bbb\nc', 3, true)).to.deep.equal(['aaa', 'bbb', 'c']);
expect(wrapCell('a b c\nd', 3, true)).to.deep.equal(['a b', 'c', 'd']);

expect(wrapCell('aaaa\nbbbb', 3, true)).to.deep.equal(['aaa', 'a', 'bbb', 'b']);

expect(wrapCell('a\\bb\nc', 3, true)).to.deep.equal(['a\\', 'bb', 'c']);
expect(wrapCell('a/bb\nc', 3, true)).to.deep.equal(['a/', 'bb', 'c']);
expect(wrapCell('a_bb\nc', 3, true)).to.deep.equal(['a_', 'bb', 'c']);
expect(wrapCell('a-bb\nc', 3, true)).to.deep.equal(['a-', 'bb', 'c']);
expect(wrapCell('a.bb\nc', 3, true)).to.deep.equal(['a.', 'bb', 'c']);
expect(wrapCell('a,bb\nc', 3, true)).to.deep.equal(['a,', 'bb', 'c']);
expect(wrapCell('a;bb\nc', 3, true)).to.deep.equal(['a;', 'bb', 'c']);

expect(wrapCell('aaa-b\nc', 3, true)).to.deep.equal(['aaa', '-b', 'c']);

expect(wrapCell(stringToRed('aaa bbb\nc'), 3, true)).to.deep.equal(arrayToRed(['aaa', 'bbb', 'c']));
expect(wrapCell(stringToRed('a b c\nd'), 3, true)).to.deep.equal(arrayToRed(['a b', 'c', 'd']));
expect(wrapCell(stringToRed('aaaa\nbbbb'), 3, true)).to.deep.equal(arrayToRed(['aaa', 'a', 'bbb', 'b']));
expect(wrapCell(stringToRed('a\\bb\nc'), 3, true)).to.deep.equal(arrayToRed(['a\\', 'bb', 'c']));
expect(wrapCell(stringToRed('a/bb\nc'), 3, true)).to.deep.equal(arrayToRed(['a/', 'bb', 'c']));
expect(wrapCell(stringToRed('a_bb\nc'), 3, true)).to.deep.equal(arrayToRed(['a_', 'bb', 'c']));
expect(wrapCell(stringToRed('a-bb\nc'), 3, true)).to.deep.equal(arrayToRed(['a-', 'bb', 'c']));
expect(wrapCell(stringToRed('a.bb\nc'), 3, true)).to.deep.equal(arrayToRed(['a.', 'bb', 'c']));
expect(wrapCell(stringToRed('a,bb\nc'), 3, true)).to.deep.equal(arrayToRed(['a,', 'bb', 'c']));
expect(wrapCell(stringToRed('a;bb\nc'), 3, true)).to.deep.equal(arrayToRed(['a;', 'bb', 'c']));
expect(wrapCell(stringToRed('aaa-b\nc'), 3, true)).to.deep.equal(arrayToRed(['aaa', '-b', 'c']));
});
});
});
Expand Down
70 changes: 70 additions & 0 deletions test/wrapWord.ts
Expand Up @@ -2,15 +2,23 @@ import {
expect,
} from 'chai';
import wrapWord from '../src/wrapWord';
import {
arrayToRed, closeRed, openRed, stringToRed,
} from './utils';

describe('wrapWord', () => {
it('wraps a string at a nearest whitespace', () => {
expect(wrapWord('aaa bbb', 5)).to.deep.equal(['aaa', 'bbb']);
expect(wrapWord('a a a bbb', 5)).to.deep.equal(['a a a', 'bbb']);

expect(wrapWord(stringToRed('aaa bbb'), 5)).to.deep.equal(arrayToRed(['aaa', 'bbb']));
expect(wrapWord(stringToRed('a a a bbb'), 5)).to.deep.equal(arrayToRed(['a a a', 'bbb']));
});
context('a single word is longer than chunk size', () => {
it('cuts the word', () => {
expect(wrapWord('aaaaa', 2)).to.deep.equal(['aa', 'aa', 'a']);

expect(wrapWord(stringToRed('aaaaa'), 2)).to.deep.equal(arrayToRed(['aa', 'aa', 'a']));
});
});
context('a long word with a special character', () => {
Expand All @@ -22,11 +30,73 @@ describe('wrapWord', () => {
expect(wrapWord('aaa.bbb', 5)).to.deep.equal(['aaa.', 'bbb']);
expect(wrapWord('aaa,bbb', 5)).to.deep.equal(['aaa,', 'bbb']);
expect(wrapWord('aaa;bbb', 5)).to.deep.equal(['aaa;', 'bbb']);

expect(wrapWord(stringToRed('aaa\\bbb'), 5)).to.deep.equal(arrayToRed(['aaa\\', 'bbb']));
expect(wrapWord(stringToRed('aaa/bbb'), 5)).to.deep.equal(arrayToRed(['aaa/', 'bbb']));
expect(wrapWord(stringToRed('aaa_bbb'), 5)).to.deep.equal(arrayToRed(['aaa_', 'bbb']));
expect(wrapWord(stringToRed('aaa-bbb'), 5)).to.deep.equal(arrayToRed(['aaa-', 'bbb']));
expect(wrapWord(stringToRed('aaa.bbb'), 5)).to.deep.equal(arrayToRed(['aaa.', 'bbb']));
expect(wrapWord(stringToRed('aaa,bbb'), 5)).to.deep.equal(arrayToRed(['aaa,', 'bbb']));
expect(wrapWord(stringToRed('aaa;bbb'), 5)).to.deep.equal(arrayToRed(['aaa;', 'bbb']));
});
});
context('a special character after the length of a container', () => {
it('does not include special character', () => {
expect(wrapWord('aa-bbbbb-cccc', 5)).to.deep.equal(['aa-', 'bbbbb', '-cccc']);

expect(wrapWord(stringToRed('aa-bbbbb-cccc'), 5)).to.deep.equal(arrayToRed(['aa-', 'bbbbb', '-cccc']));
});
});

context('mixed ansi and plain', () => {
it('returns proper strings', () => {
expect(wrapWord(`${openRed}Lorem ${closeRed}ipsum dolor ${openRed}sit amet${closeRed}`, 5)).to.deep.equal([
`${openRed}Lorem${closeRed}`,
'ipsum',
'dolor',
`${openRed}sit${closeRed}`,
`${openRed}amet${closeRed}`,
]);

expect(wrapWord(`${openRed}Lorem ${closeRed}ipsum dolor ${openRed}sit amet${closeRed}`, 11)).to.deep.equal([
`${openRed}Lorem ${closeRed}ipsum`,
`dolor ${openRed}sit${closeRed}`,
`${openRed}amet${closeRed}`,
]);

expect(wrapWord(`${openRed}Lorem ip${closeRed}sum dolor si${openRed}t amet${closeRed}`, 5)).to.deep.equal([
`${openRed}Lorem${closeRed}`,
`${openRed}ip${closeRed}sum`,
'dolor',
`si${openRed}t${closeRed}`,
`${openRed}amet${closeRed}`,
]);
});
});

context('multiple ansi', () => {
it('returns proper strings', () => {
const openBold = '\u001b[1m';
const closeBold = '\u001b[22m';

expect(wrapWord(`${openBold}${openRed}Lorem ipsum dolor sit${closeRed}${closeBold}`, 4)).to.deep.equal(
[
`${openBold}${openRed}Lore${closeRed}${closeBold}`,
`${openBold}${openRed}m${closeRed}${closeBold}`,
`${openBold}${openRed}ipsu${closeRed}${closeBold}`,
`${openBold}${openRed}m${closeRed}${closeBold}`,
`${openBold}${openRed}dolo${closeRed}${closeBold}`,
`${openBold}${openRed}r${closeRed}${closeBold}`,
`${openBold}${openRed}sit${closeBold}${closeRed}`],
);

expect(wrapWord(`${openBold}${openRed}Lorem ipsum dolor sit${closeRed}${closeBold}`, 5)).to.deep.equal(
[
`${openBold}${openRed}Lorem${closeRed}${closeBold}`,
`${openBold}${openRed}ipsum${closeRed}${closeBold}`,
`${openBold}${openRed}dolor${closeRed}${closeBold}`,
`${openBold}${openRed}sit${closeBold}${closeRed}`],
);
});
});
});

0 comments on commit 07285ae

Please sign in to comment.