2 # Author(s): Gavin Carr <gavin@openfusion.com.au>
4 # Documentation: See the bottom of this file or type: perldoc tags
11 # Uncomment next line to enable debug output (don't uncomment debug() lines)
12 #use Blosxom::Debug debug_level => 2;
14 use vars qw($tagroot $tag_cache $tag_entries $tag_counts);
16 # --- Configuration variables -----
18 # What path prefix is used for tag-based queries?
21 # What story header to you use for your tags?
22 my $tag_header = 'Tags';
24 # Where is our $tag_cache file?
25 my $tag_cache_file = "$blosxom::plugin_state_dir/tag_cache";
27 # ---------------------------------
32 my $tag_cache_dirty = 0;
39 if (-f $tag_cache_file) {
40 my $fh = FileHandle->new( $tag_cache_file, 'r' )
41 or warn "[tags] cannot open cache: $!";
54 my $path_info = "/$blosxom::path_info";
55 # debug(3, "entries, path_info $path_info");
57 if ($path_info =~ m!^$tagroot/(.*)!) {
59 # debug(3, "entries, path_info matches tagroot (taglist $taglist)");
60 if ($taglist =~ m/;/) {
61 @path_tags = split /\s*;\s*/, $taglist;
65 @path_tags = split /\s*,\s*/, $taglist;
68 # If $path_info matches $tagroot it's a virtual path, so reset
69 $blosxom::path_info = '';
76 my ($pkg, $files_ref) = @_;
77 return 1 unless @path_tags;
80 for my $tag (@path_tags) {
81 if ($tag_entries->{$tag} && @{ $tag_entries->{$tag} }) {
82 for my $entry ( @{ $tag_entries->{$tag} } ) {
83 $tagged{"$blosxom::datadir$entry.$blosxom::file_extension"}++;
87 # debug(2, "entries tagged with " . join($path_tags_op,@path_tags) . ":\n " . join("\n ", sort keys %tagged));
89 # Now delete all entries from $files_ref except those tagged
90 my $tag_count = scalar @path_tags;
91 for (keys %$files_ref) {
92 if ($path_tags_op eq ';') {
93 # OR semantics - delete unless at least one tag
94 delete $files_ref->{$_} unless exists $tagged{$_};
97 # AND semantics - delete unless ALL tags
98 delete $files_ref->{$_} unless $tagged{$_} == $tag_count;
105 # Update tag cache on new or updated stories
107 my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
109 my $file = "$blosxom::datadir$path/$filename.$blosxom::file_extension";
111 warn "[tags] cannot find story file '$file'";
116 my $st = stat($file) or die "bad stat: $!";
117 my $mtime = $st->mtime;
118 if ($tag_cache->{"$path/$filename"}->{mtime} == $mtime) {
119 # debug(3, "$path/$filename found up to date in tag_cache - skipping");
123 # mtime has changed (or story is new) - compare old and new tagsets
124 my $tags_new = $blosxom::meta{$tag_header};
125 my $tags_old = $tag_cache->{"$path/$filename"}->{tags};
126 # debug(2, "tags_new: $tags_new, tags_old: $tags_old");
128 return 1 if defined $tags_new && defined $tags_old && $tags_old eq $tags_new;
131 # debug(2, "updating tag_cache, mtime $mtime, tags '$tags_new'");
132 $tag_cache->{"$path/$filename"} = { mtime => $mtime, tags => $tags_new };
136 # Write tag_cache to disk if updated
138 if ($tag_cache_dirty) {
139 # Refresh tag entries and tag counts
142 for my $entry (keys %{$tag_cache}) {
143 next unless $tag_cache->{$entry}->{tags};
144 for (split /\s*,\s*/, $tag_cache->{$entry}->{tags}) {
145 $tag_entries->{$_} ||= [];
146 push @{ $tag_entries->{$_} }, $entry;
151 # Save tag caches back to $tag_cache_file
152 my $fh = FileHandle->new( $tag_cache_file, 'w' )
153 or warn "[tags] cannot open cache '$tag_cache_file': $!"
155 print $fh Data::Dumper->Dump([ $tag_cache, $tag_entries, $tag_counts ],
156 [ qw(tag_cache tag_entries tag_counts) ]);
167 tags - blosxom plugin to read tags from story files, maintain a tag cache,
168 and allow tag-based filtering
172 L<tags> is a blosxom plugin to read tags from story files, maintain a tag
173 cache, and allow tag-based filtering.
175 Tags are defined in a comma-separated list in a $tag_header header at the
176 beginning of a blosxom post, with $tag_header defaulting to 'Tags'. So for
177 example, your post might look like:
180 Tags: dogs, cats, pets
182 Post text goes here ...
184 L<tags> uses the L<metamail> plugin to parse story headers, and stores
185 the tags in a cache (in $tag_cache_file, which defaults to
186 $blosxom::plugin_state_dir/tag_cache). The tag cache is only updated
187 when the mtime of the story file changes, so L<tags> should perform
190 L<tags> also supports tag-based filtering. If the blosxom $path_info
191 begins with $tagroot ('/tags', by default, e.g. '/tags/dogs'), then
192 L<tags> filters the entries list to include only posts with the
193 specified tag. The following syntaxes are supported (assuming the
194 default '/tags' for $tagroot):
198 =item /tags/<tag> e.g. /tags/dog
200 Show only posts with the specified tag.
202 =item /tags/<tag1>,<tag2>[,<tag3>...] e.g. /tags/dogs,cats
204 (Comma-separated) Show only posts with ALL of the specified tags i.e.
207 =item /tags/<tag1>;<tag2>[;<tag3>...] e.g. /tags/dogs;cats
209 (Comma-separated) Show only posts with ANY of the specified tags i.e.
216 L<tags> should be loaded early as it modifies blosxom $path_info when
217 doing tag-based filtering. Specifically, it needs to be loaded BEFORE
218 any C<entries> plugins (e.g. L<entries_index>, L<entries_cache>,
219 L<entries_timestamp>, etc.)
221 L<tags> depends on the L<metamail> plugin to parse the tags header,
222 though, so it must be loaded AFTER L<metamail>.
224 Also, because L<tags> does tag-based filtering, any filter plugins
225 that you want to have a global view of your entries (like
226 L<recententries>, for example) should be loaded BEFORE L<tags>.
228 =head1 ACKNOWLEDGEMENTS
230 This plugin was inspired by xtaran's excellent L<tagging> plugin.
231 Initially I was just looking to add caching to L<tagging>, but found
232 I wanted to use a more modular approach, and wanted to do slightly
233 different filtering than L<tagging> offered as well. L<tagging> is
234 still more full-featured.
238 L<tags> only handles maintaining the tags cache and doing tag-based
239 filtering. For displaying tag lists in stories, see L<storytags>.
240 For displaying a tagcloud, see L<tagcloud>.
242 L<metamail> is used for the tags header parsing.
244 See also xtaran's L<tagging> plugin, which inspired L<tags>.
246 Blosxom: http://blosxom.sourceforge.net/
250 Gavin Carr <gavin@openfusion.com.au>, http://www.openfusion.net/
254 Copyright 2007, Gavin Carr.
256 This plugin is licensed under the same terms as blosxom itself i.e.
258 Permission is hereby granted, free of charge, to any person obtaining a
259 copy of this software and associated documentation files (the "Software"),
260 to deal in the Software without restriction, including without limitation
261 the rights to use, copy, modify, merge, publish, distribute, sublicense,
262 and/or sell copies of the Software, and to permit persons to whom the
263 Software is furnished to do so, subject to the following conditions:
265 The above copyright notice and this permission notice shall be included
266 in all copies or substantial portions of the Software.
268 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
269 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
270 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
271 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
272 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
273 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
274 OTHER DEALINGS IN THE SOFTWARE.