Skip to content

fix(box): use string-width for correct emoji and CJK alignment - #415

Open
mahmoodhamdi wants to merge 2 commits into
unjs:mainfrom
mahmoodhamdi:fix/box-emoji-width
Open

fix(box): use string-width for correct emoji and CJK alignment#415
mahmoodhamdi wants to merge 2 commits into
unjs:mainfrom
mahmoodhamdi:fix/box-emoji-width

Conversation

@mahmoodhamdi

@mahmoodhamdi mahmoodhamdi commented Mar 29, 2026

Copy link
Copy Markdown

What

Fixed consola.box() rendering misaligned right edges when content contains emoji or CJK (fullwidth) characters.

Why

stripAnsi(str).length counts UTF-16 code units, not visual terminal columns. Emoji like 🌍 and CJK characters like 漢字 occupy 2 terminal columns but .length reports them as 1-2 code units, causing the right border to shift.

The string-width package (already a project dependency, used in FancyReporter) correctly calculates visual width using Intl.Segmenter when available.

How

Replaced stripAnsi(str).length with a stringWidth() wrapper (same pattern as FancyReporter) in src/utils/box.ts for all width calculations:

  • Content line width measurement
  • Title width measurement
  • Right-side padding calculation

Added tests in test/box.test.ts covering emoji, CJK, and multiline emoji content.

Testing

  • pnpm vitest run — all tests pass
  • pnpm lint — passes
  • Verified with emoji (🌍, 🎉, 🚀) and CJK (こんにちは) content

Fixes #402

Summary by CodeRabbit

  • Bug Fixes

    • Improved box and reporter width calculations to account for actual character display widths, fixing alignment issues with emoji, CJK, and other wide/ANSI-containing text.
  • Tests

    • Added tests ensuring rendered boxes maintain consistent visual width across ASCII, emoji, CJK, and multi-line inputs.

Replace `stripAnsi(str).length` with `stringWidth()` in box rendering
to properly account for the visual width of emoji and CJK characters.
This fixes misaligned right edges when box content contains characters
that occupy more than one terminal column.
@coderabbitai

coderabbitai Bot commented Mar 29, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Updated box sizing to use a display-width-aware stringWidth() (handles emoji/CJK/ANSI) across box layout and title calculations; added stringWidth utility and re-export; removed duplicated width logic in fancy reporter; added tests ensuring consistent rendered box line widths for varied character types.

Changes

Cohort / File(s) Summary
Box sizing and layout
src/utils/box.ts
Replaced raw ANSI-stripped length checks with stringWidth() for overall box width, title centering, right-border padding, and per-line right padding to compute visual widths correctly for emoji/CJK/ANSI.
String width utility & exports
src/utils/string.ts, src/utils.ts
Added stringWidth(str) (uses string-width with fallback to ANSI-stripped length when needed) and re-exported it from src/utils.ts.
Reporter width usage
src/reporters/fancy.ts
Removed local stringWidth implementation; now imports and uses stringWidth + stripAnsi from ../utils for spacing calculations in FancyReporter.
Tests for box alignment
test/box.test.ts
New Vitest suite asserting uniform visual width across all rendered box lines for ASCII, emoji, CJK, ZWJ sequences, and multi-line mixed content.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I measured widths with careful paws,

Emoji, kanji—no more laws.
Borders neat and corners true,
A snug box made for me and you. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing box alignment by using string-width for emoji and CJK characters.
Linked Issues check ✅ Passed The pull request fully addresses issue #402 by implementing stringWidth() for all box width calculations to correctly handle emoji and CJK characters.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing box emoji/CJK alignment: stringWidth utility creation, box.ts updates, fancy.ts refactoring, and comprehensive test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
test/box.test.ts (1)

5-36: Good test coverage for the core fix.

The tests effectively validate that emoji and CJK content produce aligned box borders. Consider adding tests for additional edge cases in a follow-up:

  • Title with emoji/CJK (e.g., box("content", { title: "🎉 Title" })) since title width calculation was also changed.
  • ANSI-styled content to verify stripping works correctly with stringWidth.
  • Complex emoji like ZWJ sequences (e.g., "👨‍👩‍👧") which may behave differently across environments.
💡 Example additional test case
test("aligns box edges with emoji in title", () => {
  const result = box("Content", { title: "🎉 Title" });
  const lines = result.split("\n").filter(Boolean);
  const widths = lines.map((line) => stringWidth(line));
  const uniqueWidths = [...new Set(widths)];
  expect(uniqueWidths.length).toBe(1);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/box.test.ts` around lines 5 - 36, Add extra tests to test/box.test.ts to
cover title handling and edge cases: create tests that call box("Content", {
title: "🎉 Title" }) and box with a CJK title (e.g., "タイトル"), tests with
ANSI-styled content (e.g., colored strings) to ensure stringWidth/strip ANSI
logic still aligns, and a test using complex ZWJ emoji sequences (e.g.,
"👨‍👩‍👧") and multi-line mixtures; in each test split the result by "\n",
filter(Boolean), map with stringWidth and assert a single unique width so box,
title handling, and ANSI/ZWJ behavior are validated.
src/utils/box.ts (2)

5-11: Consider extracting stringWidth to a shared utility module.

This implementation duplicates src/reporters/fancy.ts:40-47 line-for-line. While acceptable for this fix, extracting to a shared module (e.g., src/utils/string.ts alongside stripAnsi) would reduce duplication and ensure consistent behavior across the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/box.ts` around lines 5 - 11, Extract the stringWidth implementation
into a shared utility (e.g., add a new export in the existing utilities module
where stripAnsi lives) and replace the duplicate implementations with imports;
specifically move the function stringWidth (which uses stripAnsi and
_stringWidth and checks Intl.Segmenter) into a single exported helper and update
the callers (including the fancy reporter duplicate) to import and use that
shared stringWidth to remove duplication and ensure consistent behavior.

287-289: Minor inconsistency: stripAnsi(left).length vs stringWidth(left).

For consistency with the rest of the changes, consider using stringWidth(left) instead of stripAnsi(left).length. While functionally equivalent here (border characters are single-column), using stringWidth throughout makes the intent clearer and guards against future changes to border styles.

♻️ Proposed change
     const right = borderStyle.h.repeat(
-      width - stringWidth(opts.title) - stripAnsi(left).length + paddingOffset,
+      width - stringWidth(opts.title) - stringWidth(left) + paddingOffset,
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/box.ts` around lines 287 - 289, Replace the use of
stripAnsi(left).length with stringWidth(left) in the computation of the right
border (the const right = ... expression) so border width calculation
consistently uses stringWidth; update the expression that currently uses
stripAnsi(left).length to call stringWidth(left) instead (ensure stringWidth is
in scope where this expression resides, e.g., same utils that use stringWidth
elsewhere).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/utils/box.ts`:
- Around line 5-11: Extract the stringWidth implementation into a shared utility
(e.g., add a new export in the existing utilities module where stripAnsi lives)
and replace the duplicate implementations with imports; specifically move the
function stringWidth (which uses stripAnsi and _stringWidth and checks
Intl.Segmenter) into a single exported helper and update the callers (including
the fancy reporter duplicate) to import and use that shared stringWidth to
remove duplication and ensure consistent behavior.
- Around line 287-289: Replace the use of stripAnsi(left).length with
stringWidth(left) in the computation of the right border (the const right = ...
expression) so border width calculation consistently uses stringWidth; update
the expression that currently uses stripAnsi(left).length to call
stringWidth(left) instead (ensure stringWidth is in scope where this expression
resides, e.g., same utils that use stringWidth elsewhere).

In `@test/box.test.ts`:
- Around line 5-36: Add extra tests to test/box.test.ts to cover title handling
and edge cases: create tests that call box("Content", { title: "🎉 Title" }) and
box with a CJK title (e.g., "タイトル"), tests with ANSI-styled content (e.g.,
colored strings) to ensure stringWidth/strip ANSI logic still aligns, and a test
using complex ZWJ emoji sequences (e.g., "👨‍👩‍👧") and multi-line mixtures; in
each test split the result by "\n", filter(Boolean), map with stringWidth and
assert a single unique width so box, title handling, and ANSI/ZWJ behavior are
validated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bd9931e9-5cc8-4b3e-83b3-de9a8e27b8c2

📥 Commits

Reviewing files that changed from the base of the PR and between c47faac and 9a4ac3e.

📒 Files selected for processing (2)
  • src/utils/box.ts
  • test/box.test.ts

Move the duplicated stringWidth helper from both box.ts and fancy.ts
into src/utils/string.ts as a shared export. Use stringWidth consistently
for all width calculations in box rendering (including title border).
Add tests for emoji titles, CJK titles, and ZWJ sequences.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/utils/string.ts (1)

46-58: Alignment functions use str.length instead of stringWidth, limiting support for emoji and CJK content.

The centerAlign, rightAlign, leftAlign, and align functions measure strings using str.length, which counts UTF-16 code units rather than visual terminal columns. These functions are part of the public API and would produce incorrect results if used with wide-character content (emoji, CJK).

However, note that simply switching to stringWidth() is insufficient. The character-by-character indexing approach (str[i - offset]) fundamentally cannot handle wide characters correctly since they occupy a single array element but span multiple terminal columns. A complete refactoring of the algorithm would be needed.

Worth noting: the library's box() function does not use these alignment utilities—it handles width calculations directly with stringWidth(), which is why box alignment with emoji and CJK content works correctly despite this limitation in the exported functions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/string.ts` around lines 46 - 58, The alignment helpers
(centerAlign, rightAlign, leftAlign, align) currently use str.length and index
by code unit which breaks for wide/emoji/CJK; switch to measuring widths with
stringWidth() and stop indexing by code unit — instead split the string into
visible grapheme clusters (Intl.Segmenter or a grapheme-splitter library),
compute each cluster's display width with stringWidth(cluster), then build the
output by joining clusters and adding pad characters to the left/right based on
the computed display widths (use Math.floor/ceil for centering). Update all four
functions to use the cluster array and stringWidth for length/offset
calculations so padding aligns correctly for wide characters.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/utils/string.ts`:
- Around line 46-58: The alignment helpers (centerAlign, rightAlign, leftAlign,
align) currently use str.length and index by code unit which breaks for
wide/emoji/CJK; switch to measuring widths with stringWidth() and stop indexing
by code unit — instead split the string into visible grapheme clusters
(Intl.Segmenter or a grapheme-splitter library), compute each cluster's display
width with stringWidth(cluster), then build the output by joining clusters and
adding pad characters to the left/right based on the computed display widths
(use Math.floor/ceil for centering). Update all four functions to use the
cluster array and stringWidth for length/offset calculations so padding aligns
correctly for wide characters.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e8b2a60-9ef6-445f-b34c-b6a5361e266b

📥 Commits

Reviewing files that changed from the base of the PR and between 9a4ac3e and 3d9099f.

📒 Files selected for processing (5)
  • src/reporters/fancy.ts
  • src/utils.ts
  • src/utils/box.ts
  • src/utils/string.ts
  • test/box.test.ts
✅ Files skipped from review due to trivial changes (1)
  • test/box.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Using emoji breaks consola.box

1 participant