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