@@ -4,6 +4,7 @@ import type { Readable, Writable } from 'node:stream';
4
4
import { WriteStream } from 'node:tty' ;
5
5
import { cursor , erase } from 'sisteransi' ;
6
6
import wrap from 'wrap-ansi' ;
7
+ import { strLength } from '../utils' ;
7
8
8
9
import { CANCEL_SYMBOL , diffLines , isActionKey , setRawMode , settings } from '../utils' ;
9
10
@@ -21,6 +22,88 @@ export interface PromptOptions<Self extends Prompt> {
21
22
signal ?: AbortSignal ;
22
23
}
23
24
25
+ export type LineOption = 'firstLine' | 'newLine' | 'lastLine' ;
26
+
27
+ export interface FormatLineOptions {
28
+ /**
29
+ * Define the start of line
30
+ * @example
31
+ * format('foo', {
32
+ * line: {
33
+ * start: '-'
34
+ * }
35
+ * })
36
+ * //=> '- foo'
37
+ */
38
+ start : string ;
39
+ /**
40
+ * Define the end of line
41
+ * @example
42
+ * format('foo', {
43
+ * line: {
44
+ * end: '-'
45
+ * }
46
+ * })
47
+ * //=> 'foo -'
48
+ */
49
+ end : string ;
50
+ /**
51
+ * Define the sides of line
52
+ * @example
53
+ * format('foo', {
54
+ * line: {
55
+ * sides: '-'
56
+ * }
57
+ * })
58
+ * //=> '- foo -'
59
+ */
60
+ sides : string ;
61
+ /**
62
+ * Define the style of line
63
+ * @example
64
+ * format('foo', {
65
+ * line: {
66
+ * style: (line) => `(${line})`
67
+ * }
68
+ * })
69
+ * //=> '(foo)'
70
+ */
71
+ style : ( line : string ) => string ;
72
+ }
73
+
74
+ export interface FormatOptions extends Record < LineOption , Partial < FormatLineOptions > > {
75
+ /**
76
+ * Shorthand to define values for each line
77
+ * @example
78
+ * format('foo', {
79
+ * default: {
80
+ * start: '-'
81
+ * }
82
+ * // equals
83
+ * firstLine{
84
+ * start: '-'
85
+ * },
86
+ * newLine{
87
+ * start: '-'
88
+ * },
89
+ * lastLine{
90
+ * start: '-'
91
+ * },
92
+ * })
93
+ */
94
+ default : Partial < FormatLineOptions > ;
95
+ /**
96
+ * Define the max width of each line
97
+ * @example
98
+ * format('foo bar baz', {
99
+ * maxWidth: 7
100
+ * })
101
+ * //=> 'foo bar\nbaz'
102
+ */
103
+ maxWidth : number ;
104
+ minWidth : number ;
105
+ }
106
+
24
107
export default class Prompt {
25
108
protected input : Readable ;
26
109
protected output : Writable ;
@@ -246,8 +329,105 @@ export default class Prompt {
246
329
this . output . write ( cursor . move ( - 999 , lines * - 1 ) ) ;
247
330
}
248
331
332
+ public format ( text : string , options ?: Partial < FormatOptions > ) : string {
333
+ const getLineOption = < TLine extends LineOption , TKey extends keyof FormatLineOptions > (
334
+ line : TLine ,
335
+ key : TKey
336
+ ) : NonNullable < FormatOptions [ TLine ] [ TKey ] > => {
337
+ return (
338
+ key === 'style'
339
+ ? ( options ?. [ line ] ?. [ key ] ?? options ?. default ?. [ key ] ?? ( ( line ) => line ) )
340
+ : ( options ?. [ line ] ?. [ key ] ?? options ?. [ line ] ?. sides ?? options ?. default ?. [ key ] ?? '' )
341
+ ) as NonNullable < FormatOptions [ TLine ] [ TKey ] > ;
342
+ } ;
343
+ const getLineOptions = ( line : LineOption ) : Omit < FormatLineOptions , 'sides' > => {
344
+ return {
345
+ start : getLineOption ( line , 'start' ) ,
346
+ end : getLineOption ( line , 'end' ) ,
347
+ style : getLineOption ( line , 'style' ) ,
348
+ } ;
349
+ } ;
350
+
351
+ const firstLine = getLineOptions ( 'firstLine' ) ;
352
+ const newLine = getLineOptions ( 'newLine' ) ;
353
+ const lastLine = getLineOptions ( 'lastLine' ) ;
354
+
355
+ const emptySlots =
356
+ Math . max (
357
+ strLength ( firstLine . start + firstLine . end ) ,
358
+ strLength ( newLine . start + newLine . end ) ,
359
+ strLength ( lastLine . start + lastLine . end )
360
+ ) + 2 ;
361
+ const terminalWidth = process . stdout . columns || 80 ;
362
+ const maxWidth = options ?. maxWidth ?? terminalWidth ;
363
+ const minWidth = options ?. minWidth ?? 1 ;
364
+
365
+ const formattedLines : string [ ] = [ ] ;
366
+ const paragraphs = text . split ( / \n / g) ;
367
+
368
+ for ( const paragraph of paragraphs ) {
369
+ const words = paragraph . split ( / \s / g) ;
370
+ let currentLine = '' ;
371
+
372
+ for ( const word of words ) {
373
+ if ( strLength ( currentLine + word ) + emptySlots + 1 <= maxWidth ) {
374
+ currentLine += ` ${ word } ` ;
375
+ } else if ( strLength ( word ) + emptySlots >= maxWidth ) {
376
+ const splitIndex = maxWidth - strLength ( currentLine ) - emptySlots - 1 ;
377
+ formattedLines . push ( `${ currentLine } ${ word . slice ( 0 , splitIndex ) } ` ) ;
378
+
379
+ const chunkLength = maxWidth - emptySlots ;
380
+ let chunk = word . slice ( splitIndex ) ;
381
+ while ( strLength ( chunk ) > chunkLength ) {
382
+ formattedLines . push ( chunk . slice ( 0 , chunkLength ) ) ;
383
+ chunk = chunk . slice ( chunkLength ) ;
384
+ }
385
+ currentLine = chunk ;
386
+ } else {
387
+ formattedLines . push ( currentLine ) ;
388
+ currentLine = word ;
389
+ }
390
+ }
391
+
392
+ formattedLines . push ( currentLine ) ;
393
+ }
394
+
395
+ return formattedLines
396
+ . map ( ( line , i , ar ) => {
397
+ const opt = < TPosition extends Exclude < keyof FormatLineOptions , 'sides' > > (
398
+ position : TPosition
399
+ ) : FormatLineOptions [ TPosition ] => {
400
+ return (
401
+ i === 0 && ar . length === 1
402
+ ? ( options ?. firstLine ?. [ position ] ??
403
+ options ?. lastLine ?. [ position ] ??
404
+ firstLine [ position ] )
405
+ : i === 0
406
+ ? firstLine [ position ]
407
+ : i + 1 === ar . length
408
+ ? lastLine [ position ]
409
+ : newLine [ position ]
410
+ ) as FormatLineOptions [ TPosition ] ;
411
+ } ;
412
+ const startLine = opt ( 'start' ) ;
413
+ const endLine = opt ( 'end' ) ;
414
+ const styleLine = opt ( 'style' ) ;
415
+ // only format the line without the leading space.
416
+ const leadingSpaceRegex = / ^ \s / ;
417
+ const styledLine = leadingSpaceRegex . test ( line )
418
+ ? ` ${ styleLine ( line . slice ( 1 ) ) } `
419
+ : styleLine ( line ) ;
420
+ const fullLine =
421
+ styledLine + ' ' . repeat ( Math . max ( minWidth - strLength ( styledLine ) - emptySlots , 0 ) ) ;
422
+ return [ startLine , fullLine , endLine ] . join ( ' ' ) ;
423
+ } )
424
+ . join ( '\n' ) ;
425
+ }
426
+
249
427
private render ( ) {
250
- const frame = wrap ( this . _render ( this ) ?? '' , process . stdout . columns , { hard : true } ) ;
428
+ const frame = wrap ( this . _render ( this ) ?? '' , process . stdout . columns , {
429
+ hard : true ,
430
+ } ) ;
251
431
if ( frame === this . _prevFrame ) return ;
252
432
253
433
if ( this . state === 'initial' ) {
0 commit comments