Add initial georss support to rss20 plugin.
[matthijs/upstream/blosxom-plugins.git] / gavinc / rss20
1 # Blosxom Plugin: rss20
2 # Author(s): Gavin Carr <gavin@openfusion.com.au>
3 # Version: 0.003000
4 # Requires: storydate
5 # Suggests: absolute, storytags, geo
6 # Follows:  storydate
7
8 package rss20;
9
10 use strict;
11
12 use vars qw(
13   $flavour
14   $author_email 
15   $author_name 
16   $webmaster_email 
17   $permalink
18   $trackback_link
19   $copyright
20   $generator
21   $category_list
22   $georss
23 );
24
25 # --- Configuration variables -----
26
27 # What flavour string to you want to use for your feed?
28 $flavour = 'rss';
29 #$flavour = 'rss20';
30
31 # What email address should be used as the default author email?
32 # Recommended format is 'email (name)', but bare emails or mailto emails are ok too
33 $author_email = 'author@example.com (A. U. Thor)';
34
35 # What name should be used as the default author name?
36 # Define this only if you don't want to disclose an email address in $author_email.
37 # It will be ignored if $author_email is set.
38 $author_name = '';
39
40 # What email address should feed errors be reported to?
41 # Recommended format is 'email (name)', but bare emails or mailto emails are ok too
42 $webmaster_email = '';
43 #$webmaster_email = 'webmaster@example.com (Web Master)';
44
45 # What do your story permalinks look like?
46 # This must be single-quoted, to defer evaluation; blosxom namespace applies
47 $permalink = '$url$path/$fn.$default_flavour';
48
49 # What link should be used for trackbacks on a story?
50 # This must be single-quoted, to defer evaluation; blosxom namespace applies
51 $trackback_link = '';
52 # $trackback_link = '$url$path/$fn.writeback';
53 # $trackback_link = '$url$path/$fn.$feedback::trackback_flavour';
54
55 # Copyright statement; leave blank to omit.
56 $copyright = '';
57
58 # Generator that produced this feed
59 $generator = "blosxom $blosxom::version";
60
61 # --------------------------------
62 # __END_CONFIG__
63
64 $webmaster_email ||= $author_email;
65
66 my $channel_indent = 8;
67 my $item_indent = 12;
68
69 # Escape <, >, and & to hex-encoded entities for max compatibility in text elements
70 # See http://www.rssboard.org/rss-profile#data-types-characterdata
71 my %escape_text = (
72   '<' => '&#x3C;',
73   '>' => '&#x3E;',
74   '&' => '&#x26;',
75 );
76 my $escape_text_re = join '|' => keys %escape_text;
77
78 # Escape <, >, and & to standard html-encoded entities for in html elements
79 my %escape_html = (
80   '<' => '&lt;',
81   '>' => '&gt;',
82   '&' => '&amp;',
83 );
84 my $escape_html_re = join '|' => keys %escape_html;
85
86 sub start { 
87   _load_templates();
88 }
89
90 my $do_encode = 0;
91 sub story {
92   my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
93
94   # Ignore flavours other than ours
95   return unless $blosxom::flavour eq $flavour;
96
97   # Don't double-encode if someone else has already done it
98   return unless $do_encode || $blosxom::encode_xml_entities;
99  
100   # Because we're inside a loop, we set $do_encode for runs after the first
101   unless ($do_encode) {
102     $do_encode = 1;
103     $blosxom::encode_xml_entities = 0;
104   }
105
106   # Encode and reset encode_xml_entities flag
107   $$title_ref = _escape_text( $$title_ref );
108   $$body_ref  = _escape_html( $$body_ref );
109
110   $category_list = '';
111   if ($blosxom::plugins{storytags}) {
112     for (@storytags::taglist) {
113       $category_list .= qq(<category>$_</category>\n) . ' ' x $item_indent;
114     }
115   }
116
117   $georss = '';
118   if ($blosxom::plugins{geo} && $geo::latitude && $geo::longitude) {
119     $georss = qq(<georss:point>$geo::latitude $geo::longitude</georss:point>\n) .
120       ' ' x $item_indent;
121   }
122
123   return 1;
124 }
125
126 # --- Private subroutines
127
128 sub _escape_text {
129   my ($text) = @_;
130   $text =~ s/($escape_text_re)/$escape_text{$1}/g;
131   return $text;
132 }
133
134 sub _escape_html {
135   my ($html) = @_;
136   $html =~ s/($escape_html_re)/$escape_html{$1}/g;
137   return $html;
138 }
139
140 sub _load_templates {
141   $blosxom::template{$flavour}{'content_type'} = 'text/xml; charset=$blog_encoding';
142
143   $blosxom::template{$flavour}{'date'} = "\n";
144
145   $blosxom::template{$flavour}{'head'} = <<HEAD;
146 <?xml version="1.0" encoding="$blosxom::blog_encoding"?>
147 <rss version="2.0"
148     xmlns:dc="http://purl.org/dc/elements/1.1/"
149     xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
150 HEAD
151
152   if ($blosxom::plugins{geo}) {
153     $blosxom::template{$flavour}{'head'} .= 
154       qq(    xmlns:georss="http://www.georss.org/georss"\n);
155   }
156
157   $blosxom::template{$flavour}{'head'} .= <<HEAD;
158     xmlns:atom="http://www.w3.org/2005/Atom">
159
160     <channel>
161         <title>$blosxom::blog_title</title>
162         <link>$blosxom::url/</link>
163 HEAD
164
165   if ($blosxom::path_info) {
166     $blosxom::template{$flavour}{'head'} .= 
167      ' ' x $channel_indent .
168      qq(<category>$blosxom::path_info</category>\n);
169   }
170   if ($blosxom::blog_description) {
171     $blosxom::template{$flavour}{'head'} .= 
172      ' ' x $channel_indent .
173       qq(<description>$blosxom::blog_description</description>\n);
174   }
175   if ($rss20::author_email) {
176     $blosxom::template{$flavour}{'head'} .= 
177      ' ' x $channel_indent .
178       qq(<managingEditor>$rss20::author_email</managingEditor>\n);
179   }
180   elsif ($rss20::author_name) {
181     $blosxom::template{$flavour}{'head'} .= 
182      ' ' x $channel_indent .
183       qq(<dc:creator>$rss20::author_name</dc:creator>\n);
184   }
185   if ($rss20::webmaster_email) {
186     $blosxom::template{$flavour}{'head'} .= 
187      ' ' x $channel_indent .
188       qq(<webMaster>$rss20::webmaster_email</webMaster>\n);
189   }
190   if ($rss20::copyright) {
191     $blosxom::template{$flavour}{'head'} .= 
192      ' ' x $channel_indent .
193       qq(<copyright>$rss20::copyright</copyright>\n);
194   }
195   my $path_info_full = $blosxom::path_info_full || "$blosxom::path_info/index.rss";
196   $blosxom::template{$flavour}{'head'} .= <<HEAD;
197         <pubDate>\$storydate::latest_rfc822</pubDate>
198         <language>$blosxom::blog_language</language>
199         <generator>$rss20::generator</generator>
200         <atom:link href="$blosxom::url$path_info_full" rel="self" type="application/rss+xml" />
201         <sy:updatePeriod>hourly</sy:updatePeriod>
202         <sy:updateFrequency>1</sy:updateFrequency>
203         <sy:updateBase>2000-01-01T12:00+00:00</sy:updateBase>
204 HEAD
205
206   $blosxom::template{$flavour}{'story'} = <<STORY;
207
208         <item>
209             <title>\$title</title>
210             <link>$rss20::permalink</link>
211             <guid isPermaLink="true">$rss20::permalink</guid>
212             <pubDate>\$storydate::story_rfc822</pubDate>
213 STORY
214
215   if ($rss20::trackback_link) {
216     $blosxom::template{$flavour}{'story'} .= 
217       ' ' x $item_indent .
218       qq(<comments>$rss20::trackback_link</comments>\n);
219   }
220   $blosxom::template{$flavour}{'story'} .= 
221       ' ' x $item_indent .
222       '$rss20::category_list';
223
224   # GeoRSS support
225   if ($blosxom::plugins{geo}) {
226     $blosxom::template{$flavour}{'story'} .= '$rss20::georss';
227   }
228
229   $blosxom::template{$flavour}{'story'} .= <<'STORY';
230 <description>$body
231             </description>
232         </item>
233 STORY
234
235   $blosxom::template{$flavour}{'foot'} = <<'FOOT';
236
237     </channel>
238 </rss>
239 FOOT
240
241   1;
242 }
243
244
245 1;
246
247 __END__
248
249 =head1 NAME
250
251 rss20 - blosxom plugin to generate an RSS 2.0 feed of your blog
252
253 =head1 DESCRIPTION
254
255 rss20 is a blosxom plugin to generate an RSS 2.0 feed of your blog. It
256 is self-contained, including the required flavours, and has a bunch of
257 configuration variables to allow for configuration.
258
259 If you're using the L<geo> plugin, rss20 will also add georss:point
260 entries for any items with latitude and longitude metadata set (see 
261 L<geo>).
262
263 =head2 CONFIGURATION
264
265 The following package variables can be configured:
266
267 =over 4
268
269 =item $flavour
270
271 Flavour string to use for your feed (typically 'rss' or 'rss20').
272
273 =item $author_email
274
275 Default author email address for posts.
276
277 =item $webmaster_email
278
279 Email address to use for webmaster responsible for the feed (defaults 
280 to $author_email).
281
282 =item $permalink
283
284 Story permalink. Default is '$url$path/$fn.$default_flavour'.
285 Note that this value should probably be single quoted to defer
286 variable evaluation until rendering time.
287
288 =item $trackback_link
289
290 Story trackback or comment link. Default is ''. Useful if you
291 are using a comments plugin like C<writeback> or C<feedback>,
292 and the format you want will be plugin-dependant.
293
294 Like $permalink, this value should be single quoted to defer
295 variable evaluation till rendering time.
296
297 =item $copyright
298
299 Default copyright clause for posts, if any.
300
301 =back
302
303 =head1 USAGE
304
305 rss20 should be loaded relatively late, since you'll typically want
306 plugins that manipulate your story content to have run already. It 
307 also should be run after the 'story' or 'prefs' plugins if you want
308 to use those to customise your configuration variables.
309
310 Requires the L<storydate> plugin for setting up story dates in the 
311 right formats, and recommends the L<absolute> plugin for absolutising
312 URLs.
313
314
315 =head1 SEE ALSO
316
317 L<storydate>, L<absolute>, L<geo>
318
319 Blosxom: http://blosxom.sourceforge.net/
320
321 rss20 is based on the 'atomfeed' and 'rss10' plugins, by Rael
322 Dornfest and contributors.
323
324
325 =head1 BUGS
326
327 Please report bugs either directly to the author or to the blosxom 
328 development mailing list: <blosxom-devel@lists.sourceforge.net>.
329
330
331 =head1 AUTHOR
332
333 Gavin Carr <gavin@openfusion.com.au>, http://www.openfusion.net/
334
335
336 =head1 LICENSE
337
338 Copyright 2007 Gavin Carr.
339
340 This plugin is licensed under the same terms as blosxom itself i.e.
341
342 Permission is hereby granted, free of charge, to any person obtaining a
343 copy of this software and associated documentation files (the "Software"),
344 to deal in the Software without restriction, including without limitation
345 the rights to use, copy, modify, merge, publish, distribute, sublicense,
346 and/or sell copies of the Software, and to permit persons to whom the
347 Software is furnished to do so, subject to the following conditions:
348
349 The above copyright notice and this permission notice shall be included
350 in all copies or substantial portions of the Software.
351
352 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
353 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
354 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
355 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
356 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
357 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
358 OTHER DEALINGS IN THE SOFTWARE.
359
360 =cut
361
362 # vim:ft=perl