File Coverage

blib/lib/Data/Show.pm
Criterion Covered Total %
statement 266 315 84.4
branch 74 124 59.6
condition 28 77 36.3
subroutine 53 58 91.3
pod 1 3 33.3
total 422 577 73.1


line stmt bran cond sub pod time code
1             package Data::Show;
2              
3             =encoding utf-8
4             =cut
5              
6 12     12   905814 use 5.010;
  12         48  
7 12     11   161 use strict;
  11         38  
  11         318  
8 11     11   65 use warnings;
  11         22  
  11         849  
9 11     11   6001 use utf8;
  11         3486  
  11         78  
10 11     11   15972 use PPR;
  11         824839  
  11         1486  
11              
12             our $VERSION = '0.004002';
13              
14             # Be a ninja...
15             our @CARP_NOT;
16              
17             # Useful pieces of information...
18 11 100   11   103 my $IS_UTF8_TERM; BEGIN { $IS_UTF8_TERM = grep {$_ && /utf-8/i} @ENV{qw}; }
  33         841  
19 11   50 11   666 my $IS_LIGHT_BG; BEGIN { $IS_LIGHT_BG = ($ENV{COLORFGBG} // q{}) =~ m{\A 0;15 \z}x; }
20 11 50   11   27 my $CAN_ANSICOLOR; BEGIN { $CAN_ANSICOLOR = eval { require Term::ANSIColor; 1 } ? 1 : 0; }
  11         8032  
  11         111169  
21              
22             # Various defaults...
23 11     11   385 my $MAXWIDTH; BEGIN { $MAXWIDTH = 78; }
24 11     11   245 my $INITIAL_DEFAULT_PLUGIN; BEGIN { $INITIAL_DEFAULT_PLUGIN = 'Data::Show::Plugin::Data::Pretty'; }
25 11     11   314 my $FINAL_CANDIDATE_PLUGIN; BEGIN { $FINAL_CANDIDATE_PLUGIN = 'Data::Show::Plugin'; }
26 11     11   225 my $DEFAULT_TARGET; BEGIN { $DEFAULT_TARGET = \*STDERR; }
27 11     11   276 my $RC_FILE_NAME; BEGIN { $RC_FILE_NAME = '.datashow'; }
28 11     11   656 my @PLUGIN_API; BEGIN { @PLUGIN_API = qw< stringify format >; }
29 11     11   920 my @ARGUMENT_DEFAULTS; BEGIN { @ARGUMENT_DEFAULTS = (
30             to => $DEFAULT_TARGET,
31             with => $INITIAL_DEFAULT_PLUGIN,
32             as => 'show',
33             fallback => q{},
34             warnings => 'off',
35             termwidth => $MAXWIDTH,
36              
37             grid => 'off',
38             style => 'auto',
39              
40             # DARK BACKGROUND LIGHT BACKGROUND
41             showstyle => 'bold bright_cyan , bold bright_blue',
42             datastyle => 'bold white , bold black',
43             codestyle => 'cyan , blue',
44             filestyle => 'blue , red',
45             linestyle => 'blue , red',
46             gridstyle => 'blue , red',
47             );
48             }
49             my %GRID; BEGIN {
50 11 100   11   1273 @GRID{split //, q{┏┯┓┗┷┛━┃┠─┬┴┨│} }
51             = split //, ( $IS_UTF8_TERM ? q{┏┯┓┗┷┛━┃┠─┬┴┨│}
52             : q{ _ |_|_||---|:}
53             );
54             }
55              
56              
57             # Useful regexes...
58 11     11   2269 my $OWS; BEGIN { $OWS = qr{ (?: \s++ | \# [^\n]*+ )*+ }x; }
59 11     11   631 my $IDENT; BEGIN { $IDENT = qr{ [^\W\d]\w* (?: :: [^\W\d]\w* )* | [_\W] }x; }
60 11     11   1392 my $COLOUR_CHAR; BEGIN { $COLOUR_CHAR = qr{ (?: \e[^m]*m )* [^\n] (?: \e[^m]*m )* }x; }
61 11     11   1241 my $VALID_ARG; BEGIN { $VALID_ARG = qr{ \A (?: to | with | fallback
62             | base | warnings | as
63             | style | grid | termwidth
64             | datastyle | filestyle | linestyle
65             | codestyle | showstyle | gridstyle
66             ) \z }x; }
67              
68             # Track lexically scoped output targets and styles...
69             my @OUTPUT_FH;
70             my @STYLE;
71              
72             # Export the module's API, or that of a plugin (as requested)...
73             sub import {
74             # Track load context...
75 11     11   152 my ($package, $file, $line) = _get_context();
76              
77             # Remove the module name from the argument list...
78 11         27 shift @_;
79              
80             # Handle the special case of a 'base' argument (by adding it as the caller's base class)...
81 11 50 66     74 if (@_ > 0 && $_[0] eq 'base') {
82 0 0       0 die "If 'base' is specified, it must be the only argument at $file line $line\n" if @_ > 2;
83 11     11   165 no strict 'refs';
  11         21  
  11         5583  
84 0   0     0 push @{caller().'::ISA'}, _load_plugin( $_[1] // 'Data::Show::Plugin', $file, $line, 'warn' );
  0         0  
85 0         0 return;
86             }
87              
88             # Check for missing named args and improve the usual warning for that problem...
89 11 50       51 die "No value specified for named argument '$_[-1]' at $file line $line\n"
90             if @_ % 2 != 0;
91              
92             # Unpack args (including defaults from config file)....
93 11         42 state $defaults_ref = _load_defaults($file, $line);
94 11         28 my %opt = (%{$defaults_ref}, @_);
  11         112  
95              
96             # Punish invalid arguments...
97 11         75 _validate_args(\%opt, "at $file line $line", "named argument");
98              
99             # Any 'to' arg must be a filehandle, filename, or scalar ref (and open it if necessary)...
100 11   33     72 $opt{to} = _open_target( $opt{to} // $DEFAULT_TARGET, $file, $line, $opt{warnings} ne 'off' );
101              
102             # Unpack fallback arguments into an arrayref...
103 11         50 $opt{fallback} = [ split m{ \s*,\s* }x, $opt{fallback} ];
104              
105             # Resolve style options according to terminal background (i.e. dark or light)
106 11         169 for my $option (@opt{ grep /\A.+style\z/, keys %opt}) {
107 66 50       366 $option = [split /\s*,\s*/, $option]->[$IS_LIGHT_BG ? -1 : 0];
108             }
109              
110             # Install Data::Show::Plugin base class as well...
111 11         47 $INC{'Data/Show/Plugin.pm'} = $INC{'Data/Show.pm'};
112              
113             # Track lexical options...
114             $^H{'Data::Show/with'} = _load_plugin( $opt{with}, $file, $line,
115 11         51 $opt{warnings} ne 'off', $opt{fallback} );
116 11         50 $^H{'Data::Show/termwidth'} = $opt{termwidth};
117 11         40 $^H{'Data::Show/to'} = @OUTPUT_FH;
118 11         37 $^H{'Data::Show/style'} = @STYLE;
119 11   100     106 my $existing_as = $^H{'Data::Show/as'} // '(?!)';
120 11         67 $^H{'Data::Show/as'} = "$existing_as|$opt{as}";
121 11         43 push @OUTPUT_FH, $opt{to};
122             push @STYLE, { add_grid => $opt{grid},
123             mode => $opt{style},
124 11 100       93 map { m/(.+)style/ ? ($1 => $opt{$_}) : () } keys %opt
  154         624  
125             };
126              
127             # Install the function...
128 11     11   88 no strict 'refs';
  11         20  
  11         2136  
129 11         57 *{caller() . '::' . $opt{as}} = \&show;
  11         12393  
130             }
131              
132             # A "no Data::Show" turns show() into a no-op...
133             sub unimport {
134             # Track disabling lexically...
135 1     1   16 $^H{'Data::Show/noshow'} = 1;
136              
137             # Install the function...
138 11     11   77 no strict 'refs';
  11         21  
  11         4788  
139 1         4 *{caller() . '::show'} = \&show;
  1         30  
140             }
141              
142             sub _validate_args {
143 11     11   31 my ($opt_ref, $where, $what) = @_;
144              
145             # Collect and report non-valid arguments...
146 11         23 my @unknown_args = grep { !m{$VALID_ARG} } keys %{$opt_ref};
  154         551  
  11         45  
147             die "Unknown $what" . (@unknown_args == 1 ? '' : 's') . " $where:\n",
148 11 0       52 join q{}, map { " $_\n" } @unknown_args
  0 50       0  
149             if @unknown_args;
150              
151             # By the time we're validating, we shouldn't see a 'base' option...
152 11 50       55 return if !exists $opt_ref->{base};
153 0 0       0 die $what eq 'named argument' ? "If 'base' is specified, it must be the only argument $where\n"
154             : "Can't specify 'base' as a $what $where\n"
155             }
156              
157             # Ensure output filehandles are valid (or fall back to the default)...
158             sub _open_target {
159 11     11   39 my ($target, $file, $line, $warnings) = @_;
160              
161             # Track already opened targets, and reuse them...
162 11         21 state %already_open;
163 11 100       45 return $already_open{$target} if $already_open{$target};
164              
165             # Handle stringy filenames and in-memory targets...
166 10         32 my $to_type = ref($target);
167 10 50 33     123 if (!$to_type && ref(\$target) ne 'GLOB' || $to_type eq 'SCALAR') {
    50 33        
168 0 0       0 if (open my $fh, '>', $target) {
169 0         0 return ($already_open{$target} = $fh);
170             }
171             else {
172 0 0       0 warn "Could not open named 'to' argument for output at $file line $line\n"
173             if $warnings;
174 0         0 return ($already_open{$target} = $DEFAULT_TARGET);
175             }
176             }
177              
178             # Handle filehandle-y targets...
179             elsif (_is_writeable($target)) {
180 10         62 return ($already_open{$target} = $target);
181             }
182             else {
183 0 0       0 warn "Named 'to' argument is not a writeable target at $file line $line\n"
184             if $warnings;
185 0         0 return ($already_open{$target} = $DEFAULT_TARGET);
186             }
187             }
188              
189             # -w is not reliable, so do this instead...
190             sub _is_writeable {
191 11     11   115 return eval { no warnings; print {$_[0]} q{} };
  11     10   23  
  11         15416  
  10         27  
  10         22  
  10         61  
192             }
193              
194              
195             # Extract call context, adjusting for evals...
196             sub _get_context {
197             # Start in the current caller's caller...
198 109     109   1207 my ($package, $file, $line, $hints_ref) = (caller(1))[0..2,10];
199              
200             # Keep looking up as long as next caller is a string eval...
201 109 50       569 if ($file =~ m{\A \( eval \s+ \d+ \)}x) {
202 0         0 for my $uplevel (2..1_000_000) {
203 0         0 my ($uppackage, $upfile, $upline) = caller($uplevel);
204 0         0 $upfile =~ s{.*/}{};
205 0         0 $file .= ", at $upfile line $upline";
206 0 0       0 last if $upfile !~ m{\A \( eval \s+ \d+ \)}x;
207             }
208             }
209              
210 109         403 return ($package, $file, $line, $hints_ref);
211             }
212              
213             # Strip repeated arguments from return list...
214             sub _uniq {
215 11     11   23 my %seen;
216 11         33 return grep {!$seen{$_}++} @_;
  1         7  
217             }
218              
219             # Load requested plugin (or fall back on a safe default)...
220             my %STANDARD_PLUGIN; # (Populated below)
221             sub _load_plugin {
222 11     11   41 my ($plugin, $file, $line, $warnings, $fallback_ref) = @_;
223              
224             # Build initial fallback list...
225 11   50     21 my @fallbacks = _uniq( @{ $fallback_ref // [] } );
  11         55  
226              
227             # Remember and normalize the original plugin requested (even after we start falling back)...
228 11         24 my $starting_plugin = $plugin;
229 11 100       47 if ($starting_plugin !~ m{ \A Data::Show::Plugin \b }x) {
230 6         14 $starting_plugin = "Data::Show::Plugin::$plugin";
231             }
232              
233             # Track outcomes...
234 11         17 my @failed_loads;
235 11         19 state %loaded;
236              
237             # Loop to accommodate fallbacks (if required)...
238             CANDIDATE:
239 11         23 while (1) {
240             # Normalize plugin name under the Data::Show::Plugin:: hierarchy...
241 16 100       49 if ($plugin !~ m{ \A Data::Show::Plugin \b }x) {
242 11         35 $plugin = "Data::Show::Plugin::$plugin";
243             }
244              
245             # Only load (or try to load) each plugin once (if already loaded, just return its name)...
246 16 100       55 last CANDIDATE if exists $loaded{$plugin};
247              
248             # Handle standard plugins...
249 15 100       57 if (my $standard = $STANDARD_PLUGIN{$plugin}) {
    50          
250              
251             # Validate the plugin's preconditions...
252 14         26 for my $requirement (@{$standard->{requires}}) {
  14         43  
253              
254             # If plugin can't be used, fall back to the next best alternative (if any)...
255 16 100       1363 if (!eval "require $requirement; 1") {
256 4 50       25 warn "$plugin requires $requirement, which could not be loaded.\n"
257             if $warnings;
258 4         15 push @failed_loads, $plugin;
259 4 50 33     71 if ($plugin = shift(@fallbacks) // $standard->{fallback}) {
260 4         30 next CANDIDATE;
261             }
262             else {
263 0         0 $plugin = $FINAL_CANDIDATE_PLUGIN;
264 0         0 last CANDIDATE;
265             }
266             }
267             }
268              
269             # Instantiate the plugin class, inserting the plugin-specific source code...
270 10 50 0 10   476 eval qq{
  6     6   45  
  6     42   10  
  6         1397  
  10         1145  
  42         249  
  28         67  
271             package $plugin;
272             BEGIN { our \@ISA = 'Data::Show::Plugin'; }
273             $standard->{source};
274             1;
275             } or die "Internal error: $@"; # This can never happen! ;-)
276              
277             # And we're done...
278 10         45 last CANDIDATE;
279             }
280              
281             # Otherwise, if we can load the (non-standard) module then we're also done...
282             elsif (_load_external_plugin($plugin, $warnings)) {
283 0         0 last CANDIDATE;
284             }
285              
286             # Otherwise, fall back to a specified alternative, or else try the standard fallback(s)...
287             else {
288 1 50       3 warn "Could not install $plugin at $file line $line\n"
289             if $warnings;
290 1         3 push @failed_loads, $plugin;
291 1   33     4 $plugin = shift(@fallbacks) // $INITIAL_DEFAULT_PLUGIN;
292 1         4 next CANDIDATE;
293             }
294             }
295              
296             # Report substitution-on-failure (if any)...
297 11 50 33     61 warn "Used $plugin in place of $starting_plugin at $file line $line\n"
298             if $warnings && $plugin ne $starting_plugin;
299              
300             # Remember the outcome(s) to speed things up next time...
301 11         55 $loaded{$_} = $plugin for $plugin, @failed_loads;
302              
303 11         72 return $plugin;
304             }
305              
306             # Load or otherwise verify the availability of a non-standard plugin...
307             sub _load_external_plugin {
308 29     1   261 my ($plugin, $warnings) = @_;
309              
310             # Load it (or fail silently)...
311 29         245 eval "require $plugin";
312              
313             # Are all the essential methods present in the plugin class???
314 29         2669 my @missing_methods = grep { !$plugin->can($_) } @PLUGIN_API;
  16         99  
315             warn "Requested plugin class $plugin does not provide the following essential methods:\n",
316 15 50 33     64 (map { " $_()" } @missing_methods), "\n"
  14         40  
317             if $warnings && @missing_methods;
318              
319             # Succeed if all the essential methods are available...
320 15         55 return !@missing_methods;
321             }
322              
323             # Locate and process config file(s) and/or environment variable, if any...
324             sub _load_defaults {
325 10     10   31 my ($file, $line) = @_;
326              
327             # Build up defaults, starting with the built-in defaults...
328 24         262 my %defaults = @ARGUMENT_DEFAULTS;
329              
330             # Overwrite previous defaults with any readable global or local config file(s)...
331 24         109 for my $config_file (grep {-r} "$ENV{HOME}/$RC_FILE_NAME", "./$RC_FILE_NAME") {
  20         419  
332 14         38 %defaults = ( %defaults, _load_config($config_file) );
333             }
334              
335 10         43 return \%defaults;
336             }
337              
338             sub _load_config {
339 14     0   44 my ($filename) = @_;
340              
341             # Grab contents of file...
342 14 50       50 open my $fh, '<:utf8', $filename or return;
343 14         84 local $/;
344 14   0     55 my $config = readline($fh) // return;
345              
346             # Remove empty lines (including comment lines)...
347 14         65 $config =~ s{ ^ \s* (?: \# [^\n]* )? (?:\n|\z) }{}gxms;
348              
349             # Extract keys and values of each option...
350 14         226 my %opt = $config =~ m{ ^ \h* ([^:=]*?) \h* [:=] \h* ([^\n]*) (?:\n|\z) }gxms;
351              
352             # Convert a "*NAME" string to the corresponding named filehandle...
353 0 50 0     0 if (exists $opt{to} && $opt{to} =~ m{ \A \* (.*) }x) {
354 11     11   89 no strict 'refs';
  11         21  
  11         481  
355 11     11   55 no warnings 'once';
  11         16  
  11         7191  
356 0         0 $opt{to} = \*{$1};
  0         0  
357             }
358              
359             # Validate config...
360 0         0 _validate_args(\%opt, "in $filename", "configuration option");
361              
362 0         0 return %opt;
363             }
364              
365             # The whole point of the module...
366             sub show {
367             # Find the various contexts of this call...
368 98     98 1 1559337 my ($package, $file, $line, $hints_ref) = _get_context();
369 98         254 my $call_context = wantarray();
370              
371             # Skip almost everything if "no Data::Show"...
372 98 100       394 if (!$hints_ref->{'Data::Show/noshow'}) {
373              
374             # Identify current lexically-scoped config (should already have been loaded by import())...
375 84   33     367 my $plugin_class = $hints_ref->{'Data::Show/with'} // $FINAL_CANDIDATE_PLUGIN;
376 84         189 my %style = %{ $STYLE[ $hints_ref->{'Data::Show/style'} ] };
  84         1036  
377 84         246 my $termwidth = $hints_ref->{'Data::Show/termwidth'};
378              
379             # Warn about side-effects of multi-arg calls to show() in scalar context...
380 84 0 33     317 if (defined $call_context && !$call_context && @_ > 1) {
      33        
381 0         0 warn "Call to show() may not be not transparent at $file line $line\n";
382             }
383              
384             # Serialize Contextual::Return objects (can break some dumpers in the Data::Dump family)...
385 84         249 my @data = map { ref() =~ m{\AContextual::Return::Value}
386 166 50       605 ? do {my $v = $_->Contextual::Return::DUMP(); $v =~ s[\}\n][\},\n]gxms; eval $v; }
  0         0  
  0         0  
  0         0  
387             : $_
388             } @_;
389              
390             # Extract the originating source line(s)...
391             my ($pre_source, $source, $post_source, $startline)
392 84         343 = _get_source($file, $line, $hints_ref->{'Data::Show/as'});
393              
394             # What kind of data is it???
395 84         658 my $is_single_hash = _data_is_single_hash($source, \@data, $hints_ref->{'Data::Show/as'});
396 84         403 my $is_single_arg = @data == 1;
397              
398             # Stringify the data...
399 84 100       2191 my $data = $plugin_class->stringify( $is_single_hash ? {@data}
    100          
400             : $is_single_arg ? $data[0]
401             : \@data
402             );
403              
404             # Some stringifiers add an (unwanted) empty first line, so remove it...
405 84         4314 $data =~ s{ \A \h* \n }{}xms;
406              
407             # Change delimters of any stringified arguments that were passed to the stringifier via refs...
408 84 100       396 if ($is_single_hash) { $data =~ s{ \A (\s*) \{ (.*) \} (\s*) \z }{$1($2)$3}xms; }
  6 100       84  
409 24         302 elsif (!$is_single_arg) { $data =~ s{ \A (\s*) \[ (.*) \] (\s*) \z }{$1($2)$3}xms; }
410              
411             # Where are we printing to???
412 84 50       594 my $fh = exists $hints_ref->{'Data::Show/to'} ? $OUTPUT_FH[$hints_ref->{'Data::Show/to'}]
413             : $DEFAULT_TARGET;
414              
415             # Disable styling if not outputting to a terminal or if styling is unavailable...
416 84 50 0     534 if (!-t $fh || $style{mode} eq 'auto' && !$CAN_ANSICOLOR) {
      33        
417 84         273 $style{mode} = 'off'
418             }
419              
420             # Show the data with its context header (with style!)...
421 11     11   87 no warnings 'utf8';
  11         17  
  11         5748  
422 84         206 print {$fh}
  84         1007  
423             $plugin_class->format(
424             $file, $startline, $pre_source, $source, $post_source, $data, \%style, $termwidth,
425             );
426             }
427              
428             # Return the entire argument list if possible, otherwise simulate scalar context...
429 98 50       624 return @_ if $call_context;
430 98         583 return $_[0];
431             }
432              
433             # Return the source code at a given file and line...
434             sub _get_source {
435 84     84   296 my ($file, $line, $subname) = @_;
436              
437             # Optimize look-up via a cache...
438 84         163 state %source_cache;
439              
440             # Load the entire source of requested file...
441 84 100       385 if (!$source_cache{$file}) {
442             # Load the source of an eval()...
443 6 50       505 if ($file =~ m{\A \( eval \s+ \d+ \)}x) {
    50          
444 0         0 $source_cache{$file} = (caller(2))[6];
445             }
446              
447             # Otherwise, read in the source from the file...
448             elsif (open my $filehandle, '<', $file) {
449 6         19 $source_cache{$file} = do { local $/, readline($filehandle) };
  6         349  
450             }
451              
452             else {
453             # Otherwise, see if it's a #line trick in the main file...
454 0 0 0     0 if (!defined $source_cache{$0} && open my $selfhandle, '<', $0) {
455 0         0 $source_cache{$0} = do { local $/, readline($selfhandle) };
  0         0  
456             }
457              
458 0         0 $source_cache{$file} = $source_cache{$0};
459             $source_cache{$file} =~ s{ \A .*? ^ \# \h* line \h+ (\d+) \h+ \Q$file\E \h* \n }
460 0         0 { "\n" x ($1-1) }xmse
461 0 0       0 or $source_cache{$file} = q{};
462 0         0 $source_cache{$file} =~ s{ \A .*? ^ \# \h* line \h+ (\d+) \h+ [^\n]* \n .* }{}xms;
463             }
464             }
465              
466             # This pattern detects when we have a complete show() call...
467 84         168 state %SHOW_PATTERN_FOR;
468 84   66     383322 my $SHOW_PATTERN = $SHOW_PATTERN_FOR{$subname}
469             //= qr{ (?
  [^\n]*?  ) 
470             (?>
471             (? \b(?:$subname)\b (?&PerlOWS) (?&PerlParenthesesList) )
472             (? [^\n]* )
473             |
474             (? \b(?:$subname)\b (?&PerlOWS) (?&PerlCommaList) )
475             (?> (? (?&PerlOWS) (?: ; | \Z ) )
476             | (?) (?= (?&PerlOWS) \} )
477             )
478             )
479             $PPR::GRAMMAR
480             }xms;
481              
482             # Locate the call in the source code (allowing for inaccuracies in caller() line results)...
483 11     11   94 use re 'eval';
  11         21  
  11         4930  
484 84         1741 our $prelim_lines; local $prelim_lines = $line-1;
  84         429  
485 84         5128946 my $found = $source_cache{$file} =~ m{
486             \A
487             (?
488             (? (?: [^\n]* \n ){0,$prelim_lines}? (?&PerlOWS) )
489             (?> $SHOW_PATTERN )
490             )
491 708 100       104946 (??{ ($+{showlines} =~ tr/\n//) >= $prelim_lines ? q{} : '(?!)' })
492             }xms;
493 84         35343 my %cap = %+;
494              
495             # Extract source code of call (the else should only very rarely need to be invoked)...
496 84 50       593 if ($found) {
497 84         1523 return @cap{qw
}, 1 + ($+{prelines} =~ tr/\n//); 
498             }
499             else {
500 0         0 return q{}, [0, split /\n/, $source_cache{$file}]->[$line], q{}, $line;
501             }
502              
503             }
504              
505             # Attempt to detect a show() argument list that consists of a single hash...
506             sub _data_is_single_hash {
507 84     84   288 my ($context, $data_ref, $subname) = @_;
508 84   50     264 $context //= q{};
509              
510             # What does a single hash arg look like???
511 84         145 state %SINGLE_HASH_FOR;
512 84   66     345754 my $SINGLE_HASH = $SINGLE_HASH_FOR{$subname}
513             //= qr{ \b(?:$subname) (?&PerlOWS)
514             (?: (?&PerlVariableHash)
515             | \( (?&PerlOWS) (?&PerlVariableHash) (?&PerlOWS) \)
516             )
517             (?&PerlOWS) \z
518             $PPR::GRAMMAR
519             }x;
520              
521             # Must be only one argument (plus the invocant) and must look like a single hash...
522 84   100     2101 return @{$data_ref} % 2 == 0 && $context =~ $SINGLE_HASH;
523             }
524              
525              
526             # Base class for plugins...
527             package Data::Show::Plugin;
528              
529             # When imported, make the imported plugin a base class of the importing class...
530             sub import {
531 0     0   0 my ($package) = @_;
532 11     11   82 no strict 'refs';
  11         19  
  11         12904  
533 0         0 @{caller().'::ISA'} = $package;
  0         0  
534             }
535              
536             # Visually distinguish the context string and data...
537             sub format {
538 70     70 0 332 my ($class, $file, $line, $pre_source, $source, $post_source, $data, $style, $termwidth) = @_;
539 70   50     424 $_ //= q{} for $pre_source, $source, $post_source;
540              
541             # Track previous file context between calls...
542 70         143 state $prevfile = q{};
543 70         173 my $is_new_context = $file ne $prevfile;
544 70         155 $prevfile = $file;
545              
546             # Compute line numbering width...
547 70         294 my $line_num_len = length( $line + ($pre_source . $source . $post_source) =~ tr/\n// );
548 70         235 my $data_box_len = $termwidth - 4;
549 70         153 my $code_box_len = $data_box_len - $line_num_len - 1;
550              
551             # ASCII-only decoration if explicitly requested, or if Term::ANSIcolor is unavailable...
552             my $decorate_data = $style->{mode} eq 'context'
553 70 50 33     621 || $style->{mode} eq 'off' ? \&_monochrome : \&_polychrome;
554 70 50       259 my $decorate_context = $style->{mode} eq 'off' ? \&_monochrome : \&_polychrome;
555              
556             # Set up grid components (if requested)...
557 70         283 my ($gridhead, $gridtail, $gridfsep, $gridcsep, $gridside, $gridline, $gridplus) = (q{}) x 7;
558 70         200 my $padding = q{ } x $data_box_len;
559              
560 70 100       266 if ($style->{add_grid} ne 'off') {
561             # $is_new_context !$is_new_context
562             # Top line of grid: ┏━━━━━━━━━━━━━┓ ┏━━━┯━━━━━━━━━━┓
563 28         155 $gridhead = $GRID{'┏'} . ($GRID{'━'} x ($termwidth-2)) . $GRID{'┓'} . "\n";
564 28 100       172 substr($gridhead, $line_num_len+1, 1) = $GRID{'┯'} if !$is_new_context;
565              
566             # Post-filename separator: ┠───┬─────────┨
567 28         125 $gridfsep = $GRID{'┠'} . ($GRID{'─'} x ($termwidth-2)) . $GRID{'┨'} . "\n";
568 28         84 substr($gridfsep, $line_num_len+1, 1) = $GRID{'┬'};
569              
570             # Post-code separator: ┠───┴─────────┨ ┠───┴─────────┨
571 28         93 $gridcsep = $GRID{'┠'} . ($GRID{'─'} x ($termwidth-2)) . $GRID{'┨'} . "\n";
572 28         91 substr($gridcsep, $line_num_len+1, 1) = $GRID{'┴'};
573              
574             # Bottom line of grid: ┗━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━┛
575 28         92 $gridtail = $GRID{'┗'} . ($GRID{'━'} x ($termwidth-2)) . $GRID{'┛'} . "\n";
576              
577             # Verticals of grid...
578 28         58 $gridside = $GRID{'┃'};
579 28         59 $gridplus = $GRID{'┃'} . q{ };
580 28         66 $gridline = $GRID{'│'};
581              
582             # Decorate them all in the same style...
583 28         84 for my $border ($gridhead, $gridtail, $gridfsep, $gridcsep, $gridside, $gridplus, $gridline) {
584 196         440 $border = $decorate_context->($border, $style->{grid});
585             }
586             }
587              
588             # Style the source code...
589 0         0 $source = join("\n", map {$decorate_context->($_, $style->{code})} split "\n", $pre_source)
590 85         332 . join("\n", map {$decorate_context->($_, $style->{show})} split "\n", $source)
591 70         450 . join("\n", map {$decorate_context->($_, $style->{code})} split "\n", $post_source);
  65         201  
592 70         471 $source =~ s{ ^ }{ }gxms;
593              
594             # Install the line numbers and grid/format each source line...
595             $source = join "\n",
596             map {
597 70 100       226 $style->{add_grid} eq 'off'
  85         5091  
598             ? m{ [^\n]* }xms
599             : ($_ . $padding) =~ m{ $COLOUR_CHAR {$code_box_len} }xms;
600             $gridside
601             . $decorate_context->( sprintf('%*d', $line_num_len, $line++), $style->{line} )
602 85         585 . $gridline . q{ } . $& . q{ } . $gridside
603             }
604             split "\n", $source;
605              
606             # Trim, grid, and format each data line...
607             $data = join "\n",
608             map {
609 70 100       325 $style->{add_grid} eq 'off' ? m{ [^\n]* }xms
  202         3643  
610             : ($_ . $padding) =~ m{ $COLOUR_CHAR {$data_box_len} }xms;
611 202         569 $gridplus . $decorate_data->( $&, $style->{data} ) . q{ } . $gridside
612             }
613             split "\n", $data;
614              
615             # Delineate source lines and data lines if no better styling has been specified...
616 70 50 33     483 if (!$CAN_ANSICOLOR && $style->{add_grid} eq 'off') {
617 0         0 $file =~ s{ ^ }{### }gxms;
618 0         0 $source =~ s{ ^ }{### }gxms;
619 0         0 $data =~ s{ ^ }{>>> }gxms;
620             }
621              
622             return $gridhead
623             . ($is_new_context
624 70 100       979 ? do {
625 5 100       139 $style->{add_grid} eq 'off'
626             ? $file =~ m{ [^\n]* }xms
627             : ($file . $padding) =~ m{ $COLOUR_CHAR {$data_box_len} }xms;
628             $gridplus
629             . $decorate_context->( $&, $style->{file} )
630 5         23 . q{ } . $gridside . "\n"
631             . $gridfsep
632             }
633             : q{}
634             )
635             . "$source\n"
636             . $gridcsep
637             . "$data\n"
638             . $gridtail
639             . "\n";
640             }
641              
642              
643             # Utility functions...
644              
645 638     638   2404 sub _monochrome { $_[0] }
646              
647 0     0   0 sub _polychrome { &Term::ANSIColor::colored }
648              
649 0     0   0 sub _antichrome { &Term::ANSIColor::colorstrip }
650              
651             sub _max {
652 0     0   0 my ($x, $y) = @_;
653 0 0       0 return $x > $y ? $x : $y;
654             }
655              
656             # Convert the data to a printable form (using Data::Dumper)...
657             sub stringify {
658 56     56 0 173 my ($class, $data) = @_;
659              
660             # Choose conservative defaults (derive a subclass to change these)...
661 11     11   5042 use Data::Dumper 'Dumper';
  11         61516  
  11         1322  
662 11     11   90 no warnings 'once';
  11         23  
  11         2186  
663 56         155 local $Data::Dumper::Deparse = 0;
664 56         152 local $Data::Dumper::Sortkeys = 1;
665 56         133 local $Data::Dumper::Deepcopy = 1;
666 56         119 local $Data::Dumper::Terse = 1;
667              
668             # Convert data to a string representation...
669 56         435 my $stringification = Dumper($data);
670              
671             # Remove the annoying "$VAR1 = ", and realign subsequent indented lines...
672             # $stringification =~ s{ ^ .{8} }{}gxms;
673              
674             # Remove the annoying trailing ';'...
675 56         5722 $stringification =~ s{ ; (\s*) \z }{$1}gxms;
676              
677 56         254 return $stringification;
678             }
679              
680              
681             # Template for constructing standard plugins...
682 11     11   4756 my $NULL_FORMATTER; BEGIN { $NULL_FORMATTER = q{
683             sub format {
684             my ($class, $data) = @_;
685             $data =~ s{\s*\z}{};
686             return "$data\n\n";
687             }
688             }}
689             sub _build_plugin {
690             # (SHOUTY parameters get interpolated, mousy prarameters don't)...
691 132     132   269 my ($NAME, $FALLBACK, $DUMP, $no_formatting) = @_;
692              
693             # Handle non-formatting plugins, and optimize argument look-up...
694 132 100       233 my $FORMATTER = $no_formatting ? $NULL_FORMATTER : q{};
695 132         159 my $DATA = '$_[1]';
696              
697             return
698 132         1709 "Data::Show::Plugin::$NAME" => {
699             requires => [$NAME],
700             fallback => $FALLBACK,
701             source => qq{
702             $FORMATTER
703             sub stringify {
704             use $NAME q{$DUMP};
705             return (eval { $DUMP($DATA) }
706             // '<$NAME cannot show a ' . lc(ref($DATA)) . " reference>\n")
707             . "\n";
708             }
709             },
710             };
711             }
712              
713             # Initialize the data needed to instantiate the built-in plugins on-demand...
714             BEGIN {
715 11     11   56 %STANDARD_PLUGIN = (
716              
717             # DUMPER MODULE FALLBACK DUMP FUNC VARIATIONS
718             _build_plugin( 'Data::Dmp' => 'Data::Pretty', 'dmp' ),
719             _build_plugin( 'Data::Dump' => 'Data::Dumper', 'pp', ),
720             _build_plugin( 'Data::Dump::Color' => 'Data::Dump', 'pp', 'preformatted' ),
721             _build_plugin( 'Data::Dumper::Compact' => 'Data::Dump', 'ddc' ),
722             _build_plugin( 'Data::Dumper::Concise' => 'Data::Dumper', 'Dumper' ),
723             _build_plugin( 'Data::Dumper::Table' => 'Dumpvalue', 'Tabulate' ),
724             _build_plugin( 'Data::Pretty' => 'Data::Dump', 'pp', ),
725             _build_plugin( 'Data::TreeDumper' => 'Dumpvalue', 'DumpTree' ),
726             _build_plugin( 'YAML' => 'YAML::Tiny', 'Dump' ),
727             _build_plugin( 'YAML::PP' => 'YAML::Tiny', 'Dump' ),
728             _build_plugin( 'YAML::Tiny' => 'Dumpvalue', 'Dump' ),
729             _build_plugin( 'YAML::Tiny::Color' => 'YAML::Tiny', 'Dump', 'preformatted' ),
730              
731             "Data::Show::Plugin::Data::Dumper" => {
732             requires => ['Data::Dumper'],
733             fallback => 'DumpValue',
734             source => q{}, # ...because Data::Show::Plugin base class already uses Data::Dumper
735             },
736              
737             'Data::Show::Plugin::Data::Printer' => {
738             requires => ['Data::Printer'],
739             fallback => 'Dumpvalue',
740             source => qq{
741             sub stringify {
742             use Data::Printer;
743             return np(\$_[1], colored=>$CAN_ANSICOLOR) . "\n";
744             }
745             },
746             },
747              
748             'Data::Show::Plugin::Dumpvalue' => {
749             requires => ['Dumpvalue'],
750             source => q{
751             sub stringify {
752             my ($class, $data) = @_;
753              
754             # Create a singleton Dumpvalue object to do the stringification...
755             use Dumpvalue;
756             state $DUMPER = Dumpvalue->new(subdump=>1, globPrint=>1);
757              
758             # Dumpvalue only dumps to STDOUT, so co-opt that filehandle to capture the output...
759             open +(local *STDOUT), '>', \\my $dump;
760              
761             # Stringify the data to the captured STDOUT...
762             $DUMPER->dumpValue($data);
763              
764             # Return the intercepted stringification...
765             return $dump;
766             }
767             },
768             },
769              
770             # This plugin restores the previous (pre-version-0.003) output format for the module...
771             'Data::Show::Plugin::Legacy' => {
772             requires => ['Data::Dump', 'PPR', 'List::Util'],
773             fallback => 'Data::Dump',
774             source => q{
775             sub format {
776             my ($class, $file, $line,
777             $pre_source, $source, $post_source,
778             $data, $style, $termwidth) = @_;
779              
780             use List::Util 'max';
781              
782             # Configuration for layout of representation...
783             state $DEFAULT_INDENT = 4;
784             state $MAX_DESC = 30;
785             state $MAX_FILENAME = 20;
786             state $TITLE_POS = 3;
787              
788             # Extract description of arglist from source...
789             $source =~ s{\\A show \b \\s*}{}x;
790             $source =~ s{\\s+}{ }gx;
791             $source =~ s{\\A \\( (.*) \\) \\Z}{$1}x;
792             if (length($source) > $MAX_DESC) {
793             $source = substr($source,0,$MAX_DESC-3) . q{...};
794             }
795              
796             # Trim filename and format context info and description...
797             $file =~ s{.*[/\\\\]}{}xms;
798             if (length($file) > $MAX_FILENAME) {
799             $file =~ s/ (_[^\\W_]) [^\\W_]* /$1/gxms;
800             }
801             if (length($file) > $MAX_FILENAME) {
802             $file =~ s/\\A (.{1,8}) .*? (.{1,8}) \\Z/$1...$2/gxms;
803             }
804             my $context = "[ '$file', line $line ]";
805              
806             # Insert title into header...
807             my $header = '=' x $termwidth;
808             substr($header, $TITLE_POS, length($source)+6) = "( $source )";
809             substr($header, -(length($context)+$TITLE_POS), length($context)) = $context;
810              
811             # Indent data...
812             $data =~ s{^}{ }gxms;
813              
814             # Assemble and send off...
815             return "$header\\n\\n$data\\n\\n";
816             }
817              
818             # Original stringifier was Data::Dump...
819             sub stringify {
820             use Data::Dump 'pp';
821             return pp($_[1]);
822             }
823             },
824             },
825             );
826             }
827              
828              
829             1; # Magic true value required at end of module
830             __END__