File Coverage

blib/lib/Pcore/Util/Text.pm
Criterion Covered Total %
statement 36 375 9.6
branch 8 144 5.5
condition 4 17 23.5
subroutine 5 34 14.7
pod 0 30 0.0
total 53 600 8.8


line stmt bran cond sub pod time code
1             package Pcore::Util::Text;
2              
3 5         55 use Pcore -ansi, -export, [
4             qw[
5             cut
6             cut_all
7             decode_eol
8             decode_html_entities
9             decode_utf8
10             encode_hex
11             encode_html
12             encode_html_attr
13             encode_js_string
14             encode_utf8
15             escape_scalar
16             expand_num
17             add_num_sep
18             fullchomp
19             lcut
20             lcut_all
21             ltrim
22             ltrim_multi
23             mark_raw
24             rcut
25             rcut_all
26             remove_ansi
27             remove_bom
28             rtrim
29             rtrim_multi
30             table
31             to_camel_case
32             to_snake_case
33             trim
34             trim_multi
35             unmark_raw
36             wrap
37             ]
38 5     5   38 ];
  5         11  
39 5     5   2163 use Encode qw[]; ## no critic qw[Modules::ProhibitEvilModules]
  5         50780  
  5         173  
40 5     5   1881 use Text::Xslate qw[mark_raw unmark_raw];
  5         44066  
  5         22779  
41              
42             our $ENC_CACHE = {};
43              
44             our %ESC_ANSI_CTRL = (
45             qq[\a] => q[\a],
46             qq[\b] => q[\b],
47             qq[\t] => q[\t],
48             qq[\n] => q[\n],
49             qq[\f] => q[\f],
50             qq[\r] => q[\r],
51             qq[\e] => q[\e],
52             );
53              
54             # TODO
55             # - crunch - ?;
56             # - P->text - clear trim functions names, eg, P->text->rcut_all -> P->text->trim_trailing_hs
57             # - autogenerated functions should always return ScalarRef if wantarray;
58              
59             our $CODE = {
60             decode_eol => <<'PERL', # convert EOL to internal \n representation
61             s/\x0D?\x0A/\n/smg;
62             PERL
63             remove_bom => <<'PERL', # remove BOM
64             s/\A(?:\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\xFE\xFF|\xFF\xFE|\xEF\xBB\xBF)//sm;
65             PERL
66             fullchomp => <<'PERL',
67             s/(?:\x0D|\x0A)+\z//sm;
68             PERL
69              
70             # "trim" functions removes spaces and tabs
71             trim => <<'PERL',
72             s/\A\h+//sm; # ltrim
73             s/\h+\z//sm; # rtrim
74             PERL
75             ltrim => <<'PERL', # treats string as single-line, remove all \h (space, tab) before first \n, non-space or non-tab character)
76             s/\A\h+//sm;
77             PERL
78             rtrim => <<'PERL', # treats string as single-line, remove all \h (space, tab) after last \n, non-space or non-tab character
79             s/\h+\z//sm;
80             PERL
81              
82             trim_multi => <<'PERL',
83             s/^\h+//smg; # ltrim_multi
84             s/\h+$//smg; # rtrim_multi
85             PERL
86             ltrim_multi => <<'PERL', # treats string as multi-line, remove \h just after each \n or string begin
87             s/^\h+//smg;
88             PERL
89             rtrim_multi => <<'PERL', # treats string as multi-line, remove \h before each \n
90             s/\h+$//smg;
91             PERL
92              
93             # "cut" functions compress several \n to one \n
94             cut => <<'PERL', # replace all \n series with single \n
95             s/\A\v+//sm; # lcut
96             s/\v+\z//sm; # rcut
97             s/\v+/\n/smg;
98             PERL
99             lcut => <<'PERL', # treats string as single-line, cut all \n before first character
100             s/\A\v+//sm;
101             PERL
102             rcut => <<'PERL', # treats string as single-line, remove all \n after last character, including last \n
103             s/\v+\z//sm;
104             PERL
105              
106             # "cut_all" functions combines trim and cut functionality together
107             cut_all => <<'PERL', # trim_multi + cut
108              
109             # trim_multi
110             s/^\h+//smg; # ltrim_multi
111             s/\h+$//smg; # rtrim_multi
112              
113             # cut
114             s/\A\v+//sm; # lcut
115             s/\v+\z//sm; # rcut
116             s/\v+/\n/smg;
117             PERL
118             lcut_all => <<'PERL', # remove empty lines and lines, consisting only of spaces and tabs, from string start
119             s/\A\s+//sm;
120             PERL
121             rcut_all => <<'PERL', # remove empty lines and lines, consisting only of spaces and tabs, from string end, including last \n
122             s/\s+\z//sm;
123             PERL
124              
125             # encode
126             # Used to convert HTML tags to plain text:
127             # <textarea>[% data | html %]</textarea>, <p>[% data | html %]</p>
128             encode_html => <<'PERL',
129             s/([&<>"'])/q[&#] . ord $1/smge;
130             PERL
131              
132             # Used to quote HTML tag attribute, example:
133             # <input type="text" value="[% data | html_attr %]">
134             encode_html_attr => <<'PERL',
135             s/(\W)/q[&#] . ord $1/smge;
136             PERL
137              
138             # Used to encode javascript string, such as:
139             # var a = "[% data | js_string %]";
140             # onclick="alert('[% data | js_string %]')"
141             # onclick="alert(&#34;[% data | js_string %]&#34;)" - hint: &#34; = "
142             encode_js_string => <<'PERL',
143             s/(\W)/sprintf q[\x%02lx], ord $1/smge;
144             PERL
145              
146             encode_hex => <<'PERL',
147             $_ = unpack 'H*', $_;
148             PERL
149              
150             # DECODE, ENCODE
151             decode_utf8 => <<'PERL',
152             my %args = (
153             encoding => 'UTF-8',
154             decode_eol => 1,
155             splice @_, 1,
156             );
157              
158             if ( defined && !utf8::is_utf8 $_ ) {
159             my $enc = $ENC_CACHE->{ $args{encoding} } // do {
160             $ENC_CACHE->{ $args{encoding} } = Encode::find_encoding( $args{encoding} );
161             };
162              
163             $_ = $enc->decode( $_, Encode::FB_CROAK | Encode::LEAVE_SRC );
164              
165             s/\x0D?\x0A/\n/smg if $args{decode_eol};
166             }
167             PERL
168              
169             encode_utf8 => <<'PERL',
170             # Encode::_utf8_off $_ if utf8::is_utf8 $_; ## no critic qw[Subroutines::ProtectPrivateSubs]
171              
172             utf8::encode $_ if utf8::is_utf8 $_;
173             PERL
174             };
175              
176             # create accessors
177             for my $name ( keys $CODE->%* ) {
178             my $sub = <<'PERL';
179             sub <: $name :> {
180             local $_;
181              
182             if ( defined wantarray ) {
183             $_ = $_[0];
184              
185             <: $code :>
186              
187             return $_;
188             }
189             else {
190             \$_ = \$_[0];
191              
192             <: $code :>
193              
194             return;
195             }
196             }
197             PERL
198              
199             $sub =~ s/<: \$name :>/$name/smg;
200              
201             $sub =~ s/<: \$code :>/$CODE->{$name}/smg;
202              
203 0 0 33 0 0 0 eval $sub; ## no critic qw[BuiltinFunctions::ProhibitStringyEval]
  0 0 33 0 0 0  
  0 0 33 0 0 0  
  0 50 33 8 0 0  
  0 50   0 0 0  
  0 50   0 0 0  
  0 50   0 0 0  
  0 100   0 0 0  
  0 0   0 0 0  
  0 0   0 0 0  
  0 0   0 0 0  
  0 0   0 0 0  
  0 0   0 0 0  
  0 0   0 0 0  
  0 0   0 0 0  
  0 0   0 0 0  
  0 0   0 0 0  
  0 0   0 0 0  
  0 0   0 0 0  
  0 0   642 0 0  
  0 0   0 0 0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 100       0  
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  8         29  
  8         31  
  3         8  
  3         22  
  3         27  
  3         52  
  0         0  
  3         35  
  3         456  
  3         139  
  5         19  
  5         33  
  5         62  
  5         31  
  5         34  
  5         1114  
  5         315  
  5         25  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  642         1240  
  642         1118  
  495         823  
  495         946  
  495         1252  
  495         1510  
  147         280  
  147         522  
  147         374  
  147         288  
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
204             }
205              
206             # UTIL
207             sub table {
208 0     0 0   state $init = !!require Pcore::Util::Text::Table;
209              
210 0           return Pcore::Util::Text::Table->new( {@_} );
211             }
212              
213             sub remove_ansi {
214 0 0   0 0   if ( defined wantarray ) {
215 0           return join q[], map {s/\e.+?m//smgr} @_;
  0            
216             }
217             else {
218 0           for (@_) { # convert in-place
219 0           s/\e.+?m//smg;
220             }
221              
222 0           return;
223             }
224             }
225              
226             sub escape_scalar {
227 0     0 0   local $_;
228              
229 0 0         if ( defined wantarray ) {
230 0           $_ = $_[0];
231             }
232             else {
233 0           \$_ = \$_[0];
234             }
235              
236 0           my %args = (
237             bin => undef, # if TRUE - always treats scalar as binary data
238             utf8_encode => 1, # if FALSE - in bin mode escape utf8 multi-byte chars as \x{...}
239             esc_color => undef,
240             reset_color => $RESET,
241             splice @_, 1,
242             );
243              
244             # automatically detect scalar type
245 0 0         if ( !defined $args{bin} ) {
246 0 0         if ( utf8::is_utf8 $_ ) { # UTF-8 scalar
    0          
247 0           $args{bin} = 0;
248             }
249             elsif (/[[:^ascii:]]/sm) { # latin1 octets
250 0           $args{bin} = 1;
251             }
252             else { # ASCII bytes
253 0           $args{bin} = 0;
254             }
255             }
256              
257             # escape scalar
258 0 0         if ( $args{bin} ) {
259 0 0         if ( utf8::is_utf8 $_ ) {
260 0 0         if ( $args{utf8_encode} ) {
261 0           encode_utf8 $_;
262              
263 0           s/(.)/sprintf '\x%02X', ord $1/smge;
  0            
264             }
265             else {
266 0           s/([[:ascii:]])/sprintf '\x%02X', ord $1/smge;
  0            
267              
268 0           s/([[:^ascii:]])/sprintf '\x{%X}', ord $1/smge;
  0            
269             }
270             }
271             else {
272 0           s/(.)/sprintf '\x%02X', ord $1/smge;
  0            
273             }
274             }
275             else {
276 0   0       my $esc_color = $args{esc_color} || q[];
277              
278 0 0         my $reset_color = $args{esc_color} ? $args{reset_color} : q[];
279              
280 0           s/([\a\b\t\n\f\r\e])/${esc_color}$ESC_ANSI_CTRL{$1}${reset_color}/smg; # escape ANSI
281              
282 0           s/([\x00-\x1A\x1C-\x1F\x7F])/$esc_color . sprintf( '\x%02X', ord $1 ) . $reset_color/smge; # hex ANSI non-printable chars
  0            
283             }
284              
285 0 0         if ( defined wantarray ) {
286 0           return $_;
287             }
288             else {
289 0           return;
290             }
291             }
292              
293 0     0 0   sub wrap ( $text, $width, % ) {
  0            
  0            
  0            
294 0           my %args = (
295             ansi => 1,
296             align => undef,
297             splice @_, 2,
298             );
299              
300             # remove ANSI
301 0 0         $text =~ s/\e.+?m//smg if !$args{ansi};
302              
303             # expand tabs
304 0           $text =~ s/\t/ /smg;
305              
306 0     0     state $wrap = sub ( $width, $ansi ) {
  0            
  0            
  0            
307 0           my @lines;
308              
309 0           my $wrap_re = do {
310 0 0         if ($ansi) {qr/(\e.+?m|\s)/sm}
  0            
311 0           else {qr/(\s)/sm}
312             };
313              
314 0           my $buf = q[];
315              
316 0           my $buf_len = 0;
317              
318 0           for my $word ( grep { $_ ne q[] } split $wrap_re ) {
  0            
319 0 0 0       if ( $ansi && $word =~ /\e.+?m/sm ) {
    0          
320 0           $buf .= $word;
321             }
322             elsif ( $buf_len + length $word > $width ) {
323              
324             # wrap by any character
325             # $buf .= substr $word, 0, $width - $buf_len, q[];
326              
327             # drop current buf to @lines
328 0 0         push @lines, $buf if $buf ne q[];
329              
330 0           while ( length $word > $width ) {
331 0           push @lines, substr $word, 0, $width, q[];
332             }
333              
334             # init next buf
335 0           $buf = $word;
336 0           $buf_len = length $word;
337             }
338             else {
339 0           $buf .= $word;
340 0           $buf_len += length $word;
341             }
342             }
343              
344 0 0         push @lines, $buf if $buf ne q[];
345              
346 0           return @lines;
347 0           };
348              
349 0           my @lines;
350              
351             # wrap lines
352 0           for ( split /\n/sm, $text ) {
353 0           push @lines, $wrap->( $width, $args{ansi} );
354             }
355              
356             # process ansi seq.
357 0 0         if ( $args{ansi} ) {
358 0           my $ansi_prefix = q[];
359              
360 0           for my $line (@lines) {
361 0           my $cur_ansi_prefix = $ansi_prefix;
362              
363 0 0         if ( my @ansi = $line =~ /(\e.+?m)/smg ) {
    0          
364 0 0         if ( $ansi[-1] ne "\e[0m" ) {
365 0           $line .= "\e[0m";
366              
367 0           $ansi_prefix .= join q[], @ansi;
368             }
369             else {
370 0           $ansi_prefix = q[];
371             }
372             }
373 0           elsif ($cur_ansi_prefix) { $line .= "\e[0m" }
374              
375 0 0         $line = $cur_ansi_prefix . $line if $cur_ansi_prefix;
376             }
377             }
378              
379             # align
380 0 0         if ( defined $args{align} != -1 ) {
381 0           for my $line (@lines) {
382 0 0         my $len = length( $args{ansi} ? $line =~ s/\e.+?m//smgr : $line );
383              
384 0 0         next if $len == $width;
385              
386 0 0         if ( $args{align} == -1 ) {
    0          
    0          
387              
388             # right
389 0           $line .= ( q[ ] x ( $width - $len ) );
390             }
391             elsif ( $args{align} == 1 ) {
392              
393             # left
394 0           $line = q[ ] x ( $width - $len ) . $line;
395             }
396             elsif ( $args{align} == 0 ) {
397              
398             # center
399 0           my $left = int( ( $width - $len ) / 2 );
400 0           my $right = $width - $len - $left;
401              
402 0           $line = ( q[ ] x $left ) . $line . ( q[ ] x $right );
403             }
404             else {
405 0           die q[Invalid align value];
406             }
407             }
408             }
409              
410 0           return \@lines;
411             }
412              
413             # HTML ENTITIES
414             sub decode_html_entities {
415 0     0 0   local $_;
416              
417 0 0         if ( defined wantarray ) {
418 0           $_ = $_[0];
419             }
420             else {
421 0           \$_ = \$_[0];
422             }
423              
424 0           my %args = (
425             trim => undef,
426             splice @_, 1,
427             );
428              
429 0           state $init = !!require HTML::Entities;
430              
431 0           Pcore::Util::Text::decode_utf8 $_;
432              
433 0           HTML::Entities::decode_entities $_;
434              
435 0 0         trim $_ if $args{trim};
436              
437 0 0         if ( defined wantarray ) {
438 0           return $_;
439             }
440             else {
441 0           return;
442             }
443             }
444              
445             # expand number from scientific format to ordinary
446 0     0 0   sub expand_num ($num) {
  0            
  0            
447 0 0         return $num unless $num =~ /\A(.*)e([-+]?)(.*)\z/sm;
448              
449 0           my ( $abs, $sign, $exp ) = ( $1, $2, $3 );
450              
451 0 0         my $sig = $sign eq q[-] ? q[.] . ( $exp - 1 + length $abs ) : q[];
452              
453 0           return sprintf "%${sig}f", $num;
454             }
455              
456             # pretty print number 1234567 -> 1_234_567
457 0     0 0   sub add_num_sep ( $num, $sep = q[_] ) {
  0            
  0            
  0            
458 0 0         my $sign = $num =~ s/\A([^\d])//sm ? $1 : q[];
459              
460 0 0         my $fraction = $num =~ s/[.](\d+)\z//sm ? $1 : undef;
461              
462 0           $num = scalar reverse join $sep, ( reverse $num ) =~ /(.{1,3})/smg;
463              
464 0 0         $num .= q[.] . scalar reverse join $sep, ( reverse $fraction ) =~ /(.{1,3})/smg if $fraction;
465              
466 0           return $sign . $num;
467             }
468              
469             sub to_snake_case {
470 0     0 0   my $str;
471              
472 0 0         if ( defined wantarray ) {
473 0           $str = $_[0];
474             }
475             else {
476 0           \$str = \$_[0];
477             }
478              
479 0           my %args = (
480             delim => q[_],
481             split => undef,
482             join => undef,
483             splice @_, 1,
484             );
485              
486 0 0         if ( $args{split} ) {
487 0           my @parts = split /\Q$args{split}\E/sm, $str;
488              
489 0           for (@parts) {
490 0           $_ = lcfirst;
491              
492 0           s/([[:upper:]])/$args{delim} . lc $1/smge;
  0            
493             }
494              
495 0 0         if ( $args{join} ) {
496 0           $str = join $args{join}, @parts;
497             }
498             else {
499 0           $str = join $args{split}, @parts;
500             }
501             }
502             else {
503              
504             # convert camelCase to snake_case notation
505 0           $str = lcfirst $str;
506              
507 0           $str =~ s/([[:upper:]])/$args{delim} . lc $1/smge;
  0            
508             }
509              
510 0 0         if ( defined wantarray ) {
511 0           return $str;
512             }
513             else {
514 0           return;
515             }
516             }
517              
518             sub to_camel_case {
519 0     0 0   my $str;
520              
521 0 0         if ( defined wantarray ) {
522 0           $str = $_[0];
523             }
524             else {
525 0           \$str = \$_[0];
526             }
527              
528 0           my %args = (
529             delim => q[_],
530             ucfirst => undef,
531             split => undef,
532             join => undef,
533             splice @_, 1,
534             );
535              
536 0 0         if ( $args{split} ) {
537 0           my @parts = split /\Q$args{split}\E/sm, $str;
538              
539 0           for (@parts) {
540 0           $_ = lc;
541              
542 0           s/$args{delim}(.)/uc $1/smge; # convert snake_case to camelCase notation
  0            
543              
544 0 0         $_ = ucfirst if $args{ucfirst};
545             }
546              
547 0 0         if ( $args{join} ) {
548 0           $str = join $args{join}, @parts;
549             }
550             else {
551 0           $str = join $args{split}, @parts;
552             }
553             }
554             else {
555 0           $str = lc $str;
556              
557 0           $str =~ s/$args{delim}(.)/uc $1/smge; # convert snake_case to camelCase notation
  0            
558              
559 0 0         $str = ucfirst $str if $args{ucfirst};
560             }
561              
562 0 0         if ( defined wantarray ) {
563 0           return $str;
564             }
565             else {
566 0           return;
567             }
568             }
569              
570             1;
571             ## -----SOURCE FILTER LOG BEGIN-----
572             ##
573             ## PerlCritic profile "pcore-script" policy violations:
574             ## +------+----------------------+----------------------------------------------------------------------------------------------------------------+
575             ## | Sev. | Lines | Policy |
576             ## |======+======================+================================================================================================================|
577             ## | 3 | 203 | ErrorHandling::RequireCheckingReturnValueOfEval - Return value of eval not tested |
578             ## |------+----------------------+----------------------------------------------------------------------------------------------------------------|
579             ## | 3 | 227, 415 | Variables::RequireInitializationForLocalVars - "local" variable not initialized |
580             ## |------+----------------------+----------------------------------------------------------------------------------------------------------------|
581             ## | 3 | 293 | Subroutines::ProhibitExcessComplexity - Subroutine "wrap" with high complexity score (28) |
582             ## |------+----------------------+----------------------------------------------------------------------------------------------------------------|
583             ## | 3 | | NamingConventions::ProhibitAmbiguousNames |
584             ## | | 399, 400 | * Ambiguously named variable "left" |
585             ## | | 400 | * Ambiguously named variable "right" |
586             ## |------+----------------------+----------------------------------------------------------------------------------------------------------------|
587             ## | 1 | 45, 46, 47, 48, 49, | ValuesAndExpressions::RequireInterpolationOfMetachars - String *may* require interpolation |
588             ## | | 50, 51 | |
589             ## +------+----------------------+----------------------------------------------------------------------------------------------------------------+
590             ##
591             ## -----SOURCE FILTER LOG END-----
592             __END__
593             =pod
594              
595             =encoding utf8
596              
597             =head1 NAME
598              
599             Pcore::Util::Text
600              
601             =head1 SYNOPSIS
602              
603             =head1 DESCRIPTION
604              
605             =cut