4f5b6ebc3ec01d201a416e57ec77bb9a45871fef
[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;
113       $blosxom::body =~ s/<(\@|\??\$)/<#INTERPOLATE_FANCY_DEFANG#$1/gsi;
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 : undef"/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   my $attributes = interpolate_fancy::_attributes($attr);
159
160   defined $attributes->{eq} and return $variable eq $attributes->{eq};
161   defined $attributes->{ne} and return $variable ne $attributes->{ne};
162   defined $attributes->{gt} and return $variable > $attributes->{gt};
163   defined $attributes->{lt} and return $variable < $attributes->{lt};
164   defined $attributes->{like} and return $variable  =~ /$attributes->{like}/;
165   defined $attributes->{unlike} and return $variable  !~ /$attributes->{unlike}/;
166
167   return 0;
168 }
169
170 sub _action {
171   my($plugin, $action, $attr, $content) = @_;
172   my $result;
173
174   $content =~ s#<\@((?:\w|::)+?)\.(\w+?)\s+?(.+?)?(?:>(.*?)<\/\@\1\.\2>|\s+?\/>)#&interpolate_fancy::_action($1,$2,$3,$4)#gse;
175
176   my $attributes = interpolate_fancy::_attributes($attr);
177   
178   $blosxom::plugins{$plugin} > 0
179     and $plugin->can($action) 
180       and $result = $plugin->$action($attributes, $content);
181  
182   # Optionally interpolate and/or output the result, if requested. 
183   if ($attributes->{'output'} =~ /yes/i) {
184     if ($attributes->{'interpolate'} =~ /yes/i) {
185       $result = interpolate_fancy::do_interpolate($result);
186     } 
187     return $result;
188   } else {
189     return undef;
190   }
191 }
192
193 sub _attributes {
194   my $attr = shift;
195
196   my $attributes = {};
197   while ( $attr =~ /\b(\w+?)\s*?=\s*?(["'])(.*?)\2/g ) {
198     $attributes->{$1} = $3;
199   }
200
201   return $attributes;
202 }
203
204 sub story {
205         my($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
206  
207  if ($recurse_into_story) {
208       $parsing_story = 1;
209       $$title_ref =&$blosxom::interpolate($$title_ref);
210       $$body_ref =&$blosxom::interpolate($$body_ref);
211       $parsing_story = 0;
212      }
213   return 1;
214 }
215
216 1;
217
218 __END__
219
220 =head1 NAME
221
222 Blosxom Plug-in: interpolate_fancy
223
224 =head1 SYNOPSIS
225
226 Overrides Blosxom's far simpler to use, but more limited, default interpolate() subroutine.
227
228 * Include bits of text and template variable values in templates, either conditionally or unconditionally;
229
230 * 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.
231
232 =head2 Inclusions of bits of text and variables
233  
234 This syntax will expand to the value of the referenced variable and can be used to include dynamic content in your pages.
235
236 =over 2
237
238 =item Unconditionally and recursively
239
240 e.g. include a link to the story's path using various template variables.
241
242 <a href="<$url /><$path />"><$path /></a>
243
244 Limited by the $recurse_into_story configuration directive (see the CONFIGURATION below).
245
246 =item Unconditionally (backward compatibility with Blosxom's standard interpolation)
247
248 e.g. include a link to the story's path using various template variables (non-recursive, and only inside templates).
249
250 <a href="$url$path">$path</a>
251
252 =item If the template variable has a value (i.e. is defined)
253
254 e.g. include a hyperlink to the story's path if it has a path (i.e. $path is defined).
255
256 <?$path><a href="<$url /><$path />"><$path /></a></?>
257
258 =item If the template variable doesn't have a value (i.e. is NOT defined)
259
260 e.g. include a hyperlink to home if path is undefined.
261
262 <?!$path><a href="<$url />">home</a></?>
263
264 =item If the template variable is equal (=) to a particular value
265
266 e.g. include "1 writeback" (singular) if the value of writeback count is 1
267
268 <$writeback::count /> <?$writeback::count eq="1">writeback</?>
269
270 =item If the template variable is not equal (!=) to a particular value
271
272 e.g. include "x writebacks" (plural) if the value of Writeback count (x) is not 1
273
274 <$writeback::count /> <?$writeback::count ne="1">writebacks</?>
275
276 =item If the template variable is less than (<) a particular value
277
278 e.g. include "no writebacks" if the value of writeback count is < 1
279
280 <?$writeback::count lt="1">no writebacks</?>
281
282 =item If the template variable is greater than (>) a particular value
283
284 e.g. include "oodles of writebacks" if the value of writeback count is > 50
285
286 <?$writeback::count gt="50">oodles of writebacks</?>
287
288 =item If the template variable is like (=~) a particular regular expression
289
290 e.g. Greet a visitor properly, depending on their courtesy title
291
292 Howdy, 
293 <?$user::courtesy like="^(Mr\.?|Sir)$">Sir</?>
294 <?$user::courtesy like="^(Mr?s\.?|Miss)$">M'am</?>
295
296 =item If the template variable is unlike (!~) a particular regular expression
297
298 e.g. The posting is neither a film nor a book
299
300 <?$path unlike="/(Film|Literature)">no review</?>
301
302 =back
303
304 =head2 Actions
305
306 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.
307
308 For example: 
309
310 <@thePlugin.subroutine arg1="a" arg2="bee" />
311
312 calls &Blosxom::Plugin::ThePlugin::subroutine( {'arg1'=>'a', 'arg2'=>'bee' } ).
313
314 Specify that results should be sent to the browser using the output="yes" attribute like so:
315 <@thePlugin.subroutine arg1="a" arg2="bee" output="yes" />
316
317 Otherwise, subroutines will still have their effect, but the results will be tossed out.
318
319 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:
320
321   <@plugin.subroutine arg1="a" arg2="bee" output="yes" interpolate="yes"/>
322
323 Content wrapped in the action call is sent as another argument to the subroutine:
324
325   <@thePlugin.subroutine encoding="Latin1" output="yes">
326   pass this content
327   </@thePlugin.subroutine> 
328
329 This calls &thePlugin::subroutine( {'encoding'=>'Latin1', 'output'=>'yes' }, "pass this content" ).
330
331 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.
332
333 Recursion is limited by the $recurse_into_story configuration directive (see the CONFIGURATION below).
334
335 =head3 An Example
336
337 For those of you interested in writing plugin actions or using some of the
338 more advanced features in your Blosxom blog templates, here are a couple of
339 sample actions:
340
341 --
342
343 # For the sake of this example, assume these actions live in a "myplugin" plugin
344
345 # This action strips HTML from its content
346
347 sub strip_html {
348   my($self, $attributes, $content) = @_;
349   $content =~ s!</?.+?>!!g;
350   return $content;
351 }
352
353 # This action foreshortens its content to a length specified in the call to action's length attribute
354
355 sub foreshorten {
356   my($self, $attributes, $content) = @_;
357   my $default_length = 144;
358   return substr($content, 0, $attributes->{'length'}||$default_length);
359 }
360
361 # This action includes the template specified in the 'name' attribute
362
363 sub includetemplate {
364   my($self, $attributes, $content) = @_;
365   $name = $attributes->{'name'};
366   return &$blosxom::template($blosxom::path_info, $name, $blosxom::flavour);
367 }
368
369 --
370
371 Calling these individually in a Blosxom flavour template looks something like:
372
373 The following bit of text is devoid of HTML:
374
375 <@myplugin.strip_html output="Yes">
376 Silly <a href="http://www.raelity.org/">me</a>, I plumb 
377 <em>forgot</em> to remove the HTML.
378 </@myplugin.strip_html>
379
380 The following bit of text is only 20 characters in length:
381
382 <@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
383 resulting page.
384 </@myplugin.foreshorten>
385
386 And in combination, stripping the HTML _before_ foreshortening (notice
387 the strip_html action is embedded inside the foreshorten action and
388 thus is run first).
389
390 The following bit of text is only 20 characters in length and devoid of HTML:
391
392 <@myplugin.foreshorten length="20" output="Yes"><@myplugin.strip_html output="Yes">Silly <a href="http://www.raelity.org/">me</a>, I plumb 
393 <em>forgot</em> to remove the HTML.
394 </@myplugin.strip_html>
395 </@myplugin.foreshorten>
396
397 The following includes the 'sidebar' template, while interpolating
398 the contents of that file:
399
400 <@myplugin.include name="sidebar" output="yes" interpolate="yes"/>
401
402 =head1 INSTALLATION
403
404 Drop the interpolate_fancy plug-in into your Blosxom plugins folder.
405
406 =head1 CONFIGURATION
407
408 None necessary; interpolate_fancy will run out of the box with no need
409 of additional configuration or fiddling on your part (caveat: see 
410 BACKWARD COMPATILITY below).
411
412 The interpolate_fancy plugin does sport one configuration directive
413 which you should very much consider leaving alone.  
414
415 # 0 = No (default),  1 = Yes
416 my $recurse_into_story = 0;
417
418 $recurse_into_story decides whether or not the interpolation engine 
419 should respect and interpolate (swap for the associated value) 
420 variables and actions embedded in story $title and $body themselves.
421
422 Off by default, you should consider and reconsider carefully before 
423 turning this on as if anyone other than you has the ability to post 
424 stories to your blog, there's a chance of some tomfoolery, exposing 
425 variables and calling actions/subroutines you might not want called.
426
427 =head1 BACKWARD COMPATIBILITY
428
429 If you've been using core Blosxom's interpolation style 
430 (e.g. $title), this plugin will provide backward compatibility,
431 requiring no template rewriting on your part.
432
433 If you've been using the interpolate_conditional plugin,
434 the conditional bits won't be respected by default.  You should
435 run your templates through the interpolate2fancy utility
436 [http://www.blosxom.com/downloads/utilities/interpolate2fancy.py].
437
438 =head1 VERSION
439
440 2007-09-27
441
442 =head1 VERSION HISTORY
443
444 2007-09-27 : enables more than one :: in variable names
445
446 2007-09-13 : corrects the $recurse_into_story feature in XML flavours
447
448 v20061114 : fixes from Matthijs Kooijman (including properly support for nested
449 conditions)
450
451 =head1 AUTHOR
452
453 Rael Dornfest  <rael@oreilly.com>, http://www.raelity.org/
454 Modified by Matthijs Kooijman <m.kooijman@student.utwente.nl>, http://katherina.student.utwente.nl/~matthijs/blog
455
456 This plugin is now maintained by the Blosxom Sourceforge Team,
457 <blosxom-devel@lists.sourceforge.net>.
458
459 =head1 SEE ALSO
460
461 Blosxom Home/Docs/Licensing: http://blosxom.sourceforge.net/
462
463 Blosxom Plugin Docs: http://blosxom.sourceforge.net/documentation/users/plugins.html
464
465 =head1 BUGS
466
467 None known; please send bug reports and feedback to the Blosxom
468 development mailing list <blosxom-devel@lists.sourceforge.net>.
469
470 =head1 LICENSE
471
472 Blosxom and this Blosxom Plug-in
473 Copyright 2003, Rael Dornfest 
474
475 Permission is hereby granted, free of charge, to any person obtaining a
476 copy of this software and associated documentation files (the "Software"),
477 to deal in the Software without restriction, including without limitation
478 the rights to use, copy, modify, merge, publish, distribute, sublicense,
479 and/or sell copies of the Software, and to permit persons to whom the
480 Software is furnished to do so, subject to the following conditions:
481
482 The above copyright notice and this permission notice shall be included
483 in all copies or substantial portions of the Software.
484
485 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
486 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
487 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
488 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
489 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
490 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
491 OTHER DEALINGS IN THE SOFTWARE.