Final documentation tweaks.
[artemus.git] / Art5.pm
blob7c5d2a661c21bee42727046b4cf7f80a325593e3
1 #####################################################################
3 # Artemus - Template Toolkit version 5
5 # Copyright (C) 2000/2009 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.0-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? return a NOP */
109 if (!@ret) {
110 return [ '"', '' ];
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 return $ret[0];
121 # otherwise, prepend a '?' (joiner)
122 unshift(@ret, '?');
125 return [ @ret ];
129 sub compile {
130 my $self = shift;
131 my $str = shift;
133 # was this code already compiled?
134 if (exists($self->{pc}->{$str})) {
135 return $self->{pc}->{$str};
138 # joiner opcode
139 my @ret = ( '?' );
141 # split by the Artemus5 marks
142 my @stream = split(/(<\{|\}>)/, $str);
144 # alternate between literal strings and Artemus5 code
145 while (@stream) {
146 my $p = shift(@stream);
148 if ($p eq '<{') {
149 $p = '{' . shift(@stream) . '}';
150 push(@ret, $self->parse(\$p));
151 shift(@stream);
153 elsif ($p) {
154 push(@ret, [ '"', $p ]);
158 my $ret = [ @ret ];
160 return $self->{pc}->{$str} = $ret;
164 sub code {
165 my $self = shift;
166 my $op = shift;
168 if (!exists($self->{op}->{$op})) {
169 my $src = undef;
171 # filter opcode to only allow
172 # characters valid in file names
173 $op =~ s/[^\w\d_-]//g;
175 # does a loader_func() exist?
176 if (ref($self->{loader_func}) eq 'CODE') {
177 $src = $self->{loader_func}->($op);
180 if (!defined($src)) {
181 # try to resolve by loading
182 # a source file from the path
183 foreach my $p (@{$self->{path}}) {
184 my $fp = $p . '/' . $op;
186 # does a precompiled script already exist?
187 if ($self->{cache} && -f $fp) {
188 my $cp = $self->{cache} . $op;
190 if (-f $cp && -M $cp < -M $fp) {
191 # it does and it's fresh; import wildly
192 $self->{op}->{$op} = eval "require '$cp'";
193 last;
197 # load the source
198 if (open(F, $fp)) {
199 $src = join('', <F>);
200 close F;
202 last;
207 # compile if available
208 if (defined($src)) {
209 $self->{op}->{$op} = $self->compile($src);
211 # if there is a cache directory, save the compiled code
212 if ($self->{cache} and open(F, '>' . $self->{cache} . $op)) {
213 use Data::Dumper;
215 print F Dumper($self->{op}->{$op});
216 close F;
221 return $self->{op}->{$op};
225 sub exec {
226 my $self = shift;
227 my $prg = shift;
228 my $ret;
230 # aborted or empty? do nothing more
231 if (!ref($prg) || $self->{abort}) {
232 return '';
235 # stream of Artemus5 code
236 my @stream = @{$prg};
238 # pick opcode
239 my $op = shift(@stream);
241 # pick code
242 my $c = $self->code($op);
244 if (ref($c) eq 'CODE') {
245 $ret = $c->(@stream);
247 elsif (ref($c) eq 'ARRAY') {
248 # push the arguments to the stack
249 push(@{$self->{stack}},
250 [ map { $self->exec($_); }
251 @stream ]);
253 $ret = $self->exec($c);
255 # drop stack
256 pop(@{$self->{stack}});
258 else {
259 croak "Artemus5 opcode not found: $op";
262 if (!defined($ret)) {
263 $ret = '';
266 return $ret;
270 sub exec0 {
271 my $self = shift;
273 return $self->exec(@_) || 0;
277 sub init {
278 my $self = shift;
280 $self->{stack} = [ [] ];
282 $self->{op}->{VERSION} = [ '"', $Art5::VERSION ];
284 $self->{op}->{VERSION_STR} = [
285 '?', 'Artemus ', [ 'VERSION' ]
288 # literal
289 $self->{op}->{'"'} = sub {
290 return $_[0];
293 # translateable literal
294 $self->{op}->{'@'} = sub {
295 return $self->{t}->{$_[0]} || $_[0];
298 # argument
299 $self->{op}->{'$'} = sub {
300 return $self->{stack}->[-1]->[$_[0]];
303 # external hash (e.g. CGI variables)
304 $self->{op}->{'%'} = sub {
305 return $self->{xh}->{$_[0]};
308 # joiner
309 $self->{op}->{'?'} = sub {
310 if (scalar(@_) == 1) {
311 return $self->exec($_[0]);
314 return join('', map { $self->exec($_); } @_);
317 # array
318 $self->{op}->{'&'} = sub {
319 return [ map { $self->exec($_); } @_ ];
322 # assignation
323 $self->{op}->{'='} = sub {
324 $self->{op}->{$self->exec($_[0])} =
325 [ '"', $self->exec($_[1]) ];
327 return '';
330 # list of translation pairs
331 $self->{op}->{'T'} = sub {
332 while (scalar(@_) > 1) {
333 my $k = $self->exec(shift);
334 my $v = $self->exec(shift);
336 $self->{t}->{$k} = $v;
339 return '';
342 $self->{op}->{eq} = sub {
343 $self->exec($_[0]) eq
344 $self->exec($_[1]) ? 1 : 0;
346 $self->{op}->{ne} = sub {
347 $self->exec($_[0]) ne
348 $self->exec($_[1]) ? 1 : 0;
351 $self->{op}->{and} = sub {
352 $self->exec($_[0]) && $self->exec($_[1]);
354 $self->{op}->{or} = sub {
355 $self->exec($_[0]) || $self->exec($_[1]);
357 $self->{op}->{not} = sub {
358 $self->exec($_[0]) ? 0 : 1;
361 $self->{op}->{if} = sub {
362 my $ret = '';
364 if ($self->exec($_[0])) {
365 $ret = $self->exec($_[1]);
367 elsif (scalar(@_) == 3) {
368 $ret = $self->exec($_[2]);
371 $ret;
374 $self->{op}->{add} = sub {
375 return $self->exec0($_[0]) + $self->exec0($_[1]);
377 $self->{op}->{sub} = sub {
378 return $self->exec0($_[0]) - $self->exec0($_[1]);
380 $self->{op}->{mul} = sub {
381 return $self->exec0($_[0]) * $self->exec0($_[1]);
383 $self->{op}->{div} = sub {
384 return $self->exec0($_[0]) / $self->exec0($_[1]);
387 $self->{op}->{gt} = sub {
388 return $self->exec0($_[0]) > $self->exec0($_[1]);
390 $self->{op}->{lt} = sub {
391 return $self->exec0($_[0]) < $self->exec0($_[1]);
393 $self->{op}->{random} = sub {
394 return $self->exec($_[rand(scalar(@_))]);
397 $self->{op}->{env} = sub {
398 # no arguments? return keys as an arrayref
399 if (scalar(@_) == 0) {
400 return [ keys(%ENV) ];
403 return $ENV{$self->exec($_[0])};
406 $self->{op}->{foreach} = sub {
407 my $list = shift;
408 my $code = shift || [ '$', 0 ];
409 my $sep = shift || [ '"', '' ];
410 my $header = shift || [ '"', '' ];
412 my @ret = ();
413 my $ph = '';
415 foreach my $e (@{$self->exec($list)}) {
416 # create a stack for the elements
417 # and store the element in the stack
418 push(@{$self->{stack}}, ref($e) ? $e : [ $e ]);
420 # execute the header code
421 my $o = $self->exec($header);
423 # if it's different from previous header,
424 # strip from output; otherwise, remember
425 # for next time
426 if ($ph eq $o) {
427 $o = '';
429 else {
430 $ph = $o;
433 # execute the body code
434 $o .= $self->exec($code);
436 push(@ret, $o);
438 # destroy last stack
439 pop(@{$self->{stack}});
442 return join($self->exec($sep), @ret);
445 $self->{op}->{case} = sub {
446 my $value = $self->exec(shift);
447 my $oth;
449 # if args are odd, the last one is
450 # the 'otherwise' case
451 if (scalar(@_) % 2) {
452 $oth = pop(@_);
455 # now treat the rest of arguments as
456 # pairs of case / result
457 while (@_) {
458 my $case = $self->exec(shift);
459 my $res = shift;
461 if ($value eq $case) {
462 return $self->exec($res);
466 return defined($oth) ? $self->exec($oth) : '';
469 $self->{op}->{seq} = sub {
470 my $from = $self->exec0(shift);
471 my $to = $self->exec0(shift);
473 return [ $from .. $to ];
476 $self->{op}->{sort} = sub {
477 my $list = $self->exec(shift);
478 my $code = shift || [ '$', 0 ];
480 # create a stack for the elements
481 push(@{$self->{stack}}, []);
483 my $ret = [ sort {
484 $self->{stack}->[-1] = ref($a) ? $a : [ $a ];
485 my $va = $self->exec($code);
487 $self->{stack}->[-1] = ref($b) ? $b : [ $b ];
488 my $vb = $self->exec($code);
490 $va cmp $vb;
491 } @{$list} ];
493 # destroy last stack
494 pop(@{$self->{stack}});
496 return $ret;
499 $self->{op}->{reverse} = sub {
500 return [ reverse @{$self->exec(shift)} ];
503 $self->{op}->{size} = sub { return scalar @{$self->exec($_[0])} };
505 $self->{op}->{split} = sub {
506 if (scalar(@_) == 3) {
507 return [ map { [ split($self->exec($_[1]), $_) ] }
508 split($self->exec($_[0]), $self->exec($_[2]))
511 return [ split($self->exec($_[0]), $self->exec($_[1])) ];
514 $self->{xh}->{arch} = 'Unix';
516 return $self;
520 sub process {
521 my $self = shift;
522 my $src = shift;
524 my $c = $self->compile($src);
526 return $self->exec($c, @_);
530 sub new {
531 my $class = shift;
533 my $self = bless { @_ }, $class;
535 $self->{path} ||= [];
537 if ($self->{cache}) {
538 mkdir $self->{cache};
541 return $self->init();
545 __END__
546 =pod
548 =head1 NAME
550 Art5 - Template Toolkit
552 =head1 SYNOPSIS
554 use Art5;
556 # creates a new object
557 my $art5 = Art5->new(path => \@path_to_templates);
559 # compiles and executes a string of Art5 code
560 my $r = $art5->process($source_code);
562 =head1 DESCRIPTION
564 Artemus is a template toolkit. It filters text files, parsing, compiling
565 and executing code surrounded by special marks (leaving the rest
566 untouched) and concatenating everything as output. Its main purpose is
567 to filter HTML files, but it can be used for any scripting need related
568 to text filtering and substitution.
570 The main purpose of the Art5 API is to add your own functions to the
571 Art5 machine to make them part of the programming language. For more
572 information on the Art5 Templating Language, please see the included
573 L<art5_overview> document.
575 This can be done by adding code to the C<op> component of the Art5
576 object. For example, this is a way to add a C<localtime> function to
577 Art5:
579 $art5->{op}->{localtime} = sub { return localtime(); };
581 Art5 functions can also accept arguments. They arrive as code streams
582 that must be executed before use. For example, this is a function that
583 accept two numbers and returns the average:
585 $art5->{op}->{avg} = sub {
586 my $v1 = shift;
587 my $v2 = shift;
589 return ($art5->exec($v1) + $art5->exec($v2)) / 2;
592 Art5 functions always have to return something. If you have nothing to
593 return, use an empty string. If an array must be returned (for example,
594 to be feed to C<foreach>, return a reference to it (not the array
595 itself).
597 The external hash can similarly accessed by tweaking the C<xh>
598 component. In this example, the running program process id will be
599 accesible as %pid:
601 $art5->{xh}->{pid} = $!;
603 =head1 FUNCTIONS AND METHODS
605 =cut
607 =head2 new
609 $art5 = Art5->new(
610 [ path => \@directories, ]
611 [ cache => $directory, ]
612 [ loader_func => \&function, ]
615 Creates a new Art5 object. The object creation accepts the following
616 arguments:
618 =head3 path
620 A reference to a list of directories where templates are to be found.
622 =head3 cache
624 A directory path where compiled templates are to be cached. These compiled
625 templates are raw Data::Dumper output of the compiled stream, and are
626 loaded back with simple C<eval()>, so take B<extreme care>.
628 =head3 loader_func
630 A pointer to a function to be called whenever a new template is queried
631 by the underlying system. This function should return the content of a
632 template or undef if not found. This mechanism is used to have an external
633 storage for templates (as in a SQL Database, for example). Take note that
634 templates retrived this way cannot be cached (this defect will eventually
635 be solved).
637 This function is called before any search in the L<path>.
639 =head2 process
641 my $ret_val = $art->process($art5_code);
643 Compiles a string of Art5 code, executes it and returns the exit
644 value.
646 =head2 compile
648 my $opcode_stream = $art5->compile($art5_code);
650 Reads a string of Art5 code and returns a compiled stream.
652 =head2 exec
654 my $ret_val = $art5->exec($opcode_stream);
656 Executes a compiled stream (returned by C<compile()>) and returns
657 the exit value.
659 =head1 AUTHOR
661 Angel Ortega angel@triptico.com