From 509fa462d40d9ad0e5dbee60a3f5999945f1ea3f Mon Sep 17 00:00:00 2001 From: Nam Hoang Le Date: Sat, 24 Apr 2021 02:37:40 +0700 Subject: [PATCH] feat: wrap word support ansi (#155) * Wrap word support ansi * Refactor * Implement split ansi string Co-authored-by: Nam Hoang Le --- package.json | 3 ++- src/wrapCell.ts | 22 ++++++++++++++- src/wrapWord.ts | 46 ++++++++++++++++++++----------- test/utils.ts | 12 +++++++++ test/wrapCell.ts | 33 ++++++++++++++++++++--- test/wrapWord.ts | 70 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 165 insertions(+), 21 deletions(-) create mode 100644 test/utils.ts diff --git a/package.json b/package.json index 1864036..d6d9575 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/wrapCell.ts b/src/wrapCell.ts index 7a3b126..eeada9f 100644 --- a/src/wrapCell.ts +++ b/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 * @@ -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;) { diff --git a/src/wrapWord.ts b/src/wrapWord.ts index 0aafcb6..0053a05 100644 --- a/src/wrapWord.ts +++ b/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; +}; diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000..1f26630 --- /dev/null +++ b/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); + }); +}; diff --git a/test/wrapCell.ts b/test/wrapCell.ts index 4db2f9a..d4259a8 100644 --- a/test/wrapCell.ts +++ b/test/wrapCell.ts @@ -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']; @@ -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))); } }); }); @@ -28,12 +33,23 @@ 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'])); }); }); @@ -41,9 +57,7 @@ describe('wrapCell', () => { 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']); @@ -51,8 +65,19 @@ describe('wrapCell', () => { 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'])); }); }); }); diff --git a/test/wrapWord.ts b/test/wrapWord.ts index 9376eb2..c77df51 100644 --- a/test/wrapWord.ts +++ b/test/wrapWord.ts @@ -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', () => { @@ -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}`], + ); }); }); });