File Coverage

blib/lib/Data/Dump/HTML/PopUp.pm
Criterion Covered Total %
statement 17 165 10.3
branch 0 82 0.0
condition 0 33 0.0
subroutine 6 17 35.2
pod 2 2 100.0
total 25 299 8.3


line stmt bran cond sub pod time code
1             ## no critic: Modules::ProhibitAutomaticExportation
2              
3             package Data::Dump::HTML::PopUp;
4              
5 1     1   321437 use 5.010001;
  1         4  
6 1     1   5 use strict;
  1         3  
  1         58  
7 1     1   12 use warnings;
  1         2  
  1         72  
8              
9 1     1   5 use Exporter qw(import);
  1         3  
  1         59  
10 1     1   604 use HTML::Entities qw(encode_entities);
  1         7670  
  1         96  
11 1     1   6 use Scalar::Util qw(looks_like_number blessed reftype refaddr);
  1         1  
  1         2880  
12              
13             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
14             our $DATE = '2024-03-17'; # DATE
15             our $DIST = 'Data-Dump-HTML-PopUp'; # DIST
16             our $VERSION = '0.001'; # VERSION
17              
18             our @EXPORT = qw(dd);
19             our @EXPORT_OK = qw(dump);
20              
21             # for when dealing with circular refs
22             our %_seen_refaddrs;
23             our %_subscripts;
24             our @_fixups;
25             our @_result_divs; # elem: [refaddr, subscript, html-source]
26              
27             our $OPT_PERL_VERSION = "5.010";
28             our $OPT_REMOVE_PRAGMAS = 0;
29             our $OPT_DEPARSE = 1;
30             our $OPT_STRINGIFY_NUMBERS = 0;
31             our $OPT_LIBRARY_LINK_MODE = "local";
32              
33             # BEGIN COPY PASTE FROM Data::Dump
34             my %esc = (
35             "\a" => "\\a",
36             "\b" => "\\b",
37             "\t" => "\\t",
38             "\n" => "\\n",
39             "\f" => "\\f",
40             "\r" => "\\r",
41             "\e" => "\\e",
42             );
43              
44             # put a string value in double quotes
45             sub _double_quote {
46 0     0     local($_) = $_[0];
47              
48             # If there are many '"' we might want to use qq() instead
49 0           s/([\\\"\@\$])/\\$1/g;
50 0 0         return qq("$_") unless /[^\040-\176]/; # fast exit
51              
52 0           s/([\a\b\t\n\f\r\e])/$esc{$1}/g;
53              
54             # no need for 3 digits in escape for these
55 0           s/([\0-\037])(?!\d)/sprintf('\\%o',ord($1))/eg;
  0            
56              
57 0           s/([\0-\037\177-\377])/sprintf('\\x%02X',ord($1))/eg;
  0            
58 0           s/([^\040-\176])/sprintf('\\x{%X}',ord($1))/eg;
  0            
59              
60 0           return qq("$_");
61             }
62             # END COPY PASTE FROM Data::Dump
63              
64             # BEGIN COPY PASTE FROM String::PerlQuote
65             sub _single_quote {
66 0     0     local($_) = $_[0];
67 0           s/([\\'])/\\$1/g;
68 0           return qq('$_');
69             }
70             # END COPY PASTE FROM String::PerlQuote
71              
72             sub _dump_code {
73 0     0     my $code = shift;
74              
75 0           state $deparse = do {
76 0           require B::Deparse;
77 0           B::Deparse->new("-l"); # -i option doesn't have any effect?
78             };
79              
80 0           my $res = $deparse->coderef2text($code);
81              
82 0           my ($res_before_first_line, $res_after_first_line) =
83             $res =~ /(.+?)^(#line .+)/ms;
84              
85 0 0         if ($OPT_REMOVE_PRAGMAS) {
    0          
86 0           $res_before_first_line = "{";
87             } elsif ($OPT_PERL_VERSION < 5.016) {
88             # older perls' feature.pm doesn't yet support q{no feature ':all';}
89             # so we replace it with q{no feature}.
90 0           $res_before_first_line =~ s/no feature ':all';/no feature;/m;
91             }
92 0           $res_after_first_line =~ s/^#line .+//gm;
93              
94 0           $res = "sub" . $res_before_first_line . $res_after_first_line;
95 0           $res =~ s/^\s+//gm;
96 0           $res =~ s/\n+//g;
97 0           $res =~ s/;\}\z/}/;
98 0           $res;
99             }
100              
101             sub _quote_key {
102 0 0 0 0     $_[0] =~ /\A-?[A-Za-z_][A-Za-z0-9_]*\z/ ||
103             $_[0] =~ /\A-?[1-9][0-9]{0,8}\z/ ? $_[0] : _double_quote($_[0]);
104             }
105              
106             sub _dump {
107 0     0     my ($val, $subscript, $depth) = @_;
108              
109 0           my $ref = ref($val);
110 0 0         if ($ref eq '') {
111 0 0 0       if (!defined($val)) {
    0 0        
      0        
112 0           return "undef";
113             } elsif (looks_like_number($val) && !$OPT_STRINGIFY_NUMBERS &&
114             # perl does several normalizations to number literal, e.g.
115             # "+1" becomes 1, 0123 is octal literal, etc. make sure we
116             # only leave out quote when the number is not normalized
117             $val eq $val+0 &&
118             # perl also doesn't recognize Inf and NaN as numeric
119             # literals (ref: perldata) so these unquoted literals will
120             # choke under 'use strict "subs"
121             $val !~ /\A-?(?:inf(?:inity)?|nan)\z/i
122             ) {
123 0           return $val;
124             } else {
125 0           return encode_entities(_double_quote($val));
126             }
127             }
128 0           my $refaddr = sprintf("%x", refaddr($val));
129 0   0       $_subscripts{$refaddr} //= $subscript;
130 0 0         if ($_seen_refaddrs{$refaddr}++) {
131             my $target = "\$var" .
132 0 0         ($_subscripts{$refaddr} ? "->$_subscripts{$refaddr}" : "");
133 0           push @_fixups, "\$var->$subscript = $target;\n";
134 0           return "".encode_entities(_single_quote($target))."";
135             }
136              
137 0           my $class;
138              
139 0 0 0       if ($ref eq 'Regexp' || $ref eq 'REGEXP') {
140 0           require Regexp::Stringify;
141 0           return encode_entities(
142             Regexp::Stringify::stringify_regexp(
143             regexp=>$val, with_qr=>1, plver=>$OPT_PERL_VERSION)
144             );
145             }
146              
147 0 0         if (blessed $val) {
148 0           $class = $ref;
149 0           $ref = reftype($val);
150             }
151              
152 0           my $res = "";
153 0           $res .= (" " x $depth);
154 0 0         if ($ref eq 'ARRAY') {
    0          
    0          
    0          
    0          
155 0           $res .= "[\n";
156 0           my $i = 0;
157 0           for (@$val) {
158 0 0         $res .= ", # ".("." x $depth)."[".($i-1)."]\n" if $i;
159 0           $res .= (" " x ($depth+1));
160 0           my $elem_ref = ref $_;
161 0           my $elem_res = _dump($_, "$subscript\[$i]", $depth+1);
162 0 0 0       if (($elem_ref eq 'ARRAY' || $elem_ref eq 'HASH') && length($elem_res) > 100) {
      0        
163 0           my $elem_refaddr = sprintf("%x", refaddr($_));
164 0           push @_result_divs, [$elem_refaddr, "$subscript\[$i]", $elem_res];
165 0           $res .= qq().encode_entities(_single_quote("\$var->$subscript\[$i]"))."";
166             } else {
167 0           $res .= $elem_res;
168             }
169 0           $i++;
170             }
171 0           $res .= "\n" . (" " x $depth) . "]";
172             } elsif ($ref eq 'HASH') {
173 0           $res .= "{\n";
174 0           my $i = 0;
175 0           for (sort keys %$val) {
176 0 0         $res .= ", # ".("." x $depth)."{".($i-1)."}\n" if $i;
177 0           $res .= (" " x ($depth+1));
178 0           my $k = _quote_key($_);
179 0           my $val_ref = ref $val->{$_};
180 0           my $val_res = _dump($val->{$_}, "$subscript\{$k}", $depth+1);
181 0 0 0       if (($val_ref eq 'ARRAY' || $val_ref eq 'HASH') && length($val_res) > 100) {
      0        
182 0           my $val_refaddr = sprintf("%x", refaddr($val->{$_}));
183 0           push @_result_divs, [$val_refaddr, "$subscript\{$k}", $val_res];
184 0           $res .= encode_entities($k) . " => " . qq().encode_entities(_single_quote("\$var->$subscript\{$k}"))."";
185             } else {
186 0           $res .= encode_entities($k) . " => " . $val_res;
187             }
188 0           $i++;
189             }
190 0           $res .= "\n" . (" " x $depth) . "}";
191             } elsif ($ref eq 'SCALAR') {
192 0 0         if (defined $class) {
193 0           $res .= "do { my \$o="._dump($$val, $subscript)."; \\\$o}";
194             } else {
195 0           $res .= "\\"._dump($$val, $subscript);
196             }
197             } elsif ($ref eq 'REF') {
198 0           $res .= "\\"._dump($$val, $subscript);
199             } elsif ($ref eq 'CODE') {
200 0 0         $res .= encode_entities( $OPT_DEPARSE ? _dump_code($val) : 'sub{"DUMMY"}' );
201             } else {
202 0           die "Sorry, I can't dump $val (ref=$ref) yet";
203             }
204              
205 0 0         $res = "bless($res,".encode_entities(_double_quote($class)).")" if defined($class);
206 0           $res;
207             }
208              
209             sub _escape_uri {
210 0     0     require URI::Escape;
211 0           URI::Escape::uri_escape(shift, "^A-Za-z0-9\-\._~/:"); # : for drive notation on Windows
212             }
213              
214             sub _preamble {
215 0 0   0     if ($OPT_LIBRARY_LINK_MODE eq 'none') {
216 0           return '';
217             }
218              
219 0           my $jquery_ver = '3.7.1';
220 0           my $modally_ver = '1.1.0';
221 0           my $res = '';
222 0 0         if ($OPT_LIBRARY_LINK_MODE eq 'embed') {
    0          
    0          
223 0           require File::ShareDir;
224 0           require File::Slurper;
225 0           my $dist_dir = File::ShareDir::dist_dir('Data-Dump-HTML-PopUp');
226 0           my $path;
227              
228 0           $path = "$dist_dir/modally-$modally_ver/jquery.modally.css";
229 0 0         -r $path or die "Can't embed $path: $!";
230 0           $res .= "\n\n\n";
231              
232 0           $path = "$dist_dir/jquery-$jquery_ver/jquery.min.js";
233 0 0         -r $path or die "Can't embed $path: $!";
234 0           $res .= "\n\n\n";
235              
236 0           $path = "$dist_dir/modally-$modally_ver/jquery.modally.js";
237 0 0         -r $path or die "Can't embed $path: $!";
238 0           $res .= "\n\n\n";
239             } elsif ($OPT_LIBRARY_LINK_MODE eq 'local') {
240 0           require File::ShareDir;
241 0           my $dist_dir = File::ShareDir::dist_dir('Data-Dump-HTML-PopUp');
242 0 0         $dist_dir =~ s!\\!/!g if $^O eq 'MSWin32';
243 0           $res .= qq(\n);
244 0           $res .= qq(\n);
245 0           $res .= qq(\n);
246             } elsif ($OPT_LIBRARY_LINK_MODE eq 'cdn') {
247             # no CDN yet for modally
248 0           die "'cdn' linking mode is not yet supported";
249             } else {
250 0           die "Unknown value for the '\$OPT_LIBRARY_LINK_MODE' option: '$OPT_LIBRARY_LINK_MODE', please use one of local|embed|cdn|none";
251             }
252             }
253              
254             sub _postamble {
255 0 0   0     if ($OPT_LIBRARY_LINK_MODE eq 'none') {
256 0           '';
257             } else {
258 0 0         "";
  0            
259             }
260             }
261              
262             our $_is_dd;
263             sub _dd_or_dump {
264 0     0     local %_seen_refaddrs;
265 0           local %_subscripts;
266 0           local @_fixups;
267 0           local @_result_divs;
268              
269 0           my $res;
270 0 0         if (@_ > 1) {
271 0           die "Currently multiple arguments are not supported, please only pass 1 argument";
272             #$res = "(" . join(",\n", map {_dump($_, '', 0)} @_) . ")";
273             } else {
274 0           $res = _dump($_[0], '', 0);
275             }
276 0 0         if (@_fixups) {
277 0           $res = "do { my \$var = $res;\n" . join("", map {encode_entities $_} @_fixups) . "\$var }";
  0            
278             }
279              
280             # the root variable is referenced. we need to create a modal div too for it,
281             # which duplicates the result.
282             {
283 0 0         last unless ref $_[0];
  0            
284 0           my $refaddr = sprintf("%x", refaddr($_[0]));
285 0 0         last unless $_seen_refaddrs{$refaddr} > 1;
286 0           unshift @_result_divs, [$refaddr, '', $res];
287             }
288              
289             $res = _preamble() .
290             "
$res
" .
291             join("", map {
292 0 0         qq(
  0 0          
293             "# \$var".($_result_divs[$_][1] ? "->".$_result_divs[$_][1] : '')."\n".
294             $_result_divs[$_][2].
295             qq()
296             } 0 .. $#_result_divs).
297             _postamble();
298              
299 0 0         if ($_is_dd) {
300 0           say $res;
301 0 0 0       return wantarray() || @_ > 1 ? @_ : $_[0];
302             } else {
303 0           return $res;
304             }
305             }
306              
307 0     0 1   sub dd { local $_is_dd=1; _dd_or_dump(@_) } # goto &sub doesn't work with local
  0            
308 0     0 1   sub dump { goto &_dd_or_dump }
309              
310             1;
311             # ABSTRACT: Dump Perl data structures as HTML document with nested pop ups
312              
313             __END__