File Coverage

blib/lib/MooseX/App/Utils.pm
Criterion Covered Total %
statement 147 147 100.0
branch 36 44 81.8
condition 8 14 57.1
subroutine 16 16 100.0
pod 6 6 100.0
total 213 227 93.8


line stmt bran cond sub pod time code
1             package MooseX::App::Utils;
2              
3 15     15   125970 use 5.010;
  15         38  
4 15     15   531 use utf8;
  15         23  
  15         64  
5 15     15   255 use strict;
  15         15  
  15         259  
6 15     15   53 use warnings;
  15         20  
  15         388  
7              
8 15     15   51 use List::Util qw(max);
  15         16  
  15         1261  
9              
10             our $SCREEN_WIDTH = 78;
11             our $INDENT = 4;
12              
13 15     15   3138 use Moose::Util::TypeConstraints;
  15         676385  
  15         136  
14              
15             subtype 'MooseX::App::Types::List'
16             => as 'ArrayRef';
17              
18             coerce 'MooseX::App::Types::List'
19             => from 'Str'
20             => via { [$_] };
21              
22             subtype 'MooseX::App::Types::CmdTypes'
23             => as enum([qw(proto option parameter)]);
24              
25             subtype 'MooseX::App::Types::MessageString'
26             => as 'Str';
27              
28             coerce 'MooseX::App::Types::MessageString'
29             => from 'ArrayRef'
30             => via { sprintf(@{$_}) };
31              
32             subtype 'MooseX::App::Types::Env'
33             => as 'Str'
34             => where { m/^[A-Z0-9_]+$/ };
35              
36             subtype 'MooseX::App::Types::Identifier'
37             => as 'Str'
38             => where {
39             $_ eq '?'
40             || (m/^[A-Za-z0-9][A-Za-z0-9_-]*$/ && m/[^-_]$/) };
41              
42             subtype 'MooseX::App::Types::IdentifierList'
43             => as 'ArrayRef[MooseX::App::Types::Identifier]';
44              
45             coerce 'MooseX::App::Types::IdentifierList'
46             => from 'MooseX::App::Types::Identifier'
47             => via { [$_] };
48              
49 15     15   23561 no Moose::Util::TypeConstraints;
  15         20  
  15         66  
50              
51 15     15   6098 no if $] >= 5.018000, warnings => qw/ experimental::smartmatch /;
  15         54  
  15         92  
52              
53             # Default package name to command name translation function
54             sub class_to_command {
55 26     26 1 1524 my ($class) = @_;
56              
57             return
58 26 50       53 unless defined $class;
59              
60 26         30 my @commands;
61 26         80 foreach my $part (split /\s+/,$class) {
62 28         168 my @parts = split( /_+|\b|(?<![A-Z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/, $part );
63 28         78 push (@commands,join('_',@parts));
64             }
65 26         88 return lc(join(" ",@commands));
66             }
67              
68             # Format output text for fixed screen width
69             sub format_text {
70 54     54 1 3404 my ($text) = @_;
71              
72 54         87 my @lines;
73 54         264 foreach my $line (split(/\n/,$text)) {
74 162         353 push(@lines,split_string($SCREEN_WIDTH-$INDENT,$line));
75             }
76              
77             return join(
78             "\n",
79 54         132 map { (' ' x $INDENT).$_ }
  175         657  
80             @lines
81             );
82             }
83              
84             # Format bullet list for fixed screen width
85             sub format_list {
86 79     79 1 3541 my (@list) = @_;
87              
88 79         144 my $max_length = max(map { length($_->[0]) } @list);
  258         515  
89 79         156 my $description_length = $SCREEN_WIDTH - $max_length - 7;
90 79         136 my $prefix_length = $max_length + $INDENT + 2;
91 79         94 my @return;
92              
93             # Loop all items
94 79         154 foreach my $command (@list) {
95 258   100     457 my $description = $command->[1] // '';
96 258         364 my @lines = split_string($description_length,$description);
97 258         983 push (@return,(' 'x$INDENT).sprintf('%-*s %s',$max_length,$command->[0],shift(@lines)));
98 258         613 while (my $line = shift (@lines)) {
99 16         74 push(@return,' 'x $prefix_length.$line);
100             }
101             }
102 79         611 return join("\n",@return);
103             }
104              
105             # Simple splitting of long sentences on whitespaces or punctuation
106             sub split_string {
107 420     420 1 451 my ($maxlength, $string) = @_;
108              
109             return
110 420 50       687 unless defined $string;
111              
112 420 100       1038 return $string
113             if length $string <= $maxlength;
114              
115 28         41 my (@lines,$line);
116 28         54 $line = '';
117 15     15   7922 foreach my $word (split(m/(\p{IsPunct}|\p{IsSpace})/,$string)) {
  15         171  
  15         204  
  28         671  
118 864 100       1011 if (length($line.$word) <= $maxlength) {
119 835         710 $line .= $word;
120             } else {
121 29 100       102 push(@lines,$line)
122             if ($line ne '');
123 29         45 $line = '';
124              
125 29 100       69 if (length($word) > $maxlength) {
126 7         116 my (@parts) = grep { $_ ne '' } split(/(.{$maxlength})/,$word);
  21         48  
127 7         17 my $lastline = pop(@parts);
128 7         66 push(@lines,@parts);
129 7 50 33     57 if (defined $lastline && $lastline ne '') {
130 7         19 $line = $lastline;
131             }
132             } else {
133 22         42 $line = $word;
134             }
135             }
136             }
137 28 50       140 push(@lines,$line)
138             if ($line ne '');
139              
140 28 50       55 @lines = map { m/^\s*(.+?)\s*$/ ? $1 : $_ } @lines;
  57         550  
141              
142 28         89 return @lines;
143             }
144              
145             # Try to get filename for a given package name
146             sub package_to_filename {
147 24     24 1 42 my ($package) = @_;
148              
149             # Package to filename
150 24         43 my $package_filename = $package;
151 24         107 $package_filename =~ s/::/\//g;
152 24         50 $package_filename .= '.pm';
153              
154              
155 24         37 my $package_filepath;
156 24 50       99 if (defined $INC{$package_filename}) {
157 24         53 $package_filepath = $INC{$package_filename};
158 24         62 $package_filepath =~ s/\/{2,}/\//g;
159             }
160              
161             # No filename available
162             return
163 24 100 66     840 unless defined $package_filepath
164             && -e $package_filepath;
165              
166 20         60 return $package_filepath;
167             }
168              
169             # Parse pod
170             sub parse_pod {
171 24     24 1 57 my ($package) = @_;
172              
173 24         99 my $package_filepath = package_to_filename($package);
174             return
175 24 100       94 unless $package_filepath;
176              
177             # Parse pod
178 20         202 my $document = Pod::Elemental->read_file($package_filepath);
179              
180 20         72131 Pod::Elemental::Transformer::Pod5->new->transform_node($document);
181              
182 20         51867 my $nester_head = Pod::Elemental::Transformer::Nester->new({
183             top_selector => Pod::Elemental::Selectors::s_command('head1'),
184             content_selectors => [
185             Pod::Elemental::Selectors::s_command([ qw(head2 head3 head4 over back item) ]),
186             Pod::Elemental::Selectors::s_flat()
187             ],
188             });
189 20         2127 $document = $nester_head->transform_node($document);
190              
191             # Process pod
192 20         31417 my %pod;
193 20         34 foreach my $element (@{$document->children}) {
  20         496  
194             # Distzilla ABSTRACT tag
195 35 100 66     869 if ($element->isa('Pod::Elemental::Element::Pod5::Nonpod')) {
    100          
196 20 100       546 if ($element->content =~ m/^\s*#+\s*ABSTRACT:\s*(.+)$/m) {
197 1   33     21 $pod{ABSTRACT} ||= $1;
198             }
199             # Pod head1 sections
200             } elsif ($element->isa('Pod::Elemental::Element::Nested')
201             && $element->command eq 'head1') {
202              
203 13 100       577 if ($element->content eq 'NAME') {
204 2         81 my $content = _pod_node_to_text($element->children);
205 2 50       10 next unless defined $content;
206 2         69 $content =~ s/^$package(\s-)?\s//;
207 2         11 chomp($content);
208 2         9 $pod{NAME} = $content;
209             } else {
210 11         352 my $content = _pod_node_to_text($element->children);
211 11 50       36 next unless defined $content;
212 11         24 chomp($content);
213 11         306 $pod{uc($element->content)} = $content;
214             }
215             }
216             }
217              
218 20         763 return %pod;
219             }
220              
221             # Transform POD to simple markup
222             sub _pod_node_to_text {
223 61     61   126 my ($node,$indent) = @_;
224              
225 61 100       103 unless (defined $indent) {
226 13         16 my $indent_init = 0;
227 13         22 $indent = \$indent_init;
228             }
229              
230 61         47 my (@lines);
231 61 100       127 if (ref $node eq 'ARRAY') {
232 13         26 foreach my $element (@$node) {
233 48         107 push (@lines,_pod_node_to_text($element,$indent));
234             }
235              
236             } else {
237 48         82 given (ref($node)) {
238 48         91 when ('Pod::Elemental::Element::Pod5::Ordinary') {
239 23         754 my $content = $node->content;
240             return
241 23 100       161 if $content =~ m/^=cut/;
242 21         48 $content =~ s/\n/ /g;
243 21         147 $content =~ s/\s+/ /g;
244 21         70 push (@lines,$content."\n");
245             }
246 25         23 when ('Pod::Elemental::Element::Pod5::Verbatim') {
247 2         80 push (@lines,$node->content."\n");
248             }
249 23         26 when ('Pod::Elemental::Element::Pod5::Command') {
250 20         686 given ($node->command) {
251 20         105 when ('over') {
252 4         8 ${$indent}++;
  4         9  
253             }
254 16         20 when ('item') {
255 8         272 push (@lines,(' ' x ($$indent-1)) . $node->content);
256             }
257 8         9 when ('back') {
258 4         10 push (@lines,"\n");
259 4         7 ${$indent}--;
  4         10  
260             }
261 4         40 when (qr/head\d/) {
262 4         121 push (@lines,"\n",$node->content,"\n");
263             }
264             }
265             }
266             }
267             }
268              
269             return
270 59 100       203 unless scalar @lines;
271              
272             # Convert text markup
273 52         72 my $return = join ("\n", grep { defined $_ } @lines);
  86         184  
274 52         117 $return =~ s/\n\n\n+/\n\n/g; # Max one empty line
275 52         74 $return =~ s/I<([^>]+)>/_$1_/g;
276 52         94 $return =~ s/B<([^>]+)>/*$1*/g;
277 52         73 $return =~ s/[LCBI]<([^>]+)>/$1/g;
278 52         74 $return =~ s/[LCBI]<([^>]+)>/$1/g;
279 52         135 return $return;
280             }
281              
282              
283             1;
284              
285             =pod
286              
287             =head1 NAME
288              
289             MooseX::App::Utils - Utility functions
290              
291             =head1 DESCRIPTION
292              
293             This package holds various utility functions used by MooseX-App internally.
294             Unless you develop plugins you will not need to interact with this class.
295              
296             =head1 FUNCTIONS
297              
298             =head2 class_to_command
299              
300             =head2 package_to_filename
301              
302             Tries to determine the filename containing the given package name.
303              
304             =head2 format_list
305              
306             =head2 format_text
307              
308             =head2 parse_pod
309              
310             =head2 split_string
311              
312              
313              
314             =head1 GLOBALS
315              
316             =head2 $MooseX::App::Utils::SCREEN_WIDTH
317              
318             Screen width for printing help and error messages
319              
320             =head2 $MooseX::App::Utils::INDENT
321              
322             Indent for printing help and error messages
323              
324             =cut