diff --git a/src/table.tsx b/src/table.tsx index 2988c93..d974f71 100644 --- a/src/table.tsx +++ b/src/table.tsx @@ -21,7 +21,15 @@ import { RowProps, TableOptions, } from './types.js' -import {allKeysInCollection, getColumns, getHeadings, intersperse, maybeStripAnsi, sortData} from './utils.js' +import { + allKeysInCollection, + determineWidthOfWrappedText, + getColumns, + getHeadings, + intersperse, + maybeStripAnsi, + sortData, +} from './utils.js' /** * Determines the configured width based on the provided width value. @@ -63,11 +71,6 @@ function determineWidthToUse(columns: Column[], configuredWidth: number): return tableWidth < configuredWidth ? configuredWidth : tableWidth } -function determineWidthOfWrappedText(text: string): number { - const lines = text.split('\n') - return lines.reduce((max, line) => Math.max(max, line.length), 0) -} - function determineTruncatePosition(overflow: Overflow): 'start' | 'middle' | 'end' { switch (overflow) { case 'truncate-middle': { @@ -88,7 +91,7 @@ function determineTruncatePosition(overflow: Overflow): 'start' | 'middle' | 'en } } -function formatTextWithMargins({ +export function formatTextWithMargins({ horizontalAlignment, overflow, padding, @@ -141,34 +144,26 @@ function formatTextWithMargins({ const {marginLeft, marginRight} = calculateMargins(width - determineWidthOfWrappedText(stripAnsi(wrappedText))) const lines = wrappedText.split('\n').map((line, idx) => { - const {marginLeft: lineSpecificLeftMargin} = calculateMargins(width - stripAnsi(line).length) + const {marginLeft: lineSpecificLeftMargin, marginRight: lineSpecificRightMargin} = calculateMargins( + width - stripAnsi(line).length, + ) if (horizontalAlignment === 'left') { - if (idx === 0) { - // if it's the first line, only add margin to the right side (The left margin will be applied later) - return `${line}${' '.repeat(marginRight)}` - } - - // if left alignment, add the overall margin to the left side and right sides - return `${' '.repeat(marginLeft)}${line}${' '.repeat(marginRight)}` + return idx === 0 + ? `${line}${' '.repeat(lineSpecificRightMargin - marginRight)}` + : `${' '.repeat(marginLeft)}${line}${' '.repeat(lineSpecificRightMargin - marginRight)}` } if (horizontalAlignment === 'center') { - if (idx === 0) { - // if it's the first line, only add margin to the right side (The left margin will be applied later) - return `${line}${' '.repeat(marginRight)}` - } - - // if center alignment, add line specific margin to the left side and the overall margin to the right side - return `${' '.repeat(lineSpecificLeftMargin)}${line}${' '.repeat(marginRight)}` + return idx === 0 + ? `${' '.repeat(lineSpecificLeftMargin - marginLeft)}${line}${' '.repeat(lineSpecificRightMargin)}` + : `${' '.repeat(lineSpecificLeftMargin)}${line}${' '.repeat(lineSpecificRightMargin - marginRight)}` } // right alignment - if (idx === 0) { - return `${' '.repeat(Math.max(0, lineSpecificLeftMargin - marginLeft))}${line}${' '.repeat(marginRight)}` - } - - return `${' '.repeat(lineSpecificLeftMargin)}${line}${' '.repeat(marginRight)}` + return idx === 0 + ? `${' '.repeat(Math.max(0, lineSpecificLeftMargin - marginLeft))}${line}${' '.repeat(lineSpecificRightMargin - marginRight)}` + : `${' '.repeat(lineSpecificLeftMargin)}${line}${' '.repeat(lineSpecificRightMargin - marginRight)}` }) return { @@ -178,7 +173,9 @@ function formatTextWithMargins({ } } - const text = cliTruncate(valueWithNoZeroWidthChars, spaceForText, {position: determineTruncatePosition(overflow)}) + const text = cliTruncate(valueWithNoZeroWidthChars.replaceAll('\n', ' '), spaceForText, { + position: determineTruncatePosition(overflow), + }) const spaces = width - stripAnsi(text).length return { text, diff --git a/src/utils.ts b/src/utils.ts index cd3a03c..cfdd062 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -45,6 +45,11 @@ export function allKeysInCollection>(data: T[] return [...keys] } +export function determineWidthOfWrappedText(text: string): number { + const lines = text.split('\n') + return lines.reduce((max, line) => Math.max(max, line.length), 0) +} + export function getColumns>(config: Config, headings: Partial): Column[] { const {columns, horizontalAlignment, maxWidth, overflow, verticalAlignment} = config @@ -58,7 +63,7 @@ export function getColumns>(config: Config, const value = data[key] if (value === undefined || value === null) return 0 - return stripAnsi(String(value).replaceAll('​', ' ')).length + return determineWidthOfWrappedText(stripAnsi(String(value).replaceAll('​', ' '))) }) const header = String(headings[key]).length diff --git a/test/table.test.tsx b/test/table.test.tsx index 05d6883..ce9860e 100644 --- a/test/table.test.tsx +++ b/test/table.test.tsx @@ -1,27 +1,32 @@ /* eslint-disable perfectionist/sort-objects */ import ansis from 'ansis' import {config, expect} from 'chai' -import { Box } from 'ink' -import { render } from 'ink-testing-library' +import {Box} from 'ink' +import {render} from 'ink-testing-library' import React from 'react' -import {Cell, Header, Skeleton, Table} from '../src/table.js' +import {Cell, Header, Skeleton, Table, formatTextWithMargins} from '../src/table.js' config.truncateThreshold = 0 // Helpers ------------------------------------------------------------------- const skeleton = (v: string) => {v} -// @ts-expect-error - ignore -const header = (v: string) =>
{v}
+ +const header = (v: string) => ( + // @ts-expect-error - ignore +
+ {v} +
+) const cell = (v: string) => {v} describe('Table', () => { it('renders table', () => { - const data = [{ name: 'Foo' }] + const data = [{name: 'Foo'}] - const { lastFrame: actual } = render() - const { lastFrame: expected } = render( + const {lastFrame: actual} = render(
) + const {lastFrame: expected} = render( <> {skeleton('┌')} @@ -55,10 +60,10 @@ describe('Table', () => { }) it('renders table with numbers', () => { - const data = [{ name: 'Foo', age: 12 }] - const { lastFrame: actual } = render(
) + const data = [{name: 'Foo', age: 12}] + const {lastFrame: actual} = render(
) - const { lastFrame: expected } = render( + const {lastFrame: expected} = render( <> {skeleton('┌')} @@ -103,12 +108,12 @@ describe('Table', () => { it('renders table with multiple rows', () => { const data = [ - { name: 'Foo', age: 12 }, - { name: 'Bar', age: 0 }, + {name: 'Foo', age: 12}, + {name: 'Bar', age: 0}, ] - const { lastFrame: actual } = render(
) + const {lastFrame: actual} = render(
) - const { lastFrame: expected } = render( + const {lastFrame: expected} = render( <> {skeleton('┌')} @@ -166,10 +171,10 @@ describe('Table', () => { }) it('renders table with undefined value', () => { - const data = [{ name: 'Foo' }, { age: 15, name: 'Bar' }] - const { lastFrame: actual } = render(
) + const data = [{name: 'Foo'}, {age: 15, name: 'Bar'}] + const {lastFrame: actual} = render(
) - const { lastFrame: expected } = render( + const {lastFrame: expected} = render( <> {skeleton('┌')} @@ -228,12 +233,12 @@ describe('Table', () => { it('renders table with custom padding', () => { const data = [ - { name: 'Foo', age: 12 }, - { name: 'Bar', age: 15 }, + {name: 'Foo', age: 12}, + {name: 'Bar', age: 15}, ] - const { lastFrame: actual } = render(
) + const {lastFrame: actual} = render(
) - const { lastFrame: expected } = render( + const {lastFrame: expected} = render( <> {skeleton('┌')} @@ -291,10 +296,13 @@ describe('Table', () => { }) it('renders table with values containing ansi characters', () => { - const data = [{ name: ansis.bold('Foo'), age: 12 }, { name: ansis.bold('Bar'), age: 15 }] - const { lastFrame: actual } = render(
) + const data = [ + {name: ansis.bold('Foo'), age: 12}, + {name: ansis.bold('Bar'), age: 15}, + ] + const {lastFrame: actual} = render(
) - const { lastFrame: expected } = render( + const {lastFrame: expected} = render( <> {skeleton('┌')} @@ -353,13 +361,13 @@ describe('Table', () => { it('renders table with maxWidth', () => { const data = [ - { name: 'Foo', id: 'i'.repeat(30) }, - { name: 'Bar', id: 'i'.repeat(30) }, + {name: 'Foo', id: 'i'.repeat(30)}, + {name: 'Bar', id: 'i'.repeat(30)}, ] - const { lastFrame: actual } = render(
) + const {lastFrame: actual} = render(
) - const { lastFrame: expected } = render( + const {lastFrame: expected} = render( <> {skeleton('┌')} @@ -418,13 +426,13 @@ describe('Table', () => { it('renders a table with wrapped content', () => { const data = [ - { name: 'Foo', id: 'i'.repeat(70) }, - { name: 'Bar', id: 'i'.repeat(70) }, + {name: 'Foo', id: 'i'.repeat(70)}, + {name: 'Bar', id: 'i'.repeat(70)}, ] - const { lastFrame: actual } = render(
) + const {lastFrame: actual} = render(
) - const { lastFrame: expected } = render( + const {lastFrame: expected} = render( <> {skeleton('┌')} @@ -524,3 +532,160 @@ describe('Table', () => { }) }) +describe('formatTextWithMargins', () => { + describe('wrap + align left', () => { + it('formats short string', () => { + expect( + formatTextWithMargins({ + horizontalAlignment: 'left', + overflow: 'wrap', + padding: 1, + value: 'Foo', + width: 50, + }), + ).to.deep.equal({ + marginLeft: 1, + marginRight: 46, + text: 'Foo', + }) + }) + + it('formats long string', () => { + expect( + formatTextWithMargins({ + horizontalAlignment: 'left', + overflow: 'wrap', + padding: 1, + value: 'i'.repeat(70), + width: 50, + }), + ).to.deep.equal({ + marginLeft: 1, + marginRight: 1, + text: 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii\n iiiiiiiiiiiiiiiiiiiiii ', + }) + }) + + it('formats multiline string', () => { + expect( + formatTextWithMargins({ + horizontalAlignment: 'left', + overflow: 'wrap', + padding: 1, + value: `Lorem ipsum dolor sit amet, consectetur +adipi +scing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`, + width: 50, + }), + ).to.deep.equal({ + marginLeft: 1, + marginRight: 2, + text: 'Lorem ipsum dolor sit amet, consectetur \n adipi \n scing elit. Sed do eiusmod tempor incididunt ut\n labore et dolore magna aliqua. ', + }) + }) + }) + + describe('wrap + align center', () => { + it('formats short string', () => { + expect( + formatTextWithMargins({ + horizontalAlignment: 'center', + overflow: 'wrap', + padding: 1, + value: 'Foo', + width: 50, + }), + ).to.deep.equal({ + marginLeft: 23, + marginRight: 24, + text: 'Foo', + }) + }) + + it('formats long string', () => { + expect( + formatTextWithMargins({ + horizontalAlignment: 'center', + overflow: 'wrap', + padding: 1, + value: 'i'.repeat(70), + width: 50, + }), + ).to.deep.equal({ + marginLeft: 1, + marginRight: 1, + text: 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii \n iiiiiiiiiiiiiiiiiiiiii ', + }) + }) + + it('formats multiline string', () => { + expect( + formatTextWithMargins({ + horizontalAlignment: 'center', + overflow: 'wrap', + padding: 1, + value: `Lorem ipsum dolor sit amet, consectetur +adipi +scing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`, + width: 50, + }), + ).to.deep.equal({ + marginLeft: 1, + marginRight: 2, + text: ' Lorem ipsum dolor sit amet, consectetur \n adipi \n scing elit. Sed do eiusmod tempor incididunt ut\n labore et dolore magna aliqua. ', + }) + }) + }) + + describe('wrap + align right', () => { + it('formats short string', () => { + expect( + formatTextWithMargins({ + horizontalAlignment: 'right', + overflow: 'wrap', + padding: 1, + value: 'Foo', + width: 50, + }), + ).to.deep.equal({ + marginLeft: 46, + marginRight: 1, + text: 'Foo', + }) + }) + + it('formats long string', () => { + expect( + formatTextWithMargins({ + horizontalAlignment: 'right', + overflow: 'wrap', + padding: 1, + value: 'i'.repeat(70), + width: 50, + }), + ).to.deep.equal({ + marginLeft: 1, + marginRight: 1, + text: 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii\n iiiiiiiiiiiiiiiiiiiiii', + }) + }) + + it('formats multiline string', () => { + expect( + formatTextWithMargins({ + horizontalAlignment: 'right', + overflow: 'wrap', + padding: 1, + value: `Lorem ipsum dolor sit amet, consectetur +adipi +scing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`, + width: 50, + }), + ).to.deep.equal({ + marginLeft: 2, + marginRight: 1, + text: ' Lorem ipsum dolor sit amet, consectetur\n adipi\n scing elit. Sed do eiusmod tempor incididunt ut\n labore et dolore magna aliqua.', + }) + }) + }) +})