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