File Coverage

blib/lib/App/Mowyw.pm
Criterion Covered Total %
statement 409 495 82.6
branch 78 136 57.3
condition 56 88 63.6
subroutine 51 56 91.0
pod 0 38 0.0
total 594 813 73.0


line stmt bran cond sub pod time code
1             package App::Mowyw;
2 11     11   149208 use strict;
  11         15  
  11         280  
3 11     11   33 use warnings;
  11         11  
  11         366  
4             #use warnings FATAL => 'all';
5              
6             our $VERSION = '0.8.0';
7              
8 11     11   3593 use App::Mowyw::Lexer qw(lex);
  11         15  
  11         547  
9 11     11   3324 use App::Mowyw::Datasource;
  11         20  
  11         255  
10              
11 11     11   7022 use File::Temp qw(tempfile);
  11         168393  
  11         570  
12 11     11   4343 use File::Compare;
  11         7367  
  11         434  
13 11     11   52 use Carp;
  11         12  
  11         456  
14 11     11   5899 use Storable qw(dclone);
  11         24053  
  11         611  
15 11     11   52 use Scalar::Util qw(reftype blessed);
  11         12  
  11         410  
16 11     11   4667 use File::Copy;
  11         17559  
  11         485  
17 11     11   5121 use Encode qw(encode decode);
  11         76009  
  11         683  
18 11     11   3982 use Config::File qw(read_config_file);
  11         18280  
  11         494  
19              
20 11     11   48 use Exporter qw(import);
  11         11  
  11         200  
21 11     11   5334 use Data::Dumper;
  11         42094  
  11         503  
22 11     11   51 use Carp qw(confess);
  11         12  
  11         29961  
23 11     11   45 binmode STDOUT, ':encoding(UTF-8)';
  11         12  
  11         70  
24              
25             our @EXPORT_OK = qw(
26             get_config
27             parse_file
28             process_dir
29             process_file
30             parse_str
31             parse_all_in_dir
32             );
33              
34             our %config = (
35             default => {
36             include => 'includes/',
37             source => 'source/',
38             online => 'online/',
39             postfix => '',
40             },
41             encoding => 'utf-8',
42             file_filter => [
43             [1, 10, qr{\..?htm}],
44             ],
45             );
46             $config{default}{menu} = $config{default}{include} . 'menu-';
47              
48             my $internal_error_message = "Please contact the Author at moritz\@faui2k3.org providing\nan example how to reproduce the error, including the complete error message";
49              
50             my @input_tokens = (
51             [ 'TAG_START', qr/\[\[\[\s*/],
52             [ 'TAG_START', qr/\[\%\s*/],
53             [ 'KEYWORD', qr/(?:
54             include
55             |menu
56             |system
57             |option
58             |item
59             |endverbatim
60             |verbatim
61             |comment
62             |setvar
63             |readvar
64             |synatxfile
65             |syntax
66             |endsyntax
67             |bind
68             |for
69             |endfor
70             |ifvar
71             |endifvar
72             )/x ],
73             [ 'TAG_END', qr/\s*\]\]\]/],
74             [ 'TAG_END', qr/\s*\%\]/],
75             [ 'BRACES_START', qr/\{\{/],
76             [ 'BRACES_END', qr/\}\}/],
77             );
78              
79             sub parse_all_in_dir {
80 1     1 0 17 my @todo = @_;
81 1         4 while (defined(my $fn = pop @todo)){
82 1 50       5 $fn .= '/' unless ($fn =~ m#/$#);
83 1 50       43 opendir my $DIR, $fn or die "Cannot opend directory '$fn' for reading: $!";
84 1         12 IW: while (my $f = readdir $DIR){
85             # ignore symbolic links and non-Readable files:
86 6 50       83 next IW if (-l $f);
87             # if we consider . and .., we loop infinetly.
88             # and while we are at ignoring, we can ignore a few
89             # other things as well ;-)
90 6 50 100     55 if (
      66        
      66        
      33        
91             $f eq '..'
92             or $f eq '.'
93             or $f eq '.svn'
94             or $f eq '.git'
95             or $f =~ m{(?:~|\.swp)$}){
96 2         7 next;
97             }
98 4         8 $f = $fn . $f;
99 4 50       42 if (-d $f){
100 0         0 push @todo, $f;
101 0         0 process_dir($f);
102             } else {
103 4         9 process_file($f);
104             }
105             }
106 1         17 closedir $DIR;
107             }
108             }
109              
110             sub process_dir {
111 0     0 0 0 my $fn = shift;
112 0         0 my $new_fn = get_online_fn($fn);
113 0         0 mkdir $new_fn;
114             }
115              
116             # strip leading and trailing whitespaces from a string
117             sub strip_ws {
118 44     44 0 39 my $s = shift;
119 44         176 $s =~ s/^\s+//g;
120 44         119 $s =~ s/\s+$//g;
121 44         52 return $s;
122             }
123              
124             sub escape {
125 6     6 0 7 my $str = shift;
126 6         13 my %esc = (
127             "\\" => '\\\\',
128             "\t" => '\t',
129             "\n" => '\n',
130             );
131 6         26 my $re = join '|', map quotemeta, keys %esc;
132 6         63 $str =~ s/($re)/$esc{$1}/g;
133 6         17 return $str;
134             }
135              
136             sub parse_error {
137 8     8 0 6 my $message = shift;
138 8         6 my @filenames = @{shift()};
  8         12  
139 8         8 my $token = shift;
140 8         14 my $str = "Parse error in file '$filenames[0]': $message\n";
141 8 100       11 if ($token) {
142 6         12 $str .= "in line $token->[3] near'" . escape($token->[0]) ."'\n";
143             }
144 8         17 for (@filenames[0..$#filenames]) {
145 8         38 $str .= " ...included from file '$_'\n";
146             }
147 8         969 confess $str;
148 0         0 exit 1;
149             }
150              
151             # parse sub: anything is treated as normal text that does not start or end a
152             # command
153             # the second (optional) arg contains a hash of additional tokens that are
154             # treated as plain text
155             sub p_text {
156 177     177 0 132 my $tokens = shift;
157 177         125 my %a;
158 177 100       238 %a = %{$_[0]} if ($_[0]);
  112         272  
159 177         156 my $str = "";
160 177         283 my %allowed_tokens = (
161             KEYWORD => 1,
162             UNMATCHED => 1,
163             );
164              
165 177   33     918 while ( $tokens
      66        
      66        
      33        
166             and $tokens->[0]
167             and $tokens->[0]->[0]
168             and ($allowed_tokens{$tokens->[0]->[0]}
169             or $a{$tokens->[0]->[0]})){
170              
171 56         53 $str .= $tokens->[0]->[1];
172 56         249 shift @$tokens;
173             }
174 177         541 return $str;
175             }
176              
177             # parse sub: parse an include statement.
178             # note that TAG_START and the keyword "include" are already stripped
179             sub p_include {
180 1     1 0 26 my $tokens = shift;
181 1         2 my $meta = shift;
182             # normally we'd expect an UNMATCHED token, but the user might choose
183             # a keyword as well as file name
184 1         3 my $fn = strip_ws(slurp_upto_token($tokens, 'TAG_END', $meta));
185 1         3 $fn = get_include_filename('include', $fn, $meta->{FILES}->[-1]);
186             # print Dumper $tokens;
187 1         6 my $m = my_dclone($meta);
188 1         1 unshift @{$m->{FILES}}, $fn;
  1         3  
189 1         3 return parse_file($fn, $m);
190             }
191              
192             # parse sub: parse a system statement.
193             sub p_system {
194 0     0 0 0 my $tokens = shift;
195 0         0 my $meta = shift;
196 0         0 my $fn = strip_ws(slurp_upto_token($tokens, 'TAG_END', $meta));
197 0 0       0 print STDERR "Executing external command '$fn'\n" unless $config{quiet};
198 0         0 my $tmp = `$fn`;
199 0         0 return ($tmp);
200             }
201              
202             # parse sub: parse a 'menu' statement.
203             # note that TAG_START and the keyword "menu" are already stripped
204             sub p_menu {
205 5     5 0 4 my $tokens = shift;
206 5         3 my $meta = shift;
207             # print Dumper $meta;
208 5         6 my $key = strip_ws(p_expect($tokens, "UNMATCHED", $meta));
209 5         15 my @words = split /\s+/, $key;
210 5         7 p_expect($tokens, "TAG_END", $meta);
211 5         5 my $menu_fn = shift @words;
212             # print "\nMenu: '$menu_fn'\n";
213 5         12 $menu_fn = get_include_filename('menu', $menu_fn, $meta->{FILES}->[-1]);
214             # print "Menu after frobbing: '$menu_fn'\n";
215              
216 5         29 my $m = my_dclone($meta);
217 5         3 push @{$m->{ITEMS}}, @words;
  5         9  
218 5         5 unshift @{$m->{FILES}}, $menu_fn;
  5         6  
219 5         7 return parse_file($menu_fn, $m);
220             }
221              
222             # parse sub: parse an 'option' statement
223             sub p_option {
224 3     3 0 3 my $tokens = shift;
225 3         3 my $meta = shift;
226 3         5 my $key = strip_ws(p_expect($tokens, "UNMATCHED", $meta));
227 3         10 my @words = split /\s+/, $key;
228 3         5 my $option_key = shift @words;
229 3         5 my $option_val = join " ", @words;
230 3         7 $meta->{OPTIONS}->{$option_key} = $option_val;
231 3         5 p_expect($tokens, "TAG_END", $meta);
232 3         25 return "";
233             }
234              
235             #parse sub: parse an "item" statement
236             sub p_item {
237 20     20 0 13 my $tokens = shift;
238 20         14 my $meta = shift;
239 20         23 my $content = p_expect($tokens, "UNMATCHED", $meta);
240 20         71 $content =~ s/^\s+//;
241 20         32 $content =~ m/^(\S+)/;
242 20         24 my $key = $1;
243 20         38 $content =~ s/^\S+//;
244              
245 20         25 my $m = my_dclone($meta);
246             # print Data::Dumper->Dump([$m]);
247 20 100 100     64 if ($meta->{ITEMS}->[0] and $meta->{ITEMS}->[0] eq $key){
248 5         4 shift @{$m->{ITEMS}};
  5         7  
249 5         7 $m->{CURRENT_ITEM} = $key;
250              
251             } else {
252 15         15 $m->{ITEMS} = [];
253 15         18 $m->{CURRENT_ITEM} = undef;
254             }
255 20         20 $m->{INSIDE_ITEM} = 1;
256 20         31 my $str = $content . parse_tokens($tokens, $m);
257 20         24 p_expect($tokens, "TAG_END", $meta);
258 20         143 return $str;
259              
260             }
261              
262             sub p_bind {
263 1     1 0 2 my ($tokens, $meta) = @_;
264 1         3 my $contents = strip_ws(slurp_upto_token($tokens, 'TAG_END', $meta));
265 1         5 my ($var, $rest) = split m/\s+/, $contents, 2;
266 1         4 my $string = qr{(
267             '[^'\\]*(?>\\.[^'\\]*)*'
268             |"[^"\\]*(?>\\.[^"\\]*)*'
269             |[^"']\S*
270             )}x;
271 1         3 my %options = parse_hash($rest, 'bind', $meta);
272              
273 1 50       3 if ($options{file}){
274 1         4 $options{file} = get_include_filename('include', $options{file}, $meta->{FILES}->[-1]);
275             }
276 1         7 $meta->{VARS}{$var} = App::Mowyw::Datasource->new(\%options);
277              
278 1         22 return '';
279             }
280              
281             sub p_for {
282 7     7 0 22 my ($tokens, $meta) = @_;
283 7         13 my $contents = strip_ws(slurp_upto_token($tokens, 'TAG_END', $meta));
284 7         20 my ($iter, $in, $datasource) = split m/\s+/, $contents;
285 7 100 100     26 if (!defined $datasource || lc $in ne 'in' ){
286             parse_error(
287             q{Can't parse for statement. Syntax is [% for iterator_var in datasource %] ... [% endfor %]},
288             $meta->{FILES},
289 4         8 $tokens->[0],
290             );
291             }
292 3         7 my $ds = $meta->{VARS}{$datasource};
293 3 100 66     14 if (!$ds || !blessed($ds)){
294 1         132 confess "'$datasource' is not defined or not a valid data source\n";
295             }
296              
297 2         5 my @bck_tokens = @$tokens;
298 2         3 my $str = '';
299 2         10 $ds->reset();
300 2         7 while (!$ds->is_exhausted){
301 6         16 local $meta->{VARS}{$iter} = $ds->get();
302 6         7 local $meta->{PARSE_UPTO} = 'endfor';
303 6         11 @$tokens = @bck_tokens;
304             # print "Iterating over '$datasource'\n";
305 6         9 $str .= parse_tokens($tokens, $meta);
306 6         20 $ds->next();
307             }
308 2         23 return $str;
309             }
310              
311             sub p_ifvar {
312 3     3 0 3 my ($tokens, $meta) = @_;
313 3         8 my $contents = strip_ws(slurp_upto_token($tokens, 'TAG_END', $meta));
314 3 100       10 if ($contents =~ m/\s/){
315             parse_error(
316             q{Parse error in 'ifvar' tag. Syntax is [% ifvar variable %] .. [% endifvar %]},
317             $meta->{FILES},
318 1         3 $tokens->[0],
319             );
320             }
321 2         2 my $c = do {
322 2         4 local $meta->{NO_VAR_WARN} = 1;
323 2         3 resolve_var($contents, $meta);
324             };
325 2         4 local $meta->{PARSE_UPTO} = 'endifvar';
326 2 100       4 if (defined $c){
327             # warn "Variable '$contents' is defined\n";
328 1         7 return parse_tokens($tokens, $meta);
329             } else {
330             # warn "Variable '$contents' is NOT defined\n";
331 1         1 local $meta->{NO_VAR_WARN} = 1;
332 1         2 parse_tokens($tokens, $meta);
333 1         6 return '';
334             }
335             }
336              
337             sub p_verbatim {
338 2     2 0 2 my $tokens = shift;
339 2         2 my $meta = shift;
340 2         2 my $str = "";
341 2         4 my $key = strip_ws(slurp_upto_token($tokens, 'TAG_END', $meta));
342             # print Dumper $tokens;
343 2         4 while (@$tokens){
344 9 100 100     46 if ( $tokens->[0]->[0] eq "TAG_START"
      66        
      66        
      33        
345             and $tokens->[1]->[0] eq "KEYWORD"
346             and $tokens->[1]->[1] eq "endverbatim"
347             and $tokens->[2]->[1] =~ m/\s*\Q$key\E\s*/
348             and $tokens->[3]->[0] eq "TAG_END"){
349              
350             # found end of verbatim section
351 1         6 shift @$tokens for 1 .. 4;
352 1         39 return $str;
353             } else {
354 8         7 $str .= $tokens->[0]->[1];
355 8         14 shift @$tokens;
356             }
357             }
358 1         14 die "[[[verbatim $key]]] opened but not closed until end of file\n";
359             }
360              
361             sub p_comment {
362 3     3 0 3 my $tokens = shift;
363 3         4 my $meta = shift;
364 3         7 slurp_upto_token($tokens, 'TAG_END', $meta);
365 1         10 return "";
366             }
367              
368              
369             sub resolve_var {
370 15     15 0 15 my ($name, $meta) = @_;
371 15 100       41 if ($name =~ m/\./){
372 8         16 my @parts = split m/\./, $name;
373 8         10 my $var = $meta->{VARS};
374 8         12 for (@parts){
375 17 100 66     95 if (!defined $var || !ref $var || reftype($var) ne 'HASH'){
      66        
376 1 50       3 unless ($meta->{NO_VAR_WARN}){
377 0         0 warn "\nCan't dereference '$name' at level '$_': not defined or not a hash\n";
378             }
379 1         3 return undef;
380             }
381 16         21 $var = $var->{$_};
382             }
383 7         16 return $var;
384             }
385 7 100       15 if (exists $meta->{VARS}->{$name}){
386 5         13 return $meta->{VARS}->{$name};
387             } else {
388 2 0 33     5 unless ($meta->{NO_VAR_WARN} || $config{quiet}){
389 0         0 print STDERR "Trying to access variable '$name' which is not defined\n";
390             }
391 2         3 return undef;
392             }
393             }
394              
395             sub encode_entities {
396 3     3 0 4 my $str = shift;
397 3 50       7 return '' unless defined $str;
398 3         5 $str =~ s{&}{&}g;
399 3         7 $str =~ s{<}{<}g;
400 3         6 $str =~ s{>}{>}g;
401 3         4 $str =~ s{"}{"}g;
402 3         32 return $str;
403             }
404              
405             sub slurp_upto_token {
406 30     30 0 34 my ($tokens, $expected_token, $meta) = @_;
407 30         28 my $str = '';
408 30   100     122 while (@$tokens && $tokens->[0][0] ne $expected_token){
409 31         37 $str .= $tokens->[0][1];
410 31         103 shift @$tokens;
411             }
412 30         39 p_expect($tokens, $expected_token, $meta);
413 28         57 return $str;
414             }
415              
416             sub parse_hash {
417 14     14 0 17 my ($str, $statement_name, $meta) = @_;
418 14 100       36 return unless defined $str;
419              
420 2         8 my $del_string = qr{(
421             '[^'\\]*(?>\\.[^'\\]*)*'
422             |"[^"\\]*(?>\\.[^"\\]*)*'
423             |[^"']\S*
424             )}x;
425 2         3 my %options;
426 2         27 pos($str) = 0;
427 2         133 while ($str =~ m/\G\s*(\w+):$del_string\s*/gc){
428 4         8 my $key = $1;
429 4         6 my $val = $2;
430 4         6 $val =~ s/^['"]//;
431 4         5 $val =~ s/['"]$//;
432 4         4 $val =~ s{\\(.)}{$1}g;
433 4         23 $options{$key} = $val;
434             }
435 2         14 return %options;
436 0 0       0 if (pos($str) + 1 < length($str)){
437             # end of string not reached
438             parse_error(qq{Can't parse key-value pairs in $statement_name statement. Syntax is key1:val1 key2:val2 ... },
439 0         0 $meta->{FILES});
440             }
441             }
442              
443             sub my_dclone {
444             # dclone can't handle code references, which is very bad
445             # becase DBI objects from App::Mowyw::Datasource::DBI hold code refs.
446             # so we don't clone blessed objects at all, but pass a reference instead.
447 32     32 0 26 my $meta = shift;
448 32         22 my %result;
449 32         63 for (keys %$meta){
450 92 100       108 if ($_ eq 'VARS'){
451 12         10 my %vs = %{$meta->{VARS}};
  12         29  
452 12         23 for my $v (keys %vs){
453 7 50       23 if (blessed $vs{$v}){
454 0         0 $result{VARS}{$v} = $vs{$v};
455             } else {
456 7 50       28 $result{VARS}{$v} = ref $vs{$v} ? dclone($vs{$v}) : $vs{$v};
457             }
458             }
459             } else {
460 80 100       820 $result{$_} = ref $meta->{$_} ? dclone($meta->{$_}) : $meta->{$_};
461             }
462             }
463              
464 32         54 return \%result;
465             }
466              
467             sub p_braces {
468 20     20 0 15 my $tokens = shift;
469 20         12 my $meta = shift;
470 20         16 my $str = "";
471 20         22 p_expect($tokens,"BRACES_START", $meta);
472 20 100       26 if ($meta->{CURRENT_ITEM}){
473             # print "using text inside braces\n";
474 5         6 $str .= parse_tokens($tokens, $meta);
475             } else {
476             # discard the text between opening {{ and closing }} braces
477             # print "discarding text inside braces\n";
478 15         20 parse_tokens($tokens, $meta);
479             }
480 20         22 p_expect($tokens, "BRACES_END", $meta);
481 20         24 return $str;
482             }
483              
484             sub p_setvar {
485 7     7 0 9 my $tokens = shift;
486 7         6 my $meta = shift;
487 7         7 my $str = "";
488 7         15 while ($tokens->[0]->[0] ne "TAG_END"){
489 7         10 $str .= $tokens->[0]->[1];
490 7         18 shift @$tokens;
491             }
492 7         10 p_expect($tokens, "TAG_END", $meta);
493 7         14 $str = strip_ws($str);
494 7         14 $str =~ m#^(\S+)\s#;
495 7         11 my $name = $1;
496 7         6 my $value = $str;
497 7         16 $value =~ s/^\S+\s+//;
498 7         15 $meta->{VARS}->{$name} = $value;
499 7         64 return "";
500             }
501              
502             sub p_readvar {
503 13     13 0 22 my ($tokens, $meta) = @_;
504 13         25 my $str = strip_ws(slurp_upto_token($tokens, 'TAG_END', $meta));
505 13         33 my ($name, $rest) = split m/\s+/, $str, 2;
506 13         22 my %options = parse_hash($rest, 'readvar', $meta);
507 13         24 my $c = resolve_var($name, $meta);
508              
509 13 100 66     33 if (defined $options{escape} && lc $options{escape} eq 'html'){
510 1         4 return encode_entities($c);
511             }
512 12 100       86 return $c if defined $c;
513 2         16 return '';
514             }
515              
516             sub p_syntaxfile {
517 0     0 0 0 my $tokens = shift;
518 0         0 my $meta = shift;
519 0         0 my $tag_content = shift @$tokens;
520 0         0 $tag_content = strip_ws($tag_content->[1]);
521 0         0 p_expect($tokens, "TAG_END", $meta);
522 0         0 my @t = split m/\s+/, $tag_content;
523 0 0       0 if (scalar @t != 2){
524             parse_error(
525             "Usage of syntaxfile tag: [[[syntaxfile ",
526             $meta->{FILES},
527 0         0 $tokens->[0],
528             );
529             }
530              
531             }
532              
533             sub p_syntax {
534 2     2 0 2 my $tokens = shift;
535 2         2 my $meta = shift;
536 2         3 my $lang = shift @$tokens;
537 2         4 $lang = strip_ws($lang->[1]);
538 2         5 p_expect($tokens, "TAG_END", $meta);
539 2         1 my $str = "";
540 2   66     13 while ($tokens->[0] and not ($tokens->[0]->[0] eq "TAG_START" and $tokens->[1]->[1] eq "endsyntax" and $tokens->[2]->[0] eq "TAG_END")){
      66        
541 2         3 $str .= $tokens->[0]->[1];
542 2         14 shift @$tokens;
543             }
544 2         7 p_expect($tokens, "TAG_START", $meta);
545 2         4 p_expect($tokens, "KEYWORD", $meta);
546 2         3 p_expect($tokens, "TAG_END", $meta);
547              
548 2         5 return do_hilight($str, $lang, $meta);
549             }
550              
551             sub do_hilight {
552 2     2 0 3 my ($str, $lang, $meta) = @_;
553 2 50       3 if ($lang eq 'escape'){
554 0         0 return encode_entities($str);
555             }
556 2         2 eval {
557 11     11   56 no warnings "all";
  11         13  
  11         17879  
558 2         232 require Text::VimColor;
559             };
560 2 50       6 if ($@){
561             # require was not successfull
562 2 100       24 print STDERR " Not syntax hilighting, Text::VimColor not found\n" unless $config{quiet};
563             # encode at least some special chars "by hand"
564 2         5 return encode_entities($str);
565             } else {
566 0 0       0 print STDERR "." unless $config{quiet};
567             # any encoding will do if vim automatically detects it
568 0         0 my $vim_encoding = 'utf-8';
569 0         0 my $BOM = "\x{feff}";
570 0         0 my $syn = Text::VimColor->new(
571             filetype => $lang,
572             string => encode($vim_encoding, $BOM . $str),
573             );
574 0         0 $str = decode($vim_encoding, $syn->html);
575 0         0 $str =~ s/^$BOM//;
576 0         0 return $str;
577             }
578             }
579              
580             # parse sub: expect a specific token, return its content or die if the
581             # expectation was not met.
582             sub p_expect {
583 301     301 0 378 my ($tokens, $expect, $meta) = splice @_, 0, 3;
584 301 100       363 parse_error("Unexpected End of File, expected $expect", $meta->{FILES}) unless (@$tokens);
585 299 50       425 confess("\$tokens not a array ref - this is most likely a programming error\n$internal_error_message") unless(ref($tokens) eq "ARRAY");
586 299 100       389 if ($tokens->[0]->[0] eq $expect){
587 298         190 my $e_val = shift;
588 298 50 33     471 if (not defined($e_val) or $e_val eq $tokens->[0]->[1]){
589 298         234 my $val = $tokens->[0]->[1];
590 298         211 shift @$tokens;
591 298         446 return $val;
592             } else {
593             parse_error("Expected '$e_val', got $tokens->[0][1]\n",
594 0         0 $meta->{FILES}, $tokens->[0]);
595             }
596             }
597             parse_error(
598             "Expected token $expect, got $tokens->[0]->[0]\n",
599             $meta->{FILES},
600 1         6 $tokens->[0],
601             );
602             }
603              
604              
605             sub lex_string {
606 51     51 0 60 my $text = shift;
607 51         132 my @tokens = lex($text, \@input_tokens);
608             # print Data::Dumper->Dump(\@tokens);
609 51         144 return @tokens;
610             }
611              
612             sub parse_tokens {
613 99     99 0 81 my $tokens = shift;
614 99         70 my $meta = shift;
615 99         85 my $str = "";
616 99 100       138 if ($meta->{INSIDE_ITEM}){
617 40         40 $str .= p_text($tokens);
618             } else {
619 59         175 $str .= p_text($tokens, {BRACES_START => 1, BRACES_END => 1});
620             }
621 99   100     544 while(@$tokens
      100        
622             and $tokens->[0]->[0] ne "TAG_END"
623             and $tokens->[0]->[0] ne "BRACES_END"){
624             # print scalar @$tokens;
625             # print " tokens left\n";
626             # warn $str;
627              
628 96 100       143 if ($tokens->[0]->[0] eq "TAG_START"){
    50          
629 76         114 my $start = p_expect($tokens, "TAG_START", $meta);
630 76         91 my $key = p_expect($tokens, 'KEYWORD', $meta);
631             # warn "Found keyword $key\n";
632             my $error_sub = sub {
633 268     268   240 my ($tag, $prior_tag) = @_;
634             return sub {
635 0         0 my ($tokens, $meta) = @_;
636             parse_error(
637             "Unexpected tag '$tag' without prior '$prior_tag'",
638             $meta->{FILES},
639 0         0 $tokens->[0],
640             );
641             }
642 75         207 };
  268         949  
643              
644 75 100 100     211 if ($meta->{PARSE_UPTO} && $meta->{PARSE_UPTO} eq $key){
645 8         13 p_expect($tokens, 'TAG_END', $meta);
646 8         27 return $str;
647             }
648              
649 67         169 my %dispatch = (
650             include => \&p_include,
651             system => \&p_system,
652             menu => \&p_menu,
653             item => \&p_item,
654             option => \&p_option,
655             verbatim => \&p_verbatim,
656             endverbatim => $error_sub->(qw(endverbatim verbatim)),
657             bind => \&p_bind,
658             comment => \&p_comment,
659             setvar => \&p_setvar,
660             readvar => \&p_readvar,
661             syntax => \&p_syntax,
662             syntaxfile => \&p_syntaxfile,
663             endsyntax => $error_sub->(qw(endsyntax syntax)),
664             for => \&p_for,
665             endfor => $error_sub->(qw(endfor for)),
666             ifvar => \&p_ifvar,
667             endifvar => $error_sub->(qw(endifvar ifvar)),
668             );
669 67         117 my $func = $dispatch{$key};
670 67 50       89 if ($func){
671 67         107 $str .= &$func($tokens, $meta);
672             } else {
673 0         0 confess("Action for keyword '$key' not yet implemented");
674             }
675              
676             } elsif ($tokens->[0]->[0] eq "BRACES_START") {
677 20         24 $str .= p_braces($tokens, $meta);
678             } else {
679 0         0 print "Don't know what to do with token $tokens->[0]->[0]\n";
680             }
681 78 100       118 if ($meta->{INSIDE_ITEM}){
682 25         23 $str .= p_text($tokens);
683             } else {
684 53         124 $str .= p_text($tokens, {BRACES_START => 1, BRACES_END => 1});
685             }
686              
687             }
688 81         436 return $str;
689             }
690              
691             sub parse_file {
692 20     20 0 21 my ($fn, $meta) = @_;
693             # print Dumper \%config;
694             # print "\n$config{encoding}\n";
695 20 50       497 open (my $fh, "<:encoding($config{encoding})", $fn)
696             or confess "Can't open file '$fn' for reading: $!";
697 20         1068 my $str = do { local $/; <$fh> };
  20         49  
  20         296  
698             # print $str;
699 20         190 parse_str($str, $meta);
700             }
701              
702             sub parse_str {
703 51     51 0 5168 my ($str, $meta) = @_;
704 51         94 my @tokens = lex_string($str);
705             # print Data::Dumper->Dump(\@tokens);
706 51         106 return parse_tokens(\@tokens, $meta);
707             }
708              
709             sub get_meta_data {
710 4     4 0 4 my $fn = shift;
711 4         16 my $meta = {
712             ITEMS => [],
713             FILES => [],
714             CURRENT_ITEM => undef,
715             OPTIONS => {},
716             VARS => {},
717             };
718              
719 4         9 my $global_include_fn = get_include_filename('include', 'global', $fn);
720              
721 4 50       45 if (-e $global_include_fn ){
722             # warn "Reading global include file '$global_include_fn'\n";
723 4         7 $meta->{FILES} = [$global_include_fn];
724             # use parse_file for its side effects on meta
725 4         9 my $g = parse_file($global_include_fn, $meta);
726             }
727             # replace call stack
728             # otherwise all files seem to be included from the globl include file,
729             # which is somewhat ugly
730 4         6 $meta->{FILES} = [];
731 4         7 return $meta;
732             }
733              
734              
735             sub process_file {
736 4     4 0 4 my ($fn, $config) = @_;
737              
738 4         8 my $new_fn = get_online_fn($fn);
739              
740             # process file at all?
741 4         4 my $process = 0;
742             # use Data::Dumper;
743             # print Dumper $App::Mowyw::config{file_filter};
744 4         3 for my $f(@{$App::Mowyw::config{file_filter}}){
  4         8  
745 4         5 my ($include, undef, $re) = @$f;
746 4 50       22 if ($fn =~ m/$re/){
747 4         4 $process = $include;
748 4         6 last;
749             }
750             }
751              
752             # print +($process ? '' : 'not '), "processing file $fn\n";
753              
754 4 50       6 if ($process){
755              
756 4 0 33     8 if ($config{make_behaviour} and -e $new_fn and (stat($fn))[9] < (stat($new_fn))[9]){
      33        
757 0         0 return;
758             }
759 4 50       226 print STDERR "Processing File '$fn'..." unless $config{quiet};
760              
761 4         12 my $metadata = get_meta_data($fn);
762 4         3 push @{$metadata->{FILES}}, $fn;
  4         7  
763 4         4 my $str = parse_file($fn, $metadata);
764             # print Data::Dumper->Dump([$metadata]);
765 4         5 my $header = "";
766 4         3 my $footer = "";
767              
768             # warn $str;
769 4 100       9 unless (exists $metadata->{OPTIONS}{'no-header'}){
770 3         5 my $m = my_dclone($metadata);
771 3         6 my $header_fn = get_include_filename('include', 'header', $fn);
772 3         4 unshift @{$m->{FILES}}, $header_fn;
  3         6  
773 3         4 $header = parse_file($header_fn, $m);
774             }
775 4 100       9 unless (exists $metadata->{OPTIONS}{'no-footer'}){
776 3         11 my $m = my_dclone($metadata);
777 3         7 my $footer_fn = get_include_filename('include', 'footer', $fn);
778 3         3 unshift @{$m->{FILES}}, $footer_fn;
  3         5  
779 3         4 $footer = parse_file($footer_fn, $metadata);
780             }
781 4         11 my ($tmp_fh, $tmp_name) = tempfile( UNLINK => 1);
782 4         1246 binmode $tmp_fh, ":encoding($config{encoding})";
783 4         131 print $tmp_fh $header, $str, $footer;
784 4         123 close $tmp_fh;
785 4 50       13 if (compare($new_fn, $tmp_name) == 0){
786 4 50       583 print STDERR " not changed\n" unless $config{quiet};
787             } else {
788 0         0 copy($tmp_name, $new_fn);
789 0 0       0 print STDERR " done\n" unless $config{quiet};
790             }
791             } else {
792 0 0       0 if (compare($fn, $new_fn) == 0){
793             # do nothing
794             } else {
795 0         0 copy($fn, $new_fn);
796 0         0 print "Updated file $new_fn (not processed)\n";
797             }
798             }
799             }
800              
801              
802             sub get_online_fn {
803 4     4 0 4 my $fn = shift;
804 4         3 my $new_fn = $fn;
805 4         29 $new_fn =~ s{^$config{default}{source}}{};
806             {
807 4         5 my $found = 0;
  4         2  
808 4         4 for ( keys %{$config{per_fn}} ){
  4         11  
809 0 0       0 if ( $new_fn =~ m/$_/ ){
810 0         0 $found = 1;
811 0         0 $new_fn = $config{per_fn}{$_}{online} . $new_fn;
812             last
813 0         0 }
814             }
815 4 50       7 if ($found == 0){
816 4         8 $new_fn = $config{default}{online} . $new_fn;
817             }
818             }
819 4         7 return $new_fn;
820              
821             }
822              
823             sub get_config {
824 0     0 0 0 my $cfg_file = 'mowyw.conf';
825 0 0       0 if (-e $cfg_file) {
826 0         0 my $conf_hash = read_config_file($cfg_file);
827             # print Dumper $conf_hash;
828 0         0 return transform_conf_hash($conf_hash);
829             } else {
830 0         0 print "No config file '$cfg_file'\n";
831 0         0 return {};
832             }
833             }
834              
835             sub transform_conf_hash {
836 0     0 0 0 my ($h) = @_;
837 0         0 my %nh;
838             # no warnings 'uninitialized';
839 0         0 my %d = %{$config{default}};
  0         0  
840 0         0 for (keys %{$h->{MATCH}}){
  0         0  
841 0         0 my $key = $h->{MATCH}{$_};
842 0         0 for my $feat (qw(include menu postfix online)){
843             $nh{$key}{$feat} =
844 0 0       0 defined $h->{ uc $feat }{$_} ? $h->{ uc $feat }{$_} : $d{$feat};
845             }
846             }
847 0         0 my @filter;
848             {
849 0         0 my %inc = %{$h->{INCLUDE}};
  0         0  
  0         0  
850 0 0       0 %inc = ( 50 => '\..?htm') unless keys %inc;
851 0 0       0 my %exc = %{$h->{EXCLUDE} || {}};
  0         0  
852 0         0 while (my ($k, $v) = each %inc){
853 0         0 $k =~ tr/0-9//cd;
854 0   0     0 my $re = eval { qr{$v} } || die "Invalid regex '$v' in config: $@";
855 0         0 push @filter, [1, $k, $re];
856             }
857 0         0 while (my ($k, $v) = each %exc){
858 0         0 $k =~ tr/0-9//cd;
859 0   0     0 my $re = eval { qr{$v} } || die "Invalid regex '$v' in config: $@";
860 0         0 push @filter, [0, $k, $re];
861             }
862 0         0 @filter = reverse sort { $a->[1] <=> $b->[1] } @filter;
  0         0  
863             }
864             # print Dumper \%nh, \@filter;
865 0         0 return (\%nh, \@filter);
866             }
867              
868              
869             sub get_include_filename {
870 17     17 0 20 my ($type, $base_fn, $source_fn) = @_;
871 17 50       26 confess "Usage: get_include_filename(\$type, \$base, \$source)" unless $source_fn;
872             # print "Passed options ('$type', '$base_fn', '$source_fn')\n";
873             # $type should be one of qw(include menu online)
874 17         13 my $re;
875             # print Dumper $config{per_fn};
876 17         18 for (keys %{$config{per_fn}}){
  17         44  
877 0 0       0 if ($source_fn =~ m/$_/){
878 0         0 $re = $_;
879             # warn "Found regex '$re'";
880 0         0 last;
881             }
882             }
883 17         23 my $prefix = $config{default}{$type};
884 17         19 my $postfix = $config{default}{postfix};
885 17 50       45 if (defined $re){
886 0 0       0 $prefix = $config{per_fn}{$re}{$type} if defined $config{per_fn}{$re}{$type};
887 0 0       0 $postfix = $config{per_fn}{$re}{postfix} if defined $config{per_fn}{$re}{postfix};
888             }
889              
890 17         37 return $prefix . $base_fn . $postfix;
891             }
892             1;