Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ export default function cliTruncate(text, columns, options = {}) {
return visible.slice(0, end) + prefix + visible.slice(end);
}

// Workaround: sliceAnsi rounds up when the boundary falls inside a wide character (https://github.com/chalk/slice-ansi/issues/43).
// Re-slice with a 1-column narrower range to stay within bounds.
function safeSliceAnsi(text, beginColumn, endColumn) {
const sliced = sliceAnsi(text, beginColumn, endColumn);
if (stringWidth(sliced) > endColumn - beginColumn) {
return sliceAnsi(text, beginColumn, endColumn - 1);
}

return sliced;
}

if (position === 'start') {
if (preferTruncationOnSpace) {
const nearestSpace = getIndexOfNearestSpace(text, length - columns + 1, true);
Expand All @@ -125,7 +136,7 @@ export default function cliTruncate(text, columns, options = {}) {
truncationCharacter += ' ';
}

const right = sliceAnsi(text, length - columns + stringWidth(truncationCharacter), length);
const right = safeSliceAnsi(text, length - columns + stringWidth(truncationCharacter), length);
return prependWithInheritedStyleFromStart(truncationCharacter, right);
}

Expand All @@ -142,10 +153,14 @@ export default function cliTruncate(text, columns, options = {}) {
return sliceAnsi(text, 0, spaceNearFirstBreakPoint) + truncationCharacter + sliceAnsi(text, spaceNearSecondBreakPoint, length).trim();
}

const truncationWidth = stringWidth(truncationCharacter);
const left = safeSliceAnsi(text, 0, half);
const rightColumns = columns - stringWidth(left) - truncationWidth;
const right = safeSliceAnsi(text, length - rightColumns, length);
return (
sliceAnsi(text, 0, half)
left
+ truncationCharacter
+ sliceAnsi(text, length - (columns - half) + stringWidth(truncationCharacter), length)
+ right
);
}

Expand All @@ -160,7 +175,7 @@ export default function cliTruncate(text, columns, options = {}) {
truncationCharacter = ` ${truncationCharacter}`;
}

const left = sliceAnsi(text, 0, columns - stringWidth(truncationCharacter));
const left = safeSliceAnsi(text, 0, columns - stringWidth(truncationCharacter));
return appendWithInheritedStyleFromEnd(left, truncationCharacter);
}

Expand Down
26 changes: 26 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import test from 'ava';
import stringWidth from 'string-width';
import cliTruncate from './index.js';

test('main', t => {
Expand Down Expand Up @@ -127,3 +128,28 @@ test('preserves ANSI escape codes at the end - issue #24', t => {
t.is(cliTruncate(textEndingWithReset, 11), `Hello World${reset}`);
t.is(cliTruncate(textEndingWithReset, 8), 'Hello W…');
});

test('wide characters should not exceed maxWidth - issue #28', t => {
const cjk = 'あいうえおかきくけこ|end';

// End position: every maxWidth should produce output within bounds
for (let width = 1; width <= 25; width++) {
const result = cliTruncate(cjk, width);
const actual = stringWidth(result);
t.true(actual <= width, `end: maxWidth=${width} → displayWidth=${actual} (${result})`);
}

// Start position
for (let width = 1; width <= 25; width++) {
const result = cliTruncate(cjk, width, {position: 'start'});
const actual = stringWidth(result);
t.true(actual <= width, `start: maxWidth=${width} → displayWidth=${actual} (${result})`);
}

// Middle position
for (let width = 1; width <= 25; width++) {
const result = cliTruncate(cjk, width, {position: 'middle'});
const actual = stringWidth(result);
t.true(actual <= width, `middle: maxWidth=${width} → displayWidth=${actual} (${result})`);
}
});