Add set of Todd Larason plugins to general.
[matthijs/upstream/blosxom-plugins.git] / general / blogroll
diff --git a/general/blogroll b/general/blogroll
new file mode 100644 (file)
index 0000000..621eb09
--- /dev/null
@@ -0,0 +1,503 @@
+# Blosxom Plugin: blogroll                                         -*- perl -*-
+# Author: Todd Larason (jtl@molehill.org)
+# Author: Kevin Scaldeferri (kevin@scaldeferri.com)
+# (line and version added by Doug Nerad to show latest version)
+# Version: 0+5i
+# Blosxom Home/Docs/Licensing: http://www.raelity.org/blosxom
+# Blogroll plugin Home/Docs/Licensing:
+#   http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/Blogroll/
+
+package blogroll;
+
+# -------------- Configuration Variables --- -------------
+
+# files to read; should be either OPML, NewNewsWire .plist, or 'table'
+# files (<title>tab<url>\n); with the default, just create a directory
+# "$plugin_state_dir/.blogroll" and put your files (or symlinks to
+# them) in it
+
+@source_files = glob "$blosxom::plugin_state_dir/.blogroll/*" if ($#source_files < 0);
+
+$use_caching = 1 unless defined $use_caching;
+
+$debug_level = 0 unless defined $debug_level;
+# -------------------------------------------------------------------
+
+use IO::File;
+use File::stat;
+
+my $package = 'blogroll';
+my $cachefile = "$blosxom::plugin_state_dir/.$package.cache";
+my $save_cache = 0;
+my $cache;
+
+sub debug {
+    my ($level, @msg) = @_;
+
+    $debug .= "@msg<br>\n";
+    if ($debug_level >= $level) {
+       print STDERR "$package debug $level: @msg\n";
+    }
+}
+
+sub load_template {
+    my ($bit) = @_;
+    return $blosxom::template->('', "$package.$bit", $blosxom::flavour);
+}
+\f
+# Output & formatting functions
+
+sub report {
+    my ($bit, $title, $htmlurl, $xmlurl) = @_;
+
+    my $f = load_template($bit);
+    $f =~ s/((\$[\w:]+)|(\$\{[\w:]+\}))/$1 . "||''"/gee;
+    return $f;
+}
+
+sub finish_file_tree {
+    my ($tree) = @_;
+    my $results;
+
+    if ($tree->{items}) {
+       $results .= report('sub_head', $tree->{title});
+       $results .= finish_file_tree($_) foreach @{$tree->{items}};
+       $results .= report('sub_foot', $tree->{title});
+    } else {
+       $results .= report($tree->{xmlurl} ? 'item_xml':'item_no_xml', 
+                          $tree->{title}, $tree->{htmlurl}, $tree->{xmlurl});
+    }
+    return $results;
+}
+
+sub finish_file {
+    my ($fc, $filename, $tree) = @_;
+    local $_;
+    my $results;
+
+    $filename =~ s:.*/::;
+    $filename =~ s:[^a-zA-Z0-9]+:_:g;
+    $$filename = $fc->{blogroll}{$blosxom::flavour};
+    return if defined $$filename;
+
+    $results = report('head');
+    foreach (@{$tree->{items}}) {
+       $results .= finish_file_tree($_);
+    }
+    $results .= report('foot');
+
+    $$filename = $fc->{blogroll}{$blosxom::flavour} = $results;
+}
+
+sub finish {
+    my (@filenames) = @_;
+    my $key = '';
+
+    foreach (@filenames) {
+       my $fc = $cache->{file}{$_};
+       $key .= "|$fc->{mtime}";
+       finish_file($fc, $_, $fc->{tree}) if ($fc->{tree});
+    }  
+    return $cache->{blogroll}{$blosxom::flavour} 
+      if ($cache->{blogroll_key}{$blosxom::flavour} eq $key);
+
+    debug(1, "cache miss: blogroll results: $key");
+    my @items;
+    foreach my $filename (@filenames) {
+       my $fc = $cache->{file}{$filename};
+       foreach (@{$fc->{items}}) {
+           push @items, $_;
+       }
+    }
+
+    my $results;
+    
+    $results = report('head');
+    foreach (sort {lc($a->[0]) cmp lc($b->[0])} @items) {
+       $results .= report(defined($_->[2]) ? 'item_xml':'item_no_xml', @{$_});
+    }
+    $results .= report('foot');
+
+    $cache->{blogroll_key}{$blosxom::flavour} = $key;
+    $cache->{blogroll}{$blosxom::flavour} = $results;
+    $save_cache = 1;
+
+    return $results;
+}
+\f
+# input and parsing functions
+
+sub handle_item {
+    my ($fc, @record) = @_;
+    push @{$fc->{items}}, [@record];
+    debug(3, "handle_item(@record)");
+}
+
+sub handle_tree {
+    my ($fc, $tree) = @_;
+
+    if ($tree->{items}) {
+       handle_tree($fc, $_) foreach @{$tree->{items}};
+    } else {
+       handle_item($fc, $tree->{title}, $tree->{htmlurl}, $tree->{xmlurl});
+    }
+}
+
+sub handle_opml_subscription_file {
+    my ($fh, $fc) = @_;
+    my $count = 0;
+    # XXX this should maybe do 'real' xml parsing
+    # XML::Simple fast enough?  worth requiring more
+    # modules installed?
+    my $text = join '',<$fh>;
+    while ($text =~ m!\s<outline (.*?)>!msg) {
+       $_ = $1;
+        next unless m|/$|;
+       my ($htmlurl, $title, $xmlurl);
+       ($htmlurl) = m:html[uU]rl=" ( [^\"]+ ) ":x;
+       ($title  ) = m:title     =" ( [^\"]+ ) ":x;
+       ($xmlurl ) = m:xml[uU]rl =" ( [^\"]+ ) ":x;
+       if (defined($title) && (defined($htmlurl) || defined($xmlurl))) {
+           push @{$fc->{tree}{items}}, 
+           {title   => $title, 
+            htmlurl => $htmlurl,
+            xmlurl  => $xmlurl};
+           $count++;
+       }
+    }
+    debug(2, "handle_opml_subscription_file finished, $count items");
+}
+
+sub handle_tab_file {
+    my ($fh, $fc) = @_;
+    my $count = 0;
+    while ($_ = $fh->getline) {
+       chomp;
+       my ($title, $htmlurl) = split /\t+/;
+       push @{$fc->{tree}{items}},
+       {title   => $title, 
+        htmlurl => $htmlurl,
+        xmlurl  => $xmlurl};
+    }
+    debug(2, "handle_tab_file finished, $count items");
+}
+
+sub read_plist_dict {
+    my ($fh) = @_;
+    my $self = { type => 'dict'};
+
+    my ($key, $value);
+    while ($_ = $fh->getline) {
+       if (m!<key>(.*)</key>!) {
+           $key = $1;
+       } elsif (m!<array>!) {
+           $self->{$key} = read_plist_array($fh);
+       } elsif (m!<array/>!) {
+           $self->{$key} = {type => 'array', array => []};
+       } elsif (m!<string>(.*)</string>!) {
+           $self->{$key} = $1;
+       } elsif (m!</dict>!) {
+           return $self;
+       } else {
+           die "$_ in dict";
+       }
+    }
+}
+
+sub read_plist_array {
+    my ($fh) = @_;
+    my $self = { type => 'array'};
+    
+    $self->{array} = [];
+    while ($_ = $fh->getline) {
+       if (/<dict>/) {
+           push @{$self->{array}}, read_plist_dict($fh);
+       } elsif (m!</array>!) {
+           return $self;
+       } else {
+           die "$_ in <array>";
+       }
+    }
+}
+
+sub prettify_plist_tree {
+    my ($tree) = @_;
+
+    if ($tree->{type} eq 'array') {
+       return [map {prettify_plist_tree($_)} @{$tree->{array}}];
+    } elsif ($tree->{isContainer}) {
+       return {title => $tree->{name},
+               items => prettify_plist_tree($tree->{childrenArray})};
+    } elsif ($tree->{type} eq 'dict' &&
+            $tree->{name} && $tree->{home} && $tree->{rss}) {
+       return {title   => $tree->{name},
+               htmlurl => $tree->{home},
+               xmlurl  => $tree->{rss}};
+    } else {
+       die "Unexpected node: $tree->{type}";
+    }
+}
+
+sub handle_nnw_file {
+    my ($fh, $fc) = @_;
+    my $count = 0;
+
+    do {
+       $_ = $fh->getline
+    } while ($_ && !m!<key>Subscriptions</key>!);
+    $_ = $fh->getline;
+    m:<array>: or die "Unexpected format: $_ at nnw toplevel";
+    my $tree = read_plist_array($fh);
+    $fc->{tree} = {items => prettify_plist_tree($tree)};
+}
+
+sub handle_file {
+    my ($filename) = @_;
+
+    my $filecache = $cache->{file}{$filename};
+    my $mtime = stat($filename)->mtime;
+
+    # If this file is in the cache, and hasn't been modified, we're
+    # done here
+    return if (defined($filecache) && $filecache->{mtime} == $mtime);
+
+    debug(1, "cache miss $filename: $mtime");
+
+    # Either not there or outdated, start over
+    $filecache = {mtime => $mtime, items => []};
+
+    my $fh = new IO::File("< $filename");
+    if (!$fh) {
+       warn "Couldn't open $filename";
+       return;
+    }
+
+    if ($filename =~ m:\.opml$:) {
+       handle_opml_subscription_file($fh, $filecache)
+    } elsif ($filename =~ m:\.tab$:) {
+       handle_tab_file($fh, $filecache);
+    } elsif ($filename =~ m:/com\.ranchero\.NetNewsWire\.plist:) {
+       handle_nnw_file($fh, $filecache);
+    } else {
+       warn "Unrecognized filetype $filename";
+    }
+    $fh->close;
+    handle_tree($filecache, $filecache->{tree});
+
+    $cache->{file}{$filename} = $filecache;
+}
+\f
+# blosxom plugin interface
+
+$blogroll;
+$last_flavour = '';
+
+sub prime_cache {
+    return 0 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) : undef);
+    # for this, the cache is always valid if it exists
+    if (defined($cache)) {
+       debug(1, "Loaded cache");
+       return 1;
+    }
+    $cache = {};
+    return 0;
+}
+
+sub save_cache {
+    return if (!$use_caching || !$save_cache);
+    debug(1, "Saving cache");
+    Storable::lock_store($cache, $cachefile);
+}
+
+sub start {
+    debug(1, "start() called, enabled");
+    while (<DATA>) {
+       chomp;
+       last if /^(__END__)?$/;
+       my ($flavour, $comp, $txt) = split ' ',$_,3;
+       $txt =~ s:\\n:\n:g;
+       $blosxom::template{$flavour}{"$package.$comp"} = $txt;
+    }
+    prime_cache();
+    return 1;
+}
+
+sub head {
+    my ($pkg, $currentdir, $head_ref) = @_;
+
+    local $_;
+
+    # for static generation, don't do the same work over and over
+
+    return 1 if ($blogroll && $last_flavour eq $blosxom::flavour); 
+    $last_flavour = $blosxom::flavour;
+
+    debug(1, "head() called");
+    foreach my $filename (@source_files) {
+       handle_file($filename) ;
+    }
+    $blogroll = finish(@source_files);
+    debug(1, "head() finished, length(\$blogroll) =", length($blogroll));
+
+    save_cache();
+    1;
+}
+
+1;
+# default flavour files; the 'error' flavour is default
+# 'blogroll.' is prepended to the name given here
+# to create an html flavour, then, create files 'blogroll.head.html' and so on.
+__DATA__
+error head <ul class="blogroll">\n
+error sub_head <li>$title<ul>\n
+error item_no_xml <li><a href="$htmlurl">$title</a></li>\n
+error item_xml <li><a href="$htmlurl">$title</a> (<a href="$xmlurl">xml</a>)</li>\n
+error sub_foot </ul></li>\n
+error foot </ul>\n
+__END__
+
+=head1 NAME
+
+Blosxom Plug-in: blogroll
+
+=head1 SYNOPSIS
+
+Purpose: Provides a blogroll from pre-exsting data files and/or an simple text file
+
+  * $blogroll::blogroll -- blogroll, sorted, combined from all input files
+  * $blogroll::<sanitized filename> -- blogroll of items from C<filename>, 
+    in their original order.  <sanitized filename> is C<filename> with all 
+    non-alphanumerics replaced with underscores
+
+=head1 VERSION
+
+0+4i
+
+4th test release
+
+=head1 AUTHOR
+
+Todd Larason  <jtl@molehill.org>, 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 Input files
+
+Three file formats are currently supported
+
+=head3 OPML subscription files
+
+These are recognized by a '.opml' extension.  
+
+Only subscription files are supported; general OPML files are not.  Although 
+OPML itself is standardized, the subscription subset is not, and there's
+more variation than you might expect.  This is known to work with AmphetaDesk
+and Radio native subscription files (but not Radio's other OPML files), and 
+NetNewsWire export files; I'm interested in both success and failure reports
+for files from other OPML generators.
+
+=head3 TAB files
+
+These are recognized by a '.tab' extension.
+
+This is a simple text format intended for human editing, either to supplment
+the items from the other file formats or for people who don't wish to use
+one of the others.
+
+Each line represents a record.  Each record contains two fields, separated
+by a tab.  The first field is the name of the item, the second feld is the 
+URL.
+
+=head3 NNW plist files
+
+These are recognized by the full name "com.ranchero.NetNewsWire.plist" (there
+may be other plist formats supported in the future, so ".plist" isn't enough).
+
+This is the native subscription format for NetNewsWire and NetNewsWire Pro.
+
+This format supports hierarchical categorization of entries, available via the
+$blogroll::com_ranchero_NetNewsWire_plist variable.
+
+=head2 Configuration variables
+
+C<@source_files> is the list of files to be used; by default, it's all the 
+files in $blosxom::plugin_state_dir/.blogroll.
+
+C<$use_caching> controls whether or not to try to cache data and
+formatted results; caching requires Storable, but the plugin will work
+just fine 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 Class for CSS control
+
+There's a class used, available for CSS customization.
+
+  * C<blogroll> -- the blogroll list as a whole
+
+=head2 Flavour-style files
+
+If you want a format change that can't be made by CSS, you can
+override the HTML generated by creating files similar to Blosxom's
+flavour files.  They should be named blogroll.I<bit>.I<flavour>; for
+available I<bit>s and their default meanings, see the C<__DATA__>
+section in the plugin.
+
+=head1 Caching
+
+If the Storable module is available and $use_caching is set, various
+bits of data will be cached; this includes the parsed items from the
+input files and the final formatted output of any blogrolls generated.
+
+The cache will never be entirely flushed, but relevant pieces are invalidated
+when input files are modified.  If you're making template changes, 
+you may wish to either disable the cache (by setting $use_caching to 0) or 
+manually flush the cache; this can be done by removing
+$plugin_state_dir/.calendar.cache, and is always safe to do.
+
+=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.
+
+