diff --git a/index.js b/index.js index 5a1fe86..8fb2728 100644 --- a/index.js +++ b/index.js @@ -132,20 +132,30 @@ export default function cliTruncate(text, columns, options = {}) { if (position === 'middle') { if (space) { truncationCharacter = ` ${truncationCharacter} `; + + // Drop the padding spaces if the padded character does not fit, so the + // truncation character itself never exceeds the budget. + if (stringWidth(truncationCharacter) >= columns) { + truncationCharacter = truncationCharacter.trim(); + } } - const half = Math.floor(columns / 2); + const truncationWidth = stringWidth(truncationCharacter); + // Reserve room for the truncation character before splitting the budget + // between the two sides, otherwise small budgets overflow (e.g. a width of + // 4 was returned for `columns: 2`). + const half = Math.min(Math.floor(columns / 2), Math.max(0, columns - truncationWidth)); if (preferTruncationOnSpace) { const spaceNearFirstBreakPoint = getIndexOfNearestSpace(text, half); - const spaceNearSecondBreakPoint = getIndexOfNearestSpace(text, length - (columns - half) + 1, true); + const spaceNearSecondBreakPoint = getIndexOfNearestSpace(text, length - (columns - half) + truncationWidth, true); return sliceAnsi(text, 0, spaceNearFirstBreakPoint) + truncationCharacter + sliceAnsi(text, spaceNearSecondBreakPoint, length).trim(); } return ( sliceAnsi(text, 0, half) + truncationCharacter - + sliceAnsi(text, length - (columns - half) + stringWidth(truncationCharacter), length) + + sliceAnsi(text, length - (columns - half) + truncationWidth, length) ); } diff --git a/test.js b/test.js index 3c0f66c..90bf745 100644 --- a/test.js +++ b/test.js @@ -35,6 +35,13 @@ test('space option', t => { t.is(cliTruncate('\u001B[31municornsareawesome\u001B[39m', 10, {position: 'middle', space: true}), '\u001B[31munico\u001B[39m … \u001B[31mme\u001B[39m'); t.is(cliTruncate('Plant a tree every day.', 14, {position: 'middle', space: true}), 'Plant a … day.'); t.is(cliTruncate('안녕하세요', 4, {position: 'start', space: true}), '… 요', 'wide char'); + + // `middle` + `space` must stay within the budget for small widths + t.is(cliTruncate('unicorns', 2, {position: 'middle', space: true}), 'u…'); + t.is(cliTruncate('unicorns', 3, {position: 'middle', space: true}), 'u…s'); + for (const columns of [2, 3, 4, 5, 6, 7]) { + t.true(stringWidth(cliTruncate('unicorns', columns, {position: 'middle', space: true})) <= columns, `width <= ${columns}`); + } }); test('preferTruncationOnSpace option', t => { @@ -44,6 +51,12 @@ test('preferTruncationOnSpace option', t => { t.is(cliTruncate('unicorns rainbow dragons', 6, {position: 'end', preferTruncationOnSpace: true}), 'unico…'); t.is(cliTruncate('unicorns rainbow dragons', 6, {position: 'middle', preferTruncationOnSpace: true}), 'uni…ns'); t.is(cliTruncate('unicorns partying with dragons', 20, {position: 'middle', preferTruncationOnSpace: true}), 'unicorns…dragons'); + + // `middle` + `space` + `preferTruncationOnSpace` must stay within the budget + t.is(cliTruncate('unicorns', 4, {position: 'middle', space: true, preferTruncationOnSpace: true}), 'u … '); + for (const columns of [2, 3, 4, 5, 6, 7]) { + t.true(stringWidth(cliTruncate('unicorns', columns, {position: 'middle', space: true, preferTruncationOnSpace: true})) <= columns, `width <= ${columns}`); + } }); test('truncationCharacter option', t => {