@logtape/pretty 1.4.0-dev.409 → 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/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
- }
@@ -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
- });