Skip to content

Commit 2d135f3

Browse files
committed
feat: supported unicode and east asian character width
1 parent 709799c commit 2d135f3

File tree

3 files changed

+68
-32
lines changed

3 files changed

+68
-32
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
},
2323
"dependencies": {
2424
"chalk": "^5.3.0",
25-
"chalk-pipe": "^6.0.0"
25+
"chalk-pipe": "^6.0.0",
26+
"get-east-asian-width": "^1.3.0"
2627
},
2728
"devDependencies": {
2829
"@swc/core": "^1.5.0",

src/core/PrintError.ts

+64-22
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { basename } from 'node:path';
22
import { cwd, stdout } from 'node:process';
33
import type { WriteStream } from 'node:tty';
4+
import { eastAsianWidth } from 'get-east-asian-width';
45
import type { ChalkInstance } from 'chalk';
56
import type { TraceOption } from '../shared/index.js';
67
import TraceError from './TraceError.js';
@@ -35,21 +36,32 @@ export default class PrintError extends TraceError {
3536
* @param {number} [defaultLength] - The default length to use if the title length is not provided.
3637
* @returns {string} The opening part of the error stack trace.
3738
*/
38-
private opening(defaultLength?: number) {
39-
const title = ` ${this.message} `;
40-
const { length } = title;
41-
42-
const width = this.calc((defaultLength ?? length) + 2) / 2;
43-
44-
const prefixLength = Math.floor(width);
45-
const prefixString = this.divide(prefixLength);
39+
private opening(defaultLength?: number): string {
40+
let title = this.padding(this.message);
41+
const length = this.length(title);
42+
43+
const width = this.calc((defaultLength ?? length) + 2);
44+
let halfWidth = Math.floor(width / 2);
45+
const isAlternate = halfWidth <= 0;
46+
if (isAlternate) {
47+
title = this.padding('Error Message');
48+
halfWidth = Math.floor((width + length - 15) / 2);
49+
}
50+
51+
const prefixString = this.divide(halfWidth);
4652
const prefix = this.highlight('red', prefixString);
4753

48-
const suffixLength = Math.ceil(width);
49-
const suffixString = this.divide(suffixLength);
54+
const suffixString = this.divide(halfWidth);
5055
const suffix = this.highlight('red', suffixString);
5156

52-
return `${prefix}${this.highlight('cyanBright', title)}${suffix}]`;
57+
let output = `${prefix}${this.highlight('cyanBright', title)}${suffix}]`;
58+
if (isAlternate) {
59+
output += `\n${this.message}`;
60+
const divider = this.divide((this.column() ?? 32) - 2);
61+
output += `\n${this.highlight('grey', `[${divider}]`)}`;
62+
}
63+
64+
return output;
5365
}
5466

5567
/**
@@ -58,7 +70,7 @@ export default class PrintError extends TraceError {
5870
* @param {TraceOption[]} track - The array of trace options.
5971
* @returns {string} The formatted trace information.
6072
*/
61-
private print(track: TraceOption[]) {
73+
private print(track: TraceOption[]): string {
6274
const root: string = basename(cwd());
6375
const { length } = track;
6476

@@ -93,11 +105,15 @@ export default class PrintError extends TraceError {
93105
* @param {number} [defaultLength] - The default length to use if the title length is not provided.
94106
* @returns {string} The closing part of the error stack trace.
95107
*/
96-
private closing(styles?: string, defaultLength?: number) {
97-
const title = this.padding(this.name);
98-
const { length } = title;
108+
private closing(styles?: string, defaultLength?: number): string {
109+
let title = this.padding(this.name);
110+
const length = this.length(title);
99111

100-
const width = this.calc((defaultLength ?? length) + 3);
112+
let width = this.calc((defaultLength ?? length) + 3);
113+
if (width <= 0) {
114+
title = this.padding('Exception');
115+
width += length - 9;
116+
}
101117

102118
const prefix = this.highlight('red', this.divide(width));
103119
const suffix = this.highlight('red', this.divide(1));
@@ -106,18 +122,44 @@ export default class PrintError extends TraceError {
106122
return `[${prefix}${stylish(title)}${suffix}`;
107123
}
108124

125+
/**
126+
* Calculates the display length of a string, considering East Asian Width rules.
127+
* @private
128+
* @param {string} content - The string content whose display length is to be calculated.
129+
* @returns {number} The total display length of the string, accounting for wide and narrow characters.
130+
*/
131+
private length(content: string): number {
132+
let result = 0;
133+
for (const char of content) {
134+
const codePoint = char.codePointAt(0);
135+
if (typeof codePoint !== 'number') continue;
136+
137+
result += eastAsianWidth(codePoint);
138+
}
139+
140+
return result;
141+
}
142+
109143
/**
110144
* Calculates the width of the terminal.
111145
* @private
112146
* @param {number} length - The length of the content.
113147
* @param {number} [defaultLength=32] - The default length to use if the terminal width cannot be determined.
114148
* @returns {number} The calculated width.
115149
*/
116-
private calc(length: number, defaultLength = 32) {
117-
const columns: number | undefined = (stdout as (WriteStream & { fd: 1 }) | undefined)?.columns;
150+
private calc(length: number, defaultLength = 32): number {
151+
const columns = this.column();
152+
153+
return (columns ?? defaultLength) - length;
154+
}
118155

119-
const clientWidth = (columns ?? defaultLength) - length;
120-
return clientWidth <= 0 ? defaultLength : clientWidth;
156+
/**
157+
* Retrieves the current width of the terminal in columns.
158+
* @private
159+
* @returns {number | undefined} The number of columns in the terminal, or `undefined` if the terminal width cannot be determined.
160+
*/
161+
private column(): number | undefined {
162+
return (stdout as (WriteStream & { fd: 1 }) | undefined)?.columns;
121163
}
122164

123165
/**
@@ -127,7 +169,7 @@ export default class PrintError extends TraceError {
127169
* @param {string} content - The content to highlight.
128170
* @returns {string} The highlighted content.
129171
*/
130-
private highlight(color: string, content: string) {
172+
private highlight(color: string, content: string): string {
131173
const stylish: ChalkInstance = this.palette(color);
132174
return stylish(content);
133175
}
@@ -139,7 +181,7 @@ export default class PrintError extends TraceError {
139181
* @param {string} [separator='-'] - The character to repeat.
140182
* @returns {string} The string filled with the separator character.
141183
*/
142-
private divide(length: number, separator = '-') {
184+
private divide(length: number, separator = '-'): string {
143185
const ls: string[] = Array.from({ length }).map(() => separator);
144186
return ls.join('');
145187
}

test/core/PrintError.spec.ts

+2-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, test } from 'vitest';
2-
import PrintError from '../../src/core/PrintError';
3-
import type { TraceOption } from '../../src/shared';
2+
import PrintError from '../../src/core/PrintError.js';
3+
import type { TraceOption } from '../../src/shared/index.js';
44

55
test('PrintError - should create an instance with correct message and stack', () => {
66
const errorMessage = 'Test error message';
@@ -38,13 +38,6 @@ test('PrintError.closing - should generate correct closing part of error stack t
3838
expect(closing).toContain(expectedSuffix);
3939
});
4040

41-
test('PrintError.calc - should return default length when terminal width is minimal', () => {
42-
const printError = new PrintError('');
43-
process.stdout.columns = 1;
44-
const width = printError['calc'](12);
45-
expect(width).toBe(32);
46-
});
47-
4841
test('PrintError.print - should generate correct trace information', () => {
4942
const printError = new PrintError('');
5043
const track: TraceOption[] = [

0 commit comments

Comments
 (0)