diff --git a/src/drawTable.ts b/src/drawTable.ts index d40dc90..a2c96a7 100644 --- a/src/drawTable.ts +++ b/src/drawTable.ts @@ -6,25 +6,9 @@ import drawRow from './drawRow'; import type { TableConfig, Row, } from './types/internal'; - -/** - * Group the array into sub-arrays by sizes. - * - * @example - * groupBySizes(['a', 'b', 'c', 'd', 'e'], [2, 1, 2]) = [ ['a', 'b'], ['c'], ['d', 'e'] ] - */ - -const groupBySizes = (array: T[], sizes: number[]): T[][] => { - let startIndex = 0; - - return sizes.map((size) => { - const group = array.slice(startIndex, startIndex + size); - - startIndex += size; - - return group; - }); -}; +import { + groupBySizes, +} from './utils'; export default (rows: Row[], columnWidths: number[], rowHeights: number[], config: TableConfig): string => { const { diff --git a/src/makeConfig.ts b/src/makeConfig.ts index 054eb3e..7a6d324 100644 --- a/src/makeConfig.ts +++ b/src/makeConfig.ts @@ -1,25 +1,17 @@ import cloneDeep from 'lodash.clonedeep'; import calculateColumnWidths from './calculateColumnWidths'; -import getBorderCharacters from './getBorderCharacters'; import type { ColumnUserConfig, Indexable, - BorderUserConfig, BorderConfig, TableUserConfig, + TableUserConfig, } from './types/api'; import type { ColumnConfig, Row, TableConfig, } from './types/internal'; +import { + makeBorder, +} from './utils'; import validateConfig from './validateConfig'; -/** - * Merges user provided border characters with the default border ("honeywell") characters. - */ -const makeBorder = (border: BorderUserConfig | undefined): BorderConfig => { - return { - ...getBorderCharacters('honeywell'), - ...border, - }; -}; - /** * Creates a configuration for every column using default * values for the missing configuration properties. diff --git a/src/makeStreamConfig.ts b/src/makeStreamConfig.ts index b3795d7..9752f95 100644 --- a/src/makeStreamConfig.ts +++ b/src/makeStreamConfig.ts @@ -1,29 +1,18 @@ import cloneDeep from 'lodash.clonedeep'; -import getBorderCharacters from './getBorderCharacters'; import type { ColumnUserConfig, Indexable, StreamUserConfig, - BorderUserConfig, - BorderConfig, } from './types/api'; import type { ColumnConfig, StreamConfig, } from './types/internal'; +import { + makeBorder, +} from './utils'; import validateConfig from './validateConfig'; -/** - * Merges user provided border characters with the default border ("honeywell") characters. - * - */ -const makeBorder = (border?: BorderUserConfig): BorderConfig => { - return { - ...getBorderCharacters('honeywell'), - ...border, - }; -}; - /** * Creates a configuration for every column using default * values for the missing configuration properties. diff --git a/src/stringifyTableData.ts b/src/stringifyTableData.ts index 31cb3c3..f698d56 100644 --- a/src/stringifyTableData.ts +++ b/src/stringifyTableData.ts @@ -1,9 +1,14 @@ import type { Row, } from './types/internal'; +import { + normalizeString, +} from './utils'; export default (rows: unknown[][]): Row[] => { return rows.map((cells) => { - return cells.map(String); + return cells.map((cell) => { + return normalizeString(String(cell)); + }); }); }; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..aa58926 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,70 @@ +import slice from 'slice-ansi'; +import stripAnsi from 'strip-ansi'; +import getBorderCharacters from './getBorderCharacters'; +import type { + BorderConfig, BorderUserConfig, +} from './types/api'; + +/** + * Converts Windows-style newline to Unix-style + * + * @internal + */ +export const normalizeString = (input: string): string => { + return input.replace(/\r\n/g, '\n'); +}; + +/** + * Splits ansi string by newlines + * + * @internal + */ +export const splitAnsi = (input: string): 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; +}; + +/** + * Merges user provided border characters with the default border ("honeywell") characters. + * + * @internal + */ +export const makeBorder = (border: BorderUserConfig | undefined): BorderConfig => { + return { + ...getBorderCharacters('honeywell'), + ...border, + }; +}; + +/** + * Groups the array into sub-arrays by sizes. + * + * @internal + * @example + * groupBySizes(['a', 'b', 'c', 'd', 'e'], [2, 1, 2]) = [ ['a', 'b'], ['c'], ['d', 'e'] ] + */ + +export const groupBySizes = (array: T[], sizes: number[]): T[][] => { + let startIndex = 0; + + return sizes.map((size) => { + const group = array.slice(startIndex, startIndex + size); + + startIndex += size; + + return group; + }); +}; diff --git a/src/validateTableData.ts b/src/validateTableData.ts index 83b9a0a..de6f9db 100644 --- a/src/validateTableData.ts +++ b/src/validateTableData.ts @@ -1,3 +1,7 @@ +import { + normalizeString, +} from './utils'; + export default (rows: unknown[][]): void => { if (!Array.isArray(rows)) { throw new TypeError('Table data must be an array.'); @@ -24,7 +28,7 @@ export default (rows: unknown[][]): void => { for (const cell of row) { // eslint-disable-next-line no-control-regex - if (/[\u0001-\u0006\u0008\u0009\u000B-\u001A]/.test(cell)) { + if (/[\u0001-\u0006\u0008\u0009\u000B-\u001A]/.test(normalizeString(String(cell)))) { throw new Error('Table data must not contain control characters.'); } } diff --git a/src/wrapCell.ts b/src/wrapCell.ts index 4d1f19c..a86b6d8 100644 --- a/src/wrapCell.ts +++ b/src/wrapCell.ts @@ -1,26 +1,9 @@ -import slice from 'slice-ansi'; -import stripAnsi from 'strip-ansi'; +import { + splitAnsi, +} from './utils'; 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 * diff --git a/test/validateTableData.ts b/test/validateTableData.ts index 936e317..980ae37 100644 --- a/test/validateTableData.ts +++ b/test/validateTableData.ts @@ -3,6 +3,9 @@ import { expect, } from 'chai'; +import { + table, +} from '../src'; import validateTableData from '../src/validateTableData'; describe('validateTableData', () => { @@ -52,7 +55,27 @@ describe('validateTableData', () => { context('cell data contains newlines', () => { it('does not throw', () => { - validateTableData([['ab\nc']]); + expect(() => { + validateTableData([['ab\nc']]); + }).to.not.throw(); + }); + }); + + context('cell data contains Windows-style newlines', () => { + it('does not throw and replaces by Unix-style newline', () => { + expect(() => { + validateTableData([['ab\r\nc']]); + }).to.not.throw(); + + expect(table([['ab\r\nc']])).to.equal('╔════╗\n║ ab ║\n║ c ║\n╚════╝\n'); + }); + }); + + context('cell data contains carriage return only', () => { + it('throws an error', () => { + expect(() => { + validateTableData([['ab\rc']]); + }).to.throw(Error, 'Table data must not contain control characters.'); }); }); @@ -79,7 +102,9 @@ describe('validateTableData', () => { ].join(''); it('does not throw', () => { - validateTableData([[link]]); + expect(() => { + validateTableData([[link]]); + }).to.not.throw(); }); });