All my plugins in the released stable versions, so the repository starts
authorAxel Beckert <xtaran@users.sourceforge.net>
Tue, 9 Oct 2007 11:19:12 +0000 (11:19 +0000)
committerAxel Beckert <xtaran@users.sourceforge.net>
Tue, 9 Oct 2007 11:19:12 +0000 (11:19 +0000)
with a stable base.

The comments in the README file already cover the development version of
those plugins.

xtaran/README
xtaran/acronyms [new file with mode: 0644]
xtaran/comments_antispam [new file with mode: 0644]
xtaran/date_rfc822 [new file with mode: 0644]
xtaran/dept [new file with mode: 0644]
xtaran/multcat [new file with mode: 0644]
xtaran/pathbasedtagging [new file with mode: 0644]
xtaran/tagging [new file with mode: 0644]
xtaran/xml_ping_generic [new file with mode: 0644]

index 1e60dee50d934a397d1fabb1de73f83c736890e6..ce2afda758d6e6dd2452ce0acc60c848de751b9b 100644 (file)
@@ -5,3 +5,69 @@ http://noone.org/blosxom/
 Axel's blog (of course powered by blosxom) can be found at
 http://noone.org/blog and his blog posts about and around blosxom can be
 found at http://noone.org/blog/English/Computer/Web/Blosxom
+
+Purpose and state of these plugins:
+
++ pathbasedtagging: 
+
+  Only useful together with the tagging plugin. Adds the possibility to
+  use URL paths like blosxom.cgi/tags/whatever instead of
+  blosxom.cgi?tags=whatever to show only posts tagged with "whatever".
+
+  Status: Works, but bugs have been reported.
+
++ xml_ping_generic: 
+
+  Can "ping" arbitrary services. URLs and formats have to be configured,
+  though.
+
+  Status: Worked. Should still work. Haven't checked for a long time. ;-)
+
++ multcat:
+
+  Allows the use of symbolic links in categories. Useful only on unixoid
+  operating systems which support symbolic links. The tagging plugin is
+  much more straight forward and also works on Windows platforms.
+
+  Status: Works, but won't get further development. Succeeded by the
+  tagging plugin, but it's still there for legacy reasons. May get
+  bugfixes if necessary.
+
++ tagging: 
+
+  Allows tagging of posts as used by Technorati and others. Is able to
+  filter by tags. Offers a tagcloud either with font-size, colour or both.
+
+  Status: Got a few bug reports, but seems quite popular and generally
+  working. :-)
+
++ dept: 
+
+  Shows a "From the ... department" line as on Slashdot. Cheap language
+  recognition based on paths.
+
+  Status: Small, probably not that many bugs. Language recognition could
+  be more generic or more configurable.
+
++ date_rf822: 
+
+  Offers variables with RFC822 compliant dates, e.g. for feeds.
+
+  Status: Wild hack. Not sure, if it's really necessary.
+
++ acronyms: 
+
+  Automatic marking of words as <acronym>s or <abbr>eviations inside
+  postings.
+
+  Status: Works fine, the core has been used also in other environments
+
++ comments_antispam: 
+
+  Forked from the comment plugin. Offers the use of an external
+  blacklist and a very cheap but though effective text based (Question +
+  Answer) static captcha.
+
+  Status: Has a few known bugs (traceback doesn't work anymore) and some
+  probably useless legacy code in. Needs a revamp...
+
diff --git a/xtaran/acronyms b/xtaran/acronyms
new file mode 100644 (file)
index 0000000..528284a
--- /dev/null
@@ -0,0 +1,255 @@
+# Bloxsom Plugin: Acronyms
+# Author: Axel Beckert, based on autolinks by Fletcher T. Penney
+# Version: 0.1
+# Download: http://noone.org/blosxom/acronyms
+
+package acronyms;
+
+# --- Configurable variables ----
+
+# Where is the definition page
+$link_file = "$blosxom::datadir/acronyms";
+
+
+# -------------------------------
+
+$ignore = 0;
+
+sub start {
+       open (LINKS, $link_file);
+       @abbrlist = <LINKS>;
+       close LINKS;
+       1;
+}
+
+sub story {
+       my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
+
+       if ($$story_ref =~ m/<!-- noacronyms -->/gi || $$body_ref =~ m/<!-- noacronyms -->/gi) {
+               $ignore = 1;
+       }
+
+       if ($ignore == 0) {
+               my %tags = ();
+               my $j = 0;
+
+               # remove other tags
+               $$body_ref =~ s{<\D.*?>}{
+                   do {
+                       $tags{++$j} = $&;
+                       "<$j>";
+                   };
+               }gse;
+
+               foreach $i (@abbrlist) {
+                       chomp($i);
+                       next unless $i;
+
+                       my ($word, $class, $link);
+
+                       if ($i =~ /(.*?)=(.*?)\|(.*)/) {
+                               $word  = $1;
+                               $class = lc($2);
+                               $link  = $3;
+                       } elsif ($i =~ /(.*?)=(.*)/) {
+                               $word  = $1;
+                               $link  = $2;
+                               $class = $word =~ /\./ ? 'abbr' : 'acronym';
+                       }
+
+                       #print STDERR "$word => $class + $link\n";
+
+# By default, autolinks now changes only the first occurence of a given term
+# If you want to change all occurences, 
+# change the end of the following line to '\/a>/ig'
+
+                       if ($word) {
+                               $word =~ s/\\./<.>/g;
+                               $word =~ s/\./\\./g;
+                               $word =~ s/<\.>/./g;
+                               #print STDERR sprintf "Replaced $word %i times\n",
+                               $$body_ref =~ s/\b($word)\b/<$class title="$link">$1<\/$class>/g;
+                               $$title_ref =~ s/\b($word)\b/<$class title="$link">$1<\/$class>/g
+                                   if $blosxom::flavour eq 'html';
+                       }
+               }
+
+               # reinsert tags
+               $$body_ref =~ s{<(\d+)>}{$tags{$1}}gs;
+
+       }
+       1;
+}
+
+1;
+
+
+__END__
+
+=head1 NAME
+
+Blosxom Plug-in: acronyms
+
+=head1 DESCRIPTION
+
+Allows you to create a list of keywords that are automatically
+marked as abbreviation or acronym.
+
+You can set your own location for the acronyms data file, but by
+default it lives in your data directory.
+
+=head1 EXAMPLE DATA FILE
+
+Put this in you acronyms data file and you already have a reasonable
+base to extend.
+
+ASCII=American Standard Code for Information Interchange
+BGP=Border Gateway Protocol; Beer-to-Glass-Protocol (Roger Schwentker)
+BSD=Berkeley System Distribution
+CD=Compact-Disc
+CGI=Common Gateway Interface; Computer Generated Imagery
+CLI=Command Line Interface
+CLUE=Command Line User Environment
+CS=Computer Science; Counter Strike
+CSS=Cascading Stylesheets
+CVS=Concurrent Versioning System
+DSL=Digital Subscriber Line
+DVD=Digital Video Disc
+ELF=Executable and Linking Format
+FAQ=Frequently asked question(s)
+FIFO=First In, First Out
+FOSS=Free and Open Source Software
+FLOSS=Free, Libre and Open Source Software
+FSF=Free Software Foundation
+FSFE=Free Software Foundation Europe
+FUD=Fear, Uncertainty and Doubt
+FYEO=Four Your Eyes Only
+FYI=For Your Information
+GNOME=GNU Network Object Model Environment
+GPL=GNU General Public License
+GPRS=General Packet Radio Service
+GSM=Global System for Mobile Communications
+GTK=GIMP-Toolkit
+GUI=Graphical User Interface
+HTML=Hypertext Markup Language
+HTTP=Hypertext Transport Protocol
+IIS=Internet Intrusion Server
+IMO=in my opinion
+IMHO=in my humble opinion
+IMNSHO=in my not so humble opinion
+IRC=Internet Relay Chat
+ISBN=International Standard Book Number
+ISDN=Integrated Services Digital Network (German: I schnall des nedd ;-)
+ISSN=International Standard Serial Number
+ITP=intend to package
+JFYI=Just For Your Information
+JFTR=Just For The Record
+KDE=Kolorful Diskfilling Environment
+KISS=Keep it simple, stupid
+MCSE=Minesweeper Consultant and Solitaire Expert (User Friendly)
+MHz=abbr|Megahertz
+MMS=Multimedia Messaging Service
+MMX=Multimedia Extension
+MPEG=Moving Picture Experts Group
+MP3=MPEG (Moving Picture Experts Group) 1 Audio Layer 3
+MSIE=Microsoft Internet Explorer
+MTV=Music TeleVision
+NIH=Not Invented Here
+OOP=Object-Oriented Programming
+OS=Operating System; Open Source
+OSI=Open Source Initiative; Open Systems Interconnection
+OSS=Open Source Software
+PC=Personal Computer
+PERL=Pathologically Eclectic Rubbish Lister
+PHP[2345]?=Programmers Hate PHP ;-)
+PISG=Perl IRC Statistics Generator
+QA=Quality Assurance
+RAM=Random Access Memory
+RIP=Rest in Peace
+SMD=Surface Mounted Devices
+SMS=Short Message Service
+SSI=Server-Side Includes
+TLA=Three Letter Acronym
+UI=User Interface
+UK=United Kingdom
+UMTS=Universal Mobile Telecommunications System
+URL=Uniform Resource Locator
+URI=Uniform Resource Indicator
+US=United States (of America)
+USA=United States of America
+USB=Universal Serial Bus
+VCF=Vintage Computer Festival
+VCFE=Vintage Computer Festival Europe
+VCFe=Vintage Computer Festival Europe
+VM=Virtual Machine
+VoIP=Voice over IP
+WFM=Works For Me
+WAP=Wireless Application Protocol
+WTF=what/who the fuck
+WTH=what the heck/hell
+WML=Website Meta Language; Wireless Markup Language; Wesnoth Markup Language
+WORN=Write Once, Read Never
+WWW=World Wide Waiting ;-)
+XHTML=Extensible Hypertext Markup Language
+XML=Extensible Markup Language
+aka=acronym|also known as
+btw.=abbr|by the way
+btw=abbr|by the way
+BTW=abbr|by the way
+e.g.=for example; as an example
+gpl=GNU General Public License
+i.e.=id est (that is to say; in other words) 
+irc=Internet Relay Chat
+pisg=Perl IRC Statistics Generator
+GHz=Gigahertz
+MB=Megabyte
+GB=Gigabyte; Great Britain
+TB=Terabyte
+kB=Kilobyte
+IIRC=if I remember correctly
+AFAIK=as far as I know
+AFAICS=as far as I can see
+AFAIR=as far as I remember
+YMMV=your milage may vary
+LMAA=Leck&apos; mich am Arsch
+ASAP=as soon as possible
+EFF=Electronic Frontier Foundation
+FFII=Foundation for a Free Information Infrastructure / F&ouml;rderverein f&uuml;r eine Freie Informationelle Infrastruktur
+WYSIWYG=What you see is what you get
+TOFU=Text oben, Fullquote unten
+PEBKAC=Problem exists between keyboard and chair
+FIFO=First In, First Out
+LIFO=Last In, First Out
+GIGO=Garbage In, Garbage Out
+EU=European Union
+yast2?=Yet another Setup Tool
+SuS[EI]=System- und Software-Entwicklung
+KoL=Kingdom of Loathing
+MUD=Multi User Dungeon
+XMMS=X Multimedia System
+SUV=Sport Utility Vehicle
+SUVs=Sport Utility Vehicles
+IMD[Bb]=Internet Movie Database
+FTWCA=For those who care about
+LUG=Linux User Group
+GUUG=German Unix User Group
+SPF=Sender Policy Framework, formerly Sender Permitted From
+RBL=Realtime Black-List
+SMTP=Simple Mail Transfer Protocol
+NSFW=Not Safe For Work
+ROM=Read Only Memory
+CD-ROM=Compact Disc Read-Only Memory
+ISO=International Organization for Standardization; also short for a image of an ISO9660 (CD-ROM) file system
+
+=head1 AUTHOR
+
+Axel Beckert <blosxom@deuxchevaux.org>
+
+=head1 LICENSE
+
+This source was and therefore is still submitted to the public domain.
+Feel free to use and modify it.  Credits to Fletcher Penney and
+Gregory Bair for their original work would be appreciated.
+
+THIS SOFTWARE IS PROVIDED AS IS AND WITHOUT ANY WARRANTY OF ANY KIND.
+USE AT YOUR OWN RISK!
diff --git a/xtaran/comments_antispam b/xtaran/comments_antispam
new file mode 100644 (file)
index 0000000..32553f6
--- /dev/null
@@ -0,0 +1,813 @@
+# Blosxom Plugin: comments_antispam
+# Author(s): Axel Beckert <abe@deuxchevaux.org> based on work of
+#            Kevin Lyda <kevin@ie.suberic.net> and
+#            Rael Dornfest <rael@oreilly.com>
+# Version: 0.1+0.6
+# Documentation: See the bottom of this file or type: perldoc comments
+
+package comments_antispam;
+
+use Net::hostent;
+use Mail::Send;
+
+# --- Configurable variables -----
+
+# Where is the old writeback hierarchy?
+#
+# NOTE: By setting this variable, you are telling the plug-in to go ahead
+# and create a comments directory for you.
+#my $writeback_dir = "$blosxom::plugin_state_dir/writeback";
+my $comments_dir = "$blosxom::plugin_state_dir/comments";
+
+# What flavour should I consider an incoming trackback ping?
+# Otherwise trackback pings will be ignored!
+my $trackback_flavour = "trackback";
+
+# What file extension should I use for writebacks? 
+# Make sure this is different from that used for your Blosxom weblog
+# entries, usually txt.
+my $writeback_file_ext = "wb";
+my $comments_file_ext = "comments";
+
+# max comment indent depth
+my $max_level = 5;
+
+# time configs
+my $time_approx_prefix = "some point before ";
+# time as described by the strftime man page.  aAbBcdHIjmMpSUwWxXyYZ%
+my $time_fmt           = "%a, %d %b %Y %H:%M";
+my $time_unknown       = "some point unknown";
+
+my $reply_prefix       = "Re: ";
+
+my @count_noun         = ("comments",   # if there are no comments
+                          "comment",    # if there is only 1 comment
+                         "comments");  # all the rest
+
+# What fields were used in your writeback form and by trackbacks?
+my @fields = qw! cname url title comment excerpt blog_name parent time !;
+
+my $to_mail   = 'blog-owner@example.com';
+my $from_mail = 'blog-server@example.com';
+my $blacklist_file = "$blosxom::datadir/comments_blacklist";
+
+# --- Export variables -----
+# Comments for a story; use as $comments_antispam::comments in flavour templates
+$comments;
+
+# Count of writebacks for a story; use as $comments_antispam::count in flavour templates
+$count;
+$count_noun;
+
+# parent id and title fields of replies
+$parent = "";
+$title = "";
+
+# The path and filename of the last story on the page (ideally, only 1 story
+# in this view) for displaying the trackback URL;
+# use as $comments_antispam::trackback_path_and_filename in your foot flavour templates
+$base_url;
+$trackback_path_and_filename;
+
+# Response to comments; use as $comments_antispam::comment_response in 
+# flavour templates
+$comment_response;
+
+$pref_name;
+$pref_url;
+
+# Response to a trackback ping; use as $writeback::trackback_response in
+# head.trackback flavour template
+$trackback_response =<<"TRACKBACK_RESPONSE";
+<?xml version="1.0" encoding="iso-8859-1"?>
+<response>
+<error></error>
+<message></message>
+</response>
+TRACKBACK_RESPONSE
+# --- Utility routines -----
+
+use CGI            qw/:standard/;
+use FileHandle;
+use Data::Dumper;  $Data::Dumper::Indent=1;
+use POSIX          qw/strftime/;
+
+my $fh = new FileHandle;
+
+# Strip potentially confounding bits from user-configurable variables
+$writeback_dir =~ s!/$!!; $file_extension =~ s!^\.!!;
+$comments_dir =~ s!/$!!;
+
+# Save Name and URL/Email via cookie if the cookies plug-in is available
+$cookie;
+$errormsg;
+
+sub save_comment {
+    my($path, $filename, $new) = @_;
+
+    open(BL, "< $blacklist_file") 
+        or die "Can't open blacklist file $blacklist_file: $!";
+    my @blacklist = ();
+    while (my $line = <BL>) {
+       next if $line =~ /^#/; # strip unix like comments
+       $line =~ s/^\s*(.*?)\s*$/$1/; # strip leading and trailing blanks
+       push(@blacklist, split(/\s+/s, $line));
+    }
+    close(BL);
+
+    foreach my $v (values %$new) {
+       foreach my $crit (@blacklist) {
+           if ($v =~ m($crit)is) {
+               $errormsg = "<br/><br/>Something in your post was on the blacklist. Use the back button and try changing the post to look less like comment spam. It also may be that your hostname or webbrowser is on the blacklist. Bad luck, if that's the case.<br /><br />Sorry for the inconvenience.";
+               return 0;
+           }
+       }
+    }
+
+    my($rc) = 0;
+    my($p) = "";
+    foreach ("", split(/\//, $path)) {
+       $p .= "$_/";
+       -d "$comments_dir$p" or mkdir "$comments_dir$p", 0755;
+    }
+
+    # lock and read in comments
+    # TODO: lock file in nfs-safe way - see LockFile::Simple
+    if ($fh->open("$comments_dir$path/$filename.$comments_file_ext")) {
+       eval join("", <$fh>);
+       $fh->close;
+    } else {
+       $saved_comments = {};
+    }
+
+    # determine if this is a reply
+    if (exists($new->{"parent"})) {
+       if (!exists($saved_comments->{$new->{"parent"}})) {
+           # TODO: unlock file in nfs-safe way
+           return 0;
+       }
+       $prefix = $new->{"parent"}. "-";
+       delete($new->{"parent"});
+    } else {
+       $prefix = "";
+    }
+    $id = 0;
+    while (exists($saved_comments->{$prefix. $id})) {
+       $id++;
+    }
+    $id = $prefix. $id;
+
+    my $message = '';
+    foreach (keys %{$new}) {
+       $saved_comments->{$id}->{$_} = $new->{$_};
+       $message .= "$_: $new->{$_}\n";  #append data in parallel to flatfile.
+    }
+    $saved_comments->{$id}->{"time"} = time();
+
+    #------------Begin sendmail routine -------------
+    my $msg = Mail::Send->new;
+    $msg->to($to_mail);
+    $msg->set('From', $from_mail);
+    $msg->subject("Comment to $path/$filename by $new->{cname}");
+    $mfh = $msg->open;
+    print $mfh $message;
+    $mfh->close;
+    #--------------------------------------------------
+
+    if ($fh->open(">$comments_dir$path/$filename.$comments_file_ext")) {
+       $fh->print(Data::Dumper->Dump([$saved_comments], ["saved_comments"]));
+       $fh->close;
+       $rc = 1;
+    }
+    # TODO: unlock file in nfs-safe way
+    return $rc;
+}
+
+sub print_comments {
+    my($path, $filename, $id_limit) = @_;
+
+    # lock and read in comments
+    if ($fh->open("$comments_dir$path/$filename.$comments_file_ext")) {
+       eval join("", <$fh>);
+       $fh->close;
+    } else {
+       $count_noun = $count_noun[$count = 0];
+       $comments = "";
+       return 0;
+    }
+
+    my($last_level, $this_level) = (0, 0);
+    my($push) = &$blosxom::template($path, "comments-push", $blosxom::flavour)
+                       or "<blockquote>";
+    my($pop) = &$blosxom::template($path, "comments-pop", $blosxom::flavour)
+                       or "</blockquote>";
+    my($comment) = &$blosxom::template($path, "comments", $blosxom::flavour)
+                       or '<p><b>Name:</b> '.
+                          '$comments_antispam::cname<br /><b>URL:</b> '.
+                          '$comments_antispam::url<br /><b>Title:</b> '.
+                          '$comments_antispam::title<br /><b>Comment:</b> '.
+                          '$comments_antispam::comment</p>';
+    my($trackback) = &$blosxom::template($path, "trackback", $blosxom::flavour)
+                       or '<p><b>Blog:</b> '.
+                          '$comments_antispam::blog_name<br /><b>URL:</b> '.
+                          '$comments_antispam::url<br /><b>Title:</b> '.
+                          '$comments_antispam::title<br /><b>Excerpt:</b> '.
+                          '$comments_antispam::excerpt</p>';
+
+#    print STDERR "Comment Template:\n'$comment'\n";
+
+    my(@id);
+    if (defined($id_limit)) {
+       @id = ($id_limit);
+    } else {
+       my($i);
+       @id = sort {
+                    @aa = split(/-/, $a); @bb = split(/-/, $b);
+                    $i = 0;
+                    while ($i <= $#aa
+                               and $i <= $#bb and $aa[$i] == $bb[$i]) {
+                        $i++;
+                    }
+                    $aa[$i] = -1 if ($i > $#aa);
+                    $bb[$i] = -1 if ($i > $#bb);
+                    return $aa[$i] <=> $bb[$i];
+                  } (keys(%{$saved_comments}));
+    }
+    foreach $id (@id) {
+       $this_level = split(/-/, $id) - 1;
+       $this_level = $max_level if ($max_level < $this_level);
+       if ($this_level > $last_level) {
+           $tmp = $push x ($this_level - $last_level);
+       } else {
+           $tmp = $pop x ($last_level - $this_level);
+       }
+       $last_level = $this_level;
+       if (exists($saved_comments->{$id}->{"blog_name"}) or
+           exists($saved_comments->{$id}->{"excerpt"})) {
+           $tmp .= $trackback;
+       } else {
+           $tmp .= $comment;
+       }
+       if (exists($saved_comments->{$id}->{"time"})) {
+           if ($saved_comments->{$id}->{"time"} =~ /^a(.*)/) {
+               $time = $1;
+               $saved_comments->{$id}->{"time"} = $time_approx_prefix;
+           } else {
+               $time = $saved_comments->{$id}->{"time"};
+               $saved_comments->{$id}->{"time"} = "";
+           }
+           $saved_comments->{$id}->{"time"}
+                               .= strftime($time_fmt, localtime($time));
+       } else {
+           $saved_comments->{$id}->{"time"} = $time_unknown;
+       }
+       $saved_comments->{$id}->{"parent"} = $id;
+       $saved_comments->{$id}->{"reply_title"} =
+           ($saved_comments->{$id}->{"title"} =~ /^$reply_prefix/ ?
+            '' : $reply_prefix) . $saved_comments->{$id}->{"title"};
+
+       $saved_comments->{$id}->{"reply_title_escaped"} = 
+           $saved_comments->{$id}->{"reply_title"};
+       
+       $saved_comments->{$id}->{"reply_title_escaped"} =~
+           s([^a-zA-Z0-9_:./])(sprintf('%%%02X', ord($&)))ge;
+
+       $saved_comments->{$id}->{"base_url"} = $base_url;
+       $parent = $saved_comments->{$id}->{"parent"};
+#      print STDERR "tmp1 ($id):\n$tmp";
+       $tmp =~ s/\$comments_antispam::([_\w]+)/$saved_comments->{$id}->{$1}/ge;
+#      print STDERR "tmp2 ($id):\n$tmp";
+       $comments .= $tmp;
+    }
+
+    $comments .= $pop x $last_level;
+    $count = scalar(keys(%{$saved_comments}));
+    $count_noun = $count_noun[$count < 2? $count: 2];
+
+#    use Data::Dumper;
+#    print STDERR 'Comments: '.
+#      Dumper(\@id,$comments,$count,$count_noun,$saved_comments,$tmp);
+#    print STDERR $comments;
+
+}
+
+sub convert_writebacks {
+    my($path, $filename) = @_;
+
+    # read in old writebacks
+    if ($fh->open("$writeback_dir$path/$filename.$writeback_file_ext")) {
+       $saved_comments = {};
+       $id = 0;
+       $time = (stat("$writeback_dir$path/$filename.$writeback_file_ext"))[9];
+       foreach my $line (<$fh>) {
+           $line =~ /^(.+?):(.*)$/ and $saved_comments->{$id}->{$1} = $2;
+           $field = $1;
+           $saved_comments->{$id}->{$field} =~ s/^ *//;
+           $saved_comments->{$id}->{$field} =~ s/ *$//;
+           if ($saved_comments->{$id}->{$field} eq "") {
+               delete $saved_comments->{$id}->{$field};
+           }
+           if ( $line =~ /^-----$/ ) {
+               if (!exists($saved_comments->{$id}->{"time"})) {
+                   $saved_comments->{$id}->{"time"} = "a$time";
+               }
+               $id++;
+           }
+       }
+       $fh->close;
+
+       # make sure comments dir exists.
+       my($p) = "";
+       foreach ( ("", split /\//, $path) ) {
+           $p .= "$_/";
+           -d "$comments_dir$p" or mkdir "$comments_dir$p", 0755;
+       }
+       if ($fh->open(">$comments_dir$path/$filename.$comments_file_ext")) {
+           $fh->print(Data::Dumper->Dump([$saved_comments],
+                                           ["saved_comments"]));
+           $fh->close;
+       } else {
+           warn "blosxom: comments: convert_writeback: ".
+                   "couldn't open comment file '".
+                   "$comments_dir$path/$filename.$comments_file_ext\n";
+       }
+    } else {
+       warn "blosxom: comments: convert_writeback: ".
+               "couldn't open writeback file '".
+               "$writeback_dir$path/$filename.$writeback_file_ext'\n";
+    }
+}
+
+# --- Plugin Exports -----
+
+sub start {
+
+    # $comments_dir must be set to activate writebacks
+    unless ($comments_dir) { 
+       warn "blosxom: comments: \$comments_dir".
+            " is not set; please set it to enable comments.".
+            " Comments are disabled!\n";
+       return 0;
+    }
+
+    # the $comments_dir exists, but is not a directory
+    if ( -e $comments_dir and ( !-d $comments_dir or !-w $comments_dir)) { 
+       warn "blosxom: comments: \$comments_dir, $comments_dir, must be".
+            " a writeable directory; please move or remove it and Blosxom".
+            " will create it properly for you.  Comments are disabled!\n";
+       return 0;
+    }
+  
+    # the $comments_dir does not yet exist, so Blosxom will create it
+    if ( !-e $comments_dir)  {
+       if (mkdir("$comments_dir", 0755)) {
+           warn "blosxom: comments: \$comments_dir, $comments_dir, created.\n"
+       } else {
+           warn "blosxom: comments: There was a problem creating".
+           " \$comments_dir, $comments_dir. Comments are disabled!\n";
+           return 0;
+       }
+
+       if (chmod(0755, $comments_dir)) {
+           warn "blosxom: comments: \$comments_dir, $comments_dir, set to".
+                " 0755 permissions.\n"
+       } else {
+           warn "blosxom: comments: Problem setting perms on \$comments_dir".
+                ", $comments_dir. Comments are disabled!\n";
+           return 0;
+       }
+
+       #warn "blosxom: comments: comments are enabled!\n";
+    }
+
+    $path_info = path_info();
+
+    # Symlink detection
+    my $postfile = "$blosxom::datadir$path_info";
+    $postfile =~ s/$blosxom::flavour$/$blosxom::file_extension/;
+    if (-l $postfile) {
+       my $newpath = readlink $postfile;
+       $path_info =~ s/$blosxom::file_extension$/$blosxom::flavour/;
+
+       if ($newpath =~ m(^/)) {
+           $newpath =~ s/\Q$blosxom::datadir\E//;
+           $path_info = $newpath;
+       } else {
+           $path_info =~ s(/[^/]*$)();
+           $newpath = "$path_info/$newpath";
+           while ($newpath =~ m(/\.\./)) {
+               $newpath =~ s(/[^/]+/\.\./)(/)s or last;
+           }
+           $path_info = $newpath;
+       }
+    }
+
+    my($path, $filename) = $path_info =~ m!^(?:(.*)/)?(.*)\.$blosxom::flavour!;
+    $path =~ m!^/! or $path = "/$path";
+    $path = "/$path";
+    ($path_info_escaped = $path_info) 
+       =~ s([^-\w/.])(sprintf('%%%02x',ord($&)))ge;
+
+    $base_url = $blosxom::url.$path_info_escaped;
+
+    # Only spring into action if POSTing to the writeback plug-in
+    if (request_method() eq "POST" and
+               (param("plugin") eq "comments"
+                or $blosxom::flavour eq $trackback_flavour) ) {
+       my(%new);
+       foreach ( @fields ) {
+           $new{$_} = param($_);
+           if (!defined($new{$_}) or length($new{$_}) == 0) {
+               delete $new{$_};
+           } elsif ($_ eq "url" and $new{$_} !~ /:/
+                                and length($new{$_}) > 2) {
+               $new{$_} = "mailto:". $new{$_};
+           } elsif ($_ eq "comment") {
+               $new{$_} =~ s/\n?\r\n?/\n/mg;
+               $new{$_} =~ s/\n\n/<\/p><p>/mg;
+               $new{$_} = "<p>". $new{$_}. "</p>";
+           }
+       }
+
+       $new{referrer} = $ENV{HTTP_REFERER} if $ENV{HTTP_REFERER};
+       $new{host} = $ENV{REMOTE_HOST} if $ENV{REMOTE_HOST};
+       $new{host_ip} = $ENV{REMOTE_ADDR} if $ENV{REMOTE_ADDR};
+       $new{host} ||=  gethost($new{host_ip})->name;
+       $new{user_agent} = $ENV{HTTP_USER_AGENT} if $ENV{HTTP_USER_AGENT};
+
+       if (! -f "$comments_dir$path/$filename.$comments_file_ext"
+               and -f "$writeback_dir$path/$filename.$writeback_file_ext") {
+           convert_writebacks($path, $filename);
+       }
+       if (save_comment("$comment_dir$path", $filename, \%new)) {
+
+           $trackback_response =~ s!<error></error>!<error>0</error>!m;
+           $trackback_response =~ s!<message></message>\n!!s;
+           $comment_response = "Thanks for the comment.";
+
+           # Make a note to save Name & URL/Email if save_preferences checked
+           param("save_preferences") and $cookie++;
+           # Pre-set Name and URL/Email based on submitted values
+           $pref_name = param("cname") || "";
+           $pref_url = param("url") || "";
+
+       } else {
+           warn "blosxom: comments: start: couldn't >>".
+                   " $comments_dir$path/$filename.$file_extension\n";
+           $trackback_response =~ s!<error></error>!<error>1</error>!m;
+           $trackback_response =~ s!<message>trackback error</message>!!m;
+           $comment_response = "There was a problem saving your comment. $errormsg";
+       }
+       Delete("parent");
+    }
+
+    1;
+}
+
+sub story {
+    my($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
+    
+    $path =~ s!^/*!!; $path &&= "/$path";
+
+    # Symlink detection
+    my $postfile = "$blosxom::datadir$path/$filename.$blosxom::file_extension";
+    if (-l $postfile) {
+       my $newpath = readlink $postfile;
+       $newpath =~ s/\Q$filename.$blosxom::file_extension\E$//;
+
+       if ($newpath =~ m(^/)) {
+           $newpath =~ s/\Q$blosxom::datadir\E//;
+           $path = $newpath;
+       } else {
+           $newpath = "$path/$newpath";
+           while ($newpath =~ m(/\.\./)) {
+               $newpath =~ s(/[^/]+/\.\./)(/)s or last;
+           }
+           $path = $newpath;
+       }
+    }
+
+    # convert those old writebacks!
+    if (! -f "$comments_dir$path/$filename.$comments_file_ext"
+           and -f "$writeback_dir$path/$filename.$writeback_file_ext") {
+       convert_writebacks($path, $filename);
+    }
+
+    # Prepopulate Name and URL/Email with cookie-baked preferences, if any
+    if ($blosxom::plugins{"cookies"} > 0
+               and my $cookie = &cookies::get("comments") ) {
+       $pref_name ||= $cookie->{"cname"};
+       $pref_url ||= $cookie->{"url"};
+    }
+
+    # print the comments
+    print_comments($path, $filename, param("parent"));
+    if (defined(param("parent"))) {
+       $parent = param("parent");
+       $title = param("title");
+    } else {
+       $parent = "";
+       $title = $blosxom::title;
+    }
+    $title = $reply_prefix.$title if $title !~ /^$reply_prefix/;
+
+    $trackback_path_and_filename = "$path/$filename";
+
+    1;
+}
+
+sub foot {
+    if ($pref_name or $pref_url) {
+       $blosxom::plugins{"cookies"} > 0 and $cookie and &cookies::add(
+           cookie(
+               -name=>"comments", 
+               -value=>{ "cname" => $pref_name, "url" => $pref_url }, 
+               -path=>$cookies::path,
+               -domain=>$cookies::domain,
+               -expires=>$cookies::expires
+           )
+       );
+    }
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Blosxom Plug-in: comments
+
+=head1 SYNOPSIS
+
+Provides comments and TrackBacks [http://www.movabletype.org/trackback/].
+It will also convert your old writebacks.
+
+This modified version from Axel Beckert also features mail
+notification for the blog owner and a comment and trackback spam
+blacklist.
+
+All comments and TrackBack pings for a particular story are kept in
+$comments_dir/$path/$filename.comment.
+
+=head2 QUICK START
+
+Drop this comments plug-in file into your plug-ins directory 
+(whatever you set as $plugin_dir in blosxom.cgi).
+
+Comments, being a well-behaved plug-in, won't do anything until you
+set $comments_dir.  If you were using writebacks, you need to set
+$writeback_dir.
+
+While you can use the same directory as your blosxom $datadir (comments
+are saved as path/weblog_entry_name.comment), it's probably better to keep
+them separate.
+
+Once set, the next time you visit your site, the comments plug-in will
+perform some checks, creating the $comments_dir and setting appropriate
+permissions if it doesn't already exist.  (Check your error_log for details
+of what's happening behind the scenes.)
+
+Move the contents of the flavours folder included in this distribution 
+into your Blosxom data directory (whatever you set as $datadir in blosxom.cgi).
+Don't move the folder itself, only the files it contains!  If you don't
+have the the sample flavours handy, you can download them from:
+
+FIXME: http://ie.suberic.net/~kevin/blosxom/comments.tar.gz
+
+Point your browser at one of your Blosxom entries, specifying the comments 
+flavour (e.g. http://localhost/cgi-bin/blosxom.cgi/path/to/a/post.comment)
+
+Enjoy!
+
+=back
+
+=head2 BLACKLIST
+
+The blacklist is a list of perl regular expressions separeted by blank
+characters (e.g. space, tab and newline) and supports shell script
+like comment with "#" at the beginning of the line. If any of the
+regexps in the blacklist matches any value (including title, commenty
+body, poster's name, URL, poster's hostname or user agent), the
+comment or trackback will not be accepted. So be careful and wise when
+choosing which words or domains you block. And remember: "." matches
+any character, so escape it as "\.".
+
+Example blacklist:
+
+   ---snip---
+   # Meds
+   acyclovir adipex alprazolam ativan butalbital carisoprodol
+   carisoprodol casino celexa cialis condylox cyclobenzaprine ddrx
+   diazepam didrex diethylpropion diflucan drofa ephedrine ephedrine
+   fioricet flexeril fluoxetine fluoxetine hydrocodone levitra lexapro
+   lipitor lorazepam lortab lrzpm meridia musapuk nextel norvasc paxil
+   penis pharmacy phentermine prilosec propecia prozac renova rkwdl rolex
+   skelaxin tadalafil tenuate tramadol tricyclen triphasil ultracet
+   ultram valium valtrex vaniqa viagra xanax xenical zanaflex zithromax
+   zoloft zovirax zyban
+
+   # Paths
+   ringtones/
+   lotto/
+
+   # Hostnames (used example.com here as an example for real regexps :-)
+   \.example.(com|net)\b
+
+   # Wiki syntax
+   \[(url|link)=
+   ---snap---
+
+=head2 MAIL NOTIFICATION
+
+Set $to_mail to the receipient and $from_mail to the sender
+address. Needs perl module Mail::Send installed.
+
+=head2 FLAVOUR TEMPLATE VARIABLES
+
+Wherever you wish all the comments for a particular story to appear
+in your flavour templates, use $comments_antispam::comments.
+
+A count of WriteBacks for each story may be embedded in your flavour
+templates with $comments_antispam::count.  Based on the @count_noun config array,
+$comments_antispam::count_noun will reflect a $comments_antispam::count of 0, 1, many.
+
+If you'd like, you can embed a "Thanks for the comment." or 
+"There was a problem saving your comment." message after posting with
+$comments_antispam::comment_response.
+
+=head2 SAMPLE FLAVOUR TEMPLATES
+
+I've made sample flavour templates available to you to help with any
+learning curve this plug-in might require.
+
+Take a gander at the source HTML/XML for:
+
+=item * story.comments, a basic example of a single-entry story
+flavour with comments embedded.  You should not use this as your
+default flavour since every story on the home page would have comments
+right there with the story itself.
+
+=item * head.trackback, all you need to provide the right response to
+a TrackBack ping.  Notice in story.comments that the auto-discovery
+and human-readable TrackBack ping URLs point at this flavour.
+
+=item * foot.comments provides a simple comment form for posting to the
+comments plug-in.  NOTE: The comments plug-in requires the presence
+of a "plugin" form variable with the value set to "comments"; this tells
+the plug-in that it should handle the incoming POSTing data rather than
+leaving it for another plug-in.
+
+=item * comments.comments is a sample flavour file for comments themselves. 
+Think of a comments flavour file as a story flavour file for individual 
+comments.
+
+=item * comments-push.comments and comments-pop.comments are sample
+flavour files to handling threads (descending into a thread and coming
+out respectively).
+
+=back
+
+=head2 FLAVOURING COMMENTS
+
+While the default does a pretty good job, you can flavour your comments
+in the comments flavour file (e.g. comments.comments) using the following 
+variables:
+
+=item * $comments_antispam::name$comments_antispam::blog_name - Name entered in comment
+form or weblog name used in TrackBack ping.
+
+=item * $comments_antispam::url - URL entered in comment form or that of writing
+citing your weblog entry via TrackBack ping.
+
+=item * $comments_antispam::title - Title entered into comment form or that of writing citing your weblog entry via TrackBack ping.
+
+=item * $comments_antispam::comment$comments_antispam::excerpt - Comment entered
+into comment form or excerpt of writing citing your weblog entry via
+TrackBack ping.
+
+=item * $comments_antispam::pref_name and $comments_antispam::pref_url are prepopulated
+with the values of the form you just submitted or preferences stored in a
+'comments' cookie, if you've the cookie plug-in installed an enabled.
+
+=back
+
+=head2 INVITING AND SUPPORTING TRACKBACKS
+
+You should provide the TrackBack ping URL for each story so that those
+wanting to TrackBack ping you manually know where to ping.  
+$comments_antispam::trackback_path_and_filename, together with $url and 
+a TrackBack flavour will provide them all they need.
+
+e.g. $url$comments_antispam::trackback_path_and_filename.trackback
+
+You need to provide an XML response to TrackBack pings to let them
+know whether or not the ping was successful.  Thankfully, the plug-in
+does just about all the work for you.  You should, however, create a
+head.trackback flavour file (only the head is needed) containing simply:
+
+$comments_antispam::trackback_response
+
+Be sure that the flavour of the head file (suggested: head.trackback)
+corresponds to the $trackback_flavour configurable setting otherwise
+Blosxom will ignore incoming TrackBack pings!
+
+=head1 INSTALLATION
+
+Drop comments into your plug-ins directory ($blosxom::plugin_dir).
+
+=head1 CONFIGURATION
+
+=head2 (REQUIRED) SPECIFYING A COMMENTS DIRECTORY
+
+Comments, being a well-behaved plug-in, won't do anything until you set 
+$comments_dir, create the directory, and make it write-able by Blosxom.
+Make sure to set $writeback_dir if you were using the WriteBack plugin.
+
+Create a directory to save comments to (e.g. $plugin_state_dir/comments),
+and set $comments_dir to the path to that directory.
+
+While you can use the same directory as your blosxom $datadir (comments
+are saved as path/weblog_entry_name.wb), it's probably better to keep
+them separate.
+
+The comments plug-in will create the appropriate paths to mimick your
+$datadir hierarchy on-the-fly.  So, for a weblog entry in 
+$datadir/some/path/or/other/entry.txt, comments will be kept in
+$comments_dir/some/path/or/other/entry.wb.
+
+=head2 (OPTIONAL) ALTERING THE TRACKBACK FLAVOUR
+
+The $trackback_flavour sets the flavour the plug-in associates with
+incoming TrackBack pings.  Unless this corresponds to the flavour
+associated with your trackback URL, the comments plug-in will ignore
+incoming pings.
+
+=head2 (OPTIONAL) SPECIFYING AN EXTENSION FOR COMMENT FILES
+
+The default extension for comments is comments.  You can change this
+if you wish by altering the $comments_file_ext value.  The default for
+writebacks is wb - changed by changing $writeback_file_ext.
+
+=head2 (OPTIONAL) SPECIFYING WHAT FIELDS YOU EXPECT IN YOUR COMMENTS FORM
+
+The defaults are probably ok here, but you can specify that the comments
+plug-in should look for more fields in your comments form by adding to this
+list.  You should keep at least the defaults in place so as not to break
+anything.
+
+my @fields = qw! name url title comment excerpt blog_name !;
+
+Second part of the version number is the comments plugin version on
+which it is based upon.
+
+=head1 AUTHORS
+
+  Axel Beckert <blosxom@deuxchevaux.org>, http://noone.org/blog
+  Kevin Lyda <kevin@ie.suberic.net>, http://ie.suberic.net/~kevin/cgi-bin/blog
+  Rael Dornfest <rael@oreilly.com>, http://www.raelity.org/
+
+=head DOWNLOAD
+
+Latest version can be found at http://noone.org/blosxom/comments_antispam
+
+=head1 SEE ALSO
+
+  Homepage: http://noone.org/blog?-tags=comments_antispam
+
+  Blosxom Home/Docs/Licensing: http://www.raelity.org/apps/blosxom/
+  Blosxom Download: http://blosxom.sourceforge.net/
+  Blosxom Plugin Docs: http://www.raelity.org/apps/blosxom/plugin.shtml
+  Blosxom User Group: http://blosxom.ookee.com/blog
+  WriteBack Plugin: http://www.raelity.org/apps/blosxom/downloads/plugins/writeback.zip
+
+=head1 BUGS
+
+Address bug reports and comments to the Blosxom mailing list 
+[http://www.yahoogroups.com/group/blosxom].
+
+=head1 LICENSE
+
+Blosxom and this Blosxom Plug-in
+Copyright 2003, Rael Dornfest 
+Copyright 2005-2006, Axel Beckert
+
+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.
diff --git a/xtaran/date_rfc822 b/xtaran/date_rfc822
new file mode 100644 (file)
index 0000000..8911020
--- /dev/null
@@ -0,0 +1,76 @@
+# Blosxom Plugin: date_rfc822
+# Author(s): Axel Beckert <abe@deuxchevaux.org>, based on 822-date
+# Version: 1.0
+# Documentation: See the bottom of this file or type: perldoc date_rfc822
+
+package date_rfc822;
+
+$date; # use as $date_rfc822::date
+
+use POSIX qw(strftime);
+
+sub start {
+  1;
+}
+
+sub date {
+  my ($pkg, $path, $date_ref, $mtime, @date_bits) = @_;
+
+@localtm = localtime ($mtime);
+$localtms = localtime ($mtime);
+@gmttm = gmtime ($mtime);
+$gmttms = gmtime ($mtime);
+
+if ($localtm[0] != $gmttm[0]) {
+    die (sprintf ("local timezone differs from GMT by a non-minute interval\n"
+                . "local time: %s\n"
+                . "GMT time: %s\n", $localtms, $gmttms));
+}
+
+$localmin = $localtm[1] + $localtm[2] * 60;
+$gmtmin = $gmttm[1] + $gmttm[2] * 60;
+
+if ((($gmttm[6] + 1) % 7) == $localtm[6]) {
+    $localmin += 1440;
+} elsif ((($gmttm[6] - 1) % 7) == $localtm[6]) {
+    $localmin -= 1440;
+} elsif ($gmttm[6] == $localtm[6]) {
+    1;
+} else {
+    die ("822-date: local time offset greater than or equal to 24 hours\n");
+}
+
+$offset = $localmin - $gmtmin;
+$offhour = $offset / 60;
+$offmin = abs ($offset % 60);
+
+if (abs ($offhour) >= 24) { 
+    die ("822-date: local time offset greater than or equal to 24 hours\n");
+}
+
+$date = sprintf 
+    (
+     "%s, %2d %s %d %02d:%02d:%02d %s%02d%02d",
+     (Sun,Mon,Tue,Wed,Thu,Fri,Sat)[$localtm[6]], # day of week
+     $localtm[3],              # day of month
+     (Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec)[$localtm[4]], # month
+     $localtm[5]+1900,         # year
+     $localtm[2],              # hour
+     $localtm[1],              # minute
+     $localtm[0],              # sec
+     ($offset >= 0) ? '+' : '-',# TZ offset direction
+     abs ($offhour),           # TZ offset hour
+     $offmin,                  # TZ offset minute
+     ) || die "822-date: output error: $!\n";
+
+
+  1;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Blosxom Plug-in: date_rfc822
diff --git a/xtaran/dept b/xtaran/dept
new file mode 100644 (file)
index 0000000..27f53ff
--- /dev/null
@@ -0,0 +1,150 @@
+# -*- perl -*-
+# Blosxom Plugin: dept
+# Author(s): Axel Beckert <blosxom@deuxchevaux.org>, http://noone.org/blog
+# Version: 0.01
+# Licensing: GPL v2 or newer, http://www.gnu.org/licenses/gpl.txt
+# Dept plugin web page: http://noone.org/blog?-tags=dept
+# Dept plugin download: http://noone.org/blosxom/dept
+# Blosxom web page: http://blosxom.ookee.com/
+
+### Documentation:
+#
+# This is a plugin for blosxom.
+#
+# Installation:
+#
+#  Just drop it into your blosxoms plugin directory and it should start
+#  working. If you want, change some of the configuration variables
+#  below.
+# 
+# What it does:
+#
+#  It allows you to decorate Blosxom postings with "From the XYZ
+#  dept." like on Slashdot and other Slashsites.
+#
+# Configuration:
+#
+#  You can configure different texts based on regular expressions
+#  matched on the path. By default it looks if the path starts with
+#  and indicator for German or English language and uses and
+#  apropriate text in that language.
+#
+# How to use it:
+#
+#  Add an additional line after the title, starting with "Dept: ".
+#  Between this Dept line and the body text there should be a blank
+#  line. (This is in conformance with other Plugins, e.g. the one for
+#  meta tags, which work the same way.) After this keyword, the tags
+#  should be listed and separated by commata.
+#
+# Example:
+#
+#  The follwing two examples have the same effect.
+#
+#  | Story Headline
+#  | Dept: Slashdot Fan Club
+#  |
+#  | Story Body [...]
+#
+# Including the dept line into templates:
+#
+#  Use $dept::deptline in your templates.
+#
+# Known bugs and other ugly obstacles:
+#
+#  + None yet. :-)
+#
+# Version History:
+#
+#  0.01:   Initial release, losely based on my tagging plugin.
+#
+
+package dept;
+
+###
+### Config
+###
+
+# Texts by path
+
+my %path2text = (
+               '^/(Deutsch|de)($|/)' => 'Aus der <b>%s</b> Abteilung',
+               '^/(English|en)($|/)' => 'from the <b>%s</b> dept.',
+               '*' => 'From the <b>%s</b> dept.', # Default value. Keep it!
+               );
+
+# Regular expression
+
+my $dept_re = qr/Dept:/i;
+
+# Joining white space with:
+my $joinchar = '-';
+
+# Texts for tags
+
+my $dept_prefix = '<div class="dept">';
+my $dept_suffix = '</div>';
+
+# End of configuration.
+
+# Global variables
+
+$deptline = '';
+$dept = '';
+
+# Code
+
+sub start {
+    1;
+}
+
+sub story {
+    my($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
+    my %localtags = ();
+    my $body = '';
+    my $in_header = 1;
+
+    $deptline = '';
+    $dept = '';
+
+    foreach (split /\n/, $$body_ref) {
+       $dept .= 'x';
+       if (/^\s*$/) {
+           $in_header = 0;
+           $body .= "$_\n";
+           next;
+       }
+
+       if ($in_header) {
+           if (/^$dept_re\s*(.+?)\s*$/) {
+               $dept = $1;
+               $dept =~ s/\s/$joinchar/gs;
+               my $fmtstr = $path2text{'*'};
+               foreach my $re (sort keys %path2text) {
+                   next if $re eq '*';
+                   if ($path =~ /$re/) {
+                       $fmtstr = $path2text{$re};
+                       last;
+                   }
+               }
+               $deptline = 
+                   $dept_prefix.
+                   sprintf($fmtstr, $dept).
+                   $dept_suffix;
+               $in_header = 0;
+               next;
+           }
+       }
+
+       $body .= "$_\n";
+    }
+    $$body_ref = $body;
+
+    return 1;
+}
+
+1;
+
+
+
+
diff --git a/xtaran/multcat b/xtaran/multcat
new file mode 100644 (file)
index 0000000..73a1031
--- /dev/null
@@ -0,0 +1,90 @@
+# -*- perl -*-
+# Blosxom Plugin: multcat
+# Author(s): Axel Beckert <blosxom@deuxchevaux.org>
+# Version: 0.01
+# Licensing: GPL v2 or newer, http://www.gnu.org/licenses/gpl.txt
+# multcat web page: http://noone.org/blosxom/multcat
+# Blosxom web page: http://www.blosxom.com/
+
+### Documentation:
+#
+# This is a plugin for blosxom.
+#
+# Installation:
+#
+#  Just drop it into your blosxoms plugin directory and it should start
+#  working. It needs a symbolic link supporting operating system, so it
+#  may not work under Windows. Unices should work fine, if you don't do
+#  ugly things with file systems. ;-)
+# 
+# What it does:
+#
+#  It allows to have postings in multiple categories without having
+#  them multiple times on the main page or any category's page which
+#  includes at least two categories, the posting appears in. Designed
+#  to work together with categorytree, which still counts all
+#  occurrences of a posting.
+#
+# Plugin Order:
+#
+#  If you change the plugin order and have the categorytree plugin
+#  installed, it must come before multcat to work as intended.
+#
+#  If you have the moreentries plugin installed, you may change it's
+#  position in the plugin order to run after multcat.
+#
+# Version History:
+#
+#  0.01: Initial release
+#
+
+package multcat;
+
+use File::Basename;
+
+sub start { 1; }
+
+sub filter {
+    my ($pkg, $files_ref) = @_;
+
+    my @keys = 
+       #grep { m(^$blosxom::datadir/?$blosxom::path_info) }
+       keys %$files_ref;
+    my %localfiles = ();
+    my %debug = ();
+    my %debug2 = ();
+    my %debug3 = ();
+
+    foreach (@keys) {
+       $localfiles{$_} = $files_ref->{$_} if m(/$blosxom::path_info);
+    }
+
+    foreach my $key ( @keys ) {
+       if (-l $key) {
+           my $dest = readlink($key);
+           $debug{$key} = 
+           $dest = dirname($key)."/$dest";
+           while ($dest =~ m(/../)) {
+               $dest =~ s(/[^/]+/\.\./)(/);
+           }
+           $debug2{$key} = $dest;
+
+           $debug3{$key} = 
+           my $destdate = $localfiles{$dest};
+
+           # The found symlink is not a story which is to be
+           # displayed, so we don't need to extinct it from the
+           # website
+           next unless $destdate;
+           
+           delete($files_ref->{$key});
+       }
+    }
+
+    use Data::Dumper;
+    $debug = Dumper $blosxom::path_info, $files_ref, \@keys, \%localfiles, \%debug, \%debug2, \%debug3;
+
+    1;
+}
+
+1;
diff --git a/xtaran/pathbasedtagging b/xtaran/pathbasedtagging
new file mode 100644 (file)
index 0000000..f70df50
--- /dev/null
@@ -0,0 +1,84 @@
+# -*- perl -*-
+# Blosxom Plugin: pathbasedtagging
+# Author(s): Axel Beckert <blosxom@deuxchevaux.org>, http://noone.org/blog
+# Version: 0.01
+# Licensing: GPL v2 or newer, http://www.gnu.org/licenses/gpl.txt
+# Tagging plugin web page: http://noone.org/blog?-tags=Tagging
+# Path Based Tagging plugin download: http://noone.org/blosxom/pathbasedtagging
+# Blosxom web page: http://blosxom.ookee.com/
+
+### Documentation:
+#
+# This is a plugin for blosxom.
+#
+# Installation:
+#
+#  You should use plugin sorted by numbers with this plugin. Drop it
+#  into your blosxoms plugin directory and prefix it with a quite low
+#  two digit number (e.g. 02) which should be lower than the number of
+#  plugins having an entries subroutine, e.g entries_index or
+#  entries_cache. The tagging plugin itself should have a higher
+#  number than those plugins. (I use 02pathbasedtagging,
+#  05entries_index and 25tagging.)
+#
+#  If you want, change some of the configuration variables below.
+#
+# What it does:
+#
+#  In your blog, it handles any (virtual) directory named tag or tags
+#  special and remaps the beginning of the URL and the tags coming
+#  after that virtual directory in a way the tagging plugin can work
+#  as usual.
+#
+# Configuration:
+#
+#  You can change the regular expression for matching the special
+#  directory name. You may want to configure the tagging plugin to use
+#  the blosxom_tags linking style.
+#
+# How to use it:
+#
+#  Instead of URLs like http://blog/cgi-bin/blosxom.cgi?-tags=foo,bar
+#  you now can use http://blog/cgi-bin/blosxom.cgi/tags/foo/bar
+#
+# Version History:
+#
+#  0.01:   Initial release, together with tagging 0.04
+#
+
+package pathbasedtagging;
+
+use CGI qw(:standard);
+
+###
+### Configuration
+###
+
+my $re = qr/tags?/i;
+
+###
+### The plugin itself
+###
+
+sub start {
+    1;
+}
+
+sub entries {
+    if ($blosxom::path_info =~ m!(^|/)$re/!) {
+       $blosxom::path_info = $`;
+       my $tags = $';
+
+       # Allow multiple tags separated by slashes
+       $tags =~ s(/)(,)g;
+
+       # Pass them via CGI.pm to the tagging plugin which does all
+       # the filtering
+        CGI::param(-name => '-tags', -value => $tags);
+    }
+
+    # Keep the default method of searching entries
+    return undef;
+}
+
+1;
diff --git a/xtaran/tagging b/xtaran/tagging
new file mode 100644 (file)
index 0000000..6b13426
--- /dev/null
@@ -0,0 +1,555 @@
+# -*- perl -*-
+# Blosxom Plugin: tagging
+# Author(s): Axel Beckert <blosxom@deuxchevaux.org>, http://noone.org/blog
+# Version: 0.04
+# Licensing: GPL v2 or newer, http://www.gnu.org/licenses/gpl.txt
+# Tagging plugin web page: http://noone.org/blog?-tags=Tagging
+# Tagging plugin download: http://noone.org/blosxom/tagging
+# Blosxom web page: http://blosxom.ookee.com/
+
+### Documentation:
+#
+# This is a plugin for blosxom.
+#
+# Installation:
+#
+#  Just drop it into your blosxoms plugin directory and it should start
+#  working. If you want, change some of the configuration variables
+#  below.
+# 
+# What it does:
+#
+#  It allows you to tag Blosxom postings with keywords, filter
+#  postings based on that tags and show how often which tag was
+#  used. Should work together with Technorati Tags as described on
+#  http://www.technorati.com/help/tags.html although this feature is
+#  yet untested. (Feedback regarding this and other features is
+#  welcome!)
+#
+# Configuration:
+#
+#  The only configuration option which may be necessary to make the
+#  tag cloud work with very less stories or very less tags, is
+#  $min_tag_no. Set it to 1 and you see every tag you every tag you
+#  used in the cloud. Set it to higher values, if you have a lot of
+#  tags in use. YMMV.
+#
+#  The same counts for the related stories and
+#  $min_story_relations. By default, a story is seen as related if it
+#  shares at least two tags with the current story. Set it to 1, if
+#  you want to use it with not so many tags or stories.
+#
+# How to use it:
+#
+#  Add an additional line after the title, starting with "Tags:".
+#  Between this Tag line and the body text there should be a blank
+#  line. (This is in conformance with other Plugins, e.g. the one for
+#  meta tags, which work the same way.) After this keyword, the tags
+#  should be listed and separated by commata.
+#
+# Examples:
+#
+#  The follwing two examples have the same effect.
+#
+#  | Story Headline
+#  | Tags: X, Y, Z
+#  | Tags: A, B, C
+#  |
+#  | Story Body [...]
+#
+#  | Story Headline
+#  | Tags: A, X, B, Y, C, Z
+#  |
+#  | Story Body [...]
+#
+# Including the tags into templates:
+#
+#  Use $tagging::tag_list for the story tag list,
+#  $tagging::global_tag_list for a global tag list (also called tag
+#  cloud, e.g for head.html or foot.html), $tagging::current_filter
+#  for the currently used tagging filter (if any) and
+#  $tagging::related_tags for a list of tags related to the current
+#  filter.
+#
+# Filtering by tags:
+#
+#  If you want to filter by tags, append a CGI parameter named "-tags"
+#  with a comma-seperated list of tags to the URL of the blog. By
+#  default any post having at least one of the tags will be shown. If
+#  you set the CGI parameter "-conj" to "and", only posts with all of
+#  the tags will be shown.
+#
+#  Technorati don't seem to accept URLs with tag names in the query
+#  string as tagref URLs, so with the following Apache configuration,
+#  you can do technorati accepted tagref URLs:
+#
+#     RewriteEngine On
+#     RewriteRule ^/cgi-bin/blosxom.cgi/tags/(.*)$ /cgi-bin/blosxom.cgi?-tags=$1 [PT]
+#
+#  Then you can use the prefined blosxom_tags as base URL for tag links.
+#
+#  Another, less performant but simplier option is to install the
+#  plugin pathbasedtagging, available at
+#  http://noone.org/blosxom/pathbasedtagging
+#
+# Examples:
+#
+#  http://blog/cgi-bin/blosxom.cgi?-tags=X,Y,Z will show you all
+#  posts which have at least one of the tags X, Y _or_ Z.
+#
+#  http://blog/cgi-bin/blosxom.cgi?-tags=X,Y,Z&-conj=and will show
+#  you all posts which have _all_ of the tags X, Y _and_ Z.
+#
+# Known bugs and other ugly obstacles:
+#
+#  + Being not as performant as I would like it to be, especially when
+#    using -conj=and.
+#  + Related stories are not sorted by recentness when having same
+#    number of shared tags.
+#  + Tags must be written without HTML entities.
+#  + Technorati style tags currently don't work well in the tag cloud.
+#
+# Version History:
+#
+#  0.01:   Initial release, based on Rael Dornfest's meta plugin.
+#  0.01.1: Additional documentation, small compatibility fix for newer
+#          Perl versions
+#  0.02:   Showing how often a tag has been used (in 3 different ways)
+#  0.02.1: Fixed an XSS issue
+#  0.02.2: Fixed documentation (removed multcat left-overs) and simple
+#          Technorati Tag support, see http://www.technorati.com/help/tags.html
+#  0.03:   New feature: related stories based on the already given tags
+#          (Idea by Wim de Jonge)
+#  0.03.1: Bugfix release: Missing "/" and some minor issues
+#  0.04:   Tag blacklist for tag cloud (suggestion by Wim), boolean
+#          "and" conjunction for filtering with several tags, linked
+#          current filter, related tags, option to link to technorati
+#          tags instead of own tags, several bugfixes (Thanks to Wim
+#          and blathijs!), added a lot of documentation, renamed some
+#          config variables to have more consistent names
+#
+# TODO LIST:
+#
+#  + Generalise $base_url{wikipedia_XX}, maybe with tie.
+#  + Option to accept case insensitiv tags
+#  + Option to link all tags in lower case
+#
+
+package tagging;
+
+use File::Basename;
+use CGI;
+
+###
+### Config
+###
+
+# Where to link story tags (URLs defined below)
+my $link_tag = 'blosxom';
+
+# Where to link tags in the tag cloud (URLs defined below)
+my $link_cloud = 'blosxom';
+
+# Where to link related tags (URLs defined below)
+my $link_rtag = 'blosxom';
+
+my %base_url = (
+               blosxom => "$blosxom::url?-tags=",
+               blosxom_tags => "$blosxom::url/tags/",
+               technorati => "http://www.technorati.com/tags/",
+               flickr => "http://flickr.com/photos/tags/",
+               delicious => 'http://del.icio.us/tag/',
+               delicious_popular => 'http://del.icio.us/popular/',
+               delirious => 'http://de.lirio.us/rubric/entries/tags/',
+               suprcilious => 'http://supr.c.ilio.us/tag/',
+               buzznet => 'http://www.buzznet.com/buzzwords/',
+               shadows => 'http://www.shadows.com/tags/',
+               wikipedia => "http://en.wikipedia.org/wiki/",
+               wikipedia_de => "http://de.wikipedia.org/wiki/",
+               wikipedia_fr => "http://fr.wikipedia.org/wiki/",
+               wikipedia_pl => "http://pl.wikipedia.org/wiki/",
+               wikipedia_ja => "http://ja.wikipedia.org/wiki/",
+               wikipedia_nl => "http://nl.wikipedia.org/wiki/",
+               wikipedia_it => "http://it.wikipedia.org/wiki/",
+               wikipedia_sv => "http://sv.wikipedia.org/wiki/",
+               wikipedia_pt => "http://pt.wikipedia.org/wiki/",
+               wikipedia_es => "http://es.wikipedia.org/wiki/",
+               wikipedia_da => "http://da.wikipedia.org/wiki/",
+               wikipedia_hu => "http://hu.wikipedia.org/wiki/",
+               wikipedia_no => "http://no.wikipedia.org/wiki/",
+               wikipedia_nn => "http://nn.wikipedia.org/wiki/",
+               wikipedia_lb => "http://lb.wikipedia.org/wiki/",
+               wikipedia_simple => "http://simple.wikipedia.org/wiki/",
+               );
+
+# Regular expressions
+
+my $tag_re = qr/Tags:\s*/i;
+my $split_re = qr/\s*,\s*/;
+
+# Texts for tags
+
+my $tag_prefix = 'Tagged as: ';
+my $tag_suffix = ''; #' &raquo; ';
+my $global_tag_prefix = '<p style="text-align: justify;">'; # '<p>Available tags: ';
+my $global_tag_suffix = '</p>';
+my $current_filter_prefix = '<p><em>Current filter:</em> &raquo;';
+my $current_filter_suffix = '&laquo; (Click tag to exclude it or click a conjunction to switch them.)</p>';
+
+# Displaying the tag cloud 
+
+my $min_tag_no = 2;
+my $show_tag_no = 0;
+my $show_tag_no_by_size = 1;
+my $show_tag_no_by_color = 1;
+my $max_size = 250;
+my $min_size = 75;
+
+my @tag_cloud_blacklist = ('Now Playing', 'Other Blogs', 'Screenshot');
+
+my $start_color = 'ff9900';
+my $end_color = '991100';
+#my $start_color = '0000ff';
+#my $end_color = 'ff0000';
+#my $start_color = 'ff9900';
+#my $end_color = '0000ff';
+
+# Texts for related stories
+
+my @related_stories_tag_blacklist = ('Now Playing', 'Other Blogs', 'Screenshot');
+
+my $min_story_relations = 2;
+my $max_related_stories = 5;
+my $show_shared_tags = 0;
+my $show_number_of_shared_tags = 1;
+
+my $related_stories_prefix = '<div class="blosxomstoryfoot" align="left"><h4 class="related_stories">Related stories</h4><ul class="related_stories">'."\n";
+my $related_stories_suffix = "\n</ul></div>\n";
+my $related_story_join     = "\n";
+my $related_story_prefix   = '<li class="related_stories">';
+my $related_story_suffix   = '</li>';
+my $related_story_class    = 'related_stories';
+
+my $shared_tags_text = 'shared tags';
+
+# Related Tags
+
+my $min_tag_relations = 2;
+my $max_related_tags = 5; # 0 to disable;
+my $show_tag_shares = 0;
+
+my @related_tags_tag_blacklist = ('Now Playing', 'Other Blogs', 'Screenshot');
+
+my $related_tags_prefix = '<p class="related_tags"><em>Related tags:</em> ';
+my $related_tags_suffix = "\n</p>\n";
+my $related_tag_join     = ", ";
+my $related_tag_class    = 'related_tags';
+
+###
+### Init (You can use these variables in templates prefixed with "$tagging::".)
+###
+
+$tag_list = '';
+$global_tag_list = '';
+$current_filter = '';
+$current_filter_short = '';
+$related_stories = '';
+$related_tags = '';
+
+%tags = ();
+%related_tags = ();
+
+sub start { 
+    1;
+}
+
+sub story {
+    my($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
+    my %localtags = ();
+    my $body = '';
+    my $in_header = 1;
+
+    foreach (split /\n/, $$body_ref) {
+       if (/^\s*$/) {
+           $in_header = 0;
+           $body .= "$_\n";
+           next;
+       }
+
+       if ($in_header && /^$tag_re(.+?)$/) {
+           foreach my $tag (split($split_re, $1)) {
+               $localtags{$tag} = 1;
+           }
+           next;
+       }
+
+       $body .= "$_\n";
+    }
+    $$body_ref = $body;
+
+    $tag_list = '';
+    my %other_stories = ();
+    foreach my $tag (sort { lc($a) cmp lc($b) } keys %localtags) {
+       my $l_tag = &url_escape($tag);
+       $tag_list .= 
+           qq! <a href="$base_url{$link_tag}$l_tag" rel="tag">$tag</a>,!;
+
+#      $tag_list .= qq! <a href="$base_url{blosxom}$tag&-technorati-hack=/$tag" rel="tag" title="Look for tag $tag in this blog"!.($invisible_plugin_tags ? qq! style="display:none;"! : '').qq!>$tag</a>! if $add_plugin_tags;
+
+        # Looking for similar stories
+       next if grep { $_ eq $tag } @related_stories_tag_blacklist;
+       foreach my $other (@{$tags{$tag}}) {
+           next if $other eq "$blosxom::datadir$path/$filename.$blosxom::file_extension";
+           if (exists $other_stories{$other}) {
+               push(@{$other_stories{$other}}, $tag);
+           } else {
+               $other_stories{$other} = [$tag];
+           }
+       }
+    }
+    $tag_list =~ s/,$//;
+    $tag_list = "$tag_prefix$tag_list $tag_suffix" if $tag_list;
+
+    $related_stories = '';
+    my $i = 0;
+    foreach my $other (sort { scalar @{$other_stories{$b}} <=> 
+                             scalar @{$other_stories{$a}} }
+                      keys %other_stories) {
+       last if scalar(@{$other_stories{$other}}) < $min_story_relations;
+       last if $i++ >= $max_related_stories;
+
+       $related_stories .= $related_story_join if $related_stories;
+
+       my $opath = $other;
+       $opath =~ s!\Q$blosxom::datadir\E!$blosxom::url!;
+       $opath =~ s!\Q$blosxom::file_extension\E$!$blosxom::default_flavour!;
+
+       my $title = $other;
+       $title =~ s!^.*/([^/]+)\.$blosxom::file_extension$!$1!;
+
+       my $shared_tags_list = join(', ', @{$other_stories{$other}});
+       my $shared_tags_number = scalar(@{$other_stories{$other}});
+
+       my $attr_title = "$shared_tags_number $shared_tags_text: $shared_tags_list";
+
+       $related_stories .= qq($related_story_prefix<a href="$opath" class="$related_story_class" title="$attr_title">$title</a>);
+
+       $related_stories .= ' (' 
+           if $show_shared_tags || $show_number_of_shared_tags;
+       $related_stories .= "$shared_tags_number "
+           if $show_number_of_shared_tags;
+       $related_stories .= $shared_tags_text
+           if $show_shared_tags || $show_number_of_shared_tags;
+       $related_stories .= ": $shared_tags_list"
+           if $show_shared_tags;
+       $related_stories .= ')'
+           if $show_shared_tags || $show_number_of_shared_tags;
+
+       #use Data::Dumper;
+       #$related_stories .= qw|$other: |.Dumper($other_stories{$other});
+
+       $related_stories .= $related_story_suffix;
+    }
+    $related_stories = "$related_stories_prefix$related_stories$related_stories_suffix" if $related_stories;
+
+
+    return 1;
+}
+
+sub filter {
+    my ($pkg, $files_ref) = @_;
+    my $filter_tags = CGI::param('-tags');
+    my $filter_conj = CGI::param('-conj');
+    $filter_tags =~ s/</[/gs; # No XSS here
+    $filter_tags =~ s/>/]/gs; # No XSS here
+    my @filter_tags = split(/\s*,\s*/, $filter_tags);
+
+    foreach my $key (keys %$files_ref) {
+       next if -l $key;
+       open(FILE, $key) or do { warn "Can't open $key: $!"; next; };
+       my $tags_found = 0;
+       my $empty_line_found = 0;
+       while ($_ = <FILE>) {
+           last if /^\s*$/;
+           if (m!^$tag_re(.+?)$!) {
+               my @localtags = split($split_re, $1);
+               foreach my $tag (@localtags) {
+                   if (ref $tags{$tag}) {
+                       push(@{$tags{$tag}}, $key);
+                   } else {
+                       $tags{$tag} = [$key];
+                   }
+
+                   # Related tags
+                   next unless ($filter_tags and
+                                grep { $_ eq $tag } @filter_tags);
+
+                   foreach my $rtag (@localtags) {
+                       next if ($rtag eq $tag);
+                       
+                       if ($related_tags{$rtag}) {
+                           $related_tags{$rtag}++;
+                       } else {
+                           $related_tags{$rtag} = 1;
+                       }
+                   }
+               }
+           }
+       }
+    }
+
+    my $max = 1;
+    my $min = 0;
+    foreach my $tag (keys %tags) {
+       next if grep { $_ eq $tag } @tag_cloud_blacklist;
+
+       my $list = $tags{$tag};
+       my $no = scalar @$list;
+       next if $no < $min_tag_no;
+       $max = $no if $max < $no;
+       $min = $no if $min > $no || !$min;
+    }
+
+    my $diff = $max - $min;
+    my $conj = ($filter_conj eq 'and' ? '&-conj=and' : '');
+    my $l_filter_tags = &url_escape($filter_tags);
+
+    foreach my $tag (sort { lc($a) cmp lc($b) } keys %tags) {
+       next if grep { $_ eq $tag } @tag_cloud_blacklist;
+
+       (my $url_tag = $tag) =~ s/\&/\%26/g;
+       (my $html_tag = $tag) =~ s/\&/\&amp;/g;
+       my $tag_no = scalar @{$tags{$tag}};
+       next if $tag_no < $min_tag_no;
+       my $tag_no_display = $show_tag_no ? " ($tag_no)" : '';
+       my $title = $tag_no == 1 ? "1 posting tagged" : "$tag_no postings tagged";
+       my $tag_percent = $diff ? int($min_size+((($max_size-$min_size)/$diff)*($tag_no-$min+1))) : 100;
+       my $color = $diff ? &color_calc($tag_no, $min, $max) : '';
+       my $style = '';
+       $style .= qq!font-size: $tag_percent%;! if $show_tag_no_by_size && $diff;
+       $style .= qq!color: #$color;! if $show_tag_no_by_color && $diff;
+
+       my $l_tag = &url_escape($tag);
+       $global_tag_list .= 
+           qq| <a href="$base_url{$link_cloud}|.
+            ((($link_cloud eq 'blosxom') and 
+              ($filter_tags !~ /(^|,)\Q$tag\E($|,)/) and
+              $filter_tags) ?
+             "$l_filter_tags," : '').
+            qq|$l_tag$conj" title="$title" style="$style">$tag</a>|.
+           qq|$tag_no_display,\n|;
+    }
+
+    $global_tag_list =~ s/,$//;
+    $global_tag_list = "$global_tag_prefix$global_tag_list$global_tag_suffix" 
+       if $global_tag_list;
+
+    return 1 unless $filter_tags;
+
+    my @tags = split($split_re, $filter_tags);
+    my %localfiles = ();
+    foreach my $tag (@tags) {
+       my $files = $tags{$tag};
+       next unless ref $files;
+      FILES:
+       foreach my $file (@$files) {
+           # If all tags should match
+           if ($conj) {
+               foreach my $ctag (@tags) {
+                   if (!grep { $_ eq $file } @{$tags{$ctag}}) {
+                       next FILES;
+                   }
+               }
+           }
+               
+           $localfiles{$file} = $files_ref->{$file};
+       }
+    }
+
+    %$files_ref = %localfiles;
+
+    $current_filter_short = join($conj ? ' + ' : ' | ',
+                                map { s/\&/\&amp;/g; $_; } 
+                                sort { lc($a) cmp lc($b) } 
+                                @tags);
+
+    $conj = ($conj ? 
+            qq! <em><a href="$base_url{blosxom}$l_filter_tags">and</a></em> ! : 
+            qq! <em><a href="$base_url{blosxom}$l_filter_tags&amp;-conj=and">or</a></em> !);
+    $current_filter = ($current_filter_prefix.
+                      join($conj,
+                           map {
+                               my $tags = $filter_tags;
+                               $tags =~ s/\Q$_,\E// || $tags =~ s/\Q,$_\E// || $tags =~ s/\Q$_\E//;
+
+                               s/\&/\&amp;/g; 
+
+                               my $l_tags = &url_escape($tags);
+                               qq!<a href="$base_url{blosxom}$l_tags">$_</a>!;
+                           } 
+                           sort { lc($a) cmp lc($b) } 
+                           @tags).
+                      $current_filter_suffix);
+
+    # Related tags
+    if ($max_related_tags) {
+       $related_tags = '';
+       my $i = 1;
+       foreach my $rtag (sort { $related_tags{$b} <=> $related_tags{$a} or
+                                $a cmp $b } 
+                         keys %related_tags) {
+           next if ((grep { $_ eq $rtag } @related_tags_tag_blacklist) or
+                    $related_tags{$rtag} < $min_tag_relations);
+           my $l_rtag = &url_escape($rtag);
+           my $rel_no = $show_tag_shares ? " ($related_tags{$rtag})" : '';
+           $related_tags .= 
+               qq!<a href="$base_url{$link_rtag}$l_rtag" rel="tag" class="$related_tag_class" title="Coincided $related_tags{$rtag} times">$rtag</a>$rel_no$related_tag_join!;
+           last if $i++ >= $max_related_tags;
+           }
+       $related_tags =~ s/\Q$related_tag_join\E$//;
+
+       #use Data::Dumper;
+       $related_tags = 
+           "$related_tags_prefix$related_tags$related_tags_suffix" 
+               if $related_tags;
+       #.'<pre>'.Dumper(\%related_tags).'</pre>';
+    }
+
+#    use Data::Dumper;
+#    $debug = Dumper $filter_tags, $files_ref, \@tags, \%localfiles, \%tags;
+
+    1;
+}
+
+sub color_calc {
+    my ($tag_no, $min, $max) = @_;
+    my $diff = $max - $min;
+
+    my $result = [];
+
+    foreach my $i (0..2) {
+       my $s = &get_dec($start_color, $i*2);
+       my $e = &get_dec($end_color, $i*2);
+       my $diff_se = abs($s-$e);
+
+       my $rogob = ($diff_se/$diff)*($tag_no-$min);
+       $rogob = int($s < $e ? $s + $rogob : $s - $rogob);
+       $result->[$i] = sprintf('%02x', $rogob);
+    }
+
+    #use Data::Dumper;
+    return join('', @$result);
+}
+
+sub get_dec {
+    my ($color, $offset) = @_;
+    return hex(substr($color, $offset, 2));
+}
+
+sub url_escape {
+    my $s = shift;
+    $s =~ s/[^0-9A-Za-z,.:]/sprintf('%%%02X', ord($&))/seg;
+    return $s;
+}
+
+1;
diff --git a/xtaran/xml_ping_generic b/xtaran/xml_ping_generic
new file mode 100644 (file)
index 0000000..12e31fc
--- /dev/null
@@ -0,0 +1,195 @@
+# -*- perl -*-
+# Blosxom Plugin: xml_ping_generic
+# Author(s): Axel Beckert <blosxom@deuxchevaux.org>, http://noone.org/blog
+#            Bob Schumaker <cobblers@pobox.com>
+#            Rael Dornfest <rael@oreilly.com> 
+# Version: 0.01-2.0b4
+# xml_ping_generic web page: http://noone.org/blog?-tags=Pinging
+# xml_ping_generic download: http://noone.org/blosxom/xml_ping_generic
+# Blosxom web page: http://www.blosxom.com/
+# Documentation: See the bottom of this file or type: perldoc xml_ping_weblogs
+
+package xml_ping_generic;
+
+$VERSION = '0.01-2.0b4';
+
+# --- Configurable variables -----
+
+# What URL should this plugin ping?
+require LWP::UserAgent;
+use FileHandle;
+use File::stat;
+#use Data::Dumper;
+
+# Keep track of the newest story's mtime
+my $newest = 0;
+
+my @urls_to_ping = 
+    qw(
+
+     http://rpc.technorati.com/rpc/ping
+     http://rpc.weblogs.com/RPC2
+
+       );
+
+sub start {
+
+
+    return 1;
+}
+
+sub filter {
+    my($pkg, $files_ref) = @_;
+
+    my %mtimes = ();
+    foreach my $file (keys %$files_ref) {
+       $mtimes{$file} = stat($file)->mtime;
+    }
+
+    $newest_file = ( sort { $mtimes{$b} <=> $mtimes{$a} } keys %mtimes )[0];
+    $newest = $mtimes{$newest_file};
+
+    return 1;
+}
+
+sub end {
+    my $lockfile = "$blosxom::plugin_state_dir/.ping.lock";
+    my $outfile = "$blosxom::plugin_state_dir/.ping.output";
+
+    #print STDERR "lockfile=".(-e $lockfile)." preview=$preview::is_preview newest=".localtime($newest)." mtime=".localtime(stat($outfile)->mtime)." newest_file=$newest_file\n";
+
+    return 1 if (-e $lockfile or $preview::is_preview );
+
+    # If no timestamped touch-file or newest is newer than the touch-file...
+    if (!-e $outfile or $newest > stat($outfile)->mtime) {
+
+       open(LOCK, ">$lockfile");
+       close(LOCK);
+
+       # Okay, so we can't encode the values, we have to leave the
+       # slashes in and all.
+
+       my $blog_name = $blosxom::blog_title;
+       my $blog_url  = $blosxom::url;
+       
+       my $content = <<EOF;
+<?xml version="1.0"?>
+<methodCall>
+  <methodName>weblogUpdates.ping</methodName>
+  <params>
+     <param><value>$blog_name</value></param>
+     <param><value>$blog_url</value></param>
+  </params>
+</methodCall>
+EOF
+        # Touch the touchfile (creates if doesn't already exist)
+        open TOUCH, "> $blosxom::plugin_state_dir/.ping.output";
+
+        foreach my $url_to_ping (@urls_to_ping) {
+           $header = new HTTP::Headers;
+           (my $host = $urls_to_ping) =~ s!http://(.*?)/.*$!$1!;
+           $header->header( User_Agent => "blosxom/2.0-abe-dev xml_ping_weblogs/$VERSION",
+                            Host => $host,
+                            Content_Type => 'text/xml',
+                            Content_Length => length($content) );
+       
+           $request = HTTP::Request->new('POST', 
+                                         $url_to_ping,
+                                         $header,
+                                         $content);
+       
+           $ua = LWP::UserAgent->new;
+           $response = $ua->request($request);
+
+           #my $result = $response->{_content};
+           #$result =~ /Thanks for the ping/ or return 0;
+
+           my $result = $response->as_string;
+           my $reqas  = $request->as_string;
+           print TOUCH "$reqas\n$result\n";
+       }
+       close TOUCH;
+       unlink $lockfile;
+
+    }
+  
+    return 1;
+}
+
+sub encode {
+    my ($string) = @_;
+    
+    $string =~ s/([^-A-Za-z0-9_.!~*\'()])/sprintf('%%%02X', $1)/ge;
+
+    return $string;
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+Blosxom Plug-in: xml_ping_generic
+
+=head1 SYNOPSIS
+
+Purpose: Notifies one or more configurable pages via weblogs.com's XML
+RPC API ping that your weblog has been updated upon encountering any
+new story. By default it informs weblogs.com and technorati.com.
+
+Maintains a touch-file ($blosxom::plugin_state_dir/.ping.output) to
+which to compare the newest story's creation date. (In comparsion to
+the original xml_ping_weblogs this point works fine with Rael's
+entries_caching index module, too.) Fills the touch-file with the sent
+HTTP and XML requests and responses of the latest ping.
+
+=head1 VERSION
+
+0.01-2.0b4
+
+Second part of version number coincides with the version of Bob
+Schumaker's xml_ping_weblogs plugin, it's based on.
+
+=head1 AUTHOR
+
+Axel Beckert <abe@deuxchevaux.org>
+
+Based on the xml_ping_weblogs plugin by Bob Schumaker
+<cobblers@pobox.com>, http://www.cobblers.net/blog/
+
+=head1 SEE ALSO
+
+Blosxom Home/Docs/Licensing: http://www.raelity.org/apps/blosxom/
+
+Blosxom Plugin Docs: http://www.raelity.org/apps/blosxom/plugin.shtml
+
+=head1 BUGS
+
+Address bug reports and comments to the Blosxom mailing list 
+[http://www.yahoogroups.com/group/blosxom].
+
+=head1 LICENSE
+
+Blosxom and this Blosxom Plug-in
+Copyright 2003, Rael Dornfest 
+
+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.