# Blosxom Plugin: macros -*- perl -*- # Author: Todd Larason (jtl@molehill.org) # Version: 0+1i # Blosxom Home/Docs/Licensing: http://blosxom.sourceforge.net/ # Calendar plugin Home/Docs/Licensing: # http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/Macros/ # Modelled on Brad Choate's MT-Macros, but no code in common package macros; # -*- perl -*- # --- Configuration Variables --- $macrodir = "$blosxom::plugin_state_dir/.macros" unless defined $macrodir; $use_caching = 1; $debug_level = 0; # ------------------------------------------------------------------- # types: # string implemented # pattern implemented # tag (string or pattern) implemented # ctag (string or pattern) implemented # attributes: # name implemented, auto defaults # once implemented # recurse # no_html implemented, default # for string and pattern, inhtml => 1 to # reverse; can't reverse for tag # no_case # * defaults for attribs implemented # * body implemented # in replacement: # pattern: ${1}-${9} matched () text, $<1>-$<9> escaped matched text # tag: as in pattern + ${name} tag attribute, $ escaped attribute # ctag: as in tag + ${body} use bytes; use File::stat; # XXX cache macro definitions? my @macros = (); my $cache; my $package = "macros"; my $cachefile = "$blosxom::plugin_state_dir/.$package.cache"; my $save_cache = 0; sub debug { my ($level, @msg) = @_; if ($debug_level >= $level) { print STDERR "$package debug $level: @msg\n"; } 1; } sub url_escape { local ($_) = @_; s/([^a-zA-Z0-9])/sprintf("%%%02x",ord($1))/eg; s/%20/+/g; return $_; } sub define_macro { my ($arg) = @_; my $macro = {}; if ($arg->{type} eq "string") { $macro->{type} = 'pattern'; $macro->{pattern} = qr{\Q$arg->{string}\E}; $macro->{body} = $arg->{body}; $macro->{inhtml} = $arg->{inhtml} if $arg->{inhtml}; $macro->{once} = $arg->{once} if $arg->{once}; $macro->{name} = $arg->{name} || "string_$arg->{string}"; } elsif ($arg->{type} eq "pattern") { $macro->{type} = 'pattern'; $macro->{pattern} = qr{$arg->{pattern}}; $macro->{body} = $arg->{body}; $macro->{inhtml} = $arg->{inhtml} if $arg->{inhtml}; $macro->{once} = $arg->{once} if $arg->{once}; $macro->{name} = $arg->{name} || "pattern_$arg->{pattern}"; } elsif ($arg->{type} eq "tag") { $macro->{type} = 'tag'; $macro->{container} = 0; $macro->{pattern} = qr{$arg->{name}}; $macro->{body} = $arg->{body}; $macro->{defaults} = {%{$arg->{defaults}}}; $macro->{once} = $arg->{once} if $arg->{once}; $macro->{name} = "tag_$arg->{name}"; } elsif ($arg->{type} eq "ctag") { $macro->{type} = 'tag'; $macro->{container} = 1; $macro->{pattern} = qr{$arg->{name}}; $macro->{body} = $arg->{body}; $macro->{defaults} = {%{$arg->{defaults}}}; $macro->{once} = $arg->{once} if $arg->{once}; $macro->{name} = "tag_$arg->{name}"; } push @macros, $macro; return 1; } sub replace_pattern { my ($macro, $ctx) = @_; my $replacement = $macro->{body}; $replacement =~ s{ (?: \$ { ([\w]+) } | \$ < ([\w]+) > | (\$ [\w:]+) ) }{defined($1) ? $ctx->{$1} : defined($2) ? url_escape($ctx->{$2}) : eval "$3||''"}xge; return $replacement; } sub apply_pattern_macro { my ($state, $macro, $text) = @_; $text =~ s{($macro->{pattern})} {$macro->{once} && $state->{used}{$macro->{name}} ? $1 : (++$state->{used}{$macro->{name}} and replace_pattern($macro, { 1 => $2, 2 => $3, 3 => $4, 4 => $5, 5 => $6, 6 => $7, 7 => $8, 8 => $7 })) }egms; return $text; } sub apply_tag_macro { my ($state, $macro, $entity, $attributes, $body) = @_; my $ctx; $ctx->{body} = $body; $entity =~ $macro->{pattern}; @{$ctx}{qw/1 2 3 4 5 6 7 8 9/} = ($1, $2, $3, $4, $5, $6, $7, $8, $9); while ($attributes =~ m{ (\w+) # $1 = tag = (?: " ([^\"]+) " # $2 = quoted value | ([^\s]+) # $3 = unquoted value ) }gx) { $ctx->{$1} = ($+); } foreach (keys %{$macro->{defaults}}) { next if defined($ctx->{$_}); if ($macro->{defaults}{$_} =~ m:\$(\w+):) { $ctx->{$_} = $ctx->{$1}; } else { $ctx->{$_} = $macro->{defaults}{$_}; } } my $text = $macro->{body}; $text =~ s{ (?: \$ { ([\w]+) } | \$ < ([\w]+) > | (\$ [\w:]+) ) }{defined($1) ? $ctx->{$1} : defined($2) ? url_escape($ctx->{$2}) : eval "$3||''"}xge; return $text; } sub apply_macro { my ($state, $macro, $text) = @_; if ($macro->{type} eq 'pattern') { if ($macro->{inhtml}) { $text = apply_pattern_macro($state, $macro, $text); } else { my @tokens = split /(<[^>]+>)/, $text; $text = ''; foreach (@tokens) { if (!m/^{type} eq 'tag') { my @tokens = split /(<[^>]+>)/, $text; $text = ''; while (defined($_ = shift @tokens)) { if (!($macro->{once} && $state->{used}{$macro->{name}}) && (m/<($macro->{pattern})([\s>].*)/)) { my $tag = $_; my $entity = $1; my $attributes = $+; chop $attributes; if ($macro->{container}) { my $body; while (defined($_ = shift @tokens)) { last if (m::); $body .= $_; } $_ = apply_tag_macro($state, $macro, $entity, $attributes, $body); } else { $_ = apply_tag_macro($state, $macro, $entity, $attributes); } $state->{used}{$macro->{name}}++; } $text .= $_; } } else { debug(0, "ACK: unknown macro type $macro->{type}"); } return $text; } sub apply_macros { my ($state, $text) = @_; foreach my $macro (@macros) { $text = apply_macro($state, $macro, $text); } return $text; } # caching support sub prime_cache { my ($macrokey) = @_; return if (!$use_caching); eval "require Storable"; if ($@) { debug(1, "cache disabled, Storable not available"); $use_caching = 0; return 0; } if (!Storable->can('lock_retrieve')) { debug(1, "cache disabled, Storable::lock_retrieve not available"); $use_caching = 0; return 0; } $cache = (-r $cachefile ? Storable::lock_retrieve($cachefile) : {}); if (defined $cache->{macrokey}) { if ($cache->{macrokey} eq $macrokey) { debug(1, "Using restored cache"); return 1; } $cache = {}; debug(1, "Macros changed, flushing cache"); } else { debug(1, "Cache empty, creating"); } $cache->{macrokey} = $macrokey; return 0; } sub save_cache { return if (!$use_caching || !$save_cache); debug(1, "Saving cache"); Storable::lock_store($cache, $cachefile); } sub story { my($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_; my $state = {}; use bytes; my $r = $cache->{story}{"$path/$filename"}; if ($r && $r->{orig} eq $$body_ref) { $$body_ref = $r->{expanded}; return 1; } debug(1, "Cache miss due to story change: $path/$filename") if $r; $cache->{story}{"$path/$filename"}{orig} = $$body_ref; $$body_ref = apply_macros($state, $$body_ref); $cache->{story}{"$path/$filename"}{expanded} = $$body_ref; $save_cache = 1; return 1; } sub start { my $macrokey = ''; if (opendir MACROS, $macrodir) { foreach my $macrofile (grep { /^\d*\w+$/ && -f "$macrodir/$_" } sort readdir MACROS) { my $mtime = stat("$macrodir/$macrofile")->mtime; $macrokey .= "$macrofile:$mtime|"; require "$macrodir/$macrofile"; } } prime_cache($macrokey); return 1; } sub end { save_cache(); 1; } 1; =head1 NAME Blosxom Plug-in: macros =head1 SYNOPSIS Purpose: Generalized macro system modelled on MT-Macros * String macros: replace a string with another string * Pattern macros: replace a regular-expression pattern with a string optionally based on the replaced text * Tag macros: replace html-style content-less tags (like img) (specified with either a string or a pattern) with a string, optionally based on the replaced entity and attributes, with default attributes available * Content Tag macros: relace html-style content tags (like a) (specified with either a string or a pattern) with a string, optionally based on the replaced entity, attributes, and contents, with default attributes available =head1 VERSION 0+1i 1st wide-spread test release =head1 AUTHOR Todd Larason , http://molelog.molehill.org/ This plugin is now maintained by the Blosxom Sourceforge Team, . =head1 BUGS None known; please send bug reports and feedback to the Blosxom development mailing list . =head1 Customization =head2 Configuration variables C<$macrodir> is the name of the directory to look for macro definition files in; defaults to $plugin_state_dir/.macros. Each file in this directory whose name matches /^\d*\w+$/ (that is, optional digits at the beginning, followed by letters, numbers and underscores) is read, in order sorted by filename. See "Macro Definition" section for details on file contents. C<$use_caching> controls whether or not to try to cache formatted results; caching requires Storable, but the plugin will work just fine (although possibly slowly, with lots of macros installed) without it. C<$debug_level> can be set to a value between 0 and 5; 0 will output no debug information, while 5 will be very verbose. The default is 1, and should be changed after you've verified the plugin is working correctly. =head2 Macro Definitions The macro files are simply perl scripts that are read and executed. Normally, they consist simply of literal calls to define_macro(), but any other perl content is allowed. As with all perl scripts, loading this script needs to return a true value. define_macro() returns 1, so in most cases this will be taken care of automatically, but if you're doing something fancy you need to be aware of this. define_macro() takes a single argument, a reference to a hash. The hash must contain a 'type' element, which must be one of "string", "pattern", "tag" and "ctag". The other elements depend on the type. =head3 String Macros To define a string macro, pass define_macros() a hash containing: * type => "string", required * string => string, required; the string to be replaced * body => string, required; the string to replace with; no variables are useful, but the same replacement method is used as others, so $ is magic. * inhtml => boolean, optional; if 1, then the string will be replaced even if it appears in the HTML markup; of 0, the string will only be replaced in content. The default is 0 (this is reverse MT-Macros' option, and apparently reverse MT-Macros' default) * once => boolean, optional; if 1, then the string will only be replaced the first time it's seen in a given piece of text (that is, story body). The default is 0. * name => string, optional; currently names aren't used for anything, but they may be in the future. =head3 Pattern Macros To define a pattern macro, pass define_macros() a hash containing: * type => "pattern", required * pattern => pattern, required; the regular expression to be replaced * body => string, required; the string to replace with; ${1} through ${9} are replaced with the RE match variables $1 through $9; $<1> through $<9> are the same thing, URL encoded. * inhtml => boolean, optional; if 1, then the string will be replaced even if it appears in the HTML markup; of 0, the string will only be replaced in content. The default is 0 (this is reverse MT-Macros' option, and apparently reverse MT-Macros' default). Note that if inhtml is 0, then the pattern is matched against each chunk of content separately, and thus the full pattern must be included in a single markup-less chunk to be seen. * once => boolean, optional; if 1, then the pattern will only be replaced the first time it's seen in a given piece of text (that is, story body). The default is 0. * name => string, optional; currently names aren't used for anything, but they may be in the future. =head3 Tag Macros To define a tag macro, pass define_macros() a hash containing: * type => "tag", required * pattern => pattern, required; a regular expression matching the entity tag to be replaced; in normal cases this will just be a string, but something like pattern => 'smily(\d+)' could be used to define a whole set of tags like at once. * defaults => hashref, optional; a hash reference mapping attribute names to default values. "$\w+" patterns in the default values are replaced the same way "${\w}" patterns in body strings are * body => string, required; the string to replace with; ${1} through ${9} are replaced with the RE match variables $1 through $9; $<1> through $<9> are the same thing, URL encoded. ${attrib} and $ are replaced with the values of the specified attributes, or with the default for that attribute if the attribute wasn't specified. * once => boolean, optional; if 1, then the tag will only be replaced the first time it's seen in a given piece of text (that is, story body). The default is 0. =head3 Content Tag Macros To define a content tag macro, pass define_macros() a hash containing: * type => "ctag", required * pattern => pattern, required; a regular expression matching the entity tag to be replaced; in normal cases this will just be a string. The closing tag must exactly match the opening tag, not just match the pattern. * defaults => hashref, optional; a hash reference mapping attribute names to default values. "$\w+" patterns in the default values are replaced the same way "${\w}" patterns in body strings are; in particular, $body can be useful * body => string, required; the string to replace with; ${1} through ${9} are replaced with the RE match variables $1 through $9; $<1> through $<9> are the same thing, URL encoded. ${attrib} and $ are replaced with the values of the specified attributes, or with the default for that attribute if the attribute wasn't specified. ${body} and $ are replaced with the content of the tag. * once => boolean, optional; if 1, then the tag will only be replaced the first time it's seen in a given piece of text (that is, story body). The default is 0. =head3 examples =head4 Tatu This defines a macro that replaces the word "Tatu" with its proper (Cyrllic) spelling the first time it's seen in a story; it won't much with markup, so URLs containting "Tatu" are safe. define_macro { type => 'string', string => "Tatu", body => qq!Тату!, once => 1 }; This is just like above, but is safer -- it won't match the "Tatu" in "Tatuuie". define_macro { type => 'pattern', pattern => qr/\bTatu\b/, body => qq!Тату!, once => 1 }; =head4 Line This defines a tag with an optional width= attribute define_macro { type => 'tag', name => 'line', defaults => {width => "100%"}, body => '
' }; This can be used either as just or as . =head4 Amazon this defines a fairly fancy 'ctag', name => 'amazon', defaults => {domain => 'com', assoc => 'mtmolel-20'}, body => '${body}' }; In normal use, it's something like Canon Powershot S400 but it can also be used to refer to something on one of the international Amazon sites, like on asin=B000089AS9 domain=co.uk>Angel Season 3 DVDs If you wanted to give referral credit to someone else, you could with: Canon Powershot S400 =head4 Google This defines a tag with a completely optional query attribute; if it's not given, then the phrase enclosed by the tag is what's searched for. define_macro { type => 'ctag', name => 'google', defaults => {query => "\$body"}, body => '${body}' }; =head4 Programmatic Definitions There's no reason the macro files need to be literal calls to define_macro. This example defines its own simplified syntax for defining a set of similar macros, reads the definitions, and makmes the appropriate define_macro() calls. It's directly translated from a similar MT-Macros definition file, (with more macros defined) found at http://diveintomark.org/inc/macros2 while () { chomp; my ($name, $tag, $attrlist) = m/"(.+?)"\s+(\w+)(.*)/; next if !$name; my $attrs = ''; my (@attrs) = $attrlist =~ m/\s+(\w+)\s+"(.*?)"/g; for ($i = 0; $i < scalar(@attrs); $i += 2) { my ($attr, $value) = ($attrs[$i], $attrs[$i+1]); $value =~ s/"/"/g; #"; $attrs .= qq{ $attr="$value"}; } if ($tag =~ /acronym/) { define_macro({ name => "abbr_$name", type => pattern, pattern => qr/\b$name\b/, body => "<$tag$attrs>$name", once => 1 }); } elsif ($tag =~ /img/) { define_macro({ name => "img_$name", type => string, string => $name, body => "<$tag$attrs>" }); } else { define_macro({ name => "abbr_$name", type => pattern, pattern => qr/\b$name\b/, body => "<$tag$attrs>$name" }); } } 1; __DATA__ "AOL" acronym title "America Online" "API" acronym title "Application Interface" "CGI" acronym title "Common Gateway Interface" "CMS" acronym title "Content Management System" "CSS" acronym title "Cascading Style Sheets" "DMV" acronym title "Department of Motor Vehicles" ":)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20" ":-)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20" "=)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20" "=-)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20" __END__ =head1 Possible Deficiencies * MT-Macros 'recursion' option isn't available. If this is a real problem for you, please let me know, preferably with a good example of what you can't accomplish currently (remember, macros are invoked in the order they're defined, which you can control with filename naming) * tag and ctag macros can't be used in HTML markup. This would be a big problem for Movable Type, where parameter replacement is done with psuedo-HTML, but doesn't seem to be a problem for Blosxom. If it is for you, please let me know, again along with an example. * MT-Macros 'no_case' option isn't available. This can be done by including (?i) in your patterns or defining them with qr//i, instead. * tag and ctag macros can't be explicitely named, because the 'name' parameter is already being used. Future versions may change tag and ctag to use 'string' or 'pattern' for what 'name' is currently used for, and use 'name' to define a macro. That will only be done if there's a good use for names, though. * Once defined, macros are always active. They can't be deactivated on a per-story basis. This might be handled with a meta- header at some point, if someone gives me a reasonable example for why they need it. * There's no built-in data-based macro definition syntax. It's not clear to me that a literal define_macro() call is any more difficult than MT-Macros' HTML-looking (but not HTML-acting) definition syntax, though, and as shown above simpler syntaxes ban be custom-built as appropriate. I'd be more than happy to include a simpler syntax, though, if someone were to develop one that were obviously better than define_syntax(). =head1 Caching If the Storable module is available and $use_caching is set, formatted stories will be cached; the cache is globally keyed off the list of macro files and their modification date, and per-story on the contents of the story itself. It should thus not ever be necessary to manually flush the cache, but it's always safe to do so, by removing the $plugin_state_dir/.macros.cache file. =head1 LICENSE this Blosxom Plug-in Copyright 2003, Todd Larason (This license is the same as Blosxom's) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.