File Coverage

blib/lib/Text/ANSI/Fold.pm
Criterion Covered Total %
statement 271 290 93.4
branch 137 162 84.5
condition 54 75 72.0
subroutine 39 40 97.5
pod 7 19 36.8
total 508 586 86.6


line stmt bran cond sub pod time code
1             package Text::ANSI::Fold;
2              
3 14     14   2297542 use v5.14;
  14         62  
4 14     14   86 use warnings;
  14         88  
  14         903  
5 14     14   2167 use utf8;
  14         1080  
  14         86  
6              
7             our $VERSION = "2.3305";
8              
9 14     14   7789 use Data::Dumper;
  14         127249  
  14         1605  
10             {
11 14     14   132 no warnings 'redefine';
  14         29  
  14         2107  
12 0     0   0 *Data::Dumper::qquote = sub { qq["${\(shift)}"] };
  0         0  
13             $Data::Dumper::Useperl = 1;
14             $Data::Dumper::Sortkeys = 1;
15             }
16 14     14   117 use Carp;
  14         27  
  14         1253  
17 14     14   93 use List::Util qw(pairmap pairgrep);
  14         26  
  14         1131  
18 14     14   140 use Scalar::Util qw(looks_like_number);
  14         49  
  14         907  
19 14     14   7997 use Text::VisualWidth::PP 'vwidth';
  14         81513  
  14         1450  
20              
21             ######################################################################
22 14     14   147 use Exporter 'import';
  14         30  
  14         10443  
23             our %EXPORT_TAGS = (
24             constants => [ qw(
25             &LINEBREAK_NONE
26             &LINEBREAK_ALL
27             &LINEBREAK_RUNIN
28             &LINEBREAK_RUNOUT
29             ) ],
30             regex => [ qw(
31             $reset_re
32             $color_re
33             $erase_re
34             $csi_re
35             $osc_re
36             ) ],
37             );
38              
39             our @EXPORT_OK = ( qw(&ansi_fold),
40             @{$EXPORT_TAGS{constants}},
41             @{$EXPORT_TAGS{regex}},
42             );
43              
44             sub ansi_fold {
45 437     437 1 1058133 my($text, $width, @option) = @_;
46 437         1838 __PACKAGE__->fold($text, width => $width, @option);
47             }
48             ######################################################################
49              
50             our $alphanum_re = qr{ [_\d\p{Latin}\p{Greek}\p{Cyrillic}\p{Hangul}] }x;
51             our $nonspace_re = qr{ \p{IsPrintableLatin} }x;
52             our $reset_re = qr{ \e \[ [0;]* m }x;
53             our $color_re = qr{ \e \[ [\d;]* m }x;
54             our $erase_re = qr{ \e \[ [\d;]* K }x;
55              
56             # see ECMA-48 5.4 Control sequences
57             my $csi_start = qr{ (?: \e\[ | \x9b ) }x;
58             my $csi_parameter = qr/[\x30-\x3f]*+/;
59             my $csi_itermidiate = qr/[\x20-\x2f]*+/;
60             my $csi_final = qr/[\x40-\x7e]/;
61             our $csi_body_re = qr/${csi_start}${csi_parameter}${csi_itermidiate}/;
62             our $csi_re = qr/${csi_start}${csi_parameter}${csi_itermidiate}${csi_final}/;
63              
64             our $osc_re = qr{
65             # see ECMA-48 8.3.89 OSC - OPERATING SYSTEM COMMAND
66             # Extended to accept non-ASCII (undefined but tolerated)
67             (?: \e\] | \x9d ) # osc
68             [^\x00-\x07\x0e-\x1f\x7f-\x9f]*+ # command (excludes C1 controls)
69             (?: \e\\ | \x9c | \a ) # st: string terminator
70             }x;
71              
72 14     14   161 use constant SGR_RESET => "\e[m";
  14         32  
  14         1443  
73 14     14   102 use constant OSC8_RESET => "\e]8;;\e\\";
  14         30  
  14         3749  
74              
75 161     161 0 6278 sub pwidth { vwidth $_[0] =~ s/\X\cH{1,2}//gr =~ s/${osc_re}//gr }
76              
77             sub IsPrintableLatin {
78 1     1 0 208 return <<"END";
79             +utf8::ASCII
80             +utf8::Latin
81             -utf8::White_Space
82             END
83             }
84              
85             sub IsWideSpacing {
86 14     14 0 1598 return <<"END";
87             +utf8::East_Asian_Width=Wide
88             +utf8::East_Asian_Width=FullWidth
89             -utf8::Nonspacing_Mark
90             -utf8::Default_Ignorable_Code_Point
91             END
92             }
93              
94             sub IsWideAmbiguousSpacing {
95 14     14 0 1985 return <<"END";
96             +utf8::East_Asian_Width=Wide
97             +utf8::East_Asian_Width=FullWidth
98             +utf8::East_Asian_Width=Ambiguous
99             -utf8::Nonspacing_Mark
100             -utf8::Default_Ignorable_Code_Point
101             END
102             }
103              
104             sub _startWideSpacing {
105             # look at $_
106 62 100   62   163 if ($Text::VisualWidth::PP::EastAsian) {
107 2         9 /^\p{IsWideAmbiguousSpacing}/;
108             } else {
109 60         335 /^\p{IsWideSpacing}/;
110             }
111             }
112              
113             use constant {
114 14         2945 LINEBREAK_NONE => 0,
115             LINEBREAK_RUNIN => 1,
116             LINEBREAK_RUNOUT => 2,
117             LINEBREAK_ALL => 3,
118 14     14   178 };
  14         33  
119              
120             our $DEFAULT_LINEBREAK = LINEBREAK_NONE;
121             our $DEFAULT_RUNIN_WIDTH = 2;
122             our $DEFAULT_RUNOUT_WIDTH = 2;
123              
124             BEGIN {
125 14 50   14   1790 if ($] < 5.016) {
126 0         0 require charnames;
127 0         0 charnames->import(':full');
128             }
129             }
130              
131             our %TABSTYLE = (
132             pairmap {
133             ( $a =~ s/_/-/gr => ref $b ? $b : [ $b, $b ] );
134             }
135 14     14   10528 symbol => [ "\N{SYMBOL FOR HORIZONTAL TABULATION}", # ␉
  14         145786  
  14         108  
136             "\N{SYMBOL FOR SPACE}" ], # ␠
137             shade => [ "\N{MEDIUM SHADE}", # ▒
138             "\N{LIGHT SHADE}" ], # ░
139             block => [ "\N{LOWER ONE QUARTER BLOCK}", # ▂
140             "\N{LOWER ONE EIGHTH BLOCK}" ], # ▁
141             needle => [ "\N{BOX DRAWINGS HEAVY RIGHT}", # ╺
142             "\N{BOX DRAWINGS LIGHT HORIZONTAL}" ], # ─
143             dash => [ "\N{BOX DRAWINGS HEAVY RIGHT}", # ╺
144             "\N{BOX DRAWINGS LIGHT DOUBLE DASH HORIZONTAL}" ], # ╌
145             triangle => [ "\N{BLACK RIGHT-POINTING SMALL TRIANGLE}", # ▸
146             "\N{WHITE RIGHT-POINTING SMALL TRIANGLE}" ], # ▹
147              
148             dot => '.',
149             space => ' ',
150             emspace => "\N{EM SPACE}", #  
151             blank => "\N{OPEN BOX}", # ␣
152             middle_dot => "\N{MIDDLE DOT}", # ·
153             arrow => "\N{RIGHTWARDS ARROW}", # →
154             double_arrow => "\N{RIGHTWARDS DOUBLE ARROW}", # ⇒
155             triple_arrow => "\N{RIGHTWARDS TRIPLE ARROW}", # ⇛
156             white_arrow => "\N{RIGHTWARDS WHITE ARROW}", # ⇨
157             wave_arrow => "\N{RIGHTWARDS WAVE ARROW}", # ↝
158             circle_arrow => "\N{CIRCLED HEAVY WHITE RIGHTWARDS ARROW}", # ➲
159             curved_arrow => "\N{HEAVY BLACK CURVED DOWNWARDS AND RIGHTWARDS ARROW}",# ➥
160             shadow_arrow => "\N{HEAVY UPPER RIGHT-SHADOWED WHITE RIGHTWARDS ARROW}",# ➮
161             squat_arrow => "\N{SQUAT BLACK RIGHTWARDS ARROW}", # ➧
162             squiggle => "\N{RIGHTWARDS SQUIGGLE ARROW}", # ⇝
163             harpoon => "\N{RIGHTWARDS HARPOON WITH BARB UPWARDS}", # ⇀
164             cuneiform => "\N{CUNEIFORM SIGN TAB}", # 𒋰
165              
166             );
167              
168             for my $alias (
169             [ bar => 'needle'],
170             [ pin => 'needle']
171             ) {
172             $TABSTYLE{$alias->[0]} = $TABSTYLE{$alias->[1]};
173             }
174              
175             my @default = (
176             text => '',
177             width => undef,
178             padding => 0,
179             boundary => '',
180             padchar => ' ',
181             prefix => '',
182             ambiguous => 'narrow',
183             margin => 0,
184             linebreak => $DEFAULT_LINEBREAK,
185             runin => $DEFAULT_RUNIN_WIDTH,
186             runout => $DEFAULT_RUNOUT_WIDTH,
187             expand => 0,
188             tabstop => 8,
189             tabhead => ' ',
190             tabspace => ' ',
191             discard => {},
192             splitwide => 0,
193             lefthalf => "\N{NO-BREAK SPACE}",
194             righthalf => "\N{NO-BREAK SPACE}",
195             );
196              
197             sub new {
198 16     16 1 2218528 my $class = shift;
199 16         422 my $obj = bless { @default }, $class;
200 16 100       253 $obj->configure(@_) if @_;
201 16         85 $obj;
202             }
203              
204             INTERNAL_METHODS: {
205             sub spawn {
206 715     715 0 1157 my $obj = shift;
207 715         1370 my $class = ref $obj;
208 715     762   8569 my %new = ( %$obj, pairgrep { defined $b } @_ );
  762         7733  
209 715         4604 bless \%new, $class;
210             }
211 715 100   715 0 2516 sub do_runin { $_[0]->{linebreak} & LINEBREAK_RUNIN && $_[0]->{runin} > 0 }
212 596 100   596 0 4010 sub do_runout { $_[0]->{linebreak} & LINEBREAK_RUNOUT && $_[0]->{runout} > 0 }
213             }
214              
215 14     14   541668 use Text::ANSI::Fold::Japanese::W3C qw(%prohibition);
  14         78  
  14         18585  
216              
217             sub chars_to_regex {
218 28     28 0 175 my $chars = join '', @_;
219 28         59 my($c, @s);
220 28         1556 for ($chars =~ /\X/g) {
221 2170 100       4085 if (length == 1) {
222 2156         3150 $c .= $_;
223             } else {
224 14         58 push @s, $_;
225             }
226             }
227 28 100       419 if (@s) {
228 14         41 local $" = '|';
229 14         1472 qr/(?:[\Q$c\E]|@s)/;
230             } else {
231 14         675 qr/[\Q$c\E]/;
232             }
233             }
234              
235             my %prohibition_re = do {
236             head => do {
237             my $re = chars_to_regex @prohibition{qw(head postfix)};
238             qr/(?: $re | \p{Space_Separator} )/x;
239             },
240             end => chars_to_regex @prohibition{qw(end prefix)},
241             };
242              
243             sub configure {
244 63     63 1 279852 my $obj = shift;
245 63 100       253 if (not ref $obj) {
246 12         42 $obj = state $private = __PACKAGE__->new;
247             }
248 63 50       265 croak "invalid parameter" if @_ % 2;
249 63         223 while (@_ >= 2) {
250 103         324 my($a, $b) = splice @_, 0, 2;
251              
252 103 100       260 if ($a eq 'tabstyle') {
253 1   50     25 $b // next;
254 1 50       10 my($h, $s) = $b =~ /([-\w]+)/g or croak "$b: invalid tabstyle";
255 1   33     11 $s ||= $h;
256             my %style = (
257             h => ($TABSTYLE{$h} or croak "$h: invalid tabstyle"),
258 1   33     12 s => ($TABSTYLE{$s} or croak "$s: invalid tabstyle"),
      33        
259             );
260             unshift @_,
261             tabhead => $style{h}->[0],
262 1         8 tabspace => $style{s}->[1];
263 1         5 next;
264             }
265              
266 102 50       286 croak "$a: invalid parameter" if not exists $obj->{$a};
267 102         335 $obj->{$a} = $b;
268             }
269 63 50       266 if (ref $obj->{discard} eq 'ARRAY') {
270 0         0 $obj->{discard} = { map { uc $_ => 1 } @{$obj->{discard}} };
  0         0  
  0         0  
271             }
272 63         201 $obj;
273             }
274              
275             my @color_stack;
276             my @bg_stack;
277             my @reset;
278             my $osc8_link;
279 159     159 0 655 sub put_reset { @reset = shift };
280             sub pop_reset {
281 193 100   193 0 515 @reset ? do { @color_stack = (); pop @reset } : '';
  159         400  
  159         410  
282             }
283              
284 14     14   126 use constant MAX_INT => ~0 >> 1;
  14         26  
  14         6584  
285              
286             sub IsEOL {
287 14     14 0 48557 <<"END";
288             0000\t0000
289             000A\t000D
290             2028\t2029
291             END
292             }
293              
294             sub fold {
295 715     715 1 134626 my $obj = shift;
296 715   50     2379 local $_ = shift // '';
297              
298 715 100       1891 if (not ref $obj) {
299 437         869 $obj = state $private = configure();
300             }
301 715         2449 my $opt = $obj->spawn(splice @_);
302              
303 715         1611 my $width = $opt->{width};
304 715 50 33     4183 looks_like_number $width and $width == int($width)
305             or croak "$width: invalid width";
306 715 100       1902 $opt->{padding} = 0 if $width <= 0;
307 715 100       1518 $width = MAX_INT if $width < 0;
308 715 50       2103 ($width -= $opt->{margin}) > 0 or croak "margin too big";
309              
310             my $word_char_re =
311             { word => $alphanum_re, space => $nonspace_re }
312 715   50     3345 ->{$opt->{boundary} // ''};
313              
314 715         2537 local $Text::VisualWidth::PP::EastAsian = $opt->{ambiguous} eq 'wide';
315              
316 715         1148 my $folded = '';
317 715         1095 my $eol = '';
318 715         1117 my $room = $width;
319 715         1511 @bg_stack = @color_stack = @reset = ();
320 715         1247 $osc8_link = undef;
321             my $unremarkable_re =
322 715 100       4473 $opt->{expand} ? qr/[^\p{IsEOL}\e\t]/
323             : qr/[^\p{IsEOL}\e]/;
324              
325             FOLD:
326 715         1883 while (length) {
327              
328             # newline, null, vt
329             # U+2028: Line Separator
330             # U+2029: Paragraph Separator
331 2067 100       10626 if (s/\A(\r*\n|[\0\x0b\N{U+2028}\N{U+2029}])//) {
332 20         29 $eol = $1;
333 20         22 last;
334             }
335             # form feed
336 2047 100       5303 if (m/\A(\f+)/p) {
337 3 100       10 last if length $folded;
338 2         11 ($folded, $_) = ($1, ${^POSTMATCH});
339 2         5 next;
340             }
341             # carriage return
342 2044 100       4428 if (s/\A(\r+)//) {
343 2 50       6 if (length == 0) {
344             # this must be the part of CRNL
345 0         0 $eol = $1;
346 0         0 last;
347             } else {
348 2         4 $folded .= $1;
349 2         3 $room = $width;
350 2         4 next;
351             }
352             }
353             # ECMA-48 OPERATING SYSTEM COMMAND
354 2042 100       12773 if (s/\A($osc_re)//) {
355 50         93 my $osc = $1;
356 50 100       96 unless ($obj->{discard}->{OSC}) {
357 46         74 $folded .= $osc;
358 46 50       194 if ($osc =~ /^(?:\e\]|\x9d)8;[^;]*;(.*?)(?:\e\\|\x9c|\a)$/) {
359 46 100       106 $osc8_link = $1 ne '' ? $osc : undef;
360             }
361             }
362 50         97 next;
363             }
364             # erase line (assume 0)
365 1992 100       8878 if (s/\A($erase_re)//) {
366 1 50       8 $folded .= $1 unless $obj->{discard}->{EL};
367 1         4 @bg_stack = grep { !/$erase_re/ } @color_stack;
  1         9  
368 1         4 push @color_stack, $1;
369 1         2 next;
370             }
371             # reset
372 1991 100       9619 if (s/\A($reset_re+($erase_re*))//) {
373 159         649 put_reset($1);
374 159 50       499 @bg_stack = () if $2;
375 159         431 next;
376             }
377              
378 1832 100       4129 last if $room < 1;
379 1278 100 66     3490 if ($room < 2 and !$opt->{splitwide}) {
380 124 100 100     393 last if $room != $width and &_startWideSpacing;
381             }
382              
383 1262 100       2843 if (@reset) {
384 111         327 $folded .= pop_reset();
385             }
386              
387             # ANSI color sequence
388 1262 100       5810 if (s/\A($color_re)//) {
389 215         719 $folded .= $1;
390 215         573 push @color_stack, $1;
391 215         665 next;
392             }
393              
394             # imcomplete CSI
395 1047 100       6449 if (s/\A(\e+|$csi_body_re)\z//) {
396 3         11 $folded .= $1;
397 3         7 last;
398             }
399              
400             # tab
401 1044 100 100     3977 if ($opt->{expand} and s/\A\t//) {
402 112         329 my $space = $opt->{tabstop} - ($width - $room) % $opt->{tabstop};
403 112         533 $_ = $opt->{tabhead} . $opt->{tabspace} x ($space - 1) . $_;
404 112         314 next;
405             }
406              
407             # backspace
408 932         1510 my $bs = 0;
409 932         2798 while (m/\A(?:\X\cH+)++(?\X|\z)/p) {
410 850         6637 my $w = vwidth($+{c});
411 850 100 100     126066 last FOLD if $w > $room and $room != $width;
412 846         2609 $folded .= ${^MATCH};
413 846         7283 $_ = ${^POSTMATCH};
414 846         1394 $room -= $w;
415 846         1352 $bs++;
416 846 100       1738 last FOLD if $room < 0;
417 840 100       6276 last if $room < 1;
418             }
419 922 100       2061 next if $bs;
420              
421             # consume unremarkable characters
422 860 50       7734 if (s/\A(\e+|(?:${unremarkable_re}(?!\cH))+)//) {
423 860         2518 my $s = $1;
424 860 100       2962 if ((my $w = vwidth($s)) <= $room) {
425 387         66496 $folded .= $s;
426 387         694 $room -= $w;
427 387         1416 next;
428             }
429 473         176061 my($a, $b, $w) = simple_fold($s, $room);
430 473 50       1418 if ($opt->{splitwide}) {
431 0 0 0     0 if ($w == $room - 1 && $b =~ /\A\p{IsWideSpacing}/p) {
    0          
432 0         0 $a .= $opt->{lefthalf};
433 0         0 $b = $opt->{righthalf} . ${^POSTMATCH};
434 0         0 $w++;
435             }
436             elsif ($w > $room) {
437 0         0 $a = $opt->{lefthalf};
438 0         0 $b = $opt->{righthalf} . $b;
439 0         0 $w--;
440             }
441             }
442 473 50 66     1570 if ($w > $room && $room != $width) {
443 0         0 $_ = $s . $_;
444 0         0 last;
445             }
446 473         1383 ($folded, $_) = ($folded . $a, $b . $_);
447 473         1546 $room -= $w;
448             } else {
449 0         0 die "panic ($_)";
450             }
451             }
452              
453             ##
454             ## --boundary=word
455             ##
456 715 100 100     4187 if ($word_char_re
      100        
457             and my($w2) = /\A( (?: ${word_char_re} \cH ? ) + )/x
458             and my($lead, $w1) = $folded =~ m{
459             \A ## avoid CSI/OSC final char making a word
460             ( (?: [^\e\x9b\x9d]* (?:${csi_re}|${osc_re})++ ) *+ .*? )
461             ( (?: ${word_char_re} \cH ? ) + )
462             \z }x
463             ) {
464             ## Break line before word only when enough space will be
465             ## provided for the word in the next turn.
466 39         797 my $l = pwidth($w1);
467             ## prefix length
468 39 100       2963 my $p = $opt->{prefix} eq '' ? 0 : vwidth($opt->{prefix});
469 39 100 66     873 if ($room + $l < $width - $p and $l + pwidth($w2) <= $width - $p
      66        
470             and pwidth($lead) > 0) {
471 34         2289 $folded = $lead;
472 34         90 $_ = $w1 . pop_reset() . $_;
473 34         70 $room += $l;
474             }
475             }
476              
477             ##
478             ## RUN-OUT
479             ##
480 715 100 100     3436 if ($_ ne '' and $opt->do_runout) {
481 96 100 66     3135 if ($folded =~ m{ (? (?! ${reset_re}) ${color_re}*+ )
      100        
      100        
482             (?
483             (?: ($prohibition_re{end}) (?: \cH{1,2} \g{-1})* )+
484             ) \z
485             }xp
486             and ${^PREMATCH} ne '' and pwidth(${^PREMATCH}) > 0
487             and (my $w = pwidth $+{runout}) <= $opt->{runout}) {
488              
489 19         1479 $folded = ${^PREMATCH};
490 19         95 $_ = join '', ${^MATCH}, @reset, $_;
491 19 100       523 pop_reset() if $+{color};
492 19         493 $room += $w;
493             }
494             }
495              
496 715 100       2091 $folded .= pop_reset() if @reset;
497              
498 715         1439 $room += $opt->{margin};
499              
500             ##
501             ## RUN-IN
502             ##
503 715 100       1707 if ($opt->do_runin) {
504 101         263 my @runin;
505 101         184 my $m = $opt->{runin};
506 101   100     2318 while ($m > 0 and
507             m{\A (? ${color_re}*+)
508             (? $prohibition_re{head} )
509             ( \cH{1,2} \g{runin} )* # multiple strike
510             (? (?: $erase_re* $reset_re+ $erase_re* )? )
511             }xp) {
512 30         214 my $w = vwidth $+{runin};
513 30 50       2790 last if ($m -= $w) < 0;
514 30 100       179 $+{color} and do { push @color_stack, $+{color} };
  2         13  
515 30 100       132 $+{reset} and do { @color_stack = () };
  2         7  
516 30         81 $room -= $w;
517 30         87 push @runin, ${^MATCH};
518 30         198 $_ = ${^POSTMATCH};
519             }
520 101 100       332 $folded .= join '', @runin if @runin;
521             }
522              
523 715 100       1470 if (@color_stack) {
524 56         135 $folded .= SGR_RESET;
525 56 50       324 $_ = join '', @color_stack, $_ if $_ ne '';
526             }
527 715 100       1505 if (defined $osc8_link) {
528 16         21 $folded .= OSC8_RESET;
529 16 50       64 $_ = $osc8_link . $_ if $_ ne '';
530             }
531              
532 715 100 100     1911 if ($opt->{padding} and $room > 0) {
533 13         41 my $padding = $opt->{padchar} x $room;
534 13 100       28 if (@bg_stack) {
535 1         6 $padding = join '', @bg_stack, $padding, SGR_RESET;
536             }
537 13         27 $folded .= $padding;
538             }
539              
540 715 100 100     2711 if (length and my $p = $opt->{prefix}) {
541 19 100       90 my $s = ref $p eq 'CODE' ? &$p : $p;
542 19         84 $_ = $s . $_;
543             }
544              
545 715         10015 ($folded . $eol, $_, $width - $room);
546             }
547              
548             ##
549             ## Trim off one or more *logical* characters from the beginning of a line
550             ##
551             sub simple_fold {
552 473     473 0 935 my $orig = shift;
553 473         675 my $width = shift;
554 473 50       1138 $width <= 0 and croak "parameter error";
555              
556 473 50       10163 my($left, $right) = $orig =~ m/^(\X{0,$width}+)(.*)/
557             or croak "$width: unexpected width";
558              
559 473         1601 my $w = vwidth $left;
560 473         42650 while ($w > $width) {
561 14     14   198 use integer;
  14         59  
  14         130  
562 87         424 my $trim = ($w - $width + 1) / 2;
563 87 100       2876 $left =~ s/\X \K ( \X{$trim} ) \z//x or last;
564 83         348 $right = $1 . $right;
565 83         211 $w -= vwidth $1;
566             }
567              
568 473         8860 ($left, $right, $w);
569             }
570              
571             ######################################################################
572             # EXTERNAL METHODS
573              
574             sub text :lvalue {
575 9     9 1 1545 my $obj = shift;
576 9 100       26 if (@_ == 0) {
577 6         29 $obj->{text};
578             } else {
579 3 50       11 croak "Invalid argument" if @_ > 1;
580 3         9 $obj->{text} = shift;
581 3         13 $obj;
582             }
583             }
584              
585             sub retrieve {
586 127     127 1 163 my $obj = shift;
587 127         291 local *_ = \$obj->{text};
588 127 100       294 return '' if not defined $_;
589 106         224 (my $folded, my $rest) = $obj->fold($_, @_);
590 106 50 66     368 die "panic: retrieve: no progress in fold"
591             if length $rest and $rest eq $_;
592 106 100       202 $_ = length $rest ? $rest : undef;
593 106         314 $folded;
594             }
595              
596             sub chops {
597 28     28 1 56 my $obj = shift;
598 28         55 my %opt = @_;
599 28   66     123 my $width = $opt{width} // $obj->{width};
600              
601 28         40 my @chops;
602              
603 28 100       66 if (ref $width eq 'ARRAY') {
604 10         12 for my $w (@{$width}) {
  10         23  
605 49 100       149 if ($w == 0) {
    100          
606 2         3 push @chops, '';
607             }
608             elsif ((my $chop = $obj->retrieve(width => $w)) ne '') {
609 43         128 push @chops, $chop;
610             }
611             else {
612 4         8 last;
613             }
614             }
615             }
616             else {
617 18         50 while ((my $chop = $obj->retrieve(width => $width)) ne '') {
618 61         160 push @chops, $chop;
619             }
620             }
621              
622 28         278 @chops;
623             }
624              
625             1;
626              
627             __END__