File Coverage

blib/lib/Acme/AsciiArtinator.pm
Criterion Covered Total %
statement 347 454 76.4
branch 129 204 63.2
condition 76 148 51.3
subroutine 18 19 94.7
pod 1 15 6.6
total 571 840 67.9


line stmt bran cond sub pod time code
1             package Acme::AsciiArtinator;
2 5     5   76458 use Carp;
  5         10  
  5         463  
3 5     5   29 use base 'Exporter';
  5         7  
  5         526  
4 5     5   24 use strict;
  5         13  
  5         163  
5 5     5   24 use warnings;
  5         7  
  5         38273  
6             our $VERSION = '0.04';
7             our @EXPORT = qw(asciiartinate);
8             $| = 1;
9              
10             my $DEBUG = 0;
11              
12             #############################################################################
13              
14             #
15             # run ASCII Artinization on a picture and a code string.
16             #
17             sub asciiartinate {
18 3     3 0 2103 my %opts = @_;
19 3 50 33     29 if (@_ == 1 && ref $_[0] eq "HASH") {
20 0         0 %opts = @{$_[0]};
  0         0  
21             }
22              
23 3         7 my ($PIC, $CODE, $OUTPUT);
24              
25 3 50 33     17 if (defined $opts{"debug"} && $opts{"debug"}) {
26 0         0 $DEBUG = 1;
27             }
28              
29 3 50       39 if (defined $opts{"art_file"}) {
    50          
    50          
30 0         0 my $fh;
31 0         0 local $/ = undef;
32 0 0       0 open($fh, "<", $opts{"art_file"}) || croak "Invalid art_file specification: $!\n";
33 0         0 $PIC = <$fh>;
34 0         0 close $fh;
35             } elsif (defined $opts{"art_string"}) {
36 0         0 $PIC = $opts{"art_string"};
37             } elsif (defined $opts{"art"}) {
38 3         5 $PIC = $opts{"art"};
39             } else {
40 0         0 croak "Invalid spec. Must specify art, art_file, or art_string \n";
41             }
42 3         7 $Acme::AsciiArtinator::PIC = $PIC;
43              
44 3 50       38 if (defined $opts{"code_file"}) {
    50          
    50          
45 0         0 my $fh;
46 0         0 local $/ = undef;
47 0 0       0 open($fh, "<", $opts{"code_file"}) || croak "Invalid code_file specification: $!\n";
48 0         0 $CODE = <$fh>;
49 0         0 close $fh;
50             } elsif ($opts{"code_string"}) {
51 0         0 $CODE = $opts{"code_string"};
52             } elsif ($opts{"code"}) {
53 3         7 $CODE = $opts{"code"};
54             } else {
55 0         0 croak "Invalid spec. Must specify code, code_file, or code_string \n";
56             }
57              
58 3 50       11 if (defined $opts{"output"}) {
59 0         0 $OUTPUT = $opts{"output"};
60             } else {
61 3 50       11 print STDERR "Output will go to \"ascii-art.pl\"\n" if $DEBUG;
62 3         7 $OUTPUT = "ascii-art.pl";
63             }
64              
65 3 50       10 if (defined $opts{"compile-check"}) {
66 0         0 my $fh;
67 0         0 open($fh, ">", "ascii-art.$$.pl");
68 0         0 print $fh $CODE;
69 0         0 close $fh;
70              
71 0         0 my $c1 = &compile_check("ascii-art.$$.pl");
72 0         0 unlink "ascii-art.$$.pl";
73 0 0       0 if ($c1 > 0) {
74 0         0 croak "Initial code in ",$opts{"code"},$opts{"code_string"},
75             $opts{"code_file"}," does not compile!\n";
76             }
77             }
78              
79 3         7 my $ntest = 1;
80 3   66     37 while (defined $opts{"test_argv$ntest"} || defined $opts{"test_input$ntest"}) {
81 3         13 my (@test_argv, @test_stdin) = ();
82              
83 3 50       19 @test_argv = @{$opts{"test_argv$ntest"}} if defined $opts{"test_argv$ntest"};
  3         22  
84 3 100       22 @test_stdin = @{$opts{"test_input$ntest"}} if defined $opts{"test_input$ntest"};
  2         9  
85 3         6 my $fh;
86 3 50       410 if (open($fh, ">", "ascii-art-test-$ntest-$$.pl")) {
87 3         58 print $fh $CODE;
88 3         163 close $fh;
89              
90 3         10 my $output = "";
91 3 100       13 if (defined $opts{"test_input$ntest"}) {
92 2         136 open($fh, ">", "ascii-art-test-$ntest-$$.stdin");
93 2         17 print $fh @test_stdin;
94 2         37 close $fh;
95 2 50       7 print qq{Running test: $^X ascii-art-test-$ntest-$$.pl @test_argv < ascii-art-test-$ntest-$$.stdin\n} if $DEBUG;
96 2         23503 $output = qx{$^X ascii-art-test-$ntest-$$.pl @test_argv < ascii-art-test-$ntest-$$.stdin};
97 2         475 unlink "ascii-art-test-$ntest-$$.stdin";
98             } else {
99 1         137 print qq{Running test: $^X ascii-art-test-$ntest-$$.pl @test_argv\n};
100 1         7442 $output = qx{$^X ascii-art-test-$ntest-$$.pl @test_argv};
101             }
102 3         1396 print "Ran pre-test # $ntest with argv: \"@test_argv\", stdin: \"@test_stdin\"\n";
103              
104 3         39 $Acme::AsciiArtinator::TestOutput[$ntest] = $output;
105 3         1081 unlink "ascii-art-test-$ntest-$$.pl";
106             } else {
107 0         0 carp "Could not write code to disk in order to run pre-test.\n";
108             }
109             } continue {
110 3         308 $ntest++;
111             }
112              
113              
114             ###############################################
115              
116 3   50     54 my $max_tries = $opts{"retry"} || 100;
117              
118              
119 3         3578 my @tokens = &asciiindex_code($CODE);
120 3         17 my @contexts = @asciiartinate::contexts;
121 3         13 my @blocks = &asciiindex_art($PIC);
122              
123 3         7 my $ipad;
124 3         16 for ($ipad = 0; $ipad < $max_tries; $ipad++) {
125 3         2063 print "\n\n\n\nPad try # $ipad\n\n\n\n";
126              
127 3         32 my ($newt,$newc) = &pad(\@tokens, \@contexts, \@blocks);
128 3 50       12 if (defined $newc) {
129              
130 3         11 for (my $i=0; $i<@$newt; $i++) {
131 70         9407 print $newt->[$i], "\t", $newc->[$i], "\n";
132             }
133              
134 3         60 @tokens = @$newt;
135              
136 3 50       378 if ($opts{"filler"} != 0) {
137 0         0 &tweak_padding($opts{"filler"}, \@tokens, \@contexts);
138             }
139              
140 3         21 print_code_to_pic($PIC, @tokens);
141              
142 3         8 my $fh;
143 3         454 open($fh, ">", $OUTPUT);
144 3         22 select $fh;
145 3         15 print_code_to_pic($PIC, @tokens);
146 3         10 select STDOUT;
147 3         184 close $fh;
148              
149 3         34 my $c1 = &compile_check($OUTPUT);
150 3 50       57 if ($c1 > 0) {
151 0         0 croak "Artinated code does not compile! Darn.\n";
152 0         0 exit $c1 >> 8;
153             }
154              
155             ##################################################
156             #
157             # artination complete
158             #
159             ##################################################
160              
161 3         303 open($fh,"<", $OUTPUT);
162 3         295 my @output = <$fh>;
163 3         50 close $fh;
164              
165             # test output
166             #
167             # make sure artinated code produces same outputs
168             # as the original code on the test cases.
169             #
170 3         358 $ntest = 1;
171 3 100       37 if (defined $opts{"test_argv1"}) {
172 2         873 print "Running post-tests on artinated code\n";
173             }
174 3   66     415 while (defined $opts{"test_argv$ntest"} || defined $opts{"test_input$ntest"}) {
175 3         14 my (@test_argv, @test_stdin) = ();
176              
177 3         852 print "Testing output # $ntest:\n";
178              
179 3 50       58 @test_argv = @{$opts{"test_argv$ntest"}} if defined $opts{"test_argv$ntest"};
  3         53  
180 3 100       36 @test_stdin = @{$opts{"test_input$ntest"}} if defined $opts{"test_input$ntest"};
  2         30  
181 3         16 my $fh;
182 3 50       34 next if !defined $Acme::AsciiArtinator::TestOutput[$ntest];
183              
184 3         16 my $output = "";
185 3 100       36 if (defined $opts{"test_input$ntest"}) {
186 2         661 open($fh, ">", "ascii-art-test-$ntest-$$.stdin");
187 2         36 print $fh @test_stdin;
188 2         244 close $fh;
189 2         30357 $output = qx{$^X "$OUTPUT" @test_argv < ascii-art-test-$ntest-$$.stdin};
190 2         396 unlink "ascii-art-test-$ntest-$$.stdin";
191             } else {
192 1         11370 $output = qx{$^X "$OUTPUT" @test_argv};
193             }
194 3         2291 print "Ran post-test # $ntest with argv: \"@test_argv\", stdin: \"@test_stdin\"\n";
195            
196 3 50       78 if ($output eq $Acme::AsciiArtinator::TestOutput[$ntest]) {
197 3         917 print "Post-test # $ntest: PASS\n";
198 3         121 $Acme::AsciiArtinator::TestResult[$ntest] = "PASS";
199             } else {
200 0         0 print "Post-test # $ntest: FAIL\n";
201 0         0 $Acme::AsciiArtinator::TestResult[$ntest] = "FAIL";
202 0         0 print STDERR "-- " x 13, "\n";
203 0         0 print STDERR "Original results for test # $ntest:\n";
204 0         0 print STDERR "-- " x 7, "\n";
205 0         0 print STDERR $Acme::AsciiArtinator::TestOutput[$ntest];
206 0         0 print STDERR "\n", "-- " x 13, "\n";
207 0         0 print STDERR "Final results for test # $ntest:\n";
208 0         0 print STDERR $output;
209 0         0 print STDERR "\n", "-- " x 13, "\n\n";
210             }
211             } continue {
212 3         841 $ntest++;
213             }
214 3         454 return @output;
215             }
216             }
217              
218 0 0       0 if ($ipad >= $max_tries) {
219 0         0 croak "The ASCII Artinator was unable to embed your code in the picture ",
220             "after $max_tries tries.\n";
221             }
222             }
223              
224             #
225             # run a file containing Perl code for a Perl compilation check
226             #
227             sub compile_check {
228 3     3 1 10 my ($file) = @_;
229 3         396 print "\n";
230 3         342 print "- " x 20, "\n";
231 3         408 print "Compile check for $file:\n";
232 3         336 print "- " x 20, "\n";
233 3         45646 print `$^X -cw "$file"`;
234 3         1457 print "- " x 20, "\n";
235 3         162 return $?;
236             }
237              
238             sub tweak_padding {
239 0     0 0 0 my ($filler, $tref, $cref) = @_;
240              
241             # TODO: if there are many consecutive characters of padding
242             # in the code, we can improve its appearance by
243             # inserting some quoted text in void context.
244              
245             }
246              
247             #############################################################################
248             #
249             # code tokenization -- split code into tokens that should
250             # not be further divisible by whitespace
251             #
252              
253             # You know that this [decompiling Perl code] is impossible, right ?
254             # http://www.perlmonks.org/index.pl?node_id=44722
255              
256             my @token_keywords = qw(&&= ||= <<= >>= <=> ... **= //=
257             && || ++ -- == != <= >= -> ** =~ !~
258             <= >= => .. += -= *= /= %= |= &= ^= << >> .= <> //);
259              
260             # //= is an operator in perl 5.10, I believe
261             # // is usually a regular expression, or a perl 5.10 operator
262              
263             my %sigil = qw($ 1 @ 2 % 3 & 4 & 0);
264              
265             #
266             # does the current string begin with an "operator keyword"?
267             # if so, return it
268             #
269             sub find_token_keyword {
270 59     59 0 190 my ($q) = @_;
271 59         112 foreach my $k (@token_keywords) {
272 2116 100       5793 if (substr($q,0,length($k)) eq $k) {
273 6         39 return $k;
274             }
275             }
276 53         207 return;
277             }
278              
279             #
280             # find position of a scalar in an array.
281             #
282             sub STRPOS {
283 4     4 0 22 my ($word, @array) = @_;
284 4         9 my $pos = -1;
285 4         15 for (my $i=0; $i<@array; $i++) {
286 66 50       689 $pos = $i if $array[$i] =~ /$word/;
287             }
288 4         20 return $pos;
289             }
290              
291             #
292             # what does the "/" token that we just encountered mean?
293             # this is a hard game to play.
294             # see http://www.perlmonks.org/index.pl?node_id=44722
295             #
296             sub regex_or_divide {
297 10     10 0 20 my ($tokenref, $contextref) = @_;
298 10         80 my @tokens = @$tokenref;
299 10         44 my @contexts = @$contextref;
300              
301             # regex is expected following an operator,
302             # at the beginning of a statement
303             # divide is expected following a scalar,
304             # or any token that could complete an expression
305              
306 10         16 my $c = $#contexts;
307 10         36 $c-- while $contexts[$c] eq "whitespace";
308 10 50       29 return "regex" if $contexts[$c] eq "operator";
309 10 50 33     42 return "regex" if $tokens[$c] eq ";" && $tokens[$c-1] ne "SIGIL";
310              
311 10         287 return "divide";
312             }
313              
314             sub tokenize_code {
315 16     16 0 4937 my ($INPUT) = @_;
316 16         43 local $" = '';
317 16         5047 my @INPUT = grep { /[^\n]/ } split //, $INPUT;
  369         919  
318              
319             # tokens are:
320             # quotes strings
321             # numeric literals
322             # regular expression specifications
323             # except with //x and s///x
324             # alphanumeric strings
325             # punctuation strings from @token_keywords
326             #
327              
328 16         40 my ($i, $j, $Q, @tokens, $token, $sigil, @contexts, @blocks);
329              
330 16         24 $sigil = 0;
331 16         68 for ($i = 0; $i < @INPUT; $i++) {
332 150         206 $_ = $INPUT[$i];
333 150         615 $Q = "@INPUT[$i..$#INPUT]";
334              
335 150 50 66     452 print STDERR "\$Q = ", substr($Q,0,8), "... SIGIL=$sigil\n" if $_ eq "q" && $DEBUG;
336              
337             # $# could be "the output format of printed numbers"
338             # or it could be the start of an expression like $#X or $#{@$X}
339             # in the latter case we need $# + one more token to be contiguous
340 150 50 33     743 if ($Q =~ /^\$\#\{/ || $Q =~ /^\$\#\w+/) {
341 0         0 $token = $&;
342 0         0 push @tokens, $token;
343 0         0 push @contexts, "\$# operator";
344 0         0 $i = $i - 1 + length $token;
345 0         0 $sigil = 0;
346 0         0 next;
347             }
348              
349              
350 150 100 66     466 if ($sigil{$_} && $Q !~ /^\$\#/) {
351 23         41 $sigil = $sigil{$_};
352 23         38 push @tokens, $_;
353 23         36 push @contexts, "SIGIL";
354 23         90 next;
355             }
356              
357 127 100 66     4258 if (!$sigil && ($_ eq "'" || $_ eq '"' ||
    100 66        
    50 100        
    100 66        
    100 66        
    100 66        
    100 66        
      66        
      66        
358             $_ eq "/" && regex_or_divide(\@tokens,\@contexts) eq "regex")) {
359             # walk through @INPUT looking for the end of the string
360             # manage a boolean $escaped variable handy to allow
361             # escaped strings inside strings.
362              
363 14         41 my $escaped = 0;
364 14         25 my $terminator = $_;
365 14         43 for($j = $i + 1; $j <= $#INPUT; $j++) {
366 80 100       156 if ($INPUT[$j] eq "\\") {
367 3         6 $escaped = !$escaped;
368 3         8 next;
369             }
370 77 100 66     415 last if $INPUT[$j] eq $terminator && !$escaped;
371 63         135 $escaped = 0;
372             }
373 14         185 my $token = "@INPUT[$i..$j]";
374              
375 14 0 0     39 if ($_ eq "/" && (length $token > 30 || $j >= $#INPUT)) {
      33        
376             # this regex is pretty long. Maybe we made a mistake.
377 0   0     0 my $toke2 = find_token_keyword($Q) || "/";
378 0         0 $token = $toke2;
379 0         0 $_ = "/!";
380             }
381              
382              
383 14         27 push @tokens, $token;
384 14 50       45 if ($_ eq "/!") {
    50          
385 0         0 push @contexts, "misanalyzed regex or operator";
386             } elsif ($_ eq "/") {
387 0         0 push @contexts, "regular expression C ///";
388             } else {
389 14         45 push @contexts, "quoted string";
390             }
391 14         24 $i = $j;
392              
393             } elsif (!$sigil && $Q =~ /^[0-9]*\.{0,1}[0-9]+([eE][-+]?[0-9]+)?/) {
394              
395             # if first char starts a numeric literal, include all characters
396             # from the number in the token
397              
398            
399              
400 4         8 $token = $&;
401 4         25 push @tokens, $token;
402 4         7 push @contexts, "numeric literal A";
403 4         8 $i = $i - 1 + length $token;
404              
405             } elsif (!$sigil && $Q =~ /^[0-9]+\.{0,1}[0-9]*([eE][-+]?[0-9]+)?/) {
406              
407 0         0 $token = $&;
408 0         0 push @tokens, $token;
409 0         0 push @contexts, "numeric literal B";
410 0         0 $i += length $token;
411              
412             } elsif (!$sigil && ($Q =~ /^m\W/ || $Q =~ /^qr\W/ || $Q =~ /^q[^\w\s]/ || $Q =~ /^qq\W/)) {
413 3 100       11 $j = $Q =~ /^q[rq]\W/ ? $i + 3 : $i + 2;
414              
415 3         7 my $terminator = $INPUT[$j - 1];
416 3         6 $terminator =~ tr!{}<>[]{}()!}{><][}{)(!;
417              
418              
419 3         4 my $escaped = 0;
420 3         11 for(; $j <= $#INPUT; $j++) {
421 22 50       36 if ($INPUT[$j] eq "\\") {
422 0         0 $escaped = !$escaped;
423 0         0 next;
424             }
425 22 100 66     52 last if $INPUT[$j] eq $terminator && !$escaped;
426             # XXX - if regex has 'x' modifier,
427             # then
428 19         38 $escaped = 0;
429             }
430 3         12 push @tokens, "@INPUT[$i..$j]";
431 3         9 push @contexts, "regular expression A /$terminator/";
432 3         4 $i = $j;
433              
434             } elsif (!$sigil && ($Q =~ /^s\W/ || $Q =~ /^y\W/ || $Q =~ /^tr\W/)) {
435 5 50       16 $j = $_ eq "t" ? $i + 3 : $i + 2;
436 5         7 my $terminator = $INPUT[$j-1];
437 5         10 $terminator =~ tr!{}<>[]{}()!}{><][}{)(!;
438 5         7 my $escaped = 0;
439 5         6 my $terminators_found = 0;
440 5         11 for (; $j <= $#INPUT; $j++) {
441 86 50       147 if ($INPUT[$j] eq "\\") {
442 0         0 $escaped = !$escaped;
443 0         0 next;
444             }
445 86 100 66     178 if ($INPUT[$j] eq $terminator && !$escaped) {
446 10 100       21 if ($terminators_found++) {
447 5         9 last;
448             }
449             }
450 81         151 $escaped = 0;
451             }
452 5         25 push @tokens, "@INPUT[$i..$j]";
453 5         13 push @contexts, "regular expression B /$terminator/";
454 5         9 $i = $j;
455              
456             } elsif ($Q =~ /^[a-zA-Z_]\w*/) {
457              
458              
459 42         103 $token = $&;
460              
461             # "T"x90 should be ["T",x,90] not ["T",x90]
462             # x90 should be x,90 when previous token is a scalar
463 42 100       97 if ($token =~ /^x\d+$/) {
464 2 50 33     23 if ($tokens[-1] =~ /^[\'\"]/ || $tokens[-1] eq ")"
      33        
465             || $contexts[-1] =~ /name/) {
466 2         12 $token = "x";
467             }
468             }
469              
470 42         83 push @tokens, $token;
471 42 100       110 if ($sigil) {
    100          
472 19         42 push @contexts, "name";
473             } elsif ($contexts[-1] =~ /regular expression ([ABC]) \/(.)\//) {
474 5         8 push @contexts, "regular expression modifier";
475 5         8 my $regex_type = $1;
476 5         10 my $terminator = $2;
477              
478             # with some modifiers we can be more flexible with the earlier tokens ...
479             # e - second pattern is an expression that can be flexible
480             # x - first and/or second pattern can contain whitespace
481              
482 5 100 66     26 if (0 && $token =~ /e/ && $token =~ /x/ && $tokens[-2] =~ /^s/) {
    100          
483             $DB::single=1;
484             pop @tokens;
485             pop @contexts;
486             my $regex = pop @tokens;
487             my $regex_context = pop @contexts;
488             my $terminator2 = $terminator;
489             $terminator2 =~ tr/])}>/[({})]
490             my $t1 = index($regex,$terminator2);
491             my $t2 = index($regex,$terminator,$t1+1);
492              
493             push @tokens, substr($regex,0,$t1+1);
494             push @contexts, "regular expression x /$terminator/";
495              
496             for (my $t=$t1+1; $t<=$t2; $t++) {
497             if (substr($regex,$t,1) =~ /\S/) {
498             push @tokens, substr($regex,$t,1);
499             push @contexts, "content of regex/x";
500             }
501             }
502             $i -= length($token) + length($regex) - $t2 - 1;
503              
504             # positions $i to the start of the 2nd pattern,
505             # which can be tokenized as a perl expression.
506             # Hopefully the terminator can be recognized
507              
508 0         0 } elsif ($token =~ /x/) {
509 3         4 pop @tokens;
510 3         5 pop @contexts;
511 3         5 my $regex = pop @tokens;
512 3         4 my $regex_context = pop @contexts;
513 3         4 my $terminator2 = $terminator;
514 3         5 $terminator2 =~ tr/])}>/[({
515 3         5 my $t1 = index($regex,$terminator2);
516 3         7 my $t2 = index($regex,$terminator,$t1+1);
517              
518 3         6 push @tokens, substr($regex,0,$t1+1);
519 3         6 push @contexts, "regular expression x /$terminator/";
520              
521 3         9 for (my $t=$t1+1; $t<=$t2; $t++) {
522 35 100       95 if (substr($regex,$t,1) =~ /\S/) {
523 27         37 push @tokens, substr($regex,$t,1);
524 27         50 push @contexts, "content of regex/x";
525             }
526             }
527 3         6 $i -= length($token) + length($regex) - $t2 - 1;
528              
529             } elsif ($token =~ /e/ && $tokens[-2] =~ /^s/) {
530 1 50       3 if ($regex_type eq "B") { # s///, tr///, y///
531 1         2 pop @tokens;
532 1         2 pop @contexts;
533 1         2 my $regex = pop @tokens;
534 1         1 my $regex_context = pop @contexts;
535 1         2 my $terminator2 = $terminator;
536 1         2 $terminator2 =~ tr/])}>/[({
537 1         3 my $t1 = index($regex,$terminator2);
538 1         3 my $t2 = index($regex,$terminator,$t1+1);
539              
540 1         2 push @tokens, substr($regex,0,$t2+1);
541 1         3 push @contexts, "regular expression b /$terminator/";
542 1         3 $i -= length($token) + length($regex) - $t2 - 1;
543             }
544             }
545              
546             } else {
547 18         50 push @contexts, "alphanumeric literal"; # bareword? name? label? keyword?
548             }
549 42         73 $i = $i -1 + length $token;
550              
551             } elsif (($token = find_token_keyword($Q)) && !$sigil) {
552              
553 6         10 push @tokens, $token;
554 6         12 push @contexts, "operator";
555 6         9 $i = $i - 1 + length $token;
556              
557             } else {
558              
559 53         97 push @tokens, $_;
560              
561 53 100 66     1107 if ($sigil) {
    100 66        
    100 66        
    100          
    100          
    50          
    50          
562 4         7 push @contexts, "name";
563             } elsif (/\s/) {
564 6         13 push @contexts, "whitespace";
565             } elsif (/;/ && !$sigil) {
566 9         22 push @contexts, "end of statement";
567             } elsif (/\//) {
568 8         16 push @contexts, "operator or misanalyzed regex";
569             } elsif (/[\+\-\*\/\%\^\|\&\!\~\?\:\.]/) {
570 6         13 push @contexts, "operator";
571              
572             } elsif (/\{/ && $sigil) {
573 0         0 push @contexts, "name container";
574             } elsif (/\}/ && STRPOS("name contained",@contexts) > STRPOS("name decontainer",@contexts)) {
575 0         0 push @contexts, "name decontainer";
576              
577             } else {
578 20         50 push @contexts, "unknown";
579             }
580             }
581              
582 127         423 $sigil = 0;
583             }
584              
585 16 50       37 if ($DEBUG) {
586 0         0 print "- " x 20,"\n";
587 0         0 my @c = @contexts;
588 0         0 foreach $token (@tokens) {
589 0         0 my $cc = shift @c;
590 0         0 print $token,"\t",$cc,"\n";
591             }
592 0         0 print "- " x 20,"\n";
593 0         0 print "Total token count: ", scalar @tokens, "\n";
594             }
595              
596 16         75 @asciiartinate::contexts = @contexts;
597 16         234 @asciiartinate::tokens = @tokens;
598              
599 16         192 @tokens;
600             }
601              
602             sub asciiindex_code {
603 3     3 0 100 my ($X) = @_;
604 3         17 my $endpos = index($X,"\n__END__\n");
605 3 50       28 if ($endpos >= 0) {
606 0         0 substr($X,$endpos) = "\n";
607             }
608 3         20 $X =~ s/\n\s*#[^\n]*\n/\n/g;
609 3         12 $X =~ s/\n\s*#[^\n]*\n/\n/g;
610 3         26 &tokenize_code($X);
611             }
612              
613             #############################################################################
614              
615             sub tokenize_art {
616 3     3 0 6 my ($INPUT) = @_;
617 3         337 my @INPUT = split //, $INPUT;
618              
619 3         17 my $white = 1;
620 3         5 my $block_size = 0;
621 3         6 my @blocks = ();
622 3         11 foreach my $char (@INPUT) {
623 326 100 100     1411 if ($char eq " " || $char eq "\n" || $char eq "\t") {
      66        
624 117 100       309 if ($block_size > 0) {
625 22         35 push @blocks, $block_size;
626 22         24 $block_size = 0;
627             }
628              
629             # certain token combos like the special Perl vars
630             # ($$ $" $| $! etc.) can be separated by spaces and tabs
631             # but not by newlines! Let's use block of size 0 to
632             # indicate where a newline is.
633              
634 117 100       225 if ($char eq "\n") {
635 25         41 push @blocks, 0;
636             }
637             } else {
638 209         253 ++$block_size;
639             }
640             }
641 3 50       14 if ($block_size > 0) {
642 3         5 push @blocks, $block_size;
643             }
644 3         55 return @blocks;
645             }
646              
647             sub asciiindex_art {
648 3     3 0 7 my ($X) = @_;
649 3         12 &tokenize_art($X);
650             }
651              
652             #
653             # replace darkspace on the pic with characters from the code
654             #
655             sub print_code_to_pic {
656 6     6 0 45 my ($pic, @tokens) = @_;
657 6         19 local $" = '';
658 6         31 my $code = "@tokens";
659 6         112 my @code = split //, $code;
660              
661 6 100       74 $pic =~ s/(\S)/@code==0?"#":shift @code/ge;
  418         1137  
662              
663 6         1953 print $pic;
664             }
665              
666              
667             #
668             # find misalignment between multi-character tokens and blocks
669             # and report position where additional padding is needed for
670             # alignment
671             #
672             sub padding_needed {
673 13     13 0 13 my @tokens = @{$_[0]};
  13         153  
674 13         19 my @contexts = @{$_[1]};
  13         76  
675 13         16 my @blocks = @{$_[2]};
  13         45  
676 13         19 my $ib = 0;
677 13         13 my $tc = 0;
678 13         28 my $bc = $blocks[$ib++];
679 13         17 my $it = 0;
680 13         33 while ($bc == 0) {
681 13         60 $bc = $blocks[$ib++];
682 13 50       45 if ($ib > @blocks) {
683 0         0 print "Error: picture is not large enough to contain code!\n";
684              
685 0         0 print map {(" ",length $_)} @tokens;
  0         0  
686 0         0 print "\n\n@blocks\n";
687              
688 0         0 return [-1,-1];
689             }
690             }
691 13         24 foreach my $t (@tokens) {
692 184         185 my $tt = length $t;
693 184 50       304 defined $tt or print "! \$tt is not defined! \$it=$it \$ib=$ib\n";
694 184 50       514 defined $bc or print "! \$bc is not defined! \$it=$it \$ib=$ib \$tt=$tt\n";
695 184 100       319 if ($tt > $bc) {
696 10 50       29 if ($DEBUG) {
697 0         0 print "Need to pad by $bc spaces at or before position $tc\n";
698             } else {
699 10         1211 print "\rNeed to pad by $bc spaces at or before position $tc ";
700             }
701 10         113 return [$it, $bc];
702             }
703              
704 174         157 $bc -= $tt;
705              
706             #
707             # for regular Perl variables ( "$x", "@bob" ), it is OK to split
708             # the sigil and the var name with any whitespace ("$ x", "@\n\tbob").
709             # For special Perl vars ( '$"', "$/", "$$" ), it is OK to split
710             # with spaces and tabs but not with newlines.
711             #
712             # Check for this condition here and say that padding is needed if
713             # a special var is currently aligned on a newline.
714             #
715 174 0 66     643 if ($bc == 0 && $blocks[$ib] == 0 && $tokens[$it] eq "\$"
      66        
      33        
      33        
      0        
      0        
716             && $contexts[$it] eq "SIGIL" && $contexts[$it+1] eq "name"
717             && length($tokens[$it+1]) == 1 && $tokens[$it+1] =~ /\W/) {
718              
719 0         0 warn "\$tt > \$bc but padding still needed: \n",
720             (join " : ", @tokens[0 .. $it+1]), "\n",
721             (join " : ", @contexts[0 .. $it+1]), "\n",
722             (join " : ", @blocks[0 .. $ib+1]), "\n";
723              
724 0         0 return [$it, 1] if 1;
725             }
726              
727              
728 174         341 while ($bc == 0) {
729 30         34 $bc = $blocks[$ib++];
730 30 50       79 if ($ib > @blocks) {
731 0         0 print "Error: picture is not large enough to contain code!\n";
732              
733 0         0 print map {(" ",length $_)} @tokens;
  0         0  
734 0         0 print "\n\n@blocks\n";
735              
736 0         0 return [-1,-1];
737             }
738             }
739 174         273 $tc += length $t;
740 174         203 $it++;
741             }
742 3         22 return;
743             }
744              
745             #
746             # choose a random number between 0 and n-1,
747             # with the distribution heavily weighted toward
748             # the high end of the range
749             #
750             sub hi_weighted_rand {
751 6     6 0 10 my $n = shift;
752 6         7 my (@p, $r, $p);
753 6         14 for ($r = 1; $r <= $n; $r++) {
754 22         61 push @p, $p += $r * $r * $r;
755             }
756 6         82 $p = int(rand() * $p);
757 6         17 for ($r = 1; $r <= @p; $r++) {
758 22 100       61 return $r if $p[$r-1] >= $p;
759             }
760 0         0 return $n;
761             }
762              
763             #
764             # look for opportunity to insert padding into the
765             # code at the specified location
766             #
767             sub try_to_pad {
768 61     61 0 88 my ($pos, $npad, $tref, $cref) = @_;
769              
770             # padding techniques:
771             # X SIGIL name ---> SIGIL { name }
772             # XXX ---> ( XXX )
773             # for XXX in (numeric literal,quoted string)
774             # XXX ; ---> XXX ;;
775             # for XXX in (quoted string,numeric literal,regular expression
776             # <> operator, ")"
777             # X } ---> ; } for } that ends a code BLOCK
778             # X ; } ---> ; ; }
779             # inserting strings in void context after semi-colons (for howmuch > 2)
780             # = expr ---> = 0|| expr (if expr does not have ops with lower prec than ||)
781             # = expr ---> = 1&& expr (if expr does not have ops with lower prec than &&)
782             # = expr ---> = 0 or expr , = 0 xor expr
783              
784 61         170 my $t = 0;
785 61         62 my $it = $pos;
786              
787 61 50       259 print STDERR "Trying to pad at [$it]: ", join " :: ", @{$tref}[$it-1 .. $it+1], "\n" if $DEBUG;
  0         0  
788 61 50       93 print STDERR "Contexts: ", join " :: ", @{$cref}[$it-1 .. $it+1], "\n\n" if $DEBUG;
  0         0  
789              
790 61         73 my $z = rand() * 0.5;
791 61 50       99 $z = 0.45 if $it == 0;
792 61 50 66     297 if ($z < 0.25 && $npad > 1) {
    50 0        
    0          
    0          
793              
794             # convert SIGIL name --> SIGIL { name }
795              
796 0 0 0     0 if ($cref->[$it] eq "name" && $cref->[$it-1] eq "SIGIL") {
797 0 0       0 print STDERR "Padding name $tref->[$it] at pos $it\n" if $DEBUG;
798              
799 0         0 splice @$tref, $it+1, 0, "}";
800 0         0 splice @$tref, $it, 0, "{";
801 0         0 splice @$cref, $it+1, 0, "filler";
802 0         0 splice @$cref, $it, 0, "filler";
803 0         0 return 2;
804             }
805              
806             } elsif ($z < 0.50) {
807              
808             # try to pad the beginning of a statement with filler
809              
810 61 100 100     873 if ($it == 0 || ($tref->[$it-1] eq ";" && $cref->[$it-1] eq "end of statement")
      33        
      33        
      66        
      33        
      66        
811             || ($tref->[$it] eq ";" && $cref->[$it] eq "end of statement")
812             || $cref->[$it] eq "flexible filler"
813             || $cref->[$it-1] eq "flexible filler") {
814              
815 10 50       103 print STDERR "Padding with flexible filler x $npad at pos $it\n" if $DEBUG;
816 10         30 while ($npad-- > 0) {
817 10         32 splice @$tref, $it, 0, ";";
818 10         20 splice @$cref, $it, 0, "flexible filler";
819 10         50 return $_[1];
820             }
821             }
822             } elsif ($z < 0.5 && $npad > 1) {
823              
824             # reserved for future use ?
825              
826             } elsif ($z < 0.75) {
827              
828             # this space intentionally left blank
829              
830             }
831 51         181 return 0;
832             }
833              
834             #
835             # find all misalignments and insert padding into the code
836             # until all code is aligned or until the padded code is
837             # too large for the pic.
838             #
839             sub pad {
840 3     3 0 8 my @tokens = @{$_[0]};
  3         30  
841 3         7 my @contexts = @{$_[1]};
  3         16  
842 3         5 my @blocks = @{$_[2]};
  3         10  
843              
844 3         6 my $nblocks = 0;
845 3         8 map { $nblocks += $_ } @blocks;
  50         59  
846              
847 3         6 my ($needed, $where, $howmuch);
848 3         17 while ($needed = padding_needed(\@tokens,\@contexts,\@blocks)) {
849 10         33 ($where,$howmuch) = @$needed;
850 10 50 33     30 if ($where < 0 && $howmuch < 0) {
851 0 0       0 if ($DEBUG) {
852 0         0 print_code_to_pic($Acme::AsciiArtinator::PIC,@tokens);
853 0         0 sleep 1;
854             }
855 0         0 return;
856             }
857              
858 10 100       97 my $npad = $howmuch > 1 ? $howmuch - hi_weighted_rand($howmuch-1) : $howmuch;
859 10   33     78 while (rand() > 0.95 && $where > 0) {
860 0         0 $where--;
861             }
862              
863 10   66     57 while ($where >= 0 && !try_to_pad($where, $npad, \@tokens, \@contexts)) {
864 51 100       201 $where-- if rand() > 0.4;
865             }
866              
867 10         52 my $tlength = 0;
868 10         16 map { $tlength += length $_ } @tokens;
  266         498  
869 10 50       39 if ($tlength > $nblocks) {
870 0         0 print "Padded length exceeds space length.\n";
871              
872 0 0       0 if ($DEBUG) {
873 0         0 print_code_to_pic($Acme::AsciiArtinator::PIC, @tokens);
874 0         0 print "\n\n";
875 0         0 sleep 1;
876             }
877              
878 0         0 return;
879             }
880             }
881 3         42 ([ @tokens ], [ @contexts ]);
882             }
883              
884              
885              
886             #
887             # can run from command line:
888             #
889             # perl Acme/AsciiArtinator.pm [-d] art-file code-file [output-file]
890             #
891             if ($0 =~ /AsciiArtinator.pm/) {
892             my $debug = 0;
893             my $compile_check = 1;
894             my @opts = grep { /^-/ } @ARGV;
895            
896             @ARGV = grep { !/^-/ } @ARGV;
897             foreach my $opt (@opts) {
898             $debug = 1 if $opt eq '-d';
899             # $compile_check = 1 if $opt eq '-c';
900             }
901              
902             asciiartinate( art_file => $ARGV[0] ,
903             code_file => $ARGV[1] ,
904             output => $ARGV[2] || "ascii-art.pl",
905             debug => $debug ,
906             'compile-check' => $compile_check );
907             }
908              
909             1;
910              
911             __END__