File Coverage

blib/lib/HTML/Expander.pm
Criterion Covered Total %
statement 124 156 79.4
branch 25 44 56.8
condition 27 39 69.2
subroutine 13 16 81.2
pod 0 11 0.0
total 189 266 71.0


line stmt bran cond sub pod time code
1             #########################################################################
2             #
3             # HTML::Expander
4             # Vladi Belperchinov-Shabanski "Cade"
5             #
6             # http://cade.datamax.bg
7             #
8             #########################################################################
9             package HTML::Expander;
10 1     1   13328 use Exporter;
  1         3  
  1         77  
11             @ISA = qw( Exporter );
12              
13             our $VERSION = '2.7';
14              
15 1     1   15 use Carp;
  1         3  
  1         64  
16 1     1   7 use strict;
  1         7  
  1         2373  
17              
18             #########################################################################
19              
20             sub new
21             {
22 1     1 0 16 my $pack = shift;
23 1   33     8 my $class = ref( $pack ) || $pack;
24              
25 1         3 my $self = {};
26              
27 1         3 $self->{ 'TAGS' } = {}; # tag tables
28 1         3 $self->{ 'VARS' } = {}; # var tables
29 1         3 $self->{ 'INC' } = {}; # include directories
30 1         4 $self->{ 'ENV' } = {}; # local environment, this is free for use
31              
32 1         2 $self->{ 'MODE' } = []; # mode stack
33              
34 1         4 $self->{ 'WARNINGS' } = 0; # set to 1 for debug
35              
36 1         22 bless $self, $class;
37 1         9 return $self;
38             }
39              
40             sub DESTROY
41             {
42 1     1   1339 my $self = shift;
43             # nothing
44             }
45              
46             sub define_tag
47             {
48 7     7 0 37 my $self = shift;
49              
50 7 50       20 confess "invalid arguments count, need MODENAME first" unless @_ % 2;
51              
52 7   50     19 my $mode = shift || 'main';
53 7         25 my %tags = @_;
54              
55 7   100     26 $self->{ 'TAGS' }{ $mode } ||= {};
56 7         10 %{ $self->{ 'TAGS' }{ $mode } } = ( %{ $self->{ 'TAGS' }{ $mode } }, %tags );
  7         38  
  7         20  
57             }
58              
59             sub define_var
60             {
61 3     3 0 13 my $self = shift;
62              
63 3 50       9 confess "invalid arguments count, need MODENAME first" unless @_ % 2;
64              
65 3   50     11 my $mode = shift || 'main';
66 3         7 my %vars = @_;
67              
68 3   50     8 $self->{ 'VARS' }{ $mode } ||= {};
69 3         12 %{ $self->{ 'VARS' }{ $mode } } = ( %{ $self->{ 'VARS' }{ $mode } }, %vars );
  3         12  
  3         7  
70             }
71              
72             sub add_inc_paths
73             {
74 1     1 0 16 my $self = shift;
75 1         27 $self->{ 'INC' }{ $_ }++ for @_;
76             }
77              
78             sub del_inc_paths
79             {
80 0     0 0 0 my $self = shift;
81 0         0 delete $self->{ 'INC' }{ $_ } for @_;
82             }
83              
84             sub mode_copy
85             {
86 1     1 0 9 my $self = shift;
87 1         3 my $mode = shift; # destination mode
88              
89 1         3 for my $s ( @_ ) # for each source modes
90             {
91             # print "DEBUG: mode copy: [$mode] <- [$s]\n";
92 1         3 while( my ( $k, $v ) = each %{ $self->{ 'TAGS' }{ $s } } )
  3         15  
93             {
94             # print "DEBUG: TAG ($k) = ($v)\n";
95 2         7 $self->define_tag( $mode, $k, $v );
96             }
97 1         2 while( my ( $k, $v ) = each %{ $self->{ 'VARS' }{ $s } } )
  1         21  
98             {
99             # print "DEBUG: VAR ($k) = ($v)\n";
100 0         0 $self->define_var( $mode, $k, $v );
101             }
102             }
103             }
104              
105             sub mode_load
106             {
107 0     0 0 0 my $self = shift;
108 0         0 my $file = shift;
109              
110 0         0 my $target = 'main';
111 0         0 open my $i, $file;
112 0         0 while(<$i>)
113             {
114 0 0       0 next if /^\s*[#;]/; # comments
115 0         0 chomp;
116 0 0       0 if ( /^\s*MODE/i )
117             {
118 0         0 $_ = lc $_;
119 0         0 s/\s+//g; # get rid of whitespace
120 0         0 my @a = split /[:,]/;
121 0         0 shift @a; # skip `mode' keyword
122 0         0 $target = shift @a;
123 0         0 $self->mode_copy( $target, @a );
124             }
125             else
126             {
127 0 0       0 $self->define_tag( $target, lc $1, $2 ) if /^\s*(<\S+)\s+(.*)$/;
128 0 0       0 $self->define_var( $target, lc $2, $3 ) if /^\s*(%(\S+))\s+(.*)$/;
129             }
130             }
131 0         0 close $i;
132             }
133              
134             sub expand
135             {
136 62     62 0 123 my $self = shift;
137 62         146 my $text = shift;
138 62   100     169 my $level = shift || 0;
139 62   100     151 my $visited_arg = shift || {};
140              
141 62         205 my $visited = { %$visited_arg };
142              
143             #print "DEBUG: expand (level=$level) [text=$text]\n";
144              
145 62         170 $text =~ s/\(\%([^\(\)]+)\)/$self->expand_var( $1, $level+1, $visited )/gie;
  2         10  
146             #print "DEBUG: ----------------------\n";
147 62         339 $text =~ s/<([^<>]+)>/$self->expand_tag( $1, $level+1, $visited )/gie;
  133         424  
148             #print "DEBUG: expand result: [text=$text]\n";
149 62         1150 return $text;
150             }
151              
152             sub expand_var
153             {
154 20     20 0 33 my $self = shift;
155 20         31 my $var = shift;
156 20         27 my $level = shift;
157 20         22 my $visited_arg = shift;
158              
159 20         55 my $visited = { %$visited_arg };
160              
161 20 50       89 return '***'.undef if $visited->{ "VAR::$var" }++; # avoids recursion
162 20   100     96 my $mode = $self->{ 'MODE' }[0] || 'main';
163 20   100     114 my $value = $self->{ 'VARS' }{ $mode }{ $var }
164             || $self->{ 'ENV' }{ $var };
165 1     1   3224 use Data::Dumper; # DEBUG
  1         32561  
  1         2291  
166 20         146 print "DEBUG: expand_var: [$var] = ($value)\n".Dumper($visited)."\n";
167 20         15747 return $self->expand( $value, $level + 1, $visited );
168             }
169              
170             sub expand_tag
171             {
172 133     133 0 161 my $self = shift;
173 133         2046 my $tag_org = shift;
174 133         149 my $level = shift;
175 133         137 my $visited_arg = shift;
176              
177 133         7990 my $visited = { %$visited_arg };
178              
179 133         178 my %args;
180 133         333 my ( $tag, $args ) = split /\s+/, $tag_org, 2;
181             # print "DEBUG: expand_tag: [$tag] -- ($args)\n";
182 133         207 my $tag_lc = lc $tag;
183 133         510 while( $args =~ /\s*([^=]+)(=('([^']*)'|"([^"]*)"|(\S*)))?/g ) # "' # fix string colorization
184             {
185 43         93 my $k = lc $1;
186 43   100     449 my $v = $4 || $5 || $6 || 1;
187 43         234 $args{ $k } = $v;
188             # print "DEBUG: [$k] = ($v)\n";
189             }
190              
191 133 100       711 if ( $tag_lc eq 'mode' )
    100          
192             {
193 2   50     4 unshift @{ $self->{ 'MODE' } }, ( $args{ 'name' } || 'main' );
  2         22  
194 2   50     10 $self->{ 'ENV' }{ '!MODE' } = $self->{ 'MODE' }[0] || 'main';
195 2         16 return undef;
196             }
197             elsif ( $tag_lc eq '/mode' )
198             {
199 2         2 shift @{ $self->{ 'MODE' } };
  2         8  
200 2   50     22 $self->{ 'ENV' }{ '!MODE' } = $self->{ 'MODE' }[0] || 'main';
201 2         12 return undef;
202             }
203 129 100 66     668 if ( $tag_lc eq 'var' )
    100          
    100          
204             {
205 20 100       51 if( $args{ 'set' } eq '' )
206             {
207 18         382 return $self->expand_var( $args{ 'name' }, $level + 1, $visited );
208             }
209             else
210             {
211 2         27 $self->{ 'ENV' }{ uc $args{ 'name' } } = $args{ 'set' };
212 2 50       26 return $args{ 'echo' } ? $args{ 'set' } : undef;
213             }
214             }
215             elsif ( $tag_lc eq 'include' or $tag_lc eq 'inc' )
216             {
217 2         11 my $file_arg = $args{ 'file' };
218 2 50       19 if( $file_arg !~ /^[a-zA-Z0-9_\-]+(\.[a-zA-Z0-9_\-]*)?$/ )
219             {
220 0         0 $self->warn( "forbidden include file name `$file_arg'" );
221             return undef
222 0         0 }
223 2         9 my $file;
224 2         7 for( keys %{ $self->{ 'INC' } } )
  2         30  
225             {
226 2         9 $file = $_ . '/' . $file_arg;
227 2 50       279 last if -e $file;
228 0         0 $file = undef;
229             }
230 2 100       52 return undef if $visited->{ "INC::$file" }++; # avoids recursion
231             open( my $i, $file ) || do
232 1 50       70 {
233 0         0 $self->warn( "cannot open file `$file'" );
234 0         0 return undef;
235             };
236 1         149 my $data = $self->expand( join( '', <$i> ), $level + 1, $visited );
237 1         25 close( $i );
238 1         40 return $data;
239             }
240             elsif ( $tag_lc eq 'exec' )
241             {
242 2         7 my $cmd = $args{ 'cmd' };
243 2 50       7 if( ! $self->{ 'EXEC_TAG_ALLOWED' } )
244             {
245 0         0 $self->warn( "exec is forbidden `$cmd'" );
246 0         0 return undef;
247             }
248              
249             open( my $i, $cmd . '|' ) || do
250 2 50       13660 {
251 0         0 $self->warn( "exec failed `$cmd'" );
252 0         0 return undef;
253             };
254 2         1970 my $data = $self->expand( join( '', <$i> ), $level + 1, $visited );
255 2         81 close $i;
256 2         109 return $data;
257             }
258             else
259             {
260 105         167 $tag = "<$tag>";
261              
262 105   100     435 my $mode = $self->{ 'MODE' }[0] || 'main';
263 105         585 my $value = $self->{ 'TAGS' }{ $mode }{ $tag };
264             # print "DEBUG: mode name {$mode}, tag: $tag -> ($value)\n" if defined $value;
265 105 100 100     347 if ( $value and ! $visited->{ "$mode::$tag" } )
266             {
267             # print "DEBUG: ---> ($value)\n";
268 18         58 $visited->{ "$mode::$tag" }++; # avoids recursion
269 18         58 $value = $self->expand( $value, $level + 1, $visited );
270 18         57 $value =~ s/\%([a-z_0-9]+)/$args{ lc $1 }/gi;
271 18         48 my $ret = $self->expand( $value, $level + 1, $visited );
272             # print "DEBUG: expand_tag return: [$ret]\n";
273 18         126 return $ret;
274             }
275             else
276             {
277             # print "DEBUG: expand_tag original: [$tag_org]\n";
278 87         766 return "<$tag_org>";
279             }
280             }
281             }
282              
283             sub warn
284             {
285 0     0 0   my $self = shift;
286 0 0 0       return unless $self->{ 'WARNINGS' } || $self->{ 'WARN' };
287              
288 0           carp __PACKAGE__ . ": " . join( ' ', @_ );
289             }
290              
291             =pod
292              
293             =head1 NAME
294              
295             HTML::Expander - html tag expander with inheritable tag definitions (modes)
296              
297             =head1 SYNOPSIS
298              
299             use HTML::Expander;
300              
301             # get new HTML::Expander object;
302             my $ex = new HTML::Expander;
303              
304             # load mode (tags) definitions
305             $ex->mode_load( "/path/to/mode.def.txt" );
306              
307             # define some more tags
308             $ex->define_tag( 'main', '', '

' );

309             $ex->define_tag( 'main', '', '' );
310              
311             # copy `main' into `new' mode
312             $ex->mode_copy( 'new', 'main' );
313              
314             # define one more tag
315             $ex->define_tag( 'new', '

', '

' );

316             $ex->define_tag( 'new', '', '
' ); 
317             $ex->define_tag( 'new', '', '' );
318              
319             # expand!
320             print $ex->expand( "
321             (current mode is '')
322             This is me
323            
324             (current mode is '')
325             empty
326             1.
327             2.
328             3.
329             \n" );
330             # the result will be:
331             #
(current mode is 'new')
332             #

This is me

333             #
334             # (cyrrent mode is 'main')
335             #

empty

336             # 1.
337             # 2.opala!
338             # 3.opala!
339              
340             # this should print current date
341             $self->{ 'EXEC_TAG_ALLOWED' } = 1; # allow execution of programs
342             print $ex->expand( '' ), "\n";
343             $self->{ 'EXEC_TAG_ALLOWED' } = 0; # forbid execution of programs (default)
344              
345             # add include paths
346             $ex->add_inc_paths( '/usr/html/inc', '/opt/test' );
347             $ex->del_inc_paths( '.' );
348             $ex->{ 'INC' }{ '.' } = 1;
349             $ex->{ 'INC' }{ '/usr/html/inc' } = 1;
350             $ex->{ 'INC' }{ '/opt/test' } = 1;
351              
352             # remove path
353             delete $ex->{ 'INC' }{ '/usr/html/inc' };
354              
355             # include some file (avoiding recursion if required)
356             print $ex->expand( '' ), "\n";
357              
358             =head1 DESCRIPTION
359              
360             HTML::Expander replaces html tags with other text (more tags, so it 'expands':))
361             with optional arguments. HTML::Expander uses tag tables which are called modes.
362             Modes can inherit other modes (several ones if needed). The goal is to have
363             as simple input html document as you need and have multiple different outputs.
364             For example you may want tag to render either as
 or as 
365            
in two separated modes.
366              
367             Essentially HTML::Expander works as preprocessor.
368              
369             The mode file syntax is:
370              
371             tag tag-replacement-string
372              
373             MODE: mode-name: inherited-modes-list
374              
375             tag tag-replacement-string
376              
377             etc...
378              
379             inherited-modes-list is comma or semicolon-separated list of modes that
380             should be copied (inherited) in this mode
381              
382             The mode file example:
383              
384             ### begin mode
385              
386             # top-level mode is called `main' and is silently defined by default
387             # mode: main
388              
389            

390            
391              
392            

393            
394              
395             MODE: page: main
396              
397            

398              
399             MODE: edit: page, main
400              
401             # actually `page' inherits `main' so it is not really
402             # required here to list `main'
403              
404            

405              
406             This is not exhaustive example but it is just for example...
407              
408             =head1 TAG ARGUMENTS
409              
410             Inside the tag you can define arguments that can be used later during the
411             interpolation or as argument to the special tags etc.
412              
413             Arguments cannot contain whitespace unless enclosed in " or ':
414              
415             # correct
416             # incorrect!
417             # correct
418             # correct
419              
420             There is no way to mix " and ':
421              
422             # incorrect! there is no escape syntax
423              
424             You can have unary arguments (without value) which, if used, have '1' value.
425              
426             is the same as
427              
428             =head1 SPECIAL TAGS
429              
430             There are several tags with special purposes:
431              
432            
433              
434             Sets current mode to `name' (saves it on the top of the mode stack).
435              
436            
437              
438             Removes last used mode from the stack (if stack is empty `main' is used).
439             Both and are replaced with empty strings.
440              
441            
442              
443             This tag is replaced with `command's output. 'exec' is forbidden by default.
444             Using it will lead to empty string returned. To allow it you need to:
445              
446             $ex->{ 'EXEC_TAG_ALLOWED' } = 1; # allow execution of programs
447              
448             ($ex is the HTML::Expander object you want to allow execution)
449             exec must be used only if you produce html pages from static source, i.e. NOT
450             from end-user source like html forms etc.! To avoid unwanted execution the
451             program which uses HTML::Expander must encode all <>'s into html special
452             chars:
453              
454             > must be converted to >
455             < must be converted to <
456              
457             Rule of thumb is: do not use exec! :)
458              
459            
460             or
461            
462              
463             This tag is replaced with `incfile' file's content (which will be
464             HTML::Expanded recursively).
465              
466             =head1 VARIABLES/ENVIRONMENT
467              
468             HTML::Expander object have own 'environment' which is accessed this way:
469              
470             $ex->{'ENV'}{ 'var-name' } = 'var-value';
471              
472             i.e. $ex->{'ENV'} is hash reference to the local environment. There is no
473             special access policy.
474              
475             There is syntax for variables interpolation. Values are taken from internal
476             environment table:
477              
478             (%VARNAME)
479              
480             All variables are replaced before tag expansion! This helps to handle this:
481              
482            
483              
484             If you need to interpolate variable in the tag expansion process (after the
485             variables interpolation) you need to:
486              
487            
488              
489             If you need to set variable name during tag interpolation you should:
490              
491            
492              
493             If you want to set variable and return its value at the same time you have to
494             use unary 'echo' argument:
495              
496            
497              
498             (%VAR) variables are interpolated before %arg interpolation, so it is safe to
499             use this:
500              
501            
502              
503             =head1 BUGS
504              
505             Unknown tags are left as-is, this is not bug but if you write non-html tag
506             which is not defined in mode tables it will passed into the output text.
507             (see example above for 'main' mode)
508              
509             If you find bug please contact me, thank you.
510              
511             =head1 DIAGNOSTICS
512              
513             HTML::Expander can report warnings if something wrong is going on. This is
514             assumed to be debugging or diagnostic tool so it is disabled by default.
515             To enable warnings:
516              
517             $ex->{ 'WARNINGS' } = 1;
518              
519             =head1 TODO
520              
521            
522              
523             =head1 AUTHOR
524              
525             Vladi Belperchinov-Shabanski "Cade"
526              
527            
528              
529             http://cade.datamax.bg
530              
531             =head1 VERSION
532              
533             $Id: Expander.pm,v 1.18 2006/04/30 00:30:00 cade Exp $
534              
535             =cut
536              
537             #########################################################################
538             # eof
539             #########################################################################
540             1;