File Coverage

blib/lib/App/Mowyw.pm
Criterion Covered Total %
statement 409 495 82.6
branch 78 134 58.2
condition 57 85 67.0
subroutine 51 56 91.0
pod 0 38 0.0
total 595 808 73.6


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