Fixed another return in parse().
[artemus.git] / Art5.pm
blob711d43d5911a522c0e368be4e00064cfadda1104
1 #####################################################################
3 # Artemus - Template Toolkit version 5
5 # Copyright (C) 2000/2011 Angel Ortega <angel@triptico.com>
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU General Public License
9 # as published by the Free Software Foundation; either version 2
10 # of the License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
21 # http://triptico.com
23 #####################################################################
25 use locale;
27 package Art5;
29 use strict;
30 use warnings;
31 use Carp;
33 $Art5::VERSION = '5.0.2-dev';
35 sub parse {
36 my $self = shift;
37 my $seq = shift;
38 my @ret = ();
40 # delete leading blanks and a possible brace
41 $$seq =~ s/^\s*\{?\s*//;
43 while ($$seq) {
44 # delete comments
45 if ($$seq =~ s/^#.*$//gm) {
46 $$seq =~ s/^\s+//;
48 elsif ($$seq =~ s/^(@?)"(([^"\\]*(\\.[^"\\]*)*))"\s*//) {
49 # double quoted string
50 my $op = $1 || '"';
51 my $str = $2;
53 # replace usual escaped characters
54 $str =~ s/\\n/\n/g;
55 $str =~ s/\\r/\r/g;
56 $str =~ s/\\t/\t/g;
57 $str =~ s/\\"/\"/g;
58 $str =~ s/\\\\/\\/g;
60 push(@ret, [ $op, $str ]);
62 elsif ($$seq =~ s/^(@?)'(([^'\\]*(\\.[^'\\]*)*))'\s*//) {
63 # single quoted string
64 my $op = $1 || '"';
65 my $str = $2;
67 $str =~ s/\\'/\'/g;
68 $str =~ s/\\\\/\\/g;
70 push(@ret, [ $op, $str ]);
72 elsif ($$seq =~ s/^(\d+(\.\d+)?)\s*//) {
73 # number
74 push(@ret, [ '"', $1 ]);
76 elsif ($$seq =~ /^\{\s*/) {
77 # another code sequence
78 push(@ret, $self->parse($seq));
80 elsif ($$seq =~ s/^\}\s*//) {
81 # end of sequence
82 last;
84 elsif ($$seq =~ s/^%([^\s\{\}]+)\s*//) {
85 # external hash value
86 push(@ret, [ '%', $1 ]);
88 elsif ($$seq =~ s/^\$(\d+)\s*//) {
89 # argument
90 push(@ret, [ '$', $1 ]);
92 elsif ($$seq =~ s/^([^\s\{\}]+)\s*//) {
93 # opcode
95 # nothing yet? operator call
96 if (scalar(@ret) == 0) {
97 push(@ret, $1);
99 else {
100 push(@ret, [ $1 ]);
103 else {
104 croak "Artemus5 syntax error near '$$seq'";
108 # no program? build a NOP */
109 if (!@ret) {
110 @ret = ('"', '');
113 # is the first thing in the sequence an array
114 # (instruction) and not a string (opcode)?
115 if (ref($ret[0]) eq 'ARRAY') {
116 # only one instruction? return as is
117 if (scalar(@ret) == 1) {
118 @ret = @{$ret[0]};
120 else {
121 # otherwise, prepend a '?' (joiner)
122 unshift(@ret, '?');
126 return [ @ret ];
130 sub compile {
131 my $self = shift;
132 my $str = shift;
134 # was this code already compiled?
135 if (!exists($self->{pc}->{$str})) {
136 # joiner opcode
137 my @ret = ( '?' );
139 # split by the Artemus5 marks
140 my @stream = split(/(<\{|\}>)/, $str);
142 # alternate between literal strings and Artemus5 code
143 while (@stream) {
144 my $p = shift(@stream);
146 if ($p eq '<{') {
147 $p = '{' . shift(@stream) . '}';
148 push(@ret, $self->parse(\$p));
149 shift(@stream);
151 elsif (defined $p) {
152 push(@ret, [ '"', $p ]);
156 my $ret = [ @ret ];
158 $self->{pc}->{$str} = $ret;
161 return $self->{pc}->{$str};
165 sub code {
166 my $self = shift;
167 my $op = shift;
169 if (!exists($self->{op}->{$op})) {
170 my $src = undef;
172 # filter opcode to only allow
173 # characters valid in file names
174 $op =~ s/[^\w\d_-]//g;
176 # does a loader_func() exist?
177 if (ref($self->{loader_func}) eq 'CODE') {
178 $src = $self->{loader_func}->($op);
181 if (!defined($src)) {
182 # try to resolve by loading
183 # a source file from the path
184 foreach my $p (@{$self->{path}}) {
185 my $fp = $p . '/' . $op;
187 # does a precompiled script already exist?
188 if ($self->{cache} && -f $fp) {
189 my $cp = $self->{cache} . $op;
191 if (-f $cp && -M $cp < -M $fp) {
192 # it does and it's fresh; import wildly
193 $self->{op}->{$op} = eval "require '$cp'";
194 last;
198 # load the source
199 if (open(F, $fp)) {
200 $src = join('', <F>);
201 close F;
203 last;
208 # compile if available
209 if (defined($src)) {
210 $self->{op}->{$op} = $self->compile($src);
212 # if there is a cache directory, save the compiled code
213 if ($self->{cache} and open(F, '>' . $self->{cache} . $op)) {
214 use Data::Dumper;
216 print F Dumper($self->{op}->{$op});
217 close F;
222 return $self->{op}->{$op};
226 sub exec {
227 my $self = shift;
228 my $prg = shift;
229 my $ret;
231 if (ref($prg) && !$self->{abort}) {
232 # if it has additonal arguments,
233 # wrap the call in a stack with them
234 if (scalar(@_)) {
235 push(@{$self->{stack}}, [ @_ ]);
237 $ret = $self->exec($prg);
239 pop(@{$self->{stack}});
241 else {
242 # stream of Artemus5 code
243 my @stream = @{$prg};
245 # pick opcode
246 my $op = shift(@stream);
248 # pick code
249 my $c = $self->code($op);
251 if (ref($c) eq 'CODE') {
252 $ret = $c->(@stream);
254 elsif (ref($c) eq 'ARRAY') {
255 $ret = $self->exec(
257 map { $self->exec($_) } @stream
260 else {
261 croak "Artemus5 opcode not found: $op";
266 if (!defined($ret)) {
267 $ret = '';
270 return $ret;
274 sub exec0 {
275 my $self = shift;
277 return $self->exec(@_) || 0;
281 sub init {
282 my $self = shift;
284 $self->{stack} = [ [] ];
286 $self->{op}->{VERSION} = [ '"', $Art5::VERSION ];
288 $self->{op}->{VERSION_STR} = [
289 '?', [ '"', 'Artemus ' ], [ 'VERSION' ]
292 # literal
293 $self->{op}->{'"'} = sub {
294 return $_[0];
297 # translateable literal
298 $self->{op}->{'@'} = sub {
299 return $self->{t}->{$_[0]} || $_[0];
302 # argument
303 $self->{op}->{'$'} = sub {
304 return $self->{stack}->[-1]->[$_[0]];
307 # external hash (e.g. CGI variables)
308 $self->{op}->{'%'} = sub {
309 my $var = shift;
311 return $var eq '%' ? $self->{xh} : $self->{xh}->{$var};
314 # joiner
315 $self->{op}->{'?'} = sub {
316 if (scalar(@_) == 1) {
317 return $self->exec($_[0]);
320 return join('', map { $self->exec($_); } @_);
323 # array
324 $self->{op}->{'&'} = sub {
325 return [ map { $self->exec($_); } @_ ];
328 # assignation
329 $self->{op}->{'='} = sub {
330 $self->{op}->{$self->exec($_[0])} =
331 [ '"', $self->exec($_[1]) ];
333 return '';
336 # list of translation pairs
337 $self->{op}->{'T'} = sub {
338 while (scalar(@_) > 1) {
339 my $k = $self->exec(shift);
340 my $v = $self->exec(shift);
342 $self->{t}->{$k} = $v;
345 return '';
348 $self->{op}->{eq} = sub {
349 $self->exec($_[0]) eq
350 $self->exec($_[1]) ? 1 : 0;
352 $self->{op}->{ne} = sub {
353 $self->exec($_[0]) ne
354 $self->exec($_[1]) ? 1 : 0;
357 $self->{op}->{and} = sub {
358 $self->exec($_[0]) && $self->exec($_[1]);
360 $self->{op}->{or} = sub {
361 $self->exec($_[0]) || $self->exec($_[1]);
363 $self->{op}->{not} = sub {
364 $self->exec($_[0]) ? 0 : 1;
367 $self->{op}->{if} = sub {
368 my $ret = '';
370 if ($self->exec($_[0])) {
371 $ret = $self->exec($_[1]);
373 elsif (scalar(@_) == 3) {
374 $ret = $self->exec($_[2]);
377 $ret;
380 $self->{op}->{add} = sub {
381 return $self->exec0($_[0]) + $self->exec0($_[1]);
383 $self->{op}->{sub} = sub {
384 return $self->exec0($_[0]) - $self->exec0($_[1]);
386 $self->{op}->{mul} = sub {
387 return $self->exec0($_[0]) * $self->exec0($_[1]);
389 $self->{op}->{div} = sub {
390 return $self->exec0($_[0]) / $self->exec0($_[1]);
393 $self->{op}->{gt} = sub {
394 return $self->exec0($_[0]) > $self->exec0($_[1]);
396 $self->{op}->{lt} = sub {
397 return $self->exec0($_[0]) < $self->exec0($_[1]);
399 $self->{op}->{random} = sub {
400 return $self->exec($_[rand(scalar(@_))]);
403 $self->{op}->{env} = sub {
404 # no arguments? return keys as an arrayref
405 if (scalar(@_) == 0) {
406 return [ keys(%ENV) ];
409 return $ENV{$self->exec($_[0])};
412 $self->{op}->{foreach} = sub {
413 my $list = shift;
414 my $code = shift || [ '$', 0 ];
415 my $sep = shift || [ '"', '' ];
416 my $header = shift || [ '"', '' ];
418 my @ret = ();
419 my $ph = '';
421 foreach my $e (@{$self->exec($list)}) {
422 # create a stack for the elements
423 # and store the element in the stack
424 push(@{$self->{stack}}, ref($e) ? $e : [ $e ]);
426 # execute the header code
427 my $o = $self->exec($header);
429 # if it's different from previous header,
430 # strip from output; otherwise, remember
431 # for next time
432 if ($ph eq $o) {
433 $o = '';
435 else {
436 $ph = $o;
439 # execute the body code
440 $o .= $self->exec($code);
442 push(@ret, $o);
444 # destroy last stack
445 pop(@{$self->{stack}});
448 return join($self->exec($sep), @ret);
451 $self->{op}->{case} = sub {
452 my $value = $self->exec(shift);
453 my $oth;
455 # if args are odd, the last one is
456 # the 'otherwise' case
457 if (scalar(@_) % 2) {
458 $oth = pop(@_);
461 # now treat the rest of arguments as
462 # pairs of case / result
463 while (@_) {
464 my $case = $self->exec(shift);
465 my $res = shift;
467 if ($value eq $case) {
468 return $self->exec($res);
472 return defined($oth) ? $self->exec($oth) : '';
475 $self->{op}->{seq} = sub {
476 my $from = $self->exec0(shift);
477 my $to = $self->exec0(shift);
479 return [ $from .. $to ];
482 $self->{op}->{sort} = sub {
483 my $list = $self->exec(shift);
484 my $code = shift || [ '$', 0 ];
486 # create a stack for the elements
487 push(@{$self->{stack}}, []);
489 my $ret = [ sort {
490 $self->{stack}->[-1] = ref($a) ? $a : [ $a ];
491 my $va = $self->exec($code);
493 $self->{stack}->[-1] = ref($b) ? $b : [ $b ];
494 my $vb = $self->exec($code);
496 $va cmp $vb;
497 } @{$list} ];
499 # destroy last stack
500 pop(@{$self->{stack}});
502 return $ret;
505 $self->{op}->{reverse} = sub {
506 return [ reverse @{$self->exec(shift)} ];
509 $self->{op}->{size} = sub { return scalar @{$self->exec($_[0])} };
511 $self->{op}->{split} = sub {
512 if (scalar(@_) == 3) {
513 return [ map { [ split($self->exec($_[1]), $_) ] }
514 split($self->exec($_[0]), $self->exec($_[2]))
517 return [ split($self->exec($_[0]), $self->exec($_[1])) ];
520 $self->{op}->{dump} = sub {
521 use Data::Dumper;
523 return Dumper($self->exec($_[0]));
526 $self->{xh}->{arch} = 'Unix';
528 return $self;
532 sub process {
533 my $self = shift;
534 my $src = shift;
536 my $c = $self->compile($src);
538 return $self->exec($c, @_);
542 sub new {
543 my $class = shift;
545 my $self = bless { @_ }, $class;
547 $self->{path} ||= [];
549 if ($self->{cache}) {
550 mkdir $self->{cache};
553 return $self->init();
557 __END__
558 =pod
560 =head1 NAME
562 Art5 - Template Toolkit
564 =head1 SYNOPSIS
566 use Art5;
568 # creates a new object
569 my $art5 = Art5->new(path => \@path_to_templates);
571 # compiles and executes a string of Art5 code
572 my $r = $art5->process($source_code);
574 =head1 DESCRIPTION
576 Artemus is a template toolkit. It filters text files, parsing, compiling
577 and executing code surrounded by special marks (leaving the rest
578 untouched) and concatenating everything as output. Its main purpose is
579 to filter HTML files, but it can be used for any scripting need related
580 to text filtering and substitution.
582 The main purpose of the Art5 API is to add your own functions to the
583 Art5 machine to make them part of the programming language. For more
584 information on the Art5 Templating Language, please see the included
585 L<art5_overview> document.
587 This can be done by adding code to the C<op> component of the Art5
588 object. For example, this is a way to add a C<localtime> function to
589 Art5:
591 $art5->{op}->{localtime} = sub { return localtime(); };
593 Art5 functions can also accept arguments. They arrive as code streams
594 that must be executed before use. For example, this is a function that
595 accept two numbers and returns the average:
597 $art5->{op}->{avg} = sub {
598 my $v1 = shift;
599 my $v2 = shift;
601 return ($art5->exec($v1) + $art5->exec($v2)) / 2;
604 Art5 functions always have to return something. If you have nothing to
605 return, use an empty string. If an array must be returned (for example,
606 to be feed to C<foreach>, return a reference to it (not the array
607 itself).
609 The external hash can similarly accessed by tweaking the C<xh>
610 component. In this example, the running program process id will be
611 accesible as %pid:
613 $art5->{xh}->{pid} = $!;
615 =head1 FUNCTIONS AND METHODS
617 =cut
619 =head2 new
621 $art5 = Art5->new(
622 [ path => \@directories, ]
623 [ cache => $directory, ]
624 [ loader_func => \&function, ]
627 Creates a new Art5 object. The object creation accepts the following
628 arguments:
630 =head3 path
632 A reference to a list of directories where templates are to be found.
634 =head3 cache
636 A directory path where compiled templates are to be cached. These compiled
637 templates are raw Data::Dumper output of the compiled stream, and are
638 loaded back with simple C<eval()>, so take B<extreme care>.
640 =head3 loader_func
642 A pointer to a function to be called whenever a new template is queried
643 by the underlying system. This function should return the content of a
644 template or undef if not found. This mechanism is used to have an external
645 storage for templates (as in a SQL Database, for example). Take note that
646 templates retrived this way cannot be cached (this defect will eventually
647 be solved).
649 This function is called before any search in the L<path>.
651 =head2 process
653 my $ret_val = $art->process($art5_code);
655 Compiles a string of Art5 code, executes it and returns the exit
656 value.
658 =head2 compile
660 my $opcode_stream = $art5->compile($art5_code);
662 Reads a string of Art5 code and returns a compiled stream.
664 =head2 exec
666 my $ret_val = $art5->exec($opcode_stream);
668 Executes a compiled stream (returned by C<compile()>) and returns
669 the exit value.
671 =head1 AUTHOR
673 Angel Ortega angel@triptico.com