@logtape/pretty 1.4.0-dev.408 → 1.4.0-dev.413
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/deno.json +0 -39
- package/src/formatter.test.ts +0 -954
- package/src/formatter.ts +0 -1070
- package/src/mod.ts +0 -17
- package/src/terminal.test.ts +0 -67
- package/src/terminal.ts +0 -94
- package/src/truncate.test.ts +0 -104
- package/src/truncate.ts +0 -90
- package/src/util.deno.ts +0 -21
- package/src/util.node.ts +0 -14
- package/src/util.ts +0 -13
- package/src/wcwidth.test.ts +0 -60
- package/src/wcwidth.ts +0 -414
- package/src/wordwrap.test.ts +0 -114
- package/src/wordwrap.ts +0 -148
- package/tsdown.config.ts +0 -24
package/src/wcwidth.ts
DELETED
|
@@ -1,414 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
* wcwidth implementation for JavaScript/TypeScript
|
|
4
|
-
*
|
|
5
|
-
* This module provides functions to calculate the display width of Unicode
|
|
6
|
-
* characters and strings in terminal/monospace contexts, compatible with
|
|
7
|
-
* the Python wcwidth library and POSIX wcwidth() standard.
|
|
8
|
-
*
|
|
9
|
-
* Based on Unicode 15.1.0 character width tables.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
// Pre-compiled regex for ANSI escape sequences
|
|
13
|
-
// deno-lint-ignore no-control-regex
|
|
14
|
-
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Remove all ANSI escape sequences from a string.
|
|
18
|
-
*
|
|
19
|
-
* @param text The string to clean
|
|
20
|
-
* @returns String with ANSI escape sequences removed
|
|
21
|
-
*/
|
|
22
|
-
export function stripAnsi(text: string): string {
|
|
23
|
-
return text.replace(ANSI_PATTERN, "");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Calculate the display width of a string, ignoring ANSI escape codes
|
|
28
|
-
* and accounting for Unicode character widths using wcwidth-compatible logic.
|
|
29
|
-
*
|
|
30
|
-
* @param text The string to measure
|
|
31
|
-
* @returns The display width in terminal columns
|
|
32
|
-
*/
|
|
33
|
-
export function getDisplayWidth(text: string): number {
|
|
34
|
-
// Remove all ANSI escape sequences first
|
|
35
|
-
const cleanText = stripAnsi(text);
|
|
36
|
-
|
|
37
|
-
if (cleanText.length === 0) return 0;
|
|
38
|
-
|
|
39
|
-
let width = 0;
|
|
40
|
-
let i = 0;
|
|
41
|
-
|
|
42
|
-
// Process character by character, handling surrogate pairs and combining characters
|
|
43
|
-
while (i < cleanText.length) {
|
|
44
|
-
const code = cleanText.codePointAt(i);
|
|
45
|
-
if (code === undefined) {
|
|
46
|
-
i++;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const charWidth = wcwidth(code);
|
|
51
|
-
if (charWidth >= 0) {
|
|
52
|
-
width += charWidth;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Move to next code point (handles surrogate pairs)
|
|
56
|
-
i += (code > 0xFFFF) ? 2 : 1;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return width;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Get the display width of a single Unicode code point.
|
|
64
|
-
* Based on wcwidth implementation - returns:
|
|
65
|
-
* -1: Non-printable/control character
|
|
66
|
-
* 0: Zero-width character (combining marks, etc.)
|
|
67
|
-
* 1: Normal width character
|
|
68
|
-
* 2: Wide character (East Asian, emoji, etc.)
|
|
69
|
-
*
|
|
70
|
-
* @param code Unicode code point
|
|
71
|
-
* @returns Display width (-1, 0, 1, or 2)
|
|
72
|
-
*/
|
|
73
|
-
export function wcwidth(code: number): number {
|
|
74
|
-
// C0 and C1 control characters
|
|
75
|
-
if (code < 32 || (code >= 0x7F && code < 0xA0)) {
|
|
76
|
-
return -1;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Zero-width characters (based on wcwidth table_zero.py)
|
|
80
|
-
if (isZeroWidth(code)) {
|
|
81
|
-
return 0;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Wide characters (based on wcwidth table_wide.py)
|
|
85
|
-
if (isWideCharacter(code)) {
|
|
86
|
-
return 2;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return 1;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Zero-width character ranges (sorted for binary search)
|
|
93
|
-
const ZERO_WIDTH_RANGES: Array<[number, number]> = [
|
|
94
|
-
[0x0300, 0x036F], // Combining Diacritical Marks
|
|
95
|
-
[0x0483, 0x0489], // Hebrew combining marks
|
|
96
|
-
[0x0591, 0x05BD], // Arabic combining marks
|
|
97
|
-
[0x05C1, 0x05C2],
|
|
98
|
-
[0x05C4, 0x05C5],
|
|
99
|
-
[0x0610, 0x061A], // More Arabic combining marks
|
|
100
|
-
[0x064B, 0x065F],
|
|
101
|
-
[0x06D6, 0x06DC],
|
|
102
|
-
[0x06DF, 0x06E4],
|
|
103
|
-
[0x06E7, 0x06E8],
|
|
104
|
-
[0x06EA, 0x06ED],
|
|
105
|
-
[0x0730, 0x074A],
|
|
106
|
-
[0x07A6, 0x07B0],
|
|
107
|
-
[0x07EB, 0x07F3],
|
|
108
|
-
[0x0816, 0x0819],
|
|
109
|
-
[0x081B, 0x0823],
|
|
110
|
-
[0x0825, 0x0827],
|
|
111
|
-
[0x0829, 0x082D],
|
|
112
|
-
[0x0859, 0x085B],
|
|
113
|
-
[0x08D3, 0x08E1],
|
|
114
|
-
[0x08E3, 0x0902],
|
|
115
|
-
[0x0941, 0x0948],
|
|
116
|
-
[0x0951, 0x0957],
|
|
117
|
-
[0x0962, 0x0963],
|
|
118
|
-
[0x09C1, 0x09C4],
|
|
119
|
-
[0x09E2, 0x09E3],
|
|
120
|
-
[0x0A01, 0x0A02],
|
|
121
|
-
[0x0A41, 0x0A42],
|
|
122
|
-
[0x0A47, 0x0A48],
|
|
123
|
-
[0x0A4B, 0x0A4D],
|
|
124
|
-
[0x0A70, 0x0A71],
|
|
125
|
-
[0x0A81, 0x0A82],
|
|
126
|
-
[0x0AC1, 0x0AC5],
|
|
127
|
-
[0x0AC7, 0x0AC8],
|
|
128
|
-
[0x0AE2, 0x0AE3],
|
|
129
|
-
[0x0AFA, 0x0AFF],
|
|
130
|
-
[0x0B41, 0x0B44],
|
|
131
|
-
[0x0B55, 0x0B56],
|
|
132
|
-
[0x0B62, 0x0B63],
|
|
133
|
-
[0x0C3E, 0x0C40],
|
|
134
|
-
[0x0C46, 0x0C48],
|
|
135
|
-
[0x0C4A, 0x0C4D],
|
|
136
|
-
[0x0C55, 0x0C56],
|
|
137
|
-
[0x0C62, 0x0C63],
|
|
138
|
-
[0x0CCC, 0x0CCD],
|
|
139
|
-
[0x0CE2, 0x0CE3],
|
|
140
|
-
[0x0D00, 0x0D01],
|
|
141
|
-
[0x0D3B, 0x0D3C],
|
|
142
|
-
[0x0D62, 0x0D63],
|
|
143
|
-
[0x0DD2, 0x0DD4],
|
|
144
|
-
[0x0E34, 0x0E3A],
|
|
145
|
-
[0x0E47, 0x0E4E],
|
|
146
|
-
[0x0EB4, 0x0EBC],
|
|
147
|
-
[0x0EC8, 0x0ECD],
|
|
148
|
-
[0x0F18, 0x0F19],
|
|
149
|
-
[0x0F71, 0x0F7E],
|
|
150
|
-
[0x0F80, 0x0F84],
|
|
151
|
-
[0x0F86, 0x0F87],
|
|
152
|
-
[0x0F8D, 0x0F97],
|
|
153
|
-
[0x0F99, 0x0FBC],
|
|
154
|
-
[0x102D, 0x1030],
|
|
155
|
-
[0x1032, 0x1037],
|
|
156
|
-
[0x1039, 0x103A],
|
|
157
|
-
[0x103D, 0x103E],
|
|
158
|
-
[0x1058, 0x1059],
|
|
159
|
-
[0x105E, 0x1060],
|
|
160
|
-
[0x1071, 0x1074],
|
|
161
|
-
[0x1085, 0x1086],
|
|
162
|
-
[0x135D, 0x135F],
|
|
163
|
-
[0x1712, 0x1714],
|
|
164
|
-
[0x1732, 0x1734],
|
|
165
|
-
[0x1752, 0x1753],
|
|
166
|
-
[0x1772, 0x1773],
|
|
167
|
-
[0x17B4, 0x17B5],
|
|
168
|
-
[0x17B7, 0x17BD],
|
|
169
|
-
[0x17C9, 0x17D3],
|
|
170
|
-
[0x180B, 0x180D],
|
|
171
|
-
[0x1885, 0x1886],
|
|
172
|
-
[0x1920, 0x1922],
|
|
173
|
-
[0x1927, 0x1928],
|
|
174
|
-
[0x1939, 0x193B],
|
|
175
|
-
[0x1A17, 0x1A18],
|
|
176
|
-
[0x1A58, 0x1A5E],
|
|
177
|
-
[0x1A65, 0x1A6C],
|
|
178
|
-
[0x1A73, 0x1A7C],
|
|
179
|
-
[0x1AB0, 0x1ABE],
|
|
180
|
-
[0x1B00, 0x1B03],
|
|
181
|
-
[0x1B36, 0x1B3A],
|
|
182
|
-
[0x1B6B, 0x1B73],
|
|
183
|
-
[0x1B80, 0x1B81],
|
|
184
|
-
[0x1BA2, 0x1BA5],
|
|
185
|
-
[0x1BA8, 0x1BA9],
|
|
186
|
-
[0x1BAB, 0x1BAD],
|
|
187
|
-
[0x1BE8, 0x1BE9],
|
|
188
|
-
[0x1BEF, 0x1BF1],
|
|
189
|
-
[0x1C2C, 0x1C33],
|
|
190
|
-
[0x1C36, 0x1C37],
|
|
191
|
-
[0x1CD0, 0x1CD2],
|
|
192
|
-
[0x1CD4, 0x1CE0],
|
|
193
|
-
[0x1CE2, 0x1CE8],
|
|
194
|
-
[0x1CF8, 0x1CF9],
|
|
195
|
-
[0x1DC0, 0x1DF9],
|
|
196
|
-
[0x1DFB, 0x1DFF],
|
|
197
|
-
[0x200B, 0x200F], // Zero-width spaces
|
|
198
|
-
[0x202A, 0x202E], // Bidirectional format characters
|
|
199
|
-
[0x2060, 0x2064], // Word joiner, etc.
|
|
200
|
-
[0x2066, 0x206F], // More bidirectional
|
|
201
|
-
[0xFE00, 0xFE0F], // Variation selectors
|
|
202
|
-
[0xFE20, 0xFE2F], // Combining half marks
|
|
203
|
-
];
|
|
204
|
-
|
|
205
|
-
// Single zero-width characters
|
|
206
|
-
const ZERO_WIDTH_SINGLES = new Set([
|
|
207
|
-
0x05BF,
|
|
208
|
-
0x05C7,
|
|
209
|
-
0x0670,
|
|
210
|
-
0x0711,
|
|
211
|
-
0x07FD,
|
|
212
|
-
0x093A,
|
|
213
|
-
0x093C,
|
|
214
|
-
0x094D,
|
|
215
|
-
0x0981,
|
|
216
|
-
0x09BC,
|
|
217
|
-
0x09CD,
|
|
218
|
-
0x09FE,
|
|
219
|
-
0x0A3C,
|
|
220
|
-
0x0A51,
|
|
221
|
-
0x0A75,
|
|
222
|
-
0x0ABC,
|
|
223
|
-
0x0ACD,
|
|
224
|
-
0x0B01,
|
|
225
|
-
0x0B3C,
|
|
226
|
-
0x0B3F,
|
|
227
|
-
0x0B4D,
|
|
228
|
-
0x0B82,
|
|
229
|
-
0x0BC0,
|
|
230
|
-
0x0BCD,
|
|
231
|
-
0x0C00,
|
|
232
|
-
0x0C04,
|
|
233
|
-
0x0C81,
|
|
234
|
-
0x0CBC,
|
|
235
|
-
0x0CBF,
|
|
236
|
-
0x0CC6,
|
|
237
|
-
0x0D41,
|
|
238
|
-
0x0D44,
|
|
239
|
-
0x0D4D,
|
|
240
|
-
0x0D81,
|
|
241
|
-
0x0DCA,
|
|
242
|
-
0x0DD6,
|
|
243
|
-
0x0E31,
|
|
244
|
-
0x0EB1,
|
|
245
|
-
0x0F35,
|
|
246
|
-
0x0F37,
|
|
247
|
-
0x0F39,
|
|
248
|
-
0x0FC6,
|
|
249
|
-
0x1082,
|
|
250
|
-
0x108D,
|
|
251
|
-
0x109D,
|
|
252
|
-
0x17C6,
|
|
253
|
-
0x17DD,
|
|
254
|
-
0x18A9,
|
|
255
|
-
0x1932,
|
|
256
|
-
0x1A1B,
|
|
257
|
-
0x1A56,
|
|
258
|
-
0x1A60,
|
|
259
|
-
0x1A62,
|
|
260
|
-
0x1A7F,
|
|
261
|
-
0x1B34,
|
|
262
|
-
0x1B3C,
|
|
263
|
-
0x1B42,
|
|
264
|
-
0x1BE6,
|
|
265
|
-
0x1BED,
|
|
266
|
-
0x1CED,
|
|
267
|
-
0x1CF4,
|
|
268
|
-
0xFEFF,
|
|
269
|
-
]);
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Binary search to check if a value is within any range
|
|
273
|
-
*/
|
|
274
|
-
function isInRanges(code: number, ranges: Array<[number, number]>): boolean {
|
|
275
|
-
let left = 0;
|
|
276
|
-
let right = ranges.length - 1;
|
|
277
|
-
|
|
278
|
-
while (left <= right) {
|
|
279
|
-
const mid = Math.floor((left + right) / 2);
|
|
280
|
-
const [start, end] = ranges[mid];
|
|
281
|
-
|
|
282
|
-
if (code >= start && code <= end) {
|
|
283
|
-
return true;
|
|
284
|
-
} else if (code < start) {
|
|
285
|
-
right = mid - 1;
|
|
286
|
-
} else {
|
|
287
|
-
left = mid + 1;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Check if a character is zero-width (combining marks, etc.)
|
|
296
|
-
* Based on wcwidth's zero-width table.
|
|
297
|
-
*
|
|
298
|
-
* @param code Unicode code point
|
|
299
|
-
* @returns True if the character has zero display width
|
|
300
|
-
*/
|
|
301
|
-
function isZeroWidth(code: number): boolean {
|
|
302
|
-
return ZERO_WIDTH_SINGLES.has(code) || isInRanges(code, ZERO_WIDTH_RANGES);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Check if a character code point represents a wide character.
|
|
307
|
-
* Based on wcwidth's wide character table (selected ranges from Unicode 15.1.0).
|
|
308
|
-
*
|
|
309
|
-
* @param code Unicode code point
|
|
310
|
-
* @returns True if the character has width 2
|
|
311
|
-
*/
|
|
312
|
-
function isWideCharacter(code: number): boolean {
|
|
313
|
-
// cSpell: disable
|
|
314
|
-
return (
|
|
315
|
-
// Based on wcwidth table_wide.py for Unicode 15.1.0
|
|
316
|
-
(code >= 0x1100 && code <= 0x115F) || // Hangul Jamo
|
|
317
|
-
(code >= 0x231A && code <= 0x231B) || // Watch, Hourglass
|
|
318
|
-
(code >= 0x2329 && code <= 0x232A) || // Angle brackets
|
|
319
|
-
(code >= 0x23E9 && code <= 0x23EC) || // Media controls
|
|
320
|
-
code === 0x23F0 || code === 0x23F3 || // Alarm clock, hourglass
|
|
321
|
-
(code >= 0x25FD && code <= 0x25FE) || // Small squares
|
|
322
|
-
(code >= 0x2614 && code <= 0x2615) || // Umbrella, coffee
|
|
323
|
-
(code >= 0x2648 && code <= 0x2653) || // Zodiac signs
|
|
324
|
-
code === 0x267F || code === 0x2693 || // Wheelchair, anchor
|
|
325
|
-
code === 0x26A0 || code === 0x26A1 || code === 0x26AA || code === 0x26AB || // Warning, lightning, circles
|
|
326
|
-
(code >= 0x26BD && code <= 0x26BE) || // Sports balls
|
|
327
|
-
(code >= 0x26C4 && code <= 0x26C5) || // Weather
|
|
328
|
-
code === 0x26CE || code === 0x26D4 || // Ophiuchus, no entry
|
|
329
|
-
(code >= 0x26EA && code <= 0x26EA) || // Church
|
|
330
|
-
(code >= 0x26F2 && code <= 0x26F3) || // Fountain, golf
|
|
331
|
-
code === 0x26F5 || code === 0x26FA || // Sailboat, tent
|
|
332
|
-
code === 0x26FD || // Gas pump
|
|
333
|
-
(code >= 0x2705 && code <= 0x2705) || // Check mark
|
|
334
|
-
(code >= 0x270A && code <= 0x270B) || // Raised fists
|
|
335
|
-
code === 0x2728 || // Sparkles (✨)
|
|
336
|
-
code === 0x274C || // Cross mark (❌)
|
|
337
|
-
code === 0x274E || // Cross mark button
|
|
338
|
-
(code >= 0x2753 && code <= 0x2755) || // Question marks
|
|
339
|
-
code === 0x2757 || // Exclamation
|
|
340
|
-
(code >= 0x2795 && code <= 0x2797) || // Plus signs
|
|
341
|
-
code === 0x27B0 || code === 0x27BF || // Curly loop, double curly loop
|
|
342
|
-
(code >= 0x2B1B && code <= 0x2B1C) || // Large squares
|
|
343
|
-
code === 0x2B50 || code === 0x2B55 || // Star, circle
|
|
344
|
-
(code >= 0x2E80 && code <= 0x2E99) || // CJK Radicals Supplement
|
|
345
|
-
(code >= 0x2E9B && code <= 0x2EF3) ||
|
|
346
|
-
(code >= 0x2F00 && code <= 0x2FD5) || // Kangxi Radicals
|
|
347
|
-
(code >= 0x2FF0 && code <= 0x2FFB) || // Ideographic Description Characters
|
|
348
|
-
(code >= 0x3000 && code <= 0x303E) || // CJK Symbols and Punctuation
|
|
349
|
-
(code >= 0x3041 && code <= 0x3096) || // Hiragana
|
|
350
|
-
(code >= 0x3099 && code <= 0x30FF) || // Katakana
|
|
351
|
-
(code >= 0x3105 && code <= 0x312F) || // Bopomofo
|
|
352
|
-
(code >= 0x3131 && code <= 0x318E) || // Hangul Compatibility Jamo
|
|
353
|
-
(code >= 0x3190 && code <= 0x31E3) || // Various CJK
|
|
354
|
-
(code >= 0x31F0 && code <= 0x321E) || // Katakana Phonetic Extensions
|
|
355
|
-
(code >= 0x3220 && code <= 0x3247) || // Enclosed CJK Letters and Months
|
|
356
|
-
(code >= 0x3250 && code <= 0x4DBF) || // Various CJK
|
|
357
|
-
(code >= 0x4E00 && code <= 0x9FFF) || // CJK Unified Ideographs
|
|
358
|
-
(code >= 0xA960 && code <= 0xA97F) || // Hangul Jamo Extended-A
|
|
359
|
-
(code >= 0xAC00 && code <= 0xD7A3) || // Hangul Syllables
|
|
360
|
-
(code >= 0xD7B0 && code <= 0xD7C6) || // Hangul Jamo Extended-B
|
|
361
|
-
(code >= 0xF900 && code <= 0xFAFF) || // CJK Compatibility Ideographs
|
|
362
|
-
(code >= 0xFE10 && code <= 0xFE19) || // Vertical Forms
|
|
363
|
-
(code >= 0xFE30 && code <= 0xFE6F) || // CJK Compatibility Forms
|
|
364
|
-
(code >= 0xFF00 && code <= 0xFF60) || // Fullwidth Forms
|
|
365
|
-
(code >= 0xFFE0 && code <= 0xFFE6) || // Fullwidth Forms
|
|
366
|
-
(code >= 0x16FE0 && code <= 0x16FE4) || // Tangut
|
|
367
|
-
(code >= 0x16FF0 && code <= 0x16FF1) ||
|
|
368
|
-
(code >= 0x17000 && code <= 0x187F7) || // Tangut
|
|
369
|
-
(code >= 0x18800 && code <= 0x18CD5) || // Tangut Components
|
|
370
|
-
(code >= 0x18D00 && code <= 0x18D08) || // Tangut Supplement
|
|
371
|
-
(code >= 0x1AFF0 && code <= 0x1AFF3) ||
|
|
372
|
-
(code >= 0x1AFF5 && code <= 0x1AFFB) ||
|
|
373
|
-
(code >= 0x1AFFD && code <= 0x1AFFE) ||
|
|
374
|
-
(code >= 0x1B000 && code <= 0x1B122) || // Kana Extended-A/Supplement
|
|
375
|
-
(code >= 0x1B150 && code <= 0x1B152) ||
|
|
376
|
-
(code >= 0x1B164 && code <= 0x1B167) ||
|
|
377
|
-
(code >= 0x1B170 && code <= 0x1B2FB) ||
|
|
378
|
-
code === 0x1F004 || // Mahjong Red Dragon
|
|
379
|
-
code === 0x1F0CF || // Playing Card Black Joker
|
|
380
|
-
(code >= 0x1F18E && code <= 0x1F18E) || // AB Button
|
|
381
|
-
(code >= 0x1F191 && code <= 0x1F19A) || // Various squared symbols
|
|
382
|
-
(code >= 0x1F1E6 && code <= 0x1F1FF) || // Regional Indicator Symbols (flags)
|
|
383
|
-
(code >= 0x1F200 && code <= 0x1F202) || // Squared symbols
|
|
384
|
-
(code >= 0x1F210 && code <= 0x1F23B) || // Squared CJK
|
|
385
|
-
(code >= 0x1F240 && code <= 0x1F248) || // Tortoise shell bracketed
|
|
386
|
-
(code >= 0x1F250 && code <= 0x1F251) || // Circled ideographs
|
|
387
|
-
(code >= 0x1F260 && code <= 0x1F265) ||
|
|
388
|
-
(code >= 0x1F300 && code <= 0x1F6D7) || // Large emoji block
|
|
389
|
-
(code >= 0x1F6E0 && code <= 0x1F6EC) ||
|
|
390
|
-
(code >= 0x1F6F0 && code <= 0x1F6FC) ||
|
|
391
|
-
(code >= 0x1F700 && code <= 0x1F773) ||
|
|
392
|
-
(code >= 0x1F780 && code <= 0x1F7D8) ||
|
|
393
|
-
(code >= 0x1F7E0 && code <= 0x1F7EB) ||
|
|
394
|
-
(code >= 0x1F7F0 && code <= 0x1F7F0) ||
|
|
395
|
-
(code >= 0x1F800 && code <= 0x1F80B) ||
|
|
396
|
-
(code >= 0x1F810 && code <= 0x1F847) ||
|
|
397
|
-
(code >= 0x1F850 && code <= 0x1F859) ||
|
|
398
|
-
(code >= 0x1F860 && code <= 0x1F887) ||
|
|
399
|
-
(code >= 0x1F890 && code <= 0x1F8AD) ||
|
|
400
|
-
(code >= 0x1F8B0 && code <= 0x1F8B1) ||
|
|
401
|
-
(code >= 0x1F900 && code <= 0x1FA53) || // Supplemental symbols and pictographs
|
|
402
|
-
(code >= 0x1FA60 && code <= 0x1FA6D) ||
|
|
403
|
-
(code >= 0x1FA70 && code <= 0x1FA7C) ||
|
|
404
|
-
(code >= 0x1FA80 && code <= 0x1FA88) ||
|
|
405
|
-
(code >= 0x1FA90 && code <= 0x1FABD) ||
|
|
406
|
-
(code >= 0x1FABF && code <= 0x1FAC5) ||
|
|
407
|
-
(code >= 0x1FACE && code <= 0x1FADB) ||
|
|
408
|
-
(code >= 0x1FAE0 && code <= 0x1FAE8) ||
|
|
409
|
-
(code >= 0x1FAF0 && code <= 0x1FAF8) ||
|
|
410
|
-
(code >= 0x20000 && code <= 0x2FFFD) || // CJK Extension B
|
|
411
|
-
(code >= 0x30000 && code <= 0x3FFFD) // CJK Extension C
|
|
412
|
-
);
|
|
413
|
-
// cSpell: enable
|
|
414
|
-
}
|
package/src/wordwrap.test.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { suite } from "@alinea/suite";
|
|
2
|
-
import { assertEquals } from "@std/assert/equals";
|
|
3
|
-
import { assert } from "@std/assert/assert";
|
|
4
|
-
import { wrapText } from "./wordwrap.ts";
|
|
5
|
-
|
|
6
|
-
const test = suite(import.meta);
|
|
7
|
-
|
|
8
|
-
test("wrapText() should not wrap short text", () => {
|
|
9
|
-
const result = wrapText("short text", 80, "short text".length);
|
|
10
|
-
assertEquals(result, "short text");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
test("wrapText() should wrap long text", () => {
|
|
14
|
-
const text =
|
|
15
|
-
"This is a very long line that should be wrapped at 40 characters maximum width for testing purposes.";
|
|
16
|
-
const result = wrapText(text, 40, "This is a very long line".length);
|
|
17
|
-
|
|
18
|
-
const lines = result.split("\n");
|
|
19
|
-
assert(lines.length > 1, "Should have multiple lines");
|
|
20
|
-
|
|
21
|
-
// Each line should be within the limit (with some tolerance for word boundaries)
|
|
22
|
-
for (const line of lines) {
|
|
23
|
-
assert(line.length <= 45, `Line too long: ${line.length} chars`);
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("wrapText() should preserve ANSI codes", () => {
|
|
28
|
-
const text =
|
|
29
|
-
"\x1b[31mThis is a very long red line that should be wrapped while preserving the color codes\x1b[0m";
|
|
30
|
-
const result = wrapText(text, 40, "This is a very long red line".length);
|
|
31
|
-
|
|
32
|
-
// Should contain ANSI codes
|
|
33
|
-
assert(result.includes("\x1b[31m"), "Should preserve opening ANSI code");
|
|
34
|
-
assert(result.includes("\x1b[0m"), "Should preserve closing ANSI code");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("wrapText() should handle emojis correctly", () => {
|
|
38
|
-
const text =
|
|
39
|
-
"✨ info test This is a very long message that should wrap properly with emoji alignment";
|
|
40
|
-
const result = wrapText(text, 40, "This is a very long message".length);
|
|
41
|
-
|
|
42
|
-
const lines = result.split("\n");
|
|
43
|
-
assert(lines.length > 1, "Should have multiple lines");
|
|
44
|
-
|
|
45
|
-
// Check that continuation lines are indented properly
|
|
46
|
-
// The emoji ✨ should be accounted for in width calculation
|
|
47
|
-
const firstLine = lines[0];
|
|
48
|
-
const continuationLine = lines[1];
|
|
49
|
-
|
|
50
|
-
assert(firstLine.includes("✨"), "First line should contain emoji");
|
|
51
|
-
assert(
|
|
52
|
-
continuationLine.startsWith(" "),
|
|
53
|
-
"Continuation line should be indented",
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("wrapText() should handle newlines in interpolated content", () => {
|
|
58
|
-
const textWithNewlines =
|
|
59
|
-
"Error occurred: Error: Something went wrong\n at line 1\n at line 2";
|
|
60
|
-
const result = wrapText(textWithNewlines, 40, "Error occurred".length);
|
|
61
|
-
|
|
62
|
-
const lines = result.split("\n");
|
|
63
|
-
assert(
|
|
64
|
-
lines.length >= 3,
|
|
65
|
-
"Should preserve original newlines and add more if needed",
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("wrapText() should calculate indentation based on display width", () => {
|
|
70
|
-
// Test with different emojis that have different string lengths but same display width
|
|
71
|
-
const sparklesText = "✨ info test Message content here";
|
|
72
|
-
const crossText = "❌ error test Message content here";
|
|
73
|
-
|
|
74
|
-
const sparklesResult = wrapText(
|
|
75
|
-
sparklesText,
|
|
76
|
-
25,
|
|
77
|
-
"Message content here".length,
|
|
78
|
-
);
|
|
79
|
-
const crossResult = wrapText(crossText, 25, "Message content here".length);
|
|
80
|
-
|
|
81
|
-
const sparklesLines = sparklesResult.split("\n");
|
|
82
|
-
const crossLines = crossResult.split("\n");
|
|
83
|
-
|
|
84
|
-
if (sparklesLines.length > 1 && crossLines.length > 1) {
|
|
85
|
-
// Both should have similar indentation for continuation lines
|
|
86
|
-
// (accounting for the fact that both emojis are width 2)
|
|
87
|
-
const sparklesIndent = sparklesLines[1].search(/\S/);
|
|
88
|
-
const crossIndent = crossLines[1].search(/\S/);
|
|
89
|
-
|
|
90
|
-
// The indentation should be very close (within 1-2 characters)
|
|
91
|
-
// since both emojis have width 2
|
|
92
|
-
assert(
|
|
93
|
-
Math.abs(sparklesIndent - crossIndent) <= 2,
|
|
94
|
-
`Indentation should be similar: sparkles=${sparklesIndent}, cross=${crossIndent}`,
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("wrapText() should handle zero width", () => {
|
|
100
|
-
const result = wrapText("any text", 0, "any text".length);
|
|
101
|
-
assertEquals(result, "any text");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("wrapText() should break at word boundaries", () => {
|
|
105
|
-
const text = "word1 word2 word3 word4 word5";
|
|
106
|
-
const result = wrapText(text, 15, "word1 word2 word3".length);
|
|
107
|
-
|
|
108
|
-
const lines = result.split("\n");
|
|
109
|
-
// Should break at spaces, not in the middle of words
|
|
110
|
-
for (const line of lines) {
|
|
111
|
-
const words = line.trim().split(" ");
|
|
112
|
-
assert(words.every((word) => word.length > 0), "Should not break words");
|
|
113
|
-
}
|
|
114
|
-
});
|
package/src/wordwrap.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
* Word wrapping utilities for terminal output
|
|
4
|
-
*
|
|
5
|
-
* This module provides functions for wrapping text at specified widths
|
|
6
|
-
* while preserving proper indentation and handling Unicode characters
|
|
7
|
-
* correctly.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { getDisplayWidth } from "./wcwidth.ts";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Wrap text at specified width with proper indentation for continuation lines.
|
|
14
|
-
* Automatically detects the message start position from the first line.
|
|
15
|
-
*
|
|
16
|
-
* @param text The text to wrap (may contain ANSI escape codes)
|
|
17
|
-
* @param maxWidth Maximum width in terminal columns
|
|
18
|
-
* @param indentWidth Indentation width for continuation lines
|
|
19
|
-
* @returns Wrapped text with proper indentation
|
|
20
|
-
*/
|
|
21
|
-
export function wrapText(
|
|
22
|
-
text: string,
|
|
23
|
-
maxWidth: number,
|
|
24
|
-
indentWidth: number,
|
|
25
|
-
): string {
|
|
26
|
-
if (maxWidth <= 0) return text;
|
|
27
|
-
|
|
28
|
-
const displayWidth = getDisplayWidth(text);
|
|
29
|
-
// If text has newlines (multiline interpolated values), always process it
|
|
30
|
-
// even if it fits within the width
|
|
31
|
-
if (displayWidth <= maxWidth && !text.includes("\n")) return text;
|
|
32
|
-
|
|
33
|
-
const indent = " ".repeat(Math.max(0, indentWidth));
|
|
34
|
-
|
|
35
|
-
// Check if text contains newlines (from interpolated values like Error objects)
|
|
36
|
-
if (text.includes("\n")) {
|
|
37
|
-
// Split by existing newlines and process each line
|
|
38
|
-
const lines = text.split("\n");
|
|
39
|
-
const wrappedLines: string[] = [];
|
|
40
|
-
|
|
41
|
-
for (let i = 0; i < lines.length; i++) {
|
|
42
|
-
const line = lines[i];
|
|
43
|
-
const lineDisplayWidth = getDisplayWidth(line);
|
|
44
|
-
|
|
45
|
-
if (lineDisplayWidth <= maxWidth) {
|
|
46
|
-
// Line doesn't need wrapping, but add indentation if it's not the first line
|
|
47
|
-
if (i === 0) {
|
|
48
|
-
wrappedLines.push(line);
|
|
49
|
-
} else {
|
|
50
|
-
wrappedLines.push(indent + line);
|
|
51
|
-
}
|
|
52
|
-
} else {
|
|
53
|
-
// Line needs wrapping
|
|
54
|
-
const wrappedLine = wrapSingleLine(line, maxWidth, indent);
|
|
55
|
-
if (i === 0) {
|
|
56
|
-
wrappedLines.push(wrappedLine);
|
|
57
|
-
} else {
|
|
58
|
-
// For continuation lines from interpolated values, add proper indentation
|
|
59
|
-
const subLines = wrappedLine.split("\n");
|
|
60
|
-
for (let j = 0; j < subLines.length; j++) {
|
|
61
|
-
if (j === 0) {
|
|
62
|
-
wrappedLines.push(indent + subLines[j]);
|
|
63
|
-
} else {
|
|
64
|
-
wrappedLines.push(subLines[j]);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return wrappedLines.join("\n");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Process as a single line since log records should not have newlines in the formatted output
|
|
75
|
-
return wrapSingleLine(text, maxWidth, indent);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Wrap a single line of text (without existing newlines) at word boundaries.
|
|
80
|
-
* Preserves ANSI escape codes and handles Unicode character widths correctly.
|
|
81
|
-
*
|
|
82
|
-
* @param text The text to wrap (single line, may contain ANSI codes)
|
|
83
|
-
* @param maxWidth Maximum width in terminal columns
|
|
84
|
-
* @param indent Indentation string for continuation lines
|
|
85
|
-
* @returns Wrapped text with newlines and proper indentation
|
|
86
|
-
*/
|
|
87
|
-
export function wrapSingleLine(
|
|
88
|
-
text: string,
|
|
89
|
-
maxWidth: number,
|
|
90
|
-
indent: string,
|
|
91
|
-
): string {
|
|
92
|
-
// Split text into chunks while preserving ANSI codes
|
|
93
|
-
const lines: string[] = [];
|
|
94
|
-
let currentLine = "";
|
|
95
|
-
let currentDisplayWidth = 0;
|
|
96
|
-
let i = 0;
|
|
97
|
-
|
|
98
|
-
while (i < text.length) {
|
|
99
|
-
// Check for ANSI escape sequence
|
|
100
|
-
if (text[i] === "\x1b" && text[i + 1] === "[") {
|
|
101
|
-
// Find the end of the ANSI sequence
|
|
102
|
-
let j = i + 2;
|
|
103
|
-
while (j < text.length && text[j] !== "m") {
|
|
104
|
-
j++;
|
|
105
|
-
}
|
|
106
|
-
if (j < text.length) {
|
|
107
|
-
j++; // Include the 'm'
|
|
108
|
-
currentLine += text.slice(i, j);
|
|
109
|
-
i = j;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const char = text[i];
|
|
115
|
-
|
|
116
|
-
// Check if adding this character would exceed the width
|
|
117
|
-
if (currentDisplayWidth >= maxWidth && char !== " ") {
|
|
118
|
-
// Try to find a good break point (space) before the current position
|
|
119
|
-
const breakPoint = currentLine.lastIndexOf(" ");
|
|
120
|
-
if (breakPoint > 0) {
|
|
121
|
-
// Break at the space
|
|
122
|
-
lines.push(currentLine.slice(0, breakPoint));
|
|
123
|
-
currentLine = indent + currentLine.slice(breakPoint + 1) + char;
|
|
124
|
-
currentDisplayWidth = getDisplayWidth(currentLine);
|
|
125
|
-
} else {
|
|
126
|
-
// No space found, hard break
|
|
127
|
-
lines.push(currentLine);
|
|
128
|
-
currentLine = indent + char;
|
|
129
|
-
currentDisplayWidth = getDisplayWidth(currentLine);
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
currentLine += char;
|
|
133
|
-
// Recalculate display width properly for Unicode characters
|
|
134
|
-
currentDisplayWidth = getDisplayWidth(currentLine);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
i++;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (currentLine.trim()) {
|
|
141
|
-
lines.push(currentLine);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Filter out empty lines (lines with only indentation/spaces)
|
|
145
|
-
const filteredLines = lines.filter((line) => line.trim().length > 0);
|
|
146
|
-
|
|
147
|
-
return filteredLines.join("\n");
|
|
148
|
-
}
|
package/tsdown.config.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "tsdown";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
entry: ["src/mod.ts", "src/util.ts", "src/util.deno.ts", "src/util.node.ts"],
|
|
5
|
-
dts: {
|
|
6
|
-
sourcemap: true,
|
|
7
|
-
},
|
|
8
|
-
format: ["esm", "cjs"],
|
|
9
|
-
platform: "neutral",
|
|
10
|
-
unbundle: true,
|
|
11
|
-
inputOptions: {
|
|
12
|
-
onLog(level, log, defaultHandler) {
|
|
13
|
-
if (
|
|
14
|
-
level === "warn" && log.code === "UNRESOLVED_IMPORT" &&
|
|
15
|
-
["node:util", "#util"].includes(
|
|
16
|
-
log.exporter ?? "",
|
|
17
|
-
)
|
|
18
|
-
) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
defaultHandler(level, log);
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
});
|