1 # Blosxom Plugin: blogroll -*- perl -*-
2 # Author: Todd Larason (jtl@molehill.org)
3 # Author: Kevin Scaldeferri (kevin@scaldeferri.com)
4 # (line and version added by Doug Nerad to show latest version)
6 # Blosxom Home/Docs/Licensing: http://blosxom.sourceforge.net/
7 # Blogroll plugin Home/Docs/Licensing:
8 # http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/Blogroll/
12 # -------------- Configuration Variables --- -------------
14 # files to read; should be either OPML, NewNewsWire .plist, or 'table'
15 # files (<title>tab<url>\n); with the default, just create a directory
16 # "$plugin_state_dir/.blogroll" and put your files (or symlinks to
19 @source_files = glob "$blosxom::plugin_state_dir/.blogroll/*" if ($#source_files < 0);
21 $use_caching = 1 unless defined $use_caching;
23 $debug_level = 0 unless defined $debug_level;
24 # -------------------------------------------------------------------
29 my $package = 'blogroll';
30 my $cachefile = "$blosxom::plugin_state_dir/.$package.cache";
35 my ($level, @msg) = @_;
37 $debug .= "@msg<br>\n";
38 if ($debug_level >= $level) {
39 print STDERR "$package debug $level: @msg\n";
45 return $blosxom::template->('', "$package.$bit", $blosxom::flavour);
48 # Output & formatting functions
51 my ($bit, $title, $htmlurl, $xmlurl) = @_;
53 my $f = load_template($bit);
54 $f =~ s/((\$[\w:]+)|(\$\{[\w:]+\}))/$1 . "||''"/gee;
58 sub finish_file_tree {
63 $results .= report('sub_head', $tree->{title});
64 $results .= finish_file_tree($_) foreach @{$tree->{items}};
65 $results .= report('sub_foot', $tree->{title});
67 $results .= report($tree->{xmlurl} ? 'item_xml':'item_no_xml',
68 $tree->{title}, $tree->{htmlurl}, $tree->{xmlurl});
74 my ($fc, $filename, $tree) = @_;
79 $filename =~ s:[^a-zA-Z0-9]+:_:g;
80 $$filename = $fc->{blogroll}{$blosxom::flavour};
81 return if defined $$filename;
83 $results = report('head');
84 foreach (@{$tree->{items}}) {
85 $results .= finish_file_tree($_);
87 $results .= report('foot');
89 $$filename = $fc->{blogroll}{$blosxom::flavour} = $results;
96 foreach (@filenames) {
97 my $fc = $cache->{file}{$_};
98 $key .= "|$fc->{mtime}";
99 finish_file($fc, $_, $fc->{tree}) if ($fc->{tree});
101 return $cache->{blogroll}{$blosxom::flavour}
102 if ($cache->{blogroll_key}{$blosxom::flavour} eq $key);
104 debug(1, "cache miss: blogroll results: $key");
106 foreach my $filename (@filenames) {
107 my $fc = $cache->{file}{$filename};
108 foreach (@{$fc->{items}}) {
115 $results = report('head');
116 foreach (sort {lc($a->[0]) cmp lc($b->[0])} @items) {
117 $results .= report(defined($_->[2]) ? 'item_xml':'item_no_xml', @{$_});
119 $results .= report('foot');
121 $cache->{blogroll_key}{$blosxom::flavour} = $key;
122 $cache->{blogroll}{$blosxom::flavour} = $results;
128 # input and parsing functions
131 my ($fc, @record) = @_;
132 push @{$fc->{items}}, [@record];
133 debug(3, "handle_item(@record)");
137 my ($fc, $tree) = @_;
139 if ($tree->{items}) {
140 handle_tree($fc, $_) foreach @{$tree->{items}};
142 handle_item($fc, $tree->{title}, $tree->{htmlurl}, $tree->{xmlurl});
146 sub handle_opml_subscription_file {
149 # XXX this should maybe do 'real' xml parsing
150 # XML::Simple fast enough? worth requiring more
152 my $text = join '',<$fh>;
153 while ($text =~ m!\s<outline (.*?)>!msg) {
156 my ($htmlurl, $title, $xmlurl);
157 ($htmlurl) = m:html[uU]rl=" ( [^\"]+ ) ":x;
158 ($title ) = m:title =" ( [^\"]+ ) ":x;
159 ($xmlurl ) = m:xml[uU]rl =" ( [^\"]+ ) ":x;
160 if (defined($title) && (defined($htmlurl) || defined($xmlurl))) {
161 push @{$fc->{tree}{items}},
168 debug(2, "handle_opml_subscription_file finished, $count items");
171 sub handle_tab_file {
174 while ($_ = $fh->getline) {
176 my ($title, $htmlurl) = split /\t+/;
177 push @{$fc->{tree}{items}},
182 debug(2, "handle_tab_file finished, $count items");
185 sub read_plist_dict {
187 my $self = { type => 'dict'};
190 while ($_ = $fh->getline) {
191 if (m!<key>(.*)</key>!) {
193 } elsif (m!<array>!) {
194 $self->{$key} = read_plist_array($fh);
195 } elsif (m!<array/>!) {
196 $self->{$key} = {type => 'array', array => []};
197 } elsif (m!<string>(.*)</string>!) {
199 } elsif (m!</dict>!) {
207 sub read_plist_array {
209 my $self = { type => 'array'};
212 while ($_ = $fh->getline) {
214 push @{$self->{array}}, read_plist_dict($fh);
215 } elsif (m!</array>!) {
223 sub prettify_plist_tree {
226 if ($tree->{type} eq 'array') {
227 return [map {prettify_plist_tree($_)} @{$tree->{array}}];
228 } elsif ($tree->{isContainer}) {
229 return {title => $tree->{name},
230 items => prettify_plist_tree($tree->{childrenArray})};
231 } elsif ($tree->{type} eq 'dict' &&
232 $tree->{name} && $tree->{home} && $tree->{rss}) {
233 return {title => $tree->{name},
234 htmlurl => $tree->{home},
235 xmlurl => $tree->{rss}};
237 die "Unexpected node: $tree->{type}";
241 sub handle_nnw_file {
247 } while ($_ && !m!<key>Subscriptions</key>!);
249 m:<array>: or die "Unexpected format: $_ at nnw toplevel";
250 my $tree = read_plist_array($fh);
251 $fc->{tree} = {items => prettify_plist_tree($tree)};
257 my $filecache = $cache->{file}{$filename};
258 my $mtime = stat($filename)->mtime;
260 # If this file is in the cache, and hasn't been modified, we're
262 return if (defined($filecache) && $filecache->{mtime} == $mtime);
264 debug(1, "cache miss $filename: $mtime");
266 # Either not there or outdated, start over
267 $filecache = {mtime => $mtime, items => []};
269 my $fh = new IO::File("< $filename");
271 warn "Couldn't open $filename";
275 if ($filename =~ m:\.opml$:) {
276 handle_opml_subscription_file($fh, $filecache)
277 } elsif ($filename =~ m:\.tab$:) {
278 handle_tab_file($fh, $filecache);
279 } elsif ($filename =~ m:/com\.ranchero\.NetNewsWire\.plist:) {
280 handle_nnw_file($fh, $filecache);
282 warn "Unrecognized filetype $filename";
285 handle_tree($filecache, $filecache->{tree});
287 $cache->{file}{$filename} = $filecache;
290 # blosxom plugin interface
296 return 0 if !$use_caching;
297 eval "require Storable";
299 debug(1, "cache disabled, Storable not available");
303 if (!Storable->can('lock_retrieve')) {
304 debug(1, "cache disabled, Storable::lock_retrieve not available");
308 $cache = (-r $cachefile ? Storable::lock_retrieve($cachefile) : undef);
309 # for this, the cache is always valid if it exists
310 if (defined($cache)) {
311 debug(1, "Loaded cache");
319 return if (!$use_caching || !$save_cache);
320 debug(1, "Saving cache");
321 Storable::lock_store($cache, $cachefile);
325 debug(1, "start() called, enabled");
328 last if /^(__END__)?$/;
329 my ($flavour, $comp, $txt) = split ' ',$_,3;
331 $blosxom::template{$flavour}{"$package.$comp"} = $txt;
338 my ($pkg, $currentdir, $head_ref) = @_;
342 # for static generation, don't do the same work over and over
344 return 1 if ($blogroll && $last_flavour eq $blosxom::flavour);
345 $last_flavour = $blosxom::flavour;
347 debug(1, "head() called");
348 foreach my $filename (@source_files) {
349 handle_file($filename) ;
351 $blogroll = finish(@source_files);
352 debug(1, "head() finished, length(\$blogroll) =", length($blogroll));
359 # default flavour files; the 'error' flavour is default
360 # 'blogroll.' is prepended to the name given here
361 # to create an html flavour, then, create files 'blogroll.head.html' and so on.
363 error head <ul class="blogroll">\n
364 error sub_head <li>$title<ul>\n
365 error item_no_xml <li><a href="$htmlurl">$title</a></li>\n
366 error item_xml <li><a href="$htmlurl">$title</a> (<a href="$xmlurl">xml</a>)</li>\n
367 error sub_foot </ul></li>\n
373 Blosxom Plug-in: blogroll
377 Purpose: Provides a blogroll from pre-exsting data files and/or an simple text file
379 * $blogroll::blogroll -- blogroll, sorted, combined from all input files
380 * $blogroll::<sanitized filename> -- blogroll of items from C<filename>,
381 in their original order. <sanitized filename> is C<filename> with all
382 non-alphanumerics replaced with underscores
392 Todd Larason <jtl@molehill.org>, http://molelog.molehill.org/
394 This plugin is now maintained by the Blosxom Sourceforge Team,
395 <blosxom-devel@lists.sourceforge.net>.
399 None known; please send bug reports and feedback to the Blosxom
400 development mailing list <blosxom-devel@lists.sourceforge.net>.
406 Three file formats are currently supported
408 =head3 OPML subscription files
410 These are recognized by a '.opml' extension.
412 Only subscription files are supported; general OPML files are not. Although
413 OPML itself is standardized, the subscription subset is not, and there's
414 more variation than you might expect. This is known to work with AmphetaDesk
415 and Radio native subscription files (but not Radio's other OPML files), and
416 NetNewsWire export files; I'm interested in both success and failure reports
417 for files from other OPML generators.
421 These are recognized by a '.tab' extension.
423 This is a simple text format intended for human editing, either to supplment
424 the items from the other file formats or for people who don't wish to use
427 Each line represents a record. Each record contains two fields, separated
428 by a tab. The first field is the name of the item, the second feld is the
431 =head3 NNW plist files
433 These are recognized by the full name "com.ranchero.NetNewsWire.plist" (there
434 may be other plist formats supported in the future, so ".plist" isn't enough).
436 This is the native subscription format for NetNewsWire and NetNewsWire Pro.
438 This format supports hierarchical categorization of entries, available via the
439 $blogroll::com_ranchero_NetNewsWire_plist variable.
441 =head2 Configuration variables
443 C<@source_files> is the list of files to be used; by default, it's all the
444 files in $blosxom::plugin_state_dir/.blogroll.
446 C<$use_caching> controls whether or not to try to cache data and
447 formatted results; caching requires Storable, but the plugin will work
448 just fine without it.
450 C<$debug_level> can be set to a value between 0 and 5; 0 will output
451 no debug information, while 5 will be very verbose. The default is 1,
452 and should be changed after you've verified the plugin is working
455 =head2 Class for CSS control
457 There's a class used, available for CSS customization.
459 * C<blogroll> -- the blogroll list as a whole
461 =head2 Flavour-style files
463 If you want a format change that can't be made by CSS, you can
464 override the HTML generated by creating files similar to Blosxom's
465 flavour files. They should be named blogroll.I<bit>.I<flavour>; for
466 available I<bit>s and their default meanings, see the C<__DATA__>
467 section in the plugin.
471 If the Storable module is available and $use_caching is set, various
472 bits of data will be cached; this includes the parsed items from the
473 input files and the final formatted output of any blogrolls generated.
475 The cache will never be entirely flushed, but relevant pieces are invalidated
476 when input files are modified. If you're making template changes,
477 you may wish to either disable the cache (by setting $use_caching to 0) or
478 manually flush the cache; this can be done by removing
479 $plugin_state_dir/.calendar.cache, and is always safe to do.
484 Copyright 2003, Todd Larason
486 (This license is the same as Blosxom's)
488 Permission is hereby granted, free of charge, to any person obtaining a
489 copy of this software and associated documentation files (the "Software"),
490 to deal in the Software without restriction, including without limitation
491 the rights to use, copy, modify, merge, publish, distribute, sublicense,
492 and/or sell copies of the Software, and to permit persons to whom the
493 Software is furnished to do so, subject to the following conditions:
495 The above copyright notice and this permission notice shall be included
496 in all copies or substantial portions of the Software.
498 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
499 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
500 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
501 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
502 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
503 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
504 OTHER DEALINGS IN THE SOFTWARE.