X-Git-Url: https://git.stderr.nl/gitweb?a=blobdiff_plain;f=general%2Fmacros;fp=general%2Fmacros;h=cdf3f9cfcec394d7e7078035849ea99136cf268b;hb=03e69f4317ba6a4b8f0c7dae9813c9d20511b1e9;hp=0000000000000000000000000000000000000000;hpb=175696b26ca4203a7da4899e0f08dc56d9852477;p=matthijs%2Fupstream%2Fblosxom-plugins.git diff --git a/general/macros b/general/macros new file mode 100644 index 0000000..cdf3f9c --- /dev/null +++ b/general/macros @@ -0,0 +1,650 @@ +# Blosxom Plugin: macros -*- perl -*- +# Author: Todd Larason (jtl@molehill.org) +# Version: 0+1i +# Blosxom Home/Docs/Licensing: http://www.raelity.org/blosxom +# 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 = 1; +# ------------------------------------------------------------------- +# 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/ + +=head1 BUGS + +None known; address bug reports and comments to me or to the Blosxom +mailing list [http://www.yahoogroups.com/groups.blosxom]. + +=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. + +