Only interpolate action tag content when defined.
[matthijs/upstream/blosxom-plugins.git] / general / interpolate_fancy
1 # Blosxom Plugin: interpolate_fancy
2 # Author: Rael Dornfest <rael@oreilly.com>, 
3 # Modified by: Matthijs Kooijman <m.kooijman@student.utwente.nl>
4 # and Barijaona Ramaholimihaso
5 # Version: 2007-09-27
6 # Documentation: See the bottom of this file or type: 
7 # perldoc interpolate_fancy
8
9 package interpolate_fancy;
10
11 use warnings;
12 use strict;
13
14 # --- Configurable variables -----
15
16 # Do you want me to recursively interpolate into the story $title
17 # and $body?  Consider and reconsider carefully before turning this
18 # on as if anyone other than you has the ability to post stories,
19 # there's a chance of some tomfoolery, exposing variables and calling
20 # actions/subroutines you might not want called.
21 # 0 = No (default),  1 = Yes
22 our $recurse_into_story = 0;
23
24 # --------------------------------
25
26 my $parsing_story = 0 ;
27
28
29 sub start {
30   1;
31 }
32
33
34 # (Recursively) resolve conditinal includes <?...>...</?>.
35 # Expects two arguments: The (part of the) template we are expanding and
36 # wether or not this part will be displayed. When this last argument is false,
37 # this part of the template will not be displayed, but is still parsed to
38 # properly resolve nested conditions.
39 #
40 # This will resolve and substitute all conditionals on the "current level".
41 # This effectively means, the routine will search for and resolve all tags it
42 # finds (with proper nesting), until it finds a closing tag (</?>) for which
43 # it has not seen the opening tag, or there are no more opening tags in the
44 # file.
45
46 # This function will return the template with all conditionals on the current
47 # level resolved. This will guarantee that if there are any opening or closing
48 # tags left in the template, the first one will be a closing tag (which can
49 # then be resolved in the upper level).
50 sub _resolve_nested {
51     my $template = shift;
52     my $display  = shift;
53   
54     while (1) {
55         if ($template !~ /(.*?)<\?(\!?\$\w+(?:::\w+)*)(?:\s+?(.+?))?>(.*)/s) {
56             return $template; # No open tags, normal text
57         }
58        
59         my $head = $1;
60         my $var  = $2;
61         my $attr = $3;
62         my $tail = $4;
63
64         if ($head =~ /<\/\?>/s) {
65             return $template; # There is a closing tag before the first open tag,
66                              # we're done. 
67         }
68         
69         my $displayitem = $display;
70         if ($displayitem) { # Don't care about these tests if we're not displayed anyway.
71             my $negated = ($var =~ s/^\!//); # Remove negation mark
72             if ($negated || !$attr) {
73                 if ($attr) {
74                     warn("<?!$var $attr>: Negated expression can't have attributes, ignoring them.");
75                 }
76                 $displayitem = eval("defined $var");
77                 if ($negated) { $displayitem = !$displayitem; }
78             } else {
79                 $displayitem = _test(eval($var), $attr);
80             }
81         }
82
83
84         $tail = _resolve_nested($tail, $displayitem);
85
86         if ($tail !~ /<\/\?>/s) { # Is there no closing tag?
87             warn("Invalid nesting: <?$var $attr> missing closing tag.");
88         }
89         if ($displayitem)
90         {
91             $tail =~ s/<\/\?>//s; # Remove the closing tag
92         } else {
93             $tail =~ s/.*?<\/\?>//s; # Remove up to the closing tag
94         }
95         $template = $head . $tail;
96     }
97     return $template;
98 }
99
100
101 sub interpolate {
102   return \&interpolate_fancy::do_interpolate;
103 }
104
105 sub do_interpolate {
106
107     package blosxom;
108
109     # Halt recursive interpolation of story $body
110     # by mangling interpolation tags (to be unmangled in a moment)
111     unless ($recurse_into_story) {
112       $blosxom::title =~ s/<(\@|\??\$)/<#INTERPOLATE_FANCY_DEFANG#$1/gsi if defined $blosxom::title;
113       $blosxom::body =~ s/<(\@|\??\$)/<#INTERPOLATE_FANCY_DEFANG#$1/gsi if defined $blosxom::body;
114     }
115
116     my $template = shift;
117
118     # Backward Compatibility with core Blosxom style interpolation
119     unless ($parsing_story)
120         {$template =~ s#(?<!<)(?<!<\?)(?<!<\?!)(\$\w+(?:::\w+)*)#<$1 />#gs; };
121
122     #
123     # Conditional inclusion
124     # Defined
125     # e.g. <?$var>display if defined</?>
126     # Undefined 
127     # e.g. <?!$var>display if not defined</?>
128     # Tests 
129     # eq (eq), ne (ne), lt (<), gt (>), like (=~), unlike (!~)
130     # e.g. <?$var lt="123">display if $var less than 123</?>
131     $template = interpolate_fancy::_resolve_nested($template, 1);
132
133     #
134     # Variable expansion (unconditional, recursive)
135     #
136     # e.g. <$var />
137     while( $template =~ s/<\$([a-zA-Z?]\w+(?:::\w+)*)\s+?\/>/"defined \$$1 ? \$$1 : ''"/gsee ) { }
138
139     #
140     # Actions 
141     #
142     # e.g. <@plugin.subroutine arg1="a" output="no" />
143     # e.g. <@plugin.subroutine encoding="Latin1" output="yes">pass content</@> 
144     $template =~ s#<\@((?:\w|::)+?)\.(\w+?)\s+?(.+?)?(?:>(.*?)<\/\@\1\.\2>|\s+?\/>)#&interpolate_fancy::_action($1,$2,$3,$4)#gse;
145
146     # Unmangle mangled interpolation tags in story $title and $body
147     # (by now in the template itself)
148     unless ($recurse_into_story) {
149       $template =~ s/<#INTERPOLATE_FANCY_DEFANG#(\@|\??\$)/<$1/gsi;
150     }
151
152     return $template;
153
154 }
155
156 sub _test {
157   my($variable, $attr) = @_;
158   # If the variable is not defined, treat it as the empty string in
159   # comparisons below
160   if (!defined $variable) { 
161     $variable = '';
162   }
163   my $attributes = interpolate_fancy::_attributes($attr);
164
165   defined $attributes->{eq} and return $variable eq $attributes->{eq};
166   defined $attributes->{ne} and return $variable ne $attributes->{ne};
167   defined $attributes->{gt} and return $variable > $attributes->{gt};
168   defined $attributes->{lt} and return $variable < $attributes->{lt};
169   defined $attributes->{like} and return $variable  =~ /$attributes->{like}/;
170   defined $attributes->{unlike} and return $variable  !~ /$attributes->{unlike}/;
171
172   return 0;
173 }
174
175 sub _action {
176   my($plugin, $action, $attr, $content) = @_;
177   my $result;
178
179   if (defined $content) {
180     $content =~ s#<\@((?:\w|::)+?)\.(\w+?)\s+?(.+?)?(?:>(.*?)<\/\@\1\.\2>|\s+?\/>)#&interpolate_fancy::_action($1,$2,$3,$4)#gse;
181   }
182
183   my $attributes = interpolate_fancy::_attributes($attr);
184   
185   $blosxom::plugins{$plugin} > 0
186     and $plugin->can($action) 
187       and $result = $plugin->$action($attributes, $content);
188  
189   # Optionally interpolate and/or output the result, if requested. 
190   if ($attributes->{'output'} =~ /yes/i) {
191     if ($attributes->{'interpolate'} =~ /yes/i) {
192       $result = interpolate_fancy::do_interpolate($result);
193     } 
194     return $result;
195   } else {
196     return undef;
197   }
198 }
199
200 sub _attributes {
201   my $attr = shift;
202
203   my $attributes = {};
204   while ( $attr =~ /\b(\w+?)\s*?=\s*?(["'])(.*?)\2/g ) {
205     $attributes->{$1} = $3;
206   }
207
208   return $attributes;
209 }
210
211 sub story {
212         my($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
213  
214  if ($recurse_into_story) {
215       $parsing_story = 1;
216       $$title_ref =&$blosxom::interpolate($$title_ref);
217       $$body_ref =&$blosxom::interpolate($$body_ref);
218       $parsing_story = 0;
219      }
220   return 1;
221 }
222
223 1;
224
225 __END__
226
227 =head1 NAME
228
229 Blosxom Plug-in: interpolate_fancy
230
231 =head1 SYNOPSIS
232
233 Overrides Blosxom's far simpler to use, but more limited, default interpolate() subroutine.
234
235 * Include bits of text and template variable values in templates, either conditionally or unconditionally;
236
237 * Perform actions (i.e. call plug-in subroutines) at any point in your page, whether to act on current content and return results or not.
238
239 =head2 Inclusions of bits of text and variables
240  
241 This syntax will expand to the value of the referenced variable and can be used to include dynamic content in your pages.
242
243 =over 2
244
245 =item Unconditionally and recursively
246
247 e.g. include a link to the story's path using various template variables.
248
249 <a href="<$url /><$path />"><$path /></a>
250
251 Limited by the $recurse_into_story configuration directive (see the CONFIGURATION below).
252
253 =item Unconditionally (backward compatibility with Blosxom's standard interpolation)
254
255 e.g. include a link to the story's path using various template variables (non-recursive, and only inside templates).
256
257 <a href="$url$path">$path</a>
258
259 =item If the template variable has a value (i.e. is defined)
260
261 e.g. include a hyperlink to the story's path if it has a path (i.e. $path is defined).
262
263 <?$path><a href="<$url /><$path />"><$path /></a></?>
264
265 =item If the template variable doesn't have a value (i.e. is NOT defined)
266
267 e.g. include a hyperlink to home if path is undefined.
268
269 <?!$path><a href="<$url />">home</a></?>
270
271 =item If the template variable is equal (=) to a particular value
272
273 e.g. include "1 writeback" (singular) if the value of writeback count is 1
274
275 <$writeback::count /> <?$writeback::count eq="1">writeback</?>
276
277 =item If the template variable is not equal (!=) to a particular value
278
279 e.g. include "x writebacks" (plural) if the value of Writeback count (x) is not 1
280
281 <$writeback::count /> <?$writeback::count ne="1">writebacks</?>
282
283 =item If the template variable is less than (<) a particular value
284
285 e.g. include "no writebacks" if the value of writeback count is < 1
286
287 <?$writeback::count lt="1">no writebacks</?>
288
289 =item If the template variable is greater than (>) a particular value
290
291 e.g. include "oodles of writebacks" if the value of writeback count is > 50
292
293 <?$writeback::count gt="50">oodles of writebacks</?>
294
295 =item If the template variable is like (=~) a particular regular expression
296
297 e.g. Greet a visitor properly, depending on their courtesy title
298
299 Howdy, 
300 <?$user::courtesy like="^(Mr\.?|Sir)$">Sir</?>
301 <?$user::courtesy like="^(Mr?s\.?|Miss)$">M'am</?>
302
303 =item If the template variable is unlike (!~) a particular regular expression
304
305 e.g. The posting is neither a film nor a book
306
307 <?$path unlike="/(Film|Literature)">no review</?>
308
309 =back
310
311 =head2 Actions
312
313 Performs an action (i.e. calls a plug-in's subroutine) at any point in your page. Optionally passes arguments as key/value pairs stated as XML-style attributes.
314
315 For example: 
316
317 <@thePlugin.subroutine arg1="a" arg2="bee" />
318
319 calls &Blosxom::Plugin::ThePlugin::subroutine( {'arg1'=>'a', 'arg2'=>'bee' } ).
320
321 Specify that results should be sent to the browser using the output="yes" attribute like so:
322 <@thePlugin.subroutine arg1="a" arg2="bee" output="yes" />
323
324 Otherwise, subroutines will still have their effect, but the results will be tossed out.
325
326 Normally, the result from the subroutine is sent as-is, but you can set the interpolate="yes" attribute to let interpolate_fancy interpolate the results, as follows:
327
328   <@plugin.subroutine arg1="a" arg2="bee" output="yes" interpolate="yes"/>
329
330 Content wrapped in the action call is sent as another argument to the subroutine:
331
332   <@thePlugin.subroutine encoding="Latin1" output="yes">
333   pass this content
334   </@thePlugin.subroutine> 
335
336 This calls &thePlugin::subroutine( {'encoding'=>'Latin1', 'output'=>'yes' }, "pass this content" ).
337
338 Actions are recursive, meaning that you can embed an action inside another, and so on and so on and so on.  Actions are unfolded from the inside out, with the most deeply embedded first, second deepest second, and so forth until the outermost action is performed last.
339
340 Recursion is limited by the $recurse_into_story configuration directive (see the CONFIGURATION below).
341
342 =head3 An Example
343
344 For those of you interested in writing plugin actions or using some of the
345 more advanced features in your Blosxom blog templates, here are a couple of
346 sample actions:
347
348 --
349
350 # For the sake of this example, assume these actions live in a "myplugin" plugin
351
352 # This action strips HTML from its content
353
354 sub strip_html {
355   my($self, $attributes, $content) = @_;
356   $content =~ s!</?.+?>!!g;
357   return $content;
358 }
359
360 # This action foreshortens its content to a length specified in the call to action's length attribute
361
362 sub foreshorten {
363   my($self, $attributes, $content) = @_;
364   my $default_length = 144;
365   return substr($content, 0, $attributes->{'length'}||$default_length);
366 }
367
368 # This action includes the template specified in the 'name' attribute
369
370 sub includetemplate {
371   my($self, $attributes, $content) = @_;
372   $name = $attributes->{'name'};
373   return &$blosxom::template($blosxom::path_info, $name, $blosxom::flavour);
374 }
375
376 --
377
378 Calling these individually in a Blosxom flavour template looks something like:
379
380 The following bit of text is devoid of HTML:
381
382 <@myplugin.strip_html output="Yes">
383 Silly <a href="http://www.raelity.org/">me</a>, I plumb 
384 <em>forgot</em> to remove the HTML.
385 </@myplugin.strip_html>
386
387 The following bit of text is only 20 characters in length:
388
389 <@myplugin.foreshorten length="20" output="Yes">This text is far longer than 20 characters on the page, but will only appear as "This text is far lon" in the
390 resulting page.
391 </@myplugin.foreshorten>
392
393 And in combination, stripping the HTML _before_ foreshortening (notice
394 the strip_html action is embedded inside the foreshorten action and
395 thus is run first).
396
397 The following bit of text is only 20 characters in length and devoid of HTML:
398
399 <@myplugin.foreshorten length="20" output="Yes"><@myplugin.strip_html output="Yes">Silly <a href="http://www.raelity.org/">me</a>, I plumb 
400 <em>forgot</em> to remove the HTML.
401 </@myplugin.strip_html>
402 </@myplugin.foreshorten>
403
404 The following includes the 'sidebar' template, while interpolating
405 the contents of that file:
406
407 <@myplugin.include name="sidebar" output="yes" interpolate="yes"/>
408
409 =head1 INSTALLATION
410
411 Drop the interpolate_fancy plug-in into your Blosxom plugins folder.
412
413 =head1 CONFIGURATION
414
415 None necessary; interpolate_fancy will run out of the box with no need
416 of additional configuration or fiddling on your part (caveat: see 
417 BACKWARD COMPATILITY below).
418
419 The interpolate_fancy plugin does sport one configuration directive
420 which you should very much consider leaving alone.  
421
422 # 0 = No (default),  1 = Yes
423 my $recurse_into_story = 0;
424
425 $recurse_into_story decides whether or not the interpolation engine 
426 should respect and interpolate (swap for the associated value) 
427 variables and actions embedded in story $title and $body themselves.
428
429 Off by default, you should consider and reconsider carefully before 
430 turning this on as if anyone other than you has the ability to post 
431 stories to your blog, there's a chance of some tomfoolery, exposing 
432 variables and calling actions/subroutines you might not want called.
433
434 =head1 BACKWARD COMPATIBILITY
435
436 If you've been using core Blosxom's interpolation style 
437 (e.g. $title), this plugin will provide backward compatibility,
438 requiring no template rewriting on your part.
439
440 If you've been using the interpolate_conditional plugin,
441 the conditional bits won't be respected by default.  You should
442 run your templates through the interpolate2fancy utility
443 [http://www.blosxom.com/downloads/utilities/interpolate2fancy.py].
444
445 =head1 VERSION
446
447 2007-09-27
448
449 =head1 VERSION HISTORY
450
451 2007-09-27 : enables more than one :: in variable names
452
453 2007-09-13 : corrects the $recurse_into_story feature in XML flavours
454
455 v20061114 : fixes from Matthijs Kooijman (including properly support for nested
456 conditions)
457
458 =head1 AUTHOR
459
460 Rael Dornfest  <rael@oreilly.com>, http://www.raelity.org/
461 Modified by Matthijs Kooijman <m.kooijman@student.utwente.nl>, http://katherina.student.utwente.nl/~matthijs/blog
462
463 This plugin is now maintained by the Blosxom Sourceforge Team,
464 <blosxom-devel@lists.sourceforge.net>.
465
466 =head1 SEE ALSO
467
468 Blosxom Home/Docs/Licensing: http://blosxom.sourceforge.net/
469
470 Blosxom Plugin Docs: http://blosxom.sourceforge.net/documentation/users/plugins.html
471
472 =head1 BUGS
473
474 None known; please send bug reports and feedback to the Blosxom
475 development mailing list <blosxom-devel@lists.sourceforge.net>.
476
477 =head1 LICENSE
478
479 Blosxom and this Blosxom Plug-in
480 Copyright 2003, Rael Dornfest 
481
482 Permission is hereby granted, free of charge, to any person obtaining a
483 copy of this software and associated documentation files (the "Software"),
484 to deal in the Software without restriction, including without limitation
485 the rights to use, copy, modify, merge, publish, distribute, sublicense,
486 and/or sell copies of the Software, and to permit persons to whom the
487 Software is furnished to do so, subject to the following conditions:
488
489 The above copyright notice and this permission notice shall be included
490 in all copies or substantial portions of the Software.
491
492 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
493 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
494 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
495 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
496 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
497 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
498 OTHER DEALINGS IN THE SOFTWARE.