Add set of Todd Larason plugins to general.
[blosxom-plugins.git] / general / macros
blobcdf3f9cfcec394d7e7078035849ea99136cf268b
1 # Blosxom Plugin: macros                                           -*- perl -*-
2 # Author: Todd Larason (jtl@molehill.org)
3 # Version: 0+1i
4 # Blosxom Home/Docs/Licensing: http://www.raelity.org/blosxom
5 # Calendar plugin Home/Docs/Licensing:
6 #   http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/Macros/
7 # Modelled on Brad Choate's MT-Macros, but no code in common
8 package macros; # -*- perl -*-
10 # --- Configuration Variables ---
11 $macrodir = "$blosxom::plugin_state_dir/.macros"
12     unless defined $macrodir;
14 $use_caching = 1;
15 $debug_level = 1;
16 # -------------------------------------------------------------------
17 # types:
18 #   string                              implemented
19 #   pattern                             implemented
20 #   tag (string or pattern)             implemented
21 #   ctag (string or pattern)            implemented
23 # attributes:
24 #   name                                implemented, auto defaults
25 #   once                                implemented
26 #   recurse
27 #   no_html                             implemented, default
28 #                                       for string and pattern, inhtml => 1 to
29 #                                       reverse; can't reverse for tag
30 #   no_case
31 #   * defaults for attribs              implemented
32 #   * body                              implemented
34 # in replacement:
35 #   pattern: ${1}-${9} matched () text, $<1>-$<9> escaped matched text
36 #   tag: as in pattern + ${name} tag attribute, $<name> escaped attribute
37 #   ctag: as in tag + ${body}
39 use bytes;
40 use File::stat;
42 # XXX cache macro definitions?
43 my @macros = ();
44 my $cache;
45 my $package    = "macros";
46 my $cachefile  = "$blosxom::plugin_state_dir/.$package.cache";
47 my $save_cache = 0;
49 sub debug {
50     my ($level, @msg) = @_;
52     if ($debug_level >= $level) {
53         print STDERR "$package debug $level: @msg\n";
54     }
55     1;
58 sub url_escape {
59     local ($_) = @_;
61     s/([^a-zA-Z0-9])/sprintf("%%%02x",ord($1))/eg;
62     s/%20/+/g;
63     return $_;
66 sub define_macro {
67     my ($arg) = @_;
68     my $macro = {};
70     if ($arg->{type} eq "string") {
71         $macro->{type}     = 'pattern';
72         $macro->{pattern}  = qr{\Q$arg->{string}\E};
73         $macro->{body}     = $arg->{body};
74         $macro->{inhtml}   = $arg->{inhtml} if $arg->{inhtml};
75         $macro->{once}     = $arg->{once}   if $arg->{once};
76         $macro->{name}     = $arg->{name} || "string_$arg->{string}";
77     } elsif ($arg->{type} eq "pattern") {
78         $macro->{type}     = 'pattern';
79         $macro->{pattern}  = qr{$arg->{pattern}};
80         $macro->{body}     = $arg->{body};
81         $macro->{inhtml}   = $arg->{inhtml} if $arg->{inhtml};
82         $macro->{once}     = $arg->{once}   if $arg->{once};
83         $macro->{name}     = $arg->{name} || "pattern_$arg->{pattern}";
84     } elsif ($arg->{type} eq "tag") {
85         $macro->{type}      = 'tag';
86         $macro->{container} = 0;
87         $macro->{pattern}   = qr{$arg->{name}};
88         $macro->{body}      = $arg->{body};
89         $macro->{defaults}  = {%{$arg->{defaults}}};
90         $macro->{once}      = $arg->{once}   if $arg->{once};
91         $macro->{name}      = "tag_$arg->{name}";
92     } elsif ($arg->{type} eq "ctag") {
93         $macro->{type}      = 'tag';
94         $macro->{container} = 1;
95         $macro->{pattern}   = qr{$arg->{name}};
96         $macro->{body}      = $arg->{body};
97         $macro->{defaults}  = {%{$arg->{defaults}}};
98         $macro->{once}      = $arg->{once}   if $arg->{once};
99         $macro->{name}      = "tag_$arg->{name}";
100     }
102     push @macros, $macro;
103     return 1;
106 sub replace_pattern {
107     my ($macro, $ctx) = @_;
109     my $replacement = $macro->{body};
110     $replacement =~ s{
111         (?: \$ { ([\w]+) } |
112             \$ < ([\w]+) > |
113            (\$    [\w:]+) 
114          )
115         }{defined($1) ? $ctx->{$1} : 
116               defined($2) ? url_escape($ctx->{$2}) : 
117               eval "$3||''"}xge;
119     return $replacement;
122 sub apply_pattern_macro {
123     my ($state, $macro, $text) = @_;
124     $text =~ 
125         s{($macro->{pattern})}
126         {$macro->{once} && $state->{used}{$macro->{name}} ? $1 :
127          (++$state->{used}{$macro->{name}}
128           and replace_pattern($macro, {
129               1 => $2, 2 => $3, 3 => $4,
130               4 => $5, 5 => $6, 6 => $7,
131               7 => $8, 8 => $7
132               }))
133           }egms;
134     return $text;
137 sub apply_tag_macro {
138     my ($state, $macro, $entity, $attributes, $body) = @_;
139     my $ctx;
141     $ctx->{body} = $body;
142     $entity =~ $macro->{pattern};
143     @{$ctx}{qw/1 2 3 4 5 6 7 8 9/} = ($1, $2, $3, $4, $5, $6, $7, $8, $9);
144     while ($attributes =~ m{ (\w+)         # $1 = tag
145                              =
146                              (?:
147                               " ([^\"]+) " # $2 = quoted value
148                               |
149                                 ([^\s]+)   # $3 = unquoted value
150                              )
151                          }gx) {
152         $ctx->{$1} = ($+);
153     }
154     foreach (keys %{$macro->{defaults}}) {
155         next if defined($ctx->{$_});
156         if ($macro->{defaults}{$_} =~ m:\$(\w+):) {
157             $ctx->{$_} = $ctx->{$1};
158         } else {
159             $ctx->{$_} = $macro->{defaults}{$_};
160         }
161     }
163     my $text = $macro->{body};
164     $text =~ s{
165                (?: \$ { ([\w]+) } |
166                    \$ < ([\w]+) > |
167                   (\$    [\w:]+) 
168                 )
169                }{defined($1) ? $ctx->{$1} : 
170                      defined($2) ? url_escape($ctx->{$2}) : 
171                      eval "$3||''"}xge;
172     return $text;
175 sub apply_macro {
176     my ($state, $macro, $text) = @_;
178     if ($macro->{type} eq 'pattern') {
179         if ($macro->{inhtml}) {
180             $text = apply_pattern_macro($state, $macro, $text);
181         } else {
182             my @tokens = split /(<[^>]+>)/, $text;
183             $text = '';
184             foreach (@tokens) {
185                 if (!m/^</) {
186                     $_ = apply_pattern_macro($state, $macro, $_);
187                 }
188                 $text .= $_;
189             }
190         }
191     } elsif ($macro->{type} eq 'tag') {
192         my @tokens = split /(<[^>]+>)/, $text;
193         $text = '';
194         while (defined($_ = shift @tokens)) {
195             if (!($macro->{once} && $state->{used}{$macro->{name}})
196                 && (m/<($macro->{pattern})([\s>].*)/)) {
197                 my $tag = $_;
198                 my $entity = $1;
199                 my $attributes = $+;
200                 chop $attributes;
201                 if ($macro->{container}) {
202                     my $body;
203                     while (defined($_ = shift @tokens)) {
204                         last if (m:</$entity\s*>:);
205                         $body .= $_;
206                     }
207                     $_ = apply_tag_macro($state, $macro, $entity, $attributes, $body);
208                 } else {
209                     $_ = apply_tag_macro($state, $macro, $entity, $attributes);
210                 }
211                 $state->{used}{$macro->{name}}++;
212             }
213             $text .= $_;
214         }
215     } else {
216         debug(0, "ACK: unknown macro type $macro->{type}");
217     }
218     return $text;
221 sub apply_macros {
222     my ($state, $text) = @_;
224     foreach my $macro (@macros) {
225         $text = apply_macro($state, $macro, $text);
226     }
227     return $text;
230 # caching support
232 sub prime_cache {
233     my ($macrokey) = @_;
234     return if (!$use_caching);
235     eval "require Storable";
236     if ($@) {
237         debug(1, "cache disabled, Storable not available");
238         $use_caching = 0;
239         return 0;
240     }
241     if (!Storable->can('lock_retrieve')) {
242         debug(1, "cache disabled, Storable::lock_retrieve not available");
243         $use_caching = 0;
244         return 0;
245     }
246     $cache = (-r $cachefile ? Storable::lock_retrieve($cachefile) : {});
247     if (defined $cache->{macrokey}) {
248         if ($cache->{macrokey} eq $macrokey) {
249             debug(1, "Using restored cache");
250             return 1;
251         }
252         $cache = {};
253         debug(1, "Macros changed, flushing cache");
254     } else {
255         debug(1, "Cache empty, creating");
256     }
257     $cache->{macrokey} = $macrokey;
258     return 0;
260 sub save_cache {
261     return if (!$use_caching || !$save_cache);
262     debug(1, "Saving cache");
263     Storable::lock_store($cache, $cachefile);
266 sub story {
267     my($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
268     my $state = {};
269     use bytes;
271     my $r = $cache->{story}{"$path/$filename"};
272     if ($r && $r->{orig} eq $$body_ref) {
273         $$body_ref = $r->{expanded};
274         return 1;
275     }
276     debug(1, "Cache miss due to story change: $path/$filename") if $r;
277     $cache->{story}{"$path/$filename"}{orig} = $$body_ref;
278     $$body_ref = apply_macros($state, $$body_ref);
279     $cache->{story}{"$path/$filename"}{expanded} = $$body_ref;
280     $save_cache = 1;
281     return 1;
284 sub start {
285     my $macrokey = '';
286     if (opendir MACROS, $macrodir) {
287         foreach my $macrofile (grep { /^\d*\w+$/ && -f "$macrodir/$_" } 
288                                sort readdir MACROS) {
289             my $mtime = stat("$macrodir/$macrofile")->mtime;
290             $macrokey .= "$macrofile:$mtime|";
291             require "$macrodir/$macrofile";
292         }
293     }
294     prime_cache($macrokey);
295     return 1;
298 sub end {
299     save_cache();
300     1;
304 =head1 NAME
306 Blosxom Plug-in: macros
308 =head1 SYNOPSIS
310 Purpose: Generalized macro system modelled on MT-Macros
312    * String macros: replace a string with another string
314    * Pattern macros: replace a regular-expression pattern with a
315      string optionally based on the replaced text
317    * Tag macros: replace html-style content-less tags (like img)
318      (specified with either a string or a pattern) with a string,
319      optionally based on the replaced entity and attributes, with
320      default attributes available
322    * Content Tag macros: relace html-style content tags (like a)
323      (specified with either a string or a pattern) with a string,
324      optionally based on the replaced entity, attributes, and
325      contents, with default attributes available
327 =head1 VERSION
329 0+1i
331 1st wide-spread test release
333 =head1 AUTHOR
335 Todd Larason  <jtl@molehill.org>, http://molelog.molehill.org/
337 =head1 BUGS
339 None known; address bug reports and comments to me or to the Blosxom
340 mailing list [http://www.yahoogroups.com/groups.blosxom].
342 =head1 Customization
344 =head2 Configuration variables
346 C<$macrodir> is the name of the directory to look for macro definition files
347 in; defaults to $plugin_state_dir/.macros.  Each file in this directory
348 whose name matches /^\d*\w+$/ (that is, optional digits at the beginning,
349 followed by letters, numbers and underscores) is read, in order sorted by
350 filename.  See "Macro Definition" section for details on file contents.
352 C<$use_caching> controls whether or not to try to cache formatted results;
353 caching requires Storable, but the plugin will work just fine (although
354 possibly slowly, with lots of macros installed) without it.
356 C<$debug_level> can be set to a value between 0 and 5; 0 will output
357 no debug information, while 5 will be very verbose.  The default is 1,
358 and should be changed after you've verified the plugin is working
359 correctly.
361 =head2 Macro Definitions
363 The macro files are simply perl scripts that are read and executed.  
364 Normally, they consist simply of literal calls to define_macro(), but
365 any other perl content is allowed.
367 As with all perl scripts, loading this script needs to return a true value.
368 define_macro() returns 1, so in most cases this will be taken care of
369 automatically, but if you're doing something fancy you need to be aware of
370 this.
372 define_macro() takes a single argument, a reference to a hash.  The hash 
373 must contain a 'type' element, which must be one of "string", "pattern",
374 "tag" and "ctag".  The other elements depend on the type.
376 =head3 String Macros
378 To define a string macro, pass define_macros() a hash containing:
380    * type => "string", required
381    * string => string, required; the string to be replaced
382    * body => string, required; the string to replace with; no variables are
383      useful, but the same replacement method is used as others, so $ is magic.
384    * inhtml => boolean, optional; if 1, then the string will be replaced even 
385      if it appears in the HTML markup; of 0, the string will only be replaced
386      in content.  The default is 0 (this is reverse MT-Macros' option, and
387      apparently reverse MT-Macros' default)
388    * once => boolean, optional; if 1, then the string will only be replaced
389      the first time it's seen in a given piece of text (that is, story body).
390      The default is 0.
391    * name => string, optional; currently names aren't used for anything, but
392      they may be in the future.
394 =head3 Pattern Macros
396 To define a pattern macro, pass define_macros() a hash containing:
398    * type => "pattern", required
399    * pattern => pattern, required; the regular expression to be replaced
400    * body => string, required; the string to replace with; ${1} through ${9} 
401      are replaced with the RE match variables $1 through $9; $<1> through $<9>
402      are the same thing, URL encoded.
403    * inhtml => boolean, optional; if 1, then the string will be replaced even 
404      if it appears in the HTML markup; of 0, the string will only be replaced
405      in content.  The default is 0 (this is reverse MT-Macros' option, and
406      apparently reverse MT-Macros' default).  Note that if inhtml is 0, then
407      the pattern is matched against each chunk of content separately, and thus
408      the full pattern must be included in a single markup-less chunk to be 
409      seen.
410    * once => boolean, optional; if 1, then the pattern will only be replaced
411      the first time it's seen in a given piece of text (that is, story body).
412      The default is 0.
413    * name => string, optional; currently names aren't used for anything, but
414      they may be in the future.
416 =head3 Tag Macros
418 To define a tag macro, pass define_macros() a hash containing:
420    * type => "tag", required
421    * pattern => pattern, required; a regular expression matching the entity
422      tag to be replaced; in normal cases this will just be a string, but
423      something like pattern => 'smily(\d+)' could be used to define a whole
424      set of tags like <smily47> at once.
425    * defaults => hashref, optional; a hash reference mapping attribute names
426      to default values.  "$\w+" patterns in the default values are replaced
427      the same way "${\w}" patterns in body strings are
428    * body => string, required; the string to replace with; ${1} through ${9} 
429      are replaced with the RE match variables $1 through $9; $<1> through $<9>
430      are the same thing, URL encoded. ${attrib} and $<attrib> are replaced with
431      the values of the specified attributes, or with the default for that
432      attribute if the attribute wasn't specified.
433    * once => boolean, optional; if 1, then the tag will only be replaced
434      the first time it's seen in a given piece of text (that is, story body).
435      The default is 0.
437 =head3 Content Tag Macros
439 To define a content tag macro, pass define_macros() a hash containing:
441    * type => "ctag", required
442    * pattern => pattern, required; a regular expression matching the entity
443      tag to be replaced; in normal cases this will just be a string.  The
444      closing tag must exactly match the opening tag, not just match the
445      pattern.
446    * defaults => hashref, optional; a hash reference mapping attribute names
447      to default values.  "$\w+" patterns in the default values are replaced
448      the same way "${\w}" patterns in body strings are; in particular, $body
449      can be useful
450    * body => string, required; the string to replace with; ${1} through ${9} 
451      are replaced with the RE match variables $1 through $9; $<1> through $<9>
452      are the same thing, URL encoded. ${attrib} and $<attrib> are replaced with
453      the values of the specified attributes, or with the default for that
454      attribute if the attribute wasn't specified.  ${body} and $<body> are
455      replaced with the content of the tag.
456    * once => boolean, optional; if 1, then the tag will only be replaced
457      the first time it's seen in a given piece of text (that is, story body).
458      The default is 0.
460 =head3 examples
462 =head4 Tatu
464 This defines a macro that replaces the word "Tatu" with its proper (Cyrllic) 
465 spelling the first time it's seen in a story; it won't much with markup, so
466 URLs containting "Tatu" are safe.
468 define_macro {
469     type   => 'string',
470     string => "Tatu",
471     body   => qq!<acronym title=\"Tatu\">&#x0422;&#x0430;&#x0442;&#x0443;</acronym>!,
472     once   => 1
475 This is just like above, but is safer -- it won't match the "Tatu" in 
476 "Tatuuie".
478 define_macro {
479     type    => 'pattern',
480     pattern => qr/\bTatu\b/,
481     body    => qq!<acronym title=\"Tatu\">&#x0422;&#x0430;&#x0442;&#x0443;</acronym>!,
482     once    => 1
485 =head4 Line
487 This defines a <line> tag with an optional width= attribute
489 define_macro {
490     type => 'tag',
491     name => 'line',
492     defaults => {width => "100%"},
493     body => '<hr noshade="noshade" width="${width}">'
496 This can be used either as just <line> or as <line width="50%">.
498 =head4 Amazon
500 this defines a fairly fancy <amazon tag
502 define_macro {
503     type => 'ctag',
504     name => 'amazon',
505     defaults => {domain => 'com', assoc => 'mtmolel-20'},
506     body => '<a href="http://www.amazon.${domain}/exec/obidos/ASIN/${asin}/ref=nosim/${assoc}">${body}</a>'
509 In normal use, it's something like
510 <amazon asin=B00008OE6I>Canon Powershot S400</amazon>
511 but it can also be used to refer to something on one of the international 
512 Amazon sites, like
513 on asin=B000089AS9 domain=co.uk>Angel Season 3 DVDs</amazon>
515 If you wanted to give referral credit to someone else, you could with:
516 <amazon asin=B00008OE6I assoc=rael-20>Canon Powershot S400</amazon>
518 =head4 Google
520 This defines a <google> tag with a completely optional query attribute; if
521 it's not given, then the phrase enclosed by the tag is what's searched for.
523 define_macro {
524     type => 'ctag',
525     name => 'google',
526     defaults => {query => "\$body"},
527     body => '<a href="http://www.google.com/search?q=$<query>">${body}</a>'
530 =head4 Programmatic Definitions
532 There's no reason the macro files need to be literal calls to define_macro.
534 This example defines its own simplified syntax for defining a set of similar 
535 macros, reads the definitions, and makmes the appropriate define_macro() 
536 calls.  It's directly translated from a similar MT-Macros definition file,
537 (with more macros defined) found at http://diveintomark.org/inc/macros2
539 while (<DATA>) {
540     chomp;
541     my ($name, $tag, $attrlist) = m/"(.+?)"\s+(\w+)(.*)/;
542     next if !$name;
543     my $attrs = '';
544     my (@attrs) = $attrlist =~ m/\s+(\w+)\s+"(.*?)"/g;
545     for ($i = 0; $i < scalar(@attrs); $i += 2) {
546         my ($attr, $value) = ($attrs[$i], $attrs[$i+1]);
547         $value =~ s/"/&quot;/g; #";
548         $attrs .= qq{ $attr="$value"};
549     }
550     if ($tag =~ /acronym/) {
551         define_macro({
552                       name    => "abbr_$name",
553                       type    => pattern,
554                       pattern => qr/\b$name\b/,
555                       body    => "<$tag$attrs>$name</$tag>",
556                       once    => 1
557                      });
558     } elsif ($tag =~ /img/) {
559         define_macro({
560                       name    => "img_$name",
561                       type    => string,
562                       string  => $name,
563                       body    => "<$tag$attrs>"
564                      });
565     } else {
566         define_macro({
567                       name    => "abbr_$name",
568                       type    => pattern,
569                       pattern => qr/\b$name\b/,
570                       body    => "<$tag$attrs>$name</$tag>"
571                      });
572     }
576 __DATA__
577 "AOL" acronym title "America Online"
578 "API" acronym title "Application Interface"
579 "CGI" acronym title "Common Gateway Interface"
580 "CMS" acronym title "Content Management System"
581 "CSS" acronym title "Cascading Style Sheets"
582 "DMV" acronym title "Department of Motor Vehicles"
583 ":)"     img alt "[smiley face]"  title "" src "/images/smilies/smile.gif"   width "20" height "20"
584 ":-)"    img alt "[smiley face]"  title "" src "/images/smilies/smile.gif"   width "20" height "20"
585 "=)"     img alt "[smiley face]"  title "" src "/images/smilies/smile.gif"   width "20" height "20"
586 "=-)"    img alt "[smiley face]"  title "" src "/images/smilies/smile.gif"   width "20" height "20"
587 __END__
589 =head1 Possible Deficiencies
591    * MT-Macros 'recursion' option isn't available.  If this is a real problem
592      for you, please let me know, preferably with a good example of what you
593      can't accomplish currently (remember, macros are invoked in the order
594      they're defined, which you can control with filename naming)
595    * tag and ctag macros can't be used in HTML markup.  This would be a big
596      problem for Movable Type, where parameter replacement is done with
597      psuedo-HTML, but doesn't seem to be a problem for Blosxom.  If it is
598      for you, please let me know, again along with an example.
599    * MT-Macros 'no_case' option isn't available.  This can be done by
600      including (?i) in your patterns or defining them with qr//i, instead.
601    * tag and ctag macros can't be explicitely named, because the 'name'
602      parameter is already being used.  Future versions may change tag
603      and ctag to use 'string' or 'pattern' for what 'name' is currently
604      used for, and use 'name' to define a macro.  That will only be done
605      if there's a good use for names, though.
606    * Once defined, macros are always active.  They can't be deactivated on a
607      per-story basis.  This might be handled with a meta- header at some 
608      point, if someone gives me a reasonable example for why they need it.
609    * There's no built-in data-based macro definition syntax.  It's not clear
610      to me that a literal define_macro() call is any more difficult than
611      MT-Macros' HTML-looking (but not HTML-acting) definition syntax, though,
612      and as shown above simpler syntaxes ban be custom-built as appropriate.
613      I'd be more than happy to include a simpler syntax, though, if someone
614      were to develop one that were obviously better than define_syntax().
616 =head1 Caching
618 If the Storable module is available and $use_caching is set, formatted
619 stories will be cached; the cache is globally keyed off the list of macro
620 files and their modification date, and per-story on the contents of the
621 story itself.  It should thus not ever be necessary to manually flush the
622 cache, but it's always safe to do so, by removing the 
623 $plugin_state_dir/.macros.cache file.
625 =head1 LICENSE
627 this Blosxom Plug-in
628 Copyright 2003, Todd Larason
630 (This license is the same as Blosxom's)
632 Permission is hereby granted, free of charge, to any person obtaining a
633 copy of this software and associated documentation files (the "Software"),
634 to deal in the Software without restriction, including without limitation
635 the rights to use, copy, modify, merge, publish, distribute, sublicense,
636 and/or sell copies of the Software, and to permit persons to whom the
637 Software is furnished to do so, subject to the following conditions:
639 The above copyright notice and this permission notice shall be included
640 in all copies or substantial portions of the Software.
642 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
643 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
644 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
645 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
646 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
647 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
648 OTHER DEALINGS IN THE SOFTWARE.