1 # Blosxom Plugin: macros -*- perl -*-
2 # Author: Todd Larason (jtl@molehill.org)
4 # Blosxom Home/Docs/Licensing: http://blosxom.sourceforge.net/
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;
16 # -------------------------------------------------------------------
20 # tag (string or pattern) implemented
21 # ctag (string or pattern) implemented
24 # name implemented, auto defaults
27 # no_html implemented, default
28 # for string and pattern, inhtml => 1 to
29 # reverse; can't reverse for tag
31 # * defaults for attribs implemented
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}
42 # XXX cache macro definitions?
45 my $package = "macros";
46 my $cachefile = "$blosxom::plugin_state_dir/.$package.cache";
50 my ($level, @msg) = @_;
52 if ($debug_level >= $level) {
53 print STDERR "$package debug $level: @msg\n";
61 s/([^a-zA-Z0-9])/sprintf("%%%02x",ord($1))/eg;
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}";
102 push @macros, $macro;
106 sub replace_pattern {
107 my ($macro, $ctx) = @_;
109 my $replacement = $macro->{body};
115 }{defined($1) ? $ctx->{$1} :
116 defined($2) ? url_escape($ctx->{$2}) :
122 sub apply_pattern_macro {
123 my ($state, $macro, $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,
137 sub apply_tag_macro {
138 my ($state, $macro, $entity, $attributes, $body) = @_;
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
147 " ([^\"]+) " # $2 = quoted value
149 ([^\s]+) # $3 = unquoted value
154 foreach (keys %{$macro->{defaults}}) {
155 next if defined($ctx->{$_});
156 if ($macro->{defaults}{$_} =~ m:\$(\w+):) {
157 $ctx->{$_} = $ctx->{$1};
159 $ctx->{$_} = $macro->{defaults}{$_};
163 my $text = $macro->{body};
169 }{defined($1) ? $ctx->{$1} :
170 defined($2) ? url_escape($ctx->{$2}) :
176 my ($state, $macro, $text) = @_;
178 if ($macro->{type} eq 'pattern') {
179 if ($macro->{inhtml}) {
180 $text = apply_pattern_macro($state, $macro, $text);
182 my @tokens = split /(<[^>]+>)/, $text;
186 $_ = apply_pattern_macro($state, $macro, $_);
191 } elsif ($macro->{type} eq 'tag') {
192 my @tokens = split /(<[^>]+>)/, $text;
194 while (defined($_ = shift @tokens)) {
195 if (!($macro->{once} && $state->{used}{$macro->{name}})
196 && (m/<($macro->{pattern})([\s>].*)/)) {
201 if ($macro->{container}) {
203 while (defined($_ = shift @tokens)) {
204 last if (m:</$entity\s*>:);
207 $_ = apply_tag_macro($state, $macro, $entity, $attributes, $body);
209 $_ = apply_tag_macro($state, $macro, $entity, $attributes);
211 $state->{used}{$macro->{name}}++;
216 debug(0, "ACK: unknown macro type $macro->{type}");
222 my ($state, $text) = @_;
224 foreach my $macro (@macros) {
225 $text = apply_macro($state, $macro, $text);
234 return if (!$use_caching);
235 eval "require Storable";
237 debug(1, "cache disabled, Storable not available");
241 if (!Storable->can('lock_retrieve')) {
242 debug(1, "cache disabled, Storable::lock_retrieve not available");
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");
253 debug(1, "Macros changed, flushing cache");
255 debug(1, "Cache empty, creating");
257 $cache->{macrokey} = $macrokey;
261 return if (!$use_caching || !$save_cache);
262 debug(1, "Saving cache");
263 Storable::lock_store($cache, $cachefile);
267 my($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
271 my $r = $cache->{story}{"$path/$filename"};
272 if ($r && $r->{orig} eq $$body_ref) {
273 $$body_ref = $r->{expanded};
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;
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";
294 prime_cache($macrokey);
306 Blosxom Plug-in: macros
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
331 1st wide-spread test release
335 Todd Larason <jtl@molehill.org>, http://molelog.molehill.org/
337 This plugin is now maintained by the Blosxom Sourceforge Team,
338 <blosxom-devel@lists.sourceforge.net>.
342 None known; please send bug reports and feedback to the Blosxom
343 development mailing list <blosxom-devel@lists.sourceforge.net>.
347 =head2 Configuration variables
349 C<$macrodir> is the name of the directory to look for macro definition files
350 in; defaults to $plugin_state_dir/.macros. Each file in this directory
351 whose name matches /^\d*\w+$/ (that is, optional digits at the beginning,
352 followed by letters, numbers and underscores) is read, in order sorted by
353 filename. See "Macro Definition" section for details on file contents.
355 C<$use_caching> controls whether or not to try to cache formatted results;
356 caching requires Storable, but the plugin will work just fine (although
357 possibly slowly, with lots of macros installed) without it.
359 C<$debug_level> can be set to a value between 0 and 5; 0 will output
360 no debug information, while 5 will be very verbose. The default is 1,
361 and should be changed after you've verified the plugin is working
364 =head2 Macro Definitions
366 The macro files are simply perl scripts that are read and executed.
367 Normally, they consist simply of literal calls to define_macro(), but
368 any other perl content is allowed.
370 As with all perl scripts, loading this script needs to return a true value.
371 define_macro() returns 1, so in most cases this will be taken care of
372 automatically, but if you're doing something fancy you need to be aware of
375 define_macro() takes a single argument, a reference to a hash. The hash
376 must contain a 'type' element, which must be one of "string", "pattern",
377 "tag" and "ctag". The other elements depend on the type.
381 To define a string macro, pass define_macros() a hash containing:
383 * type => "string", required
384 * string => string, required; the string to be replaced
385 * body => string, required; the string to replace with; no variables are
386 useful, but the same replacement method is used as others, so $ is magic.
387 * inhtml => boolean, optional; if 1, then the string will be replaced even
388 if it appears in the HTML markup; of 0, the string will only be replaced
389 in content. The default is 0 (this is reverse MT-Macros' option, and
390 apparently reverse MT-Macros' default)
391 * once => boolean, optional; if 1, then the string will only be replaced
392 the first time it's seen in a given piece of text (that is, story body).
394 * name => string, optional; currently names aren't used for anything, but
395 they may be in the future.
397 =head3 Pattern Macros
399 To define a pattern macro, pass define_macros() a hash containing:
401 * type => "pattern", required
402 * pattern => pattern, required; the regular expression to be replaced
403 * body => string, required; the string to replace with; ${1} through ${9}
404 are replaced with the RE match variables $1 through $9; $<1> through $<9>
405 are the same thing, URL encoded.
406 * inhtml => boolean, optional; if 1, then the string will be replaced even
407 if it appears in the HTML markup; of 0, the string will only be replaced
408 in content. The default is 0 (this is reverse MT-Macros' option, and
409 apparently reverse MT-Macros' default). Note that if inhtml is 0, then
410 the pattern is matched against each chunk of content separately, and thus
411 the full pattern must be included in a single markup-less chunk to be
413 * once => boolean, optional; if 1, then the pattern will only be replaced
414 the first time it's seen in a given piece of text (that is, story body).
416 * name => string, optional; currently names aren't used for anything, but
417 they may be in the future.
421 To define a tag macro, pass define_macros() a hash containing:
423 * type => "tag", required
424 * pattern => pattern, required; a regular expression matching the entity
425 tag to be replaced; in normal cases this will just be a string, but
426 something like pattern => 'smily(\d+)' could be used to define a whole
427 set of tags like <smily47> at once.
428 * defaults => hashref, optional; a hash reference mapping attribute names
429 to default values. "$\w+" patterns in the default values are replaced
430 the same way "${\w}" patterns in body strings are
431 * body => string, required; the string to replace with; ${1} through ${9}
432 are replaced with the RE match variables $1 through $9; $<1> through $<9>
433 are the same thing, URL encoded. ${attrib} and $<attrib> are replaced with
434 the values of the specified attributes, or with the default for that
435 attribute if the attribute wasn't specified.
436 * once => boolean, optional; if 1, then the tag will only be replaced
437 the first time it's seen in a given piece of text (that is, story body).
440 =head3 Content Tag Macros
442 To define a content tag macro, pass define_macros() a hash containing:
444 * type => "ctag", required
445 * pattern => pattern, required; a regular expression matching the entity
446 tag to be replaced; in normal cases this will just be a string. The
447 closing tag must exactly match the opening tag, not just match the
449 * defaults => hashref, optional; a hash reference mapping attribute names
450 to default values. "$\w+" patterns in the default values are replaced
451 the same way "${\w}" patterns in body strings are; in particular, $body
453 * body => string, required; the string to replace with; ${1} through ${9}
454 are replaced with the RE match variables $1 through $9; $<1> through $<9>
455 are the same thing, URL encoded. ${attrib} and $<attrib> are replaced with
456 the values of the specified attributes, or with the default for that
457 attribute if the attribute wasn't specified. ${body} and $<body> are
458 replaced with the content of the tag.
459 * once => boolean, optional; if 1, then the tag will only be replaced
460 the first time it's seen in a given piece of text (that is, story body).
467 This defines a macro that replaces the word "Tatu" with its proper (Cyrllic)
468 spelling the first time it's seen in a story; it won't much with markup, so
469 URLs containting "Tatu" are safe.
474 body => qq!<acronym title=\"Tatu\">Тату</acronym>!,
478 This is just like above, but is safer -- it won't match the "Tatu" in
483 pattern => qr/\bTatu\b/,
484 body => qq!<acronym title=\"Tatu\">Тату</acronym>!,
490 This defines a <line> tag with an optional width= attribute
495 defaults => {width => "100%"},
496 body => '<hr noshade="noshade" width="${width}">'
499 This can be used either as just <line> or as <line width="50%">.
503 this defines a fairly fancy <amazon tag
508 defaults => {domain => 'com', assoc => 'mtmolel-20'},
509 body => '<a href="http://www.amazon.${domain}/exec/obidos/ASIN/${asin}/ref=nosim/${assoc}">${body}</a>'
512 In normal use, it's something like
513 <amazon asin=B00008OE6I>Canon Powershot S400</amazon>
514 but it can also be used to refer to something on one of the international
516 on asin=B000089AS9 domain=co.uk>Angel Season 3 DVDs</amazon>
518 If you wanted to give referral credit to someone else, you could with:
519 <amazon asin=B00008OE6I assoc=rael-20>Canon Powershot S400</amazon>
523 This defines a <google> tag with a completely optional query attribute; if
524 it's not given, then the phrase enclosed by the tag is what's searched for.
529 defaults => {query => "\$body"},
530 body => '<a href="http://www.google.com/search?q=$<query>">${body}</a>'
533 =head4 Programmatic Definitions
535 There's no reason the macro files need to be literal calls to define_macro.
537 This example defines its own simplified syntax for defining a set of similar
538 macros, reads the definitions, and makmes the appropriate define_macro()
539 calls. It's directly translated from a similar MT-Macros definition file,
540 (with more macros defined) found at http://diveintomark.org/inc/macros2
544 my ($name, $tag, $attrlist) = m/"(.+?)"\s+(\w+)(.*)/;
547 my (@attrs) = $attrlist =~ m/\s+(\w+)\s+"(.*?)"/g;
548 for ($i = 0; $i < scalar(@attrs); $i += 2) {
549 my ($attr, $value) = ($attrs[$i], $attrs[$i+1]);
550 $value =~ s/"/"/g; #";
551 $attrs .= qq{ $attr="$value"};
553 if ($tag =~ /acronym/) {
555 name => "abbr_$name",
557 pattern => qr/\b$name\b/,
558 body => "<$tag$attrs>$name</$tag>",
561 } elsif ($tag =~ /img/) {
566 body => "<$tag$attrs>"
570 name => "abbr_$name",
572 pattern => qr/\b$name\b/,
573 body => "<$tag$attrs>$name</$tag>"
580 "AOL" acronym title "America Online"
581 "API" acronym title "Application Interface"
582 "CGI" acronym title "Common Gateway Interface"
583 "CMS" acronym title "Content Management System"
584 "CSS" acronym title "Cascading Style Sheets"
585 "DMV" acronym title "Department of Motor Vehicles"
586 ":)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20"
587 ":-)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20"
588 "=)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20"
589 "=-)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20"
592 =head1 Possible Deficiencies
594 * MT-Macros 'recursion' option isn't available. If this is a real problem
595 for you, please let me know, preferably with a good example of what you
596 can't accomplish currently (remember, macros are invoked in the order
597 they're defined, which you can control with filename naming)
598 * tag and ctag macros can't be used in HTML markup. This would be a big
599 problem for Movable Type, where parameter replacement is done with
600 psuedo-HTML, but doesn't seem to be a problem for Blosxom. If it is
601 for you, please let me know, again along with an example.
602 * MT-Macros 'no_case' option isn't available. This can be done by
603 including (?i) in your patterns or defining them with qr//i, instead.
604 * tag and ctag macros can't be explicitely named, because the 'name'
605 parameter is already being used. Future versions may change tag
606 and ctag to use 'string' or 'pattern' for what 'name' is currently
607 used for, and use 'name' to define a macro. That will only be done
608 if there's a good use for names, though.
609 * Once defined, macros are always active. They can't be deactivated on a
610 per-story basis. This might be handled with a meta- header at some
611 point, if someone gives me a reasonable example for why they need it.
612 * There's no built-in data-based macro definition syntax. It's not clear
613 to me that a literal define_macro() call is any more difficult than
614 MT-Macros' HTML-looking (but not HTML-acting) definition syntax, though,
615 and as shown above simpler syntaxes ban be custom-built as appropriate.
616 I'd be more than happy to include a simpler syntax, though, if someone
617 were to develop one that were obviously better than define_syntax().
621 If the Storable module is available and $use_caching is set, formatted
622 stories will be cached; the cache is globally keyed off the list of macro
623 files and their modification date, and per-story on the contents of the
624 story itself. It should thus not ever be necessary to manually flush the
625 cache, but it's always safe to do so, by removing the
626 $plugin_state_dir/.macros.cache file.
631 Copyright 2003, Todd Larason
633 (This license is the same as Blosxom's)
635 Permission is hereby granted, free of charge, to any person obtaining a
636 copy of this software and associated documentation files (the "Software"),
637 to deal in the Software without restriction, including without limitation
638 the rights to use, copy, modify, merge, publish, distribute, sublicense,
639 and/or sell copies of the Software, and to permit persons to whom the
640 Software is furnished to do so, subject to the following conditions:
642 The above copyright notice and this permission notice shall be included
643 in all copies or substantial portions of the Software.
645 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
646 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
647 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
648 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
649 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
650 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
651 OTHER DEALINGS IN THE SOFTWARE.