@oakoliver/lipgloss 1.0.0

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/style.ts ADDED
@@ -0,0 +1,1131 @@
1
+ /**
2
+ * Style — the core of lipgloss. Immutable fluent builder for terminal styling.
3
+ * Every setter returns a new Style instance (value-type semantics).
4
+ */
5
+
6
+ import { SGR, stringWidth, stripAnsi, truncate as truncateStr, setHyperlink, resetHyperlink } from './ansi.js';
7
+ import type { ColorValue, UnderlineStyle, AnsiStyleOptions } from './ansi.js';
8
+ import { styled } from './ansi.js';
9
+ import { parseColor, NO_COLOR } from './color.js';
10
+ import type { Color } from './color.js';
11
+ import { noBorder, isNoBorder, maxRuneWidth, getTopSize, getRightSize, getBottomSize, getLeftSize } from './border.js';
12
+ import type { Border } from './border.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Position type
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /** 0.0 = top/left, 0.5 = center, 1.0 = bottom/right */
19
+ export type Position = number;
20
+
21
+ export const Top: Position = 0.0;
22
+ export const Bottom: Position = 1.0;
23
+ export const Center: Position = 0.5;
24
+ export const Left: Position = 0.0;
25
+ export const Right: Position = 1.0;
26
+
27
+ function clampPos(p: Position): number {
28
+ return Math.min(1, Math.max(0, p));
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // CSS-shorthand helpers (1-4 args)
33
+ // ---------------------------------------------------------------------------
34
+
35
+ type Sides<T> = { top: T; right: T; bottom: T; left: T };
36
+
37
+ function whichSidesInt(...args: number[]): Sides<number> | null {
38
+ switch (args.length) {
39
+ case 1: return { top: args[0], right: args[0], bottom: args[0], left: args[0] };
40
+ case 2: return { top: args[0], right: args[1], bottom: args[0], left: args[1] };
41
+ case 3: return { top: args[0], right: args[1], bottom: args[2], left: args[1] };
42
+ case 4: return { top: args[0], right: args[1], bottom: args[2], left: args[3] };
43
+ default: return null;
44
+ }
45
+ }
46
+
47
+ function whichSidesBool(...args: boolean[]): Sides<boolean> | null {
48
+ switch (args.length) {
49
+ case 1: return { top: args[0], right: args[0], bottom: args[0], left: args[0] };
50
+ case 2: return { top: args[0], right: args[1], bottom: args[0], left: args[1] };
51
+ case 3: return { top: args[0], right: args[1], bottom: args[2], left: args[1] };
52
+ case 4: return { top: args[0], right: args[1], bottom: args[2], left: args[3] };
53
+ default: return null;
54
+ }
55
+ }
56
+
57
+ function whichSidesColor(...args: Color[]): Sides<Color> | null {
58
+ switch (args.length) {
59
+ case 1: return { top: args[0], right: args[0], bottom: args[0], left: args[0] };
60
+ case 2: return { top: args[0], right: args[1], bottom: args[0], left: args[1] };
61
+ case 3: return { top: args[0], right: args[1], bottom: args[2], left: args[1] };
62
+ case 4: return { top: args[0], right: args[1], bottom: args[2], left: args[3] };
63
+ default: return null;
64
+ }
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Internal text helpers
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /** Split string into lines and find the widest line's visible width. */
72
+ export function getLines(s: string): { lines: string[]; widest: number } {
73
+ s = s.replace(/\t/g, ' ').replace(/\r\n/g, '\n');
74
+ const lines = s.split('\n');
75
+ let widest = 0;
76
+ for (const l of lines) {
77
+ const w = stringWidth(l);
78
+ if (w > widest) widest = w;
79
+ }
80
+ return { lines, widest };
81
+ }
82
+
83
+ /** Horizontal alignment: pads each line so all are the same width. */
84
+ function alignTextHorizontal(str: string, pos: Position, width: number, wsStyle: AnsiStyleOptions | null): string {
85
+ const { lines, widest } = getLines(str);
86
+ const parts: string[] = [];
87
+
88
+ for (const l of lines) {
89
+ const lineWidth = stringWidth(l);
90
+ const shortAmount = Math.max(0, widest - lineWidth) + Math.max(0, width - Math.max(widest, lineWidth));
91
+
92
+ if (shortAmount > 0) {
93
+ const p = clampPos(pos);
94
+ if (p <= 0) {
95
+ // Left
96
+ parts.push(l + styledSpaces(shortAmount, wsStyle));
97
+ } else if (p >= 1) {
98
+ // Right
99
+ parts.push(styledSpaces(shortAmount, wsStyle) + l);
100
+ } else {
101
+ // Center
102
+ const leftPad = Math.floor(shortAmount * p);
103
+ const rightPad = shortAmount - leftPad;
104
+ parts.push(styledSpaces(leftPad, wsStyle) + l + styledSpaces(rightPad, wsStyle));
105
+ }
106
+ } else {
107
+ parts.push(l);
108
+ }
109
+ }
110
+
111
+ return parts.join('\n');
112
+ }
113
+
114
+ /** Vertical alignment within a given height. */
115
+ function alignTextVertical(str: string, pos: Position, height: number): string {
116
+ const strHeight = str.split('\n').length;
117
+ if (height <= strHeight) return str;
118
+ const gap = height - strHeight;
119
+ const p = clampPos(pos);
120
+
121
+ if (p <= 0) {
122
+ // Top
123
+ return str + '\n'.repeat(gap);
124
+ } else if (p >= 1) {
125
+ // Bottom
126
+ return '\n'.repeat(gap) + str;
127
+ } else {
128
+ // Center
129
+ const topPad = Math.round(gap * (1 - p));
130
+ const bottomPad = gap - topPad;
131
+ return '\n'.repeat(topPad) + str + '\n'.repeat(bottomPad);
132
+ }
133
+ }
134
+
135
+ /** Create styled spaces (optionally styled with background/reverse). */
136
+ function styledSpaces(n: number, wsStyle: AnsiStyleOptions | null, ch = ' '): string {
137
+ if (n <= 0) return '';
138
+ const sp = ch.repeat(n);
139
+ if (wsStyle) return styled(sp, wsStyle);
140
+ return sp;
141
+ }
142
+
143
+ /** Pad each line on the left. */
144
+ function padLeft(str: string, n: number, wsStyle: AnsiStyleOptions | null, ch = ' '): string {
145
+ if (n <= 0) return str;
146
+ const sp = styledSpaces(n, wsStyle, ch);
147
+ return str.split('\n').map(line => sp + line).join('\n');
148
+ }
149
+
150
+ /** Pad each line on the right. */
151
+ function padRight(str: string, n: number, wsStyle: AnsiStyleOptions | null, ch = ' '): string {
152
+ if (n <= 0) return str;
153
+ const sp = styledSpaces(n, wsStyle, ch);
154
+ return str.split('\n').map(line => line + sp).join('\n');
155
+ }
156
+
157
+ /** Simple word-wrap: break lines at width boundary. */
158
+ function wordWrap(str: string, width: number): string {
159
+ if (width <= 0) return str;
160
+ const inputLines = str.split('\n');
161
+ const out: string[] = [];
162
+
163
+ for (const line of inputLines) {
164
+ if (stringWidth(line) <= width) {
165
+ out.push(line);
166
+ continue;
167
+ }
168
+ // Break by words first, fall back to chars
169
+ const words = line.split(/(\s+)/);
170
+ let current = '';
171
+ for (const word of words) {
172
+ const combined = current + word;
173
+ if (stringWidth(combined) <= width) {
174
+ current = combined;
175
+ } else {
176
+ if (current) out.push(current);
177
+ // If a single word is wider than width, break by chars
178
+ if (stringWidth(word) > width) {
179
+ let rem = word;
180
+ while (stringWidth(rem) > width) {
181
+ let cut = '';
182
+ for (const ch of rem) {
183
+ if (stringWidth(cut + ch) > width) break;
184
+ cut += ch;
185
+ }
186
+ if (!cut) { cut = rem[0]; rem = rem.slice(1); }
187
+ else { rem = rem.slice(cut.length); }
188
+ out.push(cut);
189
+ }
190
+ current = rem;
191
+ } else {
192
+ current = word;
193
+ }
194
+ }
195
+ }
196
+ if (current) out.push(current);
197
+ }
198
+
199
+ return out.join('\n');
200
+ }
201
+
202
+ /** Render a horizontal border edge (top or bottom row). */
203
+ function renderHorizontalEdge(left: string, middle: string, right: string, width: number): string {
204
+ if (!middle) middle = ' ';
205
+ const leftW = stringWidth(left);
206
+ const rightW = stringWidth(right);
207
+ const runes = [...middle];
208
+ let j = 0;
209
+ let result = left;
210
+ let i = 0;
211
+ const target = width - leftW - rightW;
212
+ while (i < target) {
213
+ const r = runes[j % runes.length];
214
+ result += r;
215
+ i += stringWidth(r);
216
+ j++;
217
+ }
218
+ result += right;
219
+ return result;
220
+ }
221
+
222
+ /** Get first character of a string as a string (for corners). */
223
+ export function getFirstRune(s: string): string {
224
+ if (!s) return s;
225
+ return [...s][0];
226
+ }
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // Style class
230
+ // ---------------------------------------------------------------------------
231
+
232
+ const TAB_WIDTH_DEFAULT = 4;
233
+
234
+ export class Style {
235
+ // Track which properties have been explicitly set
236
+ private _set = new Set<string>();
237
+
238
+ // Internal string value (for SetString / toString)
239
+ private _value = '';
240
+
241
+ // Boolean attrs
242
+ private _bold = false;
243
+ private _italic = false;
244
+ private _strikethrough = false;
245
+ private _reverse = false;
246
+ private _blink = false;
247
+ private _faint = false;
248
+ private _underlineSpaces = false;
249
+ private _strikethroughSpaces = false;
250
+ private _colorWhitespace = false;
251
+
252
+ // Underline
253
+ private _underlineStyle: UnderlineStyle = 'none';
254
+
255
+ // Colors
256
+ private _fg: Color = null;
257
+ private _bg: Color = null;
258
+ private _ulColor: Color = null;
259
+
260
+ // Dimensions
261
+ private _width = 0;
262
+ private _height = 0;
263
+ private _maxWidth = 0;
264
+ private _maxHeight = 0;
265
+
266
+ // Alignment
267
+ private _alignH: Position = 0;
268
+ private _alignV: Position = 0;
269
+
270
+ // Padding
271
+ private _paddingTop = 0;
272
+ private _paddingRight = 0;
273
+ private _paddingBottom = 0;
274
+ private _paddingLeft = 0;
275
+
276
+ // Margin
277
+ private _marginTop = 0;
278
+ private _marginRight = 0;
279
+ private _marginBottom = 0;
280
+ private _marginLeft = 0;
281
+ private _marginBg: Color = null;
282
+
283
+ // Border
284
+ private _borderStyle: Border = noBorder;
285
+ private _borderTop = false;
286
+ private _borderRight = false;
287
+ private _borderBottom = false;
288
+ private _borderLeft = false;
289
+ private _borderTopFg: Color = null;
290
+ private _borderRightFg: Color = null;
291
+ private _borderBottomFg: Color = null;
292
+ private _borderLeftFg: Color = null;
293
+ private _borderTopBg: Color = null;
294
+ private _borderRightBg: Color = null;
295
+ private _borderBottomBg: Color = null;
296
+ private _borderLeftBg: Color = null;
297
+
298
+ // Other
299
+ private _inline = false;
300
+ private _tabWidth = TAB_WIDTH_DEFAULT;
301
+ private _transform: ((s: string) => string) | null = null;
302
+ private _link = '';
303
+ private _linkParams = '';
304
+ private _paddingChar = ' ';
305
+
306
+ /** Clone this style into a new instance. */
307
+ private _clone(): Style {
308
+ const s = new Style();
309
+ s._set = new Set(this._set);
310
+ s._value = this._value;
311
+ s._bold = this._bold;
312
+ s._italic = this._italic;
313
+ s._strikethrough = this._strikethrough;
314
+ s._reverse = this._reverse;
315
+ s._blink = this._blink;
316
+ s._faint = this._faint;
317
+ s._underlineSpaces = this._underlineSpaces;
318
+ s._strikethroughSpaces = this._strikethroughSpaces;
319
+ s._colorWhitespace = this._colorWhitespace;
320
+ s._underlineStyle = this._underlineStyle;
321
+ s._fg = this._fg;
322
+ s._bg = this._bg;
323
+ s._ulColor = this._ulColor;
324
+ s._width = this._width;
325
+ s._height = this._height;
326
+ s._maxWidth = this._maxWidth;
327
+ s._maxHeight = this._maxHeight;
328
+ s._alignH = this._alignH;
329
+ s._alignV = this._alignV;
330
+ s._paddingTop = this._paddingTop;
331
+ s._paddingRight = this._paddingRight;
332
+ s._paddingBottom = this._paddingBottom;
333
+ s._paddingLeft = this._paddingLeft;
334
+ s._marginTop = this._marginTop;
335
+ s._marginRight = this._marginRight;
336
+ s._marginBottom = this._marginBottom;
337
+ s._marginLeft = this._marginLeft;
338
+ s._marginBg = this._marginBg;
339
+ s._borderStyle = this._borderStyle;
340
+ s._borderTop = this._borderTop;
341
+ s._borderRight = this._borderRight;
342
+ s._borderBottom = this._borderBottom;
343
+ s._borderLeft = this._borderLeft;
344
+ s._borderTopFg = this._borderTopFg;
345
+ s._borderRightFg = this._borderRightFg;
346
+ s._borderBottomFg = this._borderBottomFg;
347
+ s._borderLeftFg = this._borderLeftFg;
348
+ s._borderTopBg = this._borderTopBg;
349
+ s._borderRightBg = this._borderRightBg;
350
+ s._borderBottomBg = this._borderBottomBg;
351
+ s._borderLeftBg = this._borderLeftBg;
352
+ s._inline = this._inline;
353
+ s._tabWidth = this._tabWidth;
354
+ s._transform = this._transform;
355
+ s._link = this._link;
356
+ s._linkParams = this._linkParams;
357
+ s._paddingChar = this._paddingChar;
358
+ return s;
359
+ }
360
+
361
+ // -----------------------------------------------------------------------
362
+ // Setters (all return new Style)
363
+ // -----------------------------------------------------------------------
364
+
365
+ setString(...strs: string[]): Style {
366
+ const s = this._clone();
367
+ s._value = strs.join(' ');
368
+ return s;
369
+ }
370
+
371
+ bold(v: boolean): Style {
372
+ const s = this._clone(); s._bold = v; s._set.add('bold'); return s;
373
+ }
374
+ italic(v: boolean): Style {
375
+ const s = this._clone(); s._italic = v; s._set.add('italic'); return s;
376
+ }
377
+ strikethrough(v: boolean): Style {
378
+ const s = this._clone(); s._strikethrough = v; s._set.add('strikethrough'); return s;
379
+ }
380
+ reverse(v: boolean): Style {
381
+ const s = this._clone(); s._reverse = v; s._set.add('reverse'); return s;
382
+ }
383
+ blink(v: boolean): Style {
384
+ const s = this._clone(); s._blink = v; s._set.add('blink'); return s;
385
+ }
386
+ faint(v: boolean): Style {
387
+ const s = this._clone(); s._faint = v; s._set.add('faint'); return s;
388
+ }
389
+
390
+ underline(v: boolean): Style {
391
+ return v ? this.underlineStyle('single') : this.underlineStyle('none');
392
+ }
393
+ underlineStyle(u: UnderlineStyle): Style {
394
+ const s = this._clone(); s._underlineStyle = u; s._set.add('underline'); return s;
395
+ }
396
+
397
+ foreground(c: Color): Style {
398
+ const s = this._clone(); s._fg = c; s._set.add('fg'); return s;
399
+ }
400
+ background(c: Color): Style {
401
+ const s = this._clone(); s._bg = c; s._set.add('bg'); return s;
402
+ }
403
+ underlineColor(c: Color): Style {
404
+ const s = this._clone(); s._ulColor = c; s._set.add('ulColor'); return s;
405
+ }
406
+
407
+ width(n: number): Style {
408
+ const s = this._clone(); s._width = Math.max(0, n); s._set.add('width'); return s;
409
+ }
410
+ height(n: number): Style {
411
+ const s = this._clone(); s._height = Math.max(0, n); s._set.add('height'); return s;
412
+ }
413
+ maxWidth(n: number): Style {
414
+ const s = this._clone(); s._maxWidth = Math.max(0, n); s._set.add('maxWidth'); return s;
415
+ }
416
+ maxHeight(n: number): Style {
417
+ const s = this._clone(); s._maxHeight = Math.max(0, n); s._set.add('maxHeight'); return s;
418
+ }
419
+
420
+ align(...pos: Position[]): Style {
421
+ let s = this._clone();
422
+ if (pos.length > 0) { s._alignH = pos[0]; s._set.add('alignH'); }
423
+ if (pos.length > 1) { s._alignV = pos[1]; s._set.add('alignV'); }
424
+ return s;
425
+ }
426
+ alignHorizontal(p: Position): Style {
427
+ const s = this._clone(); s._alignH = p; s._set.add('alignH'); return s;
428
+ }
429
+ alignVertical(p: Position): Style {
430
+ const s = this._clone(); s._alignV = p; s._set.add('alignV'); return s;
431
+ }
432
+
433
+ padding(...args: number[]): Style {
434
+ const sides = whichSidesInt(...args);
435
+ if (!sides) return this;
436
+ const s = this._clone();
437
+ s._paddingTop = Math.max(0, sides.top); s._set.add('paddingTop');
438
+ s._paddingRight = Math.max(0, sides.right); s._set.add('paddingRight');
439
+ s._paddingBottom = Math.max(0, sides.bottom); s._set.add('paddingBottom');
440
+ s._paddingLeft = Math.max(0, sides.left); s._set.add('paddingLeft');
441
+ return s;
442
+ }
443
+ paddingTop(n: number): Style {
444
+ const s = this._clone(); s._paddingTop = Math.max(0, n); s._set.add('paddingTop'); return s;
445
+ }
446
+ paddingRight(n: number): Style {
447
+ const s = this._clone(); s._paddingRight = Math.max(0, n); s._set.add('paddingRight'); return s;
448
+ }
449
+ paddingBottom(n: number): Style {
450
+ const s = this._clone(); s._paddingBottom = Math.max(0, n); s._set.add('paddingBottom'); return s;
451
+ }
452
+ paddingLeft(n: number): Style {
453
+ const s = this._clone(); s._paddingLeft = Math.max(0, n); s._set.add('paddingLeft'); return s;
454
+ }
455
+
456
+ margin(...args: number[]): Style {
457
+ const sides = whichSidesInt(...args);
458
+ if (!sides) return this;
459
+ const s = this._clone();
460
+ s._marginTop = Math.max(0, sides.top); s._set.add('marginTop');
461
+ s._marginRight = Math.max(0, sides.right); s._set.add('marginRight');
462
+ s._marginBottom = Math.max(0, sides.bottom); s._set.add('marginBottom');
463
+ s._marginLeft = Math.max(0, sides.left); s._set.add('marginLeft');
464
+ return s;
465
+ }
466
+ marginTop(n: number): Style {
467
+ const s = this._clone(); s._marginTop = Math.max(0, n); s._set.add('marginTop'); return s;
468
+ }
469
+ marginRight(n: number): Style {
470
+ const s = this._clone(); s._marginRight = Math.max(0, n); s._set.add('marginRight'); return s;
471
+ }
472
+ marginBottom(n: number): Style {
473
+ const s = this._clone(); s._marginBottom = Math.max(0, n); s._set.add('marginBottom'); return s;
474
+ }
475
+ marginLeft(n: number): Style {
476
+ const s = this._clone(); s._marginLeft = Math.max(0, n); s._set.add('marginLeft'); return s;
477
+ }
478
+ marginBackground(c: Color): Style {
479
+ const s = this._clone(); s._marginBg = c; s._set.add('marginBg'); return s;
480
+ }
481
+
482
+ border(b: Border, ...sides: boolean[]): Style {
483
+ const s = this._clone();
484
+ s._borderStyle = b; s._set.add('borderStyle');
485
+ const bs = whichSidesBool(...sides);
486
+ if (bs) {
487
+ s._borderTop = bs.top; s._set.add('borderTop');
488
+ s._borderRight = bs.right; s._set.add('borderRight');
489
+ s._borderBottom = bs.bottom; s._set.add('borderBottom');
490
+ s._borderLeft = bs.left; s._set.add('borderLeft');
491
+ } else {
492
+ s._borderTop = true; s._set.add('borderTop');
493
+ s._borderRight = true; s._set.add('borderRight');
494
+ s._borderBottom = true; s._set.add('borderBottom');
495
+ s._borderLeft = true; s._set.add('borderLeft');
496
+ }
497
+ return s;
498
+ }
499
+ borderStyle(b: Border): Style {
500
+ const s = this._clone(); s._borderStyle = b; s._set.add('borderStyle'); return s;
501
+ }
502
+ borderTop(v: boolean): Style {
503
+ const s = this._clone(); s._borderTop = v; s._set.add('borderTop'); return s;
504
+ }
505
+ borderRight(v: boolean): Style {
506
+ const s = this._clone(); s._borderRight = v; s._set.add('borderRight'); return s;
507
+ }
508
+ borderBottom(v: boolean): Style {
509
+ const s = this._clone(); s._borderBottom = v; s._set.add('borderBottom'); return s;
510
+ }
511
+ borderLeft(v: boolean): Style {
512
+ const s = this._clone(); s._borderLeft = v; s._set.add('borderLeft'); return s;
513
+ }
514
+
515
+ borderForeground(...colors: Color[]): Style {
516
+ if (colors.length === 0) return this;
517
+ const sides = whichSidesColor(...colors);
518
+ if (!sides) return this;
519
+ const s = this._clone();
520
+ s._borderTopFg = sides.top; s._set.add('borderTopFg');
521
+ s._borderRightFg = sides.right; s._set.add('borderRightFg');
522
+ s._borderBottomFg = sides.bottom; s._set.add('borderBottomFg');
523
+ s._borderLeftFg = sides.left; s._set.add('borderLeftFg');
524
+ return s;
525
+ }
526
+ borderTopForeground(c: Color): Style {
527
+ const s = this._clone(); s._borderTopFg = c; s._set.add('borderTopFg'); return s;
528
+ }
529
+ borderRightForeground(c: Color): Style {
530
+ const s = this._clone(); s._borderRightFg = c; s._set.add('borderRightFg'); return s;
531
+ }
532
+ borderBottomForeground(c: Color): Style {
533
+ const s = this._clone(); s._borderBottomFg = c; s._set.add('borderBottomFg'); return s;
534
+ }
535
+ borderLeftForeground(c: Color): Style {
536
+ const s = this._clone(); s._borderLeftFg = c; s._set.add('borderLeftFg'); return s;
537
+ }
538
+
539
+ borderBackground(...colors: Color[]): Style {
540
+ if (colors.length === 0) return this;
541
+ const sides = whichSidesColor(...colors);
542
+ if (!sides) return this;
543
+ const s = this._clone();
544
+ s._borderTopBg = sides.top; s._set.add('borderTopBg');
545
+ s._borderRightBg = sides.right; s._set.add('borderRightBg');
546
+ s._borderBottomBg = sides.bottom; s._set.add('borderBottomBg');
547
+ s._borderLeftBg = sides.left; s._set.add('borderLeftBg');
548
+ return s;
549
+ }
550
+ borderTopBackground(c: Color): Style {
551
+ const s = this._clone(); s._borderTopBg = c; s._set.add('borderTopBg'); return s;
552
+ }
553
+ borderRightBackground(c: Color): Style {
554
+ const s = this._clone(); s._borderRightBg = c; s._set.add('borderRightBg'); return s;
555
+ }
556
+ borderBottomBackground(c: Color): Style {
557
+ const s = this._clone(); s._borderBottomBg = c; s._set.add('borderBottomBg'); return s;
558
+ }
559
+ borderLeftBackground(c: Color): Style {
560
+ const s = this._clone(); s._borderLeftBg = c; s._set.add('borderLeftBg'); return s;
561
+ }
562
+
563
+ inline(v: boolean): Style {
564
+ const s = this._clone(); s._inline = v; s._set.add('inline'); return s;
565
+ }
566
+ tabWidth(n: number): Style {
567
+ const s = this._clone(); s._tabWidth = n <= -1 ? -1 : n; s._set.add('tabWidth'); return s;
568
+ }
569
+ transform(fn: (s: string) => string): Style {
570
+ const s = this._clone(); s._transform = fn; s._set.add('transform'); return s;
571
+ }
572
+ hyperlink(url: string, params?: string): Style {
573
+ const s = this._clone();
574
+ s._link = url; s._set.add('link');
575
+ if (params !== undefined) { s._linkParams = params; s._set.add('linkParams'); }
576
+ return s;
577
+ }
578
+ underlineSpaces(v: boolean): Style {
579
+ const s = this._clone(); s._underlineSpaces = v; s._set.add('underlineSpaces'); return s;
580
+ }
581
+ strikethroughSpaces(v: boolean): Style {
582
+ const s = this._clone(); s._strikethroughSpaces = v; s._set.add('strikethroughSpaces'); return s;
583
+ }
584
+ colorWhitespace(v: boolean): Style {
585
+ const s = this._clone(); s._colorWhitespace = v; s._set.add('colorWhitespace'); return s;
586
+ }
587
+ paddingChar(ch: string): Style {
588
+ const s = this._clone(); s._paddingChar = ch || ' '; s._set.add('paddingChar'); return s;
589
+ }
590
+
591
+ // -----------------------------------------------------------------------
592
+ // Copy — returns a full copy preserving ALL properties
593
+ // -----------------------------------------------------------------------
594
+
595
+ copy(): Style {
596
+ return this._clone();
597
+ }
598
+
599
+ // -----------------------------------------------------------------------
600
+ // Unset methods — remove a property from the set so it reverts to default
601
+ // -----------------------------------------------------------------------
602
+
603
+ unsetBold(): Style { const s = this._clone(); s._set.delete('bold'); s._bold = false; return s; }
604
+ unsetItalic(): Style { const s = this._clone(); s._set.delete('italic'); s._italic = false; return s; }
605
+ unsetUnderline(): Style { const s = this._clone(); s._set.delete('underline'); s._underlineStyle = 'none'; return s; }
606
+ unsetUnderlineSpaces(): Style { const s = this._clone(); s._set.delete('underlineSpaces'); s._underlineSpaces = false; return s; }
607
+ unsetStrikethrough(): Style { const s = this._clone(); s._set.delete('strikethrough'); s._strikethrough = false; return s; }
608
+ unsetStrikethroughSpaces(): Style { const s = this._clone(); s._set.delete('strikethroughSpaces'); s._strikethroughSpaces = false; return s; }
609
+ unsetReverse(): Style { const s = this._clone(); s._set.delete('reverse'); s._reverse = false; return s; }
610
+ unsetBlink(): Style { const s = this._clone(); s._set.delete('blink'); s._blink = false; return s; }
611
+ unsetFaint(): Style { const s = this._clone(); s._set.delete('faint'); s._faint = false; return s; }
612
+ unsetInline(): Style { const s = this._clone(); s._set.delete('inline'); s._inline = false; return s; }
613
+ unsetForeground(): Style { const s = this._clone(); s._set.delete('fg'); s._fg = null; return s; }
614
+ unsetBackground(): Style { const s = this._clone(); s._set.delete('bg'); s._bg = null; return s; }
615
+ unsetUnderlineColor(): Style { const s = this._clone(); s._set.delete('ulColor'); s._ulColor = null; return s; }
616
+ unsetWidth(): Style { const s = this._clone(); s._set.delete('width'); s._width = 0; return s; }
617
+ unsetHeight(): Style { const s = this._clone(); s._set.delete('height'); s._height = 0; return s; }
618
+ unsetMaxWidth(): Style { const s = this._clone(); s._set.delete('maxWidth'); s._maxWidth = 0; return s; }
619
+ unsetMaxHeight(): Style { const s = this._clone(); s._set.delete('maxHeight'); s._maxHeight = 0; return s; }
620
+ unsetPaddingTop(): Style { const s = this._clone(); s._set.delete('paddingTop'); s._paddingTop = 0; return s; }
621
+ unsetPaddingRight(): Style { const s = this._clone(); s._set.delete('paddingRight'); s._paddingRight = 0; return s; }
622
+ unsetPaddingBottom(): Style { const s = this._clone(); s._set.delete('paddingBottom'); s._paddingBottom = 0; return s; }
623
+ unsetPaddingLeft(): Style { const s = this._clone(); s._set.delete('paddingLeft'); s._paddingLeft = 0; return s; }
624
+ unsetPaddingChar(): Style { const s = this._clone(); s._set.delete('paddingChar'); s._paddingChar = ' '; return s; }
625
+ unsetMarginTop(): Style { const s = this._clone(); s._set.delete('marginTop'); s._marginTop = 0; return s; }
626
+ unsetMarginRight(): Style { const s = this._clone(); s._set.delete('marginRight'); s._marginRight = 0; return s; }
627
+ unsetMarginBottom(): Style { const s = this._clone(); s._set.delete('marginBottom'); s._marginBottom = 0; return s; }
628
+ unsetMarginLeft(): Style { const s = this._clone(); s._set.delete('marginLeft'); s._marginLeft = 0; return s; }
629
+ unsetBorderTop(): Style { const s = this._clone(); s._set.delete('borderTop'); s._borderTop = false; return s; }
630
+ unsetBorderRight(): Style { const s = this._clone(); s._set.delete('borderRight'); s._borderRight = false; return s; }
631
+ unsetBorderBottom(): Style { const s = this._clone(); s._set.delete('borderBottom'); s._borderBottom = false; return s; }
632
+ unsetBorderLeft(): Style { const s = this._clone(); s._set.delete('borderLeft'); s._borderLeft = false; return s; }
633
+ unsetBorderStyle(): Style { const s = this._clone(); s._set.delete('borderStyle'); s._borderStyle = noBorder; return s; }
634
+ unsetTabWidth(): Style { const s = this._clone(); s._set.delete('tabWidth'); s._tabWidth = TAB_WIDTH_DEFAULT; return s; }
635
+ unsetTransform(): Style { const s = this._clone(); s._set.delete('transform'); s._transform = null; return s; }
636
+ unsetHyperlink(): Style { const s = this._clone(); s._set.delete('link'); s._link = ''; s._set.delete('linkParams'); s._linkParams = ''; return s; }
637
+ unsetColorWhitespace(): Style { const s = this._clone(); s._set.delete('colorWhitespace'); s._colorWhitespace = false; return s; }
638
+
639
+ // -----------------------------------------------------------------------
640
+ // Getters
641
+ // -----------------------------------------------------------------------
642
+
643
+ getBold(): boolean { return this._set.has('bold') ? this._bold : false; }
644
+ getItalic(): boolean { return this._set.has('italic') ? this._italic : false; }
645
+ getUnderline(): boolean { return this._underlineStyle !== 'none'; }
646
+ getUnderlineStyle(): UnderlineStyle { return this._underlineStyle; }
647
+ getStrikethrough(): boolean { return this._set.has('strikethrough') ? this._strikethrough : false; }
648
+ getReverse(): boolean { return this._set.has('reverse') ? this._reverse : false; }
649
+ getBlink(): boolean { return this._set.has('blink') ? this._blink : false; }
650
+ getFaint(): boolean { return this._set.has('faint') ? this._faint : false; }
651
+ getForeground(): Color { return this._set.has('fg') ? this._fg : null; }
652
+ getBackground(): Color { return this._set.has('bg') ? this._bg : null; }
653
+ getUnderlineColor(): Color { return this._set.has('ulColor') ? this._ulColor : null; }
654
+ getWidth(): number { return this._set.has('width') ? this._width : 0; }
655
+ getHeight(): number { return this._set.has('height') ? this._height : 0; }
656
+ getMaxWidth(): number { return this._set.has('maxWidth') ? this._maxWidth : 0; }
657
+ getMaxHeight(): number { return this._set.has('maxHeight') ? this._maxHeight : 0; }
658
+ getAlignHorizontal(): Position { return this._set.has('alignH') ? this._alignH : Left; }
659
+ getAlignVertical(): Position { return this._set.has('alignV') ? this._alignV : Top; }
660
+ getInline(): boolean { return this._set.has('inline') ? this._inline : false; }
661
+ getTabWidth(): number { return this._set.has('tabWidth') ? this._tabWidth : TAB_WIDTH_DEFAULT; }
662
+ getUnderlineSpaces(): boolean { return this._set.has('underlineSpaces') ? this._underlineSpaces : false; }
663
+ getStrikethroughSpaces(): boolean { return this._set.has('strikethroughSpaces') ? this._strikethroughSpaces : false; }
664
+ getColorWhitespace(): boolean { return this._set.has('colorWhitespace') ? this._colorWhitespace : false; }
665
+ getTransform(): ((s: string) => string) | null { return this._set.has('transform') ? this._transform : null; }
666
+ getPaddingChar(): string { return this._set.has('paddingChar') ? this._paddingChar : ' '; }
667
+ getHyperlink(): { link: string; params: string } {
668
+ return { link: this._set.has('link') ? this._link : '', params: this._set.has('linkParams') ? this._linkParams : '' };
669
+ }
670
+
671
+ getPadding(): { top: number; right: number; bottom: number; left: number } {
672
+ return {
673
+ top: this._set.has('paddingTop') ? this._paddingTop : 0,
674
+ right: this._set.has('paddingRight') ? this._paddingRight : 0,
675
+ bottom: this._set.has('paddingBottom') ? this._paddingBottom : 0,
676
+ left: this._set.has('paddingLeft') ? this._paddingLeft : 0,
677
+ };
678
+ }
679
+ getPaddingTop(): number { return this._set.has('paddingTop') ? this._paddingTop : 0; }
680
+ getPaddingRight(): number { return this._set.has('paddingRight') ? this._paddingRight : 0; }
681
+ getPaddingBottom(): number { return this._set.has('paddingBottom') ? this._paddingBottom : 0; }
682
+ getPaddingLeft(): number { return this._set.has('paddingLeft') ? this._paddingLeft : 0; }
683
+ getHorizontalPadding(): number { return this.getPaddingLeft() + this.getPaddingRight(); }
684
+ getVerticalPadding(): number { return this.getPaddingTop() + this.getPaddingBottom(); }
685
+
686
+ getMargin(): { top: number; right: number; bottom: number; left: number } {
687
+ return {
688
+ top: this._set.has('marginTop') ? this._marginTop : 0,
689
+ right: this._set.has('marginRight') ? this._marginRight : 0,
690
+ bottom: this._set.has('marginBottom') ? this._marginBottom : 0,
691
+ left: this._set.has('marginLeft') ? this._marginLeft : 0,
692
+ };
693
+ }
694
+ getMarginTop(): number { return this._set.has('marginTop') ? this._marginTop : 0; }
695
+ getMarginRight(): number { return this._set.has('marginRight') ? this._marginRight : 0; }
696
+ getMarginBottom(): number { return this._set.has('marginBottom') ? this._marginBottom : 0; }
697
+ getMarginLeft(): number { return this._set.has('marginLeft') ? this._marginLeft : 0; }
698
+ getHorizontalMargins(): number { return this.getMarginLeft() + this.getMarginRight(); }
699
+ getVerticalMargins(): number { return this.getMarginTop() + this.getMarginBottom(); }
700
+
701
+ getBorderStyle(): Border { return this._set.has('borderStyle') ? this._borderStyle : noBorder; }
702
+ getBorderTop(): boolean { return this._set.has('borderTop') ? this._borderTop : false; }
703
+ getBorderRight(): boolean { return this._set.has('borderRight') ? this._borderRight : false; }
704
+ getBorderBottom(): boolean { return this._set.has('borderBottom') ? this._borderBottom : false; }
705
+ getBorderLeft(): boolean { return this._set.has('borderLeft') ? this._borderLeft : false; }
706
+
707
+ /** True when border style is set but no individual side bools are set. */
708
+ private _isBorderStyleSetWithoutSides(): boolean {
709
+ const b = this.getBorderStyle();
710
+ const anySideSet = this._set.has('borderTop') || this._set.has('borderRight') ||
711
+ this._set.has('borderBottom') || this._set.has('borderLeft');
712
+ return !isNoBorder(b) && !anySideSet;
713
+ }
714
+
715
+ getBorderTopSize(): number {
716
+ if (this._isBorderStyleSetWithoutSides()) return 1;
717
+ if (!this.getBorderTop()) return 0;
718
+ return getTopSize(this.getBorderStyle());
719
+ }
720
+ getBorderRightSize(): number {
721
+ if (this._isBorderStyleSetWithoutSides()) return 1;
722
+ if (!this.getBorderRight()) return 0;
723
+ return getRightSize(this.getBorderStyle());
724
+ }
725
+ getBorderBottomSize(): number {
726
+ if (this._isBorderStyleSetWithoutSides()) return 1;
727
+ if (!this.getBorderBottom()) return 0;
728
+ return getBottomSize(this.getBorderStyle());
729
+ }
730
+ getBorderLeftSize(): number {
731
+ if (this._isBorderStyleSetWithoutSides()) return 1;
732
+ if (!this.getBorderLeft()) return 0;
733
+ return getLeftSize(this.getBorderStyle());
734
+ }
735
+
736
+ getHorizontalBorderSize(): number { return this.getBorderLeftSize() + this.getBorderRightSize(); }
737
+ getVerticalBorderSize(): number { return this.getBorderTopSize() + this.getBorderBottomSize(); }
738
+
739
+ getHorizontalFrameSize(): number {
740
+ return this.getHorizontalMargins() + this.getHorizontalPadding() + this.getHorizontalBorderSize();
741
+ }
742
+ getVerticalFrameSize(): number {
743
+ return this.getVerticalMargins() + this.getVerticalPadding() + this.getVerticalBorderSize();
744
+ }
745
+ getFrameSize(): { x: number; y: number } {
746
+ return { x: this.getHorizontalFrameSize(), y: this.getVerticalFrameSize() };
747
+ }
748
+
749
+ // -----------------------------------------------------------------------
750
+ // Inherit
751
+ // -----------------------------------------------------------------------
752
+
753
+ /** Copy explicitly-set values from `other` that are NOT set on this style. Margins/padding are not inherited. */
754
+ inherit(other: Style): Style {
755
+ const s = this._clone();
756
+ const skip = new Set(['paddingTop','paddingRight','paddingBottom','paddingLeft','marginTop','marginRight','marginBottom','marginLeft']);
757
+ for (const key of other._set) {
758
+ if (skip.has(key)) continue;
759
+ // If background is inherited, also inherit margin bg
760
+ if (key === 'bg' && !s._set.has('marginBg') && !other._set.has('marginBg')) {
761
+ s._marginBg = other._bg;
762
+ s._set.add('marginBg');
763
+ }
764
+ if (s._set.has(key)) continue;
765
+ // Copy value
766
+ (s as any)['_' + this._keyToField(key)] = (other as any)['_' + this._keyToField(key)];
767
+ s._set.add(key);
768
+ }
769
+ return s;
770
+ }
771
+
772
+ private _keyToField(key: string): string {
773
+ const map: Record<string, string> = {
774
+ bold: 'bold', italic: 'italic', strikethrough: 'strikethrough',
775
+ reverse: 'reverse', blink: 'blink', faint: 'faint',
776
+ underlineSpaces: 'underlineSpaces', strikethroughSpaces: 'strikethroughSpaces',
777
+ colorWhitespace: 'colorWhitespace', underline: 'underlineStyle',
778
+ fg: 'fg', bg: 'bg', ulColor: 'ulColor',
779
+ width: 'width', height: 'height', maxWidth: 'maxWidth', maxHeight: 'maxHeight',
780
+ alignH: 'alignH', alignV: 'alignV',
781
+ paddingTop: 'paddingTop', paddingRight: 'paddingRight',
782
+ paddingBottom: 'paddingBottom', paddingLeft: 'paddingLeft',
783
+ marginTop: 'marginTop', marginRight: 'marginRight',
784
+ marginBottom: 'marginBottom', marginLeft: 'marginLeft', marginBg: 'marginBg',
785
+ borderStyle: 'borderStyle',
786
+ borderTop: 'borderTop', borderRight: 'borderRight',
787
+ borderBottom: 'borderBottom', borderLeft: 'borderLeft',
788
+ borderTopFg: 'borderTopFg', borderRightFg: 'borderRightFg',
789
+ borderBottomFg: 'borderBottomFg', borderLeftFg: 'borderLeftFg',
790
+ borderTopBg: 'borderTopBg', borderRightBg: 'borderRightBg',
791
+ borderBottomBg: 'borderBottomBg', borderLeftBg: 'borderLeftBg',
792
+ inline: 'inline', tabWidth: 'tabWidth', transform: 'transform',
793
+ link: 'link', linkParams: 'linkParams', paddingChar: 'paddingChar',
794
+ };
795
+ return map[key] || key;
796
+ }
797
+
798
+ // -----------------------------------------------------------------------
799
+ // Render
800
+ // -----------------------------------------------------------------------
801
+
802
+ /** Render the style applied to the given strings (joined with space). */
803
+ render(...strs: string[]): string {
804
+ if (this._value) strs = [this._value, ...strs];
805
+ let str = strs.join(' ');
806
+
807
+ const hasUnderline = this._underlineStyle !== 'none';
808
+ const isBold = this._set.has('bold') && this._bold;
809
+ const isItalic = this._set.has('italic') && this._italic;
810
+ const isStrikethrough = this._set.has('strikethrough') && this._strikethrough;
811
+ const isReverse = this._set.has('reverse') && this._reverse;
812
+ const isBlink = this._set.has('blink') && this._blink;
813
+ const isFaint = this._set.has('faint') && this._faint;
814
+ const fg = this._set.has('fg') ? parseColor(this._fg) : null;
815
+ const bg = this._set.has('bg') ? parseColor(this._bg) : null;
816
+ const ul = this._set.has('ulColor') ? parseColor(this._ulColor) : null;
817
+
818
+ const w = this._set.has('width') ? this._width : 0;
819
+ const h = this._set.has('height') ? this._height : 0;
820
+ const hAlign = this._set.has('alignH') ? this._alignH : Left;
821
+ const vAlign = this._set.has('alignV') ? this._alignV : Top;
822
+ const topPad = this._set.has('paddingTop') ? this._paddingTop : 0;
823
+ const rightPad = this._set.has('paddingRight') ? this._paddingRight : 0;
824
+ const bottomPad = this._set.has('paddingBottom') ? this._paddingBottom : 0;
825
+ const leftPad = this._set.has('paddingLeft') ? this._paddingLeft : 0;
826
+ const hBorderSize = this.getHorizontalBorderSize();
827
+ const vBorderSize = this.getVerticalBorderSize();
828
+ const colorWS = this._set.has('colorWhitespace') ? this._colorWhitespace : true;
829
+ const isInline = this._set.has('inline') ? this._inline : false;
830
+ const mw = this._set.has('maxWidth') ? this._maxWidth : 0;
831
+ const mh = this._set.has('maxHeight') ? this._maxHeight : 0;
832
+ const ulSpaces = (this._set.has('underlineSpaces') && this._underlineSpaces) || (hasUnderline && (!this._set.has('underlineSpaces') || this._underlineSpaces));
833
+ const stSpaces = (this._set.has('strikethroughSpaces') && this._strikethroughSpaces) || (isStrikethrough && (!this._set.has('strikethroughSpaces') || this._strikethroughSpaces));
834
+ const styleWhitespace = isReverse;
835
+ const useSpaceStyler = (hasUnderline && !ulSpaces) || (isStrikethrough && !stSpaces) || ulSpaces || stSpaces;
836
+ const transform = this._set.has('transform') ? this._transform : null;
837
+ const link = this._set.has('link') ? this._link : '';
838
+ const linkParams = this._set.has('linkParams') ? this._linkParams : '';
839
+
840
+ if (transform) str = transform(str);
841
+
842
+ // If no props set at all, just do tab conversion
843
+ if (this._set.size === 0) return this._maybeConvertTabs(str);
844
+
845
+ // Build main text style options
846
+ const teOpts: AnsiStyleOptions = {};
847
+ const teSpaceOpts: AnsiStyleOptions = {};
848
+ const teWSOpts: AnsiStyleOptions = {};
849
+
850
+ if (isBold) teOpts.bold = true;
851
+ if (isItalic) teOpts.italic = true;
852
+ if (hasUnderline) { teOpts.underline = true; teOpts.underlineStyle = this._underlineStyle; }
853
+ if (isReverse) { teOpts.reverse = true; teWSOpts.reverse = true; }
854
+ if (isBlink) teOpts.blink = true;
855
+ if (isFaint) teOpts.faint = true;
856
+ if (isStrikethrough) teOpts.strikethrough = true;
857
+
858
+ if (fg) {
859
+ teOpts.fg = fg;
860
+ if (styleWhitespace) teWSOpts.fg = fg;
861
+ if (useSpaceStyler) teSpaceOpts.fg = fg;
862
+ }
863
+ if (bg) {
864
+ teOpts.bg = bg;
865
+ if (colorWS) teWSOpts.bg = bg;
866
+ if (useSpaceStyler) teSpaceOpts.bg = bg;
867
+ }
868
+ if (ul) {
869
+ teOpts.ul = ul;
870
+ if (colorWS) teWSOpts.ul = ul;
871
+ if (useSpaceStyler) teSpaceOpts.ul = ul;
872
+ }
873
+ if (ulSpaces) teSpaceOpts.underline = true;
874
+ if (stSpaces) teSpaceOpts.strikethrough = true;
875
+
876
+ // Tab conversion
877
+ str = this._maybeConvertTabs(str);
878
+ str = str.replace(/\r\n/g, '\n');
879
+
880
+ // Strip newlines in inline mode
881
+ if (isInline) str = str.replace(/\n/g, '');
882
+
883
+ // Include borders in block size
884
+ let width = w - hBorderSize;
885
+ let height = h - vBorderSize;
886
+
887
+ // Word wrap
888
+ if (!isInline && width > 0) {
889
+ const wrapAt = width - leftPad - rightPad;
890
+ str = wordWrap(str, wrapAt);
891
+ }
892
+
893
+ // Render core text — apply ANSI styles line by line
894
+ {
895
+ const lines = str.split('\n');
896
+ const rendered: string[] = [];
897
+ for (const line of lines) {
898
+ if (useSpaceStyler) {
899
+ let buf = '';
900
+ for (const ch of line) {
901
+ if (ch === ' ' || ch === '\t') {
902
+ buf += styled(ch, teSpaceOpts);
903
+ } else {
904
+ buf += styled(ch, teOpts);
905
+ }
906
+ }
907
+ rendered.push(buf);
908
+ } else {
909
+ rendered.push(line ? styled(line, teOpts) : line);
910
+ }
911
+ }
912
+ str = rendered.join('\n');
913
+
914
+ if (link) {
915
+ str = setHyperlink(link, linkParams || undefined) + str + resetHyperlink();
916
+ }
917
+ }
918
+
919
+ // Padding
920
+ if (!isInline) {
921
+ const wsStyle = (colorWS || styleWhitespace) ? teWSOpts : null;
922
+ const padCh = this._set.has('paddingChar') ? this._paddingChar : ' ';
923
+ if (leftPad > 0) str = padLeft(str, leftPad, wsStyle, padCh);
924
+ if (rightPad > 0) str = padRight(str, rightPad, wsStyle, padCh);
925
+ if (topPad > 0) str = '\n'.repeat(topPad) + str;
926
+ if (bottomPad > 0) str = str + '\n'.repeat(bottomPad);
927
+ }
928
+
929
+ // Height
930
+ if (height > 0) str = alignTextVertical(str, vAlign, height);
931
+
932
+ // Horizontal alignment (also equalizes line widths)
933
+ {
934
+ const numLines = str.split('\n').length - 1;
935
+ if (numLines > 0 || width > 0) {
936
+ const wsStyle = (colorWS || styleWhitespace) ? teWSOpts : null;
937
+ str = alignTextHorizontal(str, hAlign, width, wsStyle);
938
+ }
939
+ }
940
+
941
+ // Borders and margins
942
+ if (!isInline) {
943
+ str = this._applyBorder(str);
944
+ str = this._applyMargins(str, isInline);
945
+ }
946
+
947
+ // MaxWidth truncation
948
+ if (mw > 0) {
949
+ str = str.split('\n').map(line => truncateStr(line, mw)).join('\n');
950
+ }
951
+
952
+ // MaxHeight truncation
953
+ if (mh > 0) {
954
+ const lines = str.split('\n');
955
+ if (lines.length > mh) str = lines.slice(0, mh).join('\n');
956
+ }
957
+
958
+ return str;
959
+ }
960
+
961
+ // -----------------------------------------------------------------------
962
+ // Internal: tab conversion
963
+ // -----------------------------------------------------------------------
964
+
965
+ private _maybeConvertTabs(str: string): string {
966
+ const tw = this._set.has('tabWidth') ? this._tabWidth : TAB_WIDTH_DEFAULT;
967
+ if (tw === -1) return str;
968
+ if (tw === 0) return str.replace(/\t/g, '');
969
+ return str.replace(/\t/g, ' '.repeat(tw));
970
+ }
971
+
972
+ // -----------------------------------------------------------------------
973
+ // Internal: border rendering
974
+ // -----------------------------------------------------------------------
975
+
976
+ private _applyBorder(str: string): string {
977
+ let borderDef = this.getBorderStyle();
978
+ let hasT = this.getBorderTop();
979
+ let hasR = this.getBorderRight();
980
+ let hasB = this.getBorderBottom();
981
+ let hasL = this.getBorderLeft();
982
+
983
+ // Default: if border style set but no sides specified, show all sides
984
+ if (this._isBorderStyleSetWithoutSides()) {
985
+ hasT = true; hasR = true; hasB = true; hasL = true;
986
+ }
987
+
988
+ if (isNoBorder(borderDef) || (!hasT && !hasR && !hasB && !hasL)) return str;
989
+
990
+ // Clone border to mutate corners
991
+ borderDef = { ...borderDef };
992
+
993
+ const { lines, widest } = getLines(str);
994
+ let width = widest;
995
+
996
+ if (hasL) {
997
+ if (!borderDef.left) borderDef.left = ' ';
998
+ width += maxRuneWidth(borderDef.left);
999
+ }
1000
+ if (hasR) {
1001
+ if (!borderDef.right) borderDef.right = ' ';
1002
+ width += maxRuneWidth(borderDef.right);
1003
+ }
1004
+
1005
+ // Fill empty corners with space if both adjacent sides exist
1006
+ if (hasT && hasL && !borderDef.topLeft) borderDef.topLeft = ' ';
1007
+ if (hasT && hasR && !borderDef.topRight) borderDef.topRight = ' ';
1008
+ if (hasB && hasL && !borderDef.bottomLeft) borderDef.bottomLeft = ' ';
1009
+ if (hasB && hasR && !borderDef.bottomRight) borderDef.bottomRight = ' ';
1010
+
1011
+ // Clear corners when adjacent side is off
1012
+ if (hasT) {
1013
+ if (!hasL && !hasR) { borderDef.topLeft = ''; borderDef.topRight = ''; }
1014
+ else if (!hasL) { borderDef.topLeft = ''; }
1015
+ else if (!hasR) { borderDef.topRight = ''; }
1016
+ }
1017
+ if (hasB) {
1018
+ if (!hasL && !hasR) { borderDef.bottomLeft = ''; borderDef.bottomRight = ''; }
1019
+ else if (!hasL) { borderDef.bottomLeft = ''; }
1020
+ else if (!hasR) { borderDef.bottomRight = ''; }
1021
+ }
1022
+
1023
+ // Limit corners to one rune
1024
+ borderDef.topLeft = getFirstRune(borderDef.topLeft);
1025
+ borderDef.topRight = getFirstRune(borderDef.topRight);
1026
+ borderDef.bottomLeft = getFirstRune(borderDef.bottomLeft);
1027
+ borderDef.bottomRight = getFirstRune(borderDef.bottomRight);
1028
+
1029
+ const topFg = this._set.has('borderTopFg') ? parseColor(this._borderTopFg) : null;
1030
+ const rightFg = this._set.has('borderRightFg') ? parseColor(this._borderRightFg) : null;
1031
+ const bottomFg = this._set.has('borderBottomFg') ? parseColor(this._borderBottomFg) : null;
1032
+ const leftFg = this._set.has('borderLeftFg') ? parseColor(this._borderLeftFg) : null;
1033
+ const topBg = this._set.has('borderTopBg') ? parseColor(this._borderTopBg) : null;
1034
+ const rightBg = this._set.has('borderRightBg') ? parseColor(this._borderRightBg) : null;
1035
+ const bottomBg = this._set.has('borderBottomBg') ? parseColor(this._borderBottomBg) : null;
1036
+ const leftBg = this._set.has('borderLeftBg') ? parseColor(this._borderLeftBg) : null;
1037
+
1038
+ let out = '';
1039
+
1040
+ // Top edge
1041
+ if (hasT) {
1042
+ const top = renderHorizontalEdge(borderDef.topLeft, borderDef.top, borderDef.topRight, width);
1043
+ out += styleBorderStr(top, topFg, topBg) + '\n';
1044
+ }
1045
+
1046
+ // Side edges
1047
+ const leftRunes = [...borderDef.left];
1048
+ const rightRunes = [...borderDef.right];
1049
+ let li = 0, ri = 0;
1050
+
1051
+ for (let i = 0; i < lines.length; i++) {
1052
+ if (hasL) {
1053
+ const r = leftRunes[li % leftRunes.length];
1054
+ li++;
1055
+ out += styleBorderStr(r, leftFg, leftBg);
1056
+ }
1057
+ out += lines[i];
1058
+ if (hasR) {
1059
+ const r = rightRunes[ri % rightRunes.length];
1060
+ ri++;
1061
+ out += styleBorderStr(r, rightFg, rightBg);
1062
+ }
1063
+ if (i < lines.length - 1) out += '\n';
1064
+ }
1065
+
1066
+ // Bottom edge
1067
+ if (hasB) {
1068
+ const bottom = renderHorizontalEdge(borderDef.bottomLeft, borderDef.bottom, borderDef.bottomRight, width);
1069
+ out += '\n' + styleBorderStr(bottom, bottomFg, bottomBg);
1070
+ }
1071
+
1072
+ return out;
1073
+ }
1074
+
1075
+ // -----------------------------------------------------------------------
1076
+ // Internal: margin rendering
1077
+ // -----------------------------------------------------------------------
1078
+
1079
+ private _applyMargins(str: string, isInline: boolean): string {
1080
+ const topM = this._set.has('marginTop') ? this._marginTop : 0;
1081
+ const rightM = this._set.has('marginRight') ? this._marginRight : 0;
1082
+ const bottomM = this._set.has('marginBottom') ? this._marginBottom : 0;
1083
+ const leftM = this._set.has('marginLeft') ? this._marginLeft : 0;
1084
+
1085
+ const marginBg = this._set.has('marginBg') ? parseColor(this._marginBg) : null;
1086
+ const marginStyle: AnsiStyleOptions | null = marginBg ? { bg: marginBg } : null;
1087
+
1088
+ if (leftM > 0) str = padLeft(str, leftM, marginStyle);
1089
+ if (rightM > 0) str = padRight(str, rightM, marginStyle);
1090
+
1091
+ if (!isInline) {
1092
+ const { widest: totalWidth } = getLines(str);
1093
+ const blankLine = marginStyle ? styled(' '.repeat(totalWidth), marginStyle) : ' '.repeat(totalWidth);
1094
+
1095
+ if (topM > 0) {
1096
+ str = (blankLine + '\n').repeat(topM) + str;
1097
+ }
1098
+ if (bottomM > 0) {
1099
+ str = str + ('\n' + blankLine).repeat(bottomM);
1100
+ }
1101
+ }
1102
+
1103
+ return str;
1104
+ }
1105
+
1106
+ // -----------------------------------------------------------------------
1107
+ // toString
1108
+ // -----------------------------------------------------------------------
1109
+
1110
+ toString(): string {
1111
+ return this.render();
1112
+ }
1113
+ }
1114
+
1115
+ // ---------------------------------------------------------------------------
1116
+ // Module-level helper: style a border string with fg/bg colors
1117
+ // ---------------------------------------------------------------------------
1118
+
1119
+ function styleBorderStr(border: string, fg: ColorValue | null, bg: ColorValue | null): string {
1120
+ if (!fg && !bg) return border;
1121
+ return styled(border, { fg, bg });
1122
+ }
1123
+
1124
+ // ---------------------------------------------------------------------------
1125
+ // Factory
1126
+ // ---------------------------------------------------------------------------
1127
+
1128
+ /** Create a new empty Style. */
1129
+ export function newStyle(): Style {
1130
+ return new Style();
1131
+ }