diff --git a/src/utils/box.ts b/src/utils/box.ts index e400a15a..70a804c8 100644 --- a/src/utils/box.ts +++ b/src/utils/box.ts @@ -1,6 +1,11 @@ +import _stringWidth from "string-width"; import { getColor } from "./color"; import { stripAnsi } from "./string"; +function stringWidth(str: string) { + return _stringWidth(stripAnsi(str)); +} + export type BoxBorderStyle = { /** * Top left corner @@ -255,11 +260,10 @@ export function box(text: string, _opts: BoxOpts = {}) { const paddingOffset = opts.style.padding % 2 === 0 ? opts.style.padding : opts.style.padding + 1; const height = textLines.length + paddingOffset; + const titleWidth = opts.title ? stringWidth(opts.title) : 0; const width = - Math.max( - ...textLines.map((line) => stripAnsi(line).length), - opts.title ? stripAnsi(opts.title).length : 0, - ) + paddingOffset; + Math.max(...textLines.map((line) => stringWidth(line)), titleWidth) + + paddingOffset; const widthOffset = width + paddingOffset; const leftSpace = @@ -272,14 +276,9 @@ export function box(text: string, _opts: BoxOpts = {}) { // Include the title if it exists with borders if (opts.title) { const title = _color ? _color(opts.title) : opts.title; - const left = borderStyle.h.repeat( - Math.floor((width - stripAnsi(opts.title).length) / 2), - ); + const left = borderStyle.h.repeat(Math.floor((width - titleWidth) / 2)); const right = borderStyle.h.repeat( - width - - stripAnsi(opts.title).length - - stripAnsi(left).length + - paddingOffset, + width - titleWidth - stringWidth(left) + paddingOffset, ); boxLines.push( `${leftSpace}${borderStyle.tl}${left}${title}${right}${borderStyle.tr}`, @@ -312,7 +311,7 @@ export function box(text: string, _opts: BoxOpts = {}) { // Text line const line = textLines[i - valignOffset]; const left = " ".repeat(paddingOffset); - const right = " ".repeat(width - stripAnsi(line).length); + const right = " ".repeat(width - stringWidth(line)); boxLines.push( `${leftSpace}${borderStyle.v}${left}${line}${right}${borderStyle.v}`, ); diff --git a/test/box.test.ts b/test/box.test.ts new file mode 100644 index 00000000..aa48f97a --- /dev/null +++ b/test/box.test.ts @@ -0,0 +1,21 @@ +import stringWidth from "string-width"; +import { describe, expect, test } from "vitest"; +import { box } from "../src/utils/box"; + +describe("box", () => { + test.each(["hello 🚀️ world", "漢字 test"])( + "keeps all rendered lines aligned for %s", + (text) => { + const lines = box(text, { + style: { + marginTop: 0, + marginBottom: 0, + }, + }).split("\n"); + + const widths = lines.map((line) => stringWidth(line)); + + expect(widths.every((width) => width === widths[0])).toBe(true); + }, + ); +});