1 # Blosxom Plugin: macros -*- perl -*-
2 # Author: Todd Larason (jtl@molehill.org)
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;
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/
339 None known; address bug reports and comments to me or to the Blosxom
340 mailing list [http://www.yahoogroups.com/groups.blosxom].
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
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
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.
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).
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
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).
413 * name => string, optional; currently names aren't used for anything, but
414 they may be in the future.
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).
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
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
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).
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.
471 body => qq!<acronym title=\"Tatu\">Тату</acronym>!,
475 This is just like above, but is safer -- it won't match the "Tatu" in
480 pattern => qr/\bTatu\b/,
481 body => qq!<acronym title=\"Tatu\">Тату</acronym>!,
487 This defines a <line> tag with an optional width= attribute
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%">.
500 this defines a fairly fancy <amazon tag
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
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>
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.
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
541 my ($name, $tag, $attrlist) = m/"(.+?)"\s+(\w+)(.*)/;
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/"/"/g; #";
548 $attrs .= qq{ $attr="$value"};
550 if ($tag =~ /acronym/) {
552 name => "abbr_$name",
554 pattern => qr/\b$name\b/,
555 body => "<$tag$attrs>$name</$tag>",
558 } elsif ($tag =~ /img/) {
563 body => "<$tag$attrs>"
567 name => "abbr_$name",
569 pattern => qr/\b$name\b/,
570 body => "<$tag$attrs>$name</$tag>"
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"
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().
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.
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.