New argument 'inverse' to 'sort'.
[artemus.git] / Artemus.pm
blob7a9c4566d1ca1c98b4d8c99aecf2a24162699754
1 #####################################################################
3 # Artemus - Template Toolkit
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 Artemus;
29 use strict;
30 use warnings;
32 $Artemus::VERSION = '4.1.2-dev';
34 =pod
36 =head1 NAME
38 Artemus - Template Toolkit
40 =head1 SYNOPSIS
42 use Artemus;
44 # normal variables
45 %vars = (
46 "copyright" => 'Copyright 2002', # normal variable
47 "number" => 100, # another
48 "about" => '{-copyright} My Self', # can be nested
49 "link" => '<a href="$0">$1</a>' # can accept parameters
52 # functions as templates
53 %funcs = (
54 "rnd" => sub { int(rand(100)) }, # normal function
55 "sqrt" => sub { sqrt($_[0]) } # can accept parameters
58 # create a new Artemus instance
59 $ah = new Artemus( "vars" => \%vars, "funcs" => \%funcs );
61 # do it
62 $out = $ah->process('Click on {-link|http://my.page|my page}, {-about}');
63 $out2 = $ah->process('The square root of {-number} is {-sqrt|{-number}}');
65 =head1 DESCRIPTION
67 Artemus is yet another template toolkit. Though it was designed
68 to preprocess HTML, it can be used for any task that involves
69 text substitution. These templates can be plain text, text with
70 parameters and hooks to real Perl code. This document describes
71 the Artemus markup as well as the API.
73 =for html <->
75 You can download the latest version of this package and get
76 more information from its home page at
78 http://triptico.com/software/artemus.html
80 =head1 THE ARTEMUS MARKUP
82 =head2 Simple templates
84 The simplest Artemus template is just a text substitution. If
85 you set the 'about' template to '(C) 2000/2002 My Self', you
86 can just write in your text
88 This software is {-about}.
90 and found it replaced by
92 This software is (C) 2000/2002 My Self.
94 Artemus templates can be nestable; so, if you set another
95 template, called 'copyright' and containing '(C) 2000/2002', you
96 can set 'about' to be '{-copyright} My Self', and obtain the
97 same result. Though they can be nested nearly ad-infinitum, making
98 circular references is unwise.
100 =head2 Templates with parameters
102 This wouldn't be any cool if templates where just text substitutions.
103 But you can create templates that accept parameters just by including
104 $0, $1, $2... marks inside its content. This marks will be replaced
105 by the parameters used when inserting the call.
107 So, if you create the 'link' template containing
109 <a href = "$0">$1</a>
111 you can insert the following call:
113 {-link|http://triptico.com|Angel Ortega's Home Page}
115 As you can see, you use the | character as a separator
116 among the parameters and the template name itself.
118 =head2 Perl functions as templates
120 Anything more complicated than this would require the definition
121 of special functions provided by you. To do it, you just add
122 templates to the 'funcs' hash reference when the Artemus object
123 is created which values are references to Perl functions. For
124 example, you can create a function returning a random value
125 by using:
127 $funcs{'rnd'} = sub { int(rand(100)) };
129 And each time the {-random} template is found, it is evaluated
130 and returns a random number between 0 and 99.
132 Functions also can accept parameters; so, if you define it as
134 $funcs{'rnd'} = sub { int(rand($_[0])) };
136 then calling the template as
138 {-rnd|500}
140 will return each time it's evaluated a random value between 0 and 499.
142 =head2 Aborting further execution from a function
144 If the I<abort-flag> argument is set to a scalar reference when creating
145 the Artemus object, template processing can be aborted by setting
146 this scalar to non-zero from inside a template function.
148 =head2 Caching templates
150 If a template is expensive or time consuming (probably because it
151 calls several template functions that take very much time), it can be
152 marked as cacheable. You must set the 'cache-path' argument for
153 this to work, and include the following special Artemus code
154 inside the template:
156 {-\CACHE|number}
158 where I<number> is a number of days (or fraction of day) the
159 cache will remain cached before being re-evaluated. Individual
160 template functions cannot be cached; you must wrap them in a
161 normal template if need it.
163 =head2 Documenting templates
165 Artemus templates can contain documentation in Perl's POD format.
166 This POD documentation is stripped each time the template is evaluated
167 unless you create the Artemus object with the I<contains-pod> argument
168 set.
170 See http://www.perldoc.com/perl5.8.0/pod/perlpod.html and
171 http://www.perldoc.com/perl5.8.0/pod/perlpodspec.html for information
172 about writing POD documentation.
174 =head2 Unresolved templates
176 If a template is not found, it will be replaced by its name (that is,
177 stripped out of the {- and } and left there). Also, the names of the
178 unresolved templates are appended to an array referenced by the
179 I<unresolved> argument, if one was defined when the Artemus object
180 was created.
182 =head2 Predefined templates
184 =over 4
186 =item B<if>
188 {-if|condition|text}
189 {-if|condition|text_if_true|text_unless_true}
191 If I<condition> is true, this template returns I<text>, or nothing
192 otherwise; in the 3 argument version, returns I<text_if_true> or
193 I<text_unless_true>. A condition is true if is not zero or the empty
194 string (the same as in Perl).
196 =item B<ifelse>
198 This is an alias for the I<if> template provided for backwards-compatibility.
199 Don't use it.
201 =item B<ifeq>
203 {-ifeq|term1|term2|text}
204 {-ifeq|term1|term2|text_if_true|text_unless_true}
206 If I<term1> is equal to I<term2>, this template returns I<text>, or nothing
207 otherwise. in the 4 argument version, returns I<text_if_true> or
208 I<text_unless_true>.
210 =item B<ifneq>
212 {-ifneq|term1|term2|text}
214 If I<term1> is not equal to I<term2>, this template returns I<text>, or
215 nothing otherwise.
217 =item B<ifeqelse>
219 This is an alias for the I<ifeq> template provided for backwards-compatibility.
220 Don't use it.
222 =item B<add>, B<sub>
224 {-add|num1|num2}
225 {-sub|num1|num2}
227 This functions add or substract the values and returns the result.
229 =item B<gt>, B<lt>, B<eq>
231 {-gt|value1|value2}
232 {-lt|value1|value2}
233 {-eq|value1|value2}
235 This functions compare if I<value1> is greater-than, lesser-than or equal to
236 I<value2>. Meant primarily to use with the I<if> template.
238 =item B<random>
240 {-random|value1|value2|...}
242 This function returns randomly one of the values sent as arguments. There can
243 any number of arguments.
245 =item B<and>
247 {-and|value_or_condition_1|value_or_condition_2}
249 If both values are true or defined, returns I<value_or_condition_2>; otherwise,
250 returns the empty string.
252 =item B<or>
254 {-or|value_or_condition_1|value_or_condition_2}
256 If I<value_or_condition_1> is true or defined, returns it; otherwise, if
257 I<value_or_condition_2> is true or defined, returns it; otherwise, returns
258 the empty string.
260 =item <not>
262 {-not|condition}
264 Returns the negation of I<condition>.
266 =item B<set>
268 {-set|template_name|value}
270 Assigns a value to a template. Same as setting a value from the 'vars'
271 argument to B<new>, but from Artemus code.
273 If you must change a variable from inside an I<if> directive, don't
274 forget to escape the I<set> directive, as in
276 {-ifeq|{-user}|admin|\{-set\|powers\|EVERYTHING\}}
278 IF you don't escape it, the I<powers> variable will be inevitably set
279 to EVERYTHING.
281 =item B<foreach>
283 {-foreach|list:of:colon:separated:values|output_text|separator}
285 Iterates the list of colon separated values and returns I<output_text>
286 for each one of the values, separating each of them with I<separator>
287 (if one is defined). Each element itself can be a list of comma
288 separated values that will be split and assigned to the $0, $1... etc
289 parameters set to I<output_text>. For example, to create a I<select>
290 HTML tag:
292 <select name = 'work_days'>
293 {-foreach|Monday,1:Tuesday,2:Wednesday,3:Thursday,4:Friday,5|
294 <option value = '\$1'>\$0</option>
296 </select>
298 Remember to escape the dollar signs to avoid being expanded too early,
299 and if the I<output_text> include calls to other Artemus templates,
300 to escape them as well.
302 =item B<case>
304 {-case|string|value_1|return_1|value_2|return_2|...}
305 {-case|string|value_1|return_1|value_2|return_2|...|default_value}
307 Compares I<string> against the list of I<value_1>, I<value_2>... and
308 returns the appropriate I<return_1>, I<return_2>... value. If I<default_value>
309 is set (that is, I<case> has an odd number of arguments) it's returned
310 if I<string> does not match any value.
312 =item B<env>
314 {-env|environment_variable}
315 {-env}
317 If I<environment_variable> has a value set in the environment, it's returned,
318 or the empty string otherwise. If no environment variable is set, returns
319 a colon-separated list of environment variable names.
321 =item B<size>
323 {-size|colon_separated_list}
325 Returns the number of elements in I<colon_separated_list>.
327 =item B<seq>
329 {-seq|from_number|to_number}
331 Generates a colon-separated list of the numbers from I<from_number>
332 to I<to_number>. Useful in a I<foreach> loop.
334 =item B<sort>
336 {-sort|list}
337 {-sort|list|field}
338 {-sort|list|field|inverse}
340 Sorts the colon-separated list. The optional I<field> is the field
341 to sort on (assuming the elements of the list are comma-separated
342 lists themselves). If I<inverse> is set, the sorting is reversed.
344 =item B<\CACHE>
346 {-\CACHE|time}
348 Marks a template as cacheable and sets its cache time. See above.
350 =item B<\VERSION>
352 {-\VERSION}
354 Returns current Artemus version.
356 =item B<\BEGIN>
358 =item B<\END>
360 If you set these templates, they will be appended (\BEGIN) and
361 prepended (\END) to the text being processed.
363 =back
365 =head2 Escaping
367 Escaping has been briefly mentioned above; this is a way to avoid
368 prematurely expanding and executing Artemus templates, and a direct
369 derivative of the simple text substitution approach of the Artemus
370 engine.
372 To escape an Artemus template call you must escape ALL characters
373 that has special meaning to the uber-simple Artemus parser (that is,
374 the opening and closing braces, the pipe argument separator and
375 the optional dollar prefixes for arguments). If you nest some
376 directives (for example, two I<foreach> calls), you must
377 double-escape everything. Yes, this can get really cumbersome.
379 =head1 FUNCTIONS AND METHODS
381 =cut
383 =head2 B<new>
385 $ah = new Artemus(
386 [ "vars" => \%variables, ]
387 [ "funcs" => \%functions, ]
388 [ "inv-vars" => \%inverse_variables, ]
389 [ "include-path" => $dir_with_templates_in_files, ]
390 [ "cache-path" => $dir_to_store_cached_templates, ]
391 [ "abort-flag" => \$abort_flag, ]
392 [ "unresolved" => \@unresolved_templates, ]
393 [ "use-cr-lf" => $boolean, ]
394 [ "contains-pod" => $boolean, ]
395 [ "paragraph-separator" => $separator, ]
396 [ "strip-html-comments" => $boolean, ]
397 [ "AUTOLOAD" => \&autoload_func ]
400 Creates a new Artemus object. The following arguments (passed to it
401 as a hash) can be used:
403 =over 4
405 =item I<vars>
407 This argument must be a reference to a hash containing
408 I<template> - I<content> pairs.
410 =item I<funcs>
412 This argument must be a reference to a hash containing
413 I<template name> - I<code reference> pairs. Each time one of these
414 templates is evaluated, the function will be called with
415 the template parameters passed as the function's arguments.
417 =item I<inv-vars>
419 This argument must be a reference to a hash containing
420 I<text> - I<content> pairs. Any occurrence of I<text> will be
421 replaced by I<content>. They are called 'inverse variables'
422 because they use to store variables that expand to Artemus
423 markup, but can contain anything. This is really a plain
424 text substitution, so use it with care (B<NOTE>: this
425 option is disabled by now until it works correctly).
427 =item I<include-path>
429 If this string is set, it must point to a readable directory
430 that contains templates, one on each file. The file names
431 will be treated as template names. Many directories can
432 be specified by separating them with colons.
434 =item I<cache-path>
436 If this string is set, it must contain the path to a readable
437 and writable directory where the cacheable templates are cached.
438 See L<Caching templates> for further information.
440 =item I<abort-flag>
442 This argument must be a reference to a scalar. When the template
443 processing is started, this scalar is set to 0. Template functions
444 can set it to any other non-zero value to stop template processing.
446 =item I<unresolved>
448 If this argument points to an array reference, it will be filled
449 with the name of any unresolved templates. Each time a template
450 processing is started, the array is emptied.
452 =item I<use-cr-lf>
454 If this flag is set, all lines are separated using CR/LF instead
455 of just LF (useful to generate MSDOS/Windows compatible text files).
457 =item I<contains-pod>
459 If this flag is set, the (possible) POD documentation inside the
460 templates are not stripped-out. Understand this flag as saying
461 'this template has pod as part of its content, so do not strip it'.
462 See L<Documenting templates>.
464 =item I<paragraph-separator>
466 If this argument is set to some string, all empty lines will be
467 substituted by it (can be another Artemus template).
469 =item I<strip-html-comments>
471 If this flag is set, HTML comments are stripped before any
472 processing.
474 =item I<AUTOLOAD>
476 If this argument points to a sub reference, the subrutine will
477 be executed when a template is unresolved and its return value used
478 as the final substitution value. Similar to the AUTOLOAD function
479 in Perl standard modules. The unresolved template name will be
480 sent as the first argument.
482 =back
484 =cut
486 sub new
488 my ($class, %params) = @_;
490 my $self = bless({ %params }, $class);
492 # special variables
493 $self->{vars}->{'\n'} = "\n";
494 $self->{vars}->{'\BEGIN'} ||= '';
495 $self->{vars}->{'\END'} ||= '';
496 $self->{vars}->{'\VERSION'} = $Artemus::VERSION;
498 # special functions
499 $self->{funcs}->{localtime} = sub { scalar(localtime) };
501 $self->{funcs}->{if} = sub { $_[0] ? $_[1] : (scalar(@_) == 3 ? $_[2] : '') };
502 $self->{funcs}->{ifelse} = $self->{funcs}->{if};
504 $self->{funcs}->{ifeq} = sub { $_[0] eq $_[1] ? $_[2] : (scalar(@_) == 4 ? $_[3] : '') };
505 $self->{funcs}->{ifneq} = sub { $_[0] ne $_[1] ? $_[2] : (scalar(@_) == 4 ? $_[3] : '') };
506 $self->{funcs}->{ifeqelse} = $self->{funcs}->{ifeq};
508 $self->{funcs}->{add} = sub { ($_[0] || 0) + ($_[1] || 0); };
509 $self->{funcs}->{sub} = sub { ($_[0] || 0) - ($_[1] || 0); };
510 $self->{funcs}->{gt} = sub { ($_[0] || 0) > ($_[1] || 0); };
511 $self->{funcs}->{lt} = sub { ($_[0] || 0) < ($_[1] || 0); };
512 $self->{funcs}->{eq} = sub { $_[0] eq $_[1] ? 1 : 0; };
513 $self->{funcs}->{random} = sub { $_[rand(scalar(@_))]; };
515 $self->{funcs}->{and} = sub { ($_[0] && $_[1]) || ''; };
516 $self->{funcs}->{or} = sub { $_[0] || $_[1] || ''; };
517 $self->{funcs}->{not} = sub { $_[0] ? 0 : 1; };
519 $self->{funcs}->{foreach} = sub {
520 my $list = shift;
521 my $code = shift || '$0';
522 my $sep = shift || '';
524 my @ret = ();
525 my @l = split(/\s*:\s*/, $list);
527 foreach my $l (@l) {
528 my @e = split(/\s*,\s*/, $l);
530 push(@ret, $self->params($code, @e));
533 return join($sep, @ret);
536 $self->{funcs}->{set} = sub { $self->{vars}->{$_[0]} = $_[1]; return ''; };
538 $self->{funcs}->{case} = sub {
539 my $var = shift;
540 my $ret = '';
542 chomp($var);
544 # if args are odd, the last one is
545 # the 'otherwise' case
546 if (scalar(@_) / 2 != int(scalar(@_) / 2)) {
547 $ret = pop(@_);
550 while (@_) {
551 my $val = shift;
552 my $out = shift;
554 chomp($val);
556 if ($var eq $val) {
557 $ret = $out;
558 last;
562 return $ret;
565 $self->{funcs}->{env} = sub { scalar(@_) ? ($ENV{$_[0]} || '') : join(':', keys(%ENV)); };
566 $self->{funcs}->{size} = sub { scalar(@_) ? split(/\s*:\s*/, $_[0]) : 0; };
567 $self->{funcs}->{seq} = sub { join(':', ($_[0] || 0) .. ($_[1] || 0)); };
569 $self->{funcs}->{sort} = sub {
570 my $list = shift;
571 my $field = shift || 0;
572 my $inverse = shift || 0;
574 join(':',
575 sort {
576 my @a = split(',', $a);
577 my @b = split(',', $b);
579 return $inverse ? $b[$field] cmp $a[$field] :
580 $a[$field] cmp $b[$field];
581 } split(':', $list)
585 $self->{_abort} = 0;
586 $self->{_unresolved} = [];
588 # ensure 'abort-flag' and 'unresolved' point to
589 # appropriate holders
590 $self->{'abort-flag'} ||= \$self->{_abort};
591 $self->{unresolved} ||= \$self->{_unresolved};
593 return $self;
597 =head2 B<armor>
599 $str = $ah->armor($str);
601 Translate Artemus markup to HTML entities, to avoid being
602 interpreted by the parser.
604 =cut
606 sub armor
608 my ($ah, $t) = @_;
610 $t =~ s/{/\&#123;/g;
611 $t =~ s/\|/\&#124;/g;
612 $t =~ s/}/\&#125;/g;
613 $t =~ s/\$/\&#36;/g;
614 # $t =~ s/=/\&#61;/g;
616 return $t;
620 =head2 B<unarmor>
622 $str = $ah->unarmor($str);
624 Translate back the Artemus markup from HTML entities. This
625 is the reverse operation of B<armor>.
627 =cut
629 sub unarmor
631 my ($ah, $t) = @_;
633 $t =~ s/\&#123;/{/g;
634 $t =~ s/\&#124;/\|/g;
635 $t =~ s/\&#125;/}/g;
636 $t =~ s/\&#36;/\$/g;
637 # $t =~ s/\&#61;/=/g;
639 return $t;
643 =head2 B<strip>
645 $str = $ah->strip($str);
647 Strips all Artemus markup from the string.
649 =cut
651 sub strip
653 my ($ah, $t) = @_;
655 $t =~ s/{-([-\\\w_ \.]+)[^{}]*}/$1/g;
657 return $t;
661 =head2 B<params>
663 $str = $ah->params($str,@params);
665 Interpolates all $0, $1, $2... occurrences in the string into
666 the equivalent element from @params.
668 =cut
670 sub params
672 my ($ah, $t, @params) = @_;
674 for(my $n = 0; $t =~ /\$$n/; $n++) {
675 my $s = $params[$n] || '';
676 $t =~ s/(^|[^\\])\$$n/$1$s/g;
679 return $t;
683 =head2 B<process>
685 $str = $ah->process($str);
687 Processes the string, translating all Artemus markup. This
688 is the main template processing method. The I<abort-flag> flag and
689 I<unresolved> list are reset on each call to this method.
691 =cut
693 sub process
695 my ($ah, $data) = @_;
697 # not aborted by now
698 ${$ah->{'abort-flag'}} = 0;
700 # no unresolved templates by now
701 @{$ah->{'unresolved'}} = ();
703 # reset calling stack
704 @{$ah->{call_stack}} = ();
706 # surround with \BEGIN and \END
707 $data = $ah->{'vars'}->{'\BEGIN'} . $data . $ah->{'vars'}->{'\END'};
709 # really do it, recursively
710 $data = $ah->_process_do($data, 0);
712 # finally, convert end of lines if necessary
713 if ($ah->{'use-cr-lf'}) {
714 $data =~ s/\n/\r\n/g;
717 # strip comments
718 $data =~ s/{%[^}]+}//g;
720 return $data;
724 sub _process_do
726 my ($ah, $data, $level, $template_name) = @_;
727 my ($cache_time);
729 # test if the template includes cache info
730 if ($data =~ s/{-\\CACHE\W([^}]*)}//) {
731 if ($template_name and $ah->{'cache-path'}) {
732 $cache_time = $1;
734 # convert strange chars to :
735 $template_name =~ s/[^\w\d_]/:/g;
737 my ($f) = "$ah->{'cache-path'}/$template_name";
739 if (-r $f and -M $f < $cache_time) {
740 open F, $f;
741 flock F, 1;
742 $data = join('', <F>);
743 close F;
745 return $data;
750 # strip POD documentation, if any
751 if ($data =~ /=cut/ and not $ah->{'contains-pod'}) {
752 my (@d);
754 foreach (split("\n", $data)) {
755 unless (/^=/ .. /^=cut/) {
756 push(@d, $_);
760 $data = join("\n", @d);
763 # strips HTML comments
764 if ($ah->{'strip-html-comments'}) {
765 $data =~ s/<!--.*?-->//gs;
768 # if defined, substitute the paragraphs
769 # with the paragraph separator
770 if ($ah->{'paragraph-separator'}) {
771 $data =~ s/\n\n/\n$ah->{'paragraph-separator'}\n/g;
774 # inverse substitutions
775 # (disabled until it works)
776 # while (my ($i, $v) = each(%{$ah->{'inv-vars'}})) {
777 # $data =~ s/\b$i\b/$v/g;
780 # main function, variable and include substitutions
781 while ($data =~ /{-([^{}\\]*(\\.[^{}\\]*)*)}/s) {
782 my ($found) = $1;
784 # take key and params
785 my ($key, $params) = ($found =~ /^([-\\\w_]+)\|?(.*)$/s);
787 # replace escaped chars
788 $params =~ s/\\{/{/g;
789 $params =~ s/\\}/}/g;
790 $params =~ s/\\\$/\$/g;
792 # split parameters
793 my @params = ();
795 while (length($params) && $params =~ s/^([^\|\\]*(\\.[^\|\\]*)*)\|?//s) {
796 my $p = $1;
797 $p =~ s/\\\|/\|/g;
799 push(@params, $p);
802 my $text = '';
804 # is it a variable?
805 if (defined $ah->{'vars'}->{$key}) {
806 $text = $ah->{'vars'}->{$key};
807 $text = $ah->params($text, @params);
810 # is it a function?
811 elsif (defined $ah->{'funcs'}->{$key}) {
812 my ($func);
814 $func = $ah->{'funcs'}->{$key};
815 $text = $func->(@params);
817 # functions can abort further execution
819 if (${$ah->{'abort-flag'}}) {
820 last;
824 # is it an include?
825 elsif ($ah->{'include-path'}) {
826 foreach my $p (split(/:/, $ah->{'include-path'})) {
827 if (open(INC, "$p/$key")) {
828 $text = join('', <INC>);
829 close INC;
831 # cache it as a variable
832 $ah->{vars}->{$key} = $text;
834 $text = $ah->params($text, @params);
836 last;
840 else {
841 $text = $found;
843 push(@{$ah->{'unresolved'}}, $found);
845 if (ref $ah->{'AUTOLOAD'}) {
846 $text = $ah->{'AUTOLOAD'}($found);
850 $text ||= '';
852 if ($ah->{debug}) {
853 push(@{$ah->{call_stack}},
854 [ $key, $level, $found, $text ]
858 # do the recursivity
859 $text = $ah->_process_do($text, $level + 1, $key) || '';
861 # make the substitution
862 $data =~ s/{-\Q$found\E}/$text/;
865 # if the template included cache info,
866 # store the result there
867 if ($cache_time) {
868 open F, '>' . $ah->{'cache-path'} . '/' . $template_name;
869 flock F, 2;
870 print F $data;
871 close F;
874 return $data;
878 =head1 AUTHOR
880 Angel Ortega angel@triptico.com
882 =cut